From 41c572beb297f565894e94795d3f112e66ee6499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Langenk=C3=A4mper?= Date: Wed, 23 Jul 2025 13:38:30 +0200 Subject: [PATCH] Implements Confidence filtering for Largo --- ...ilterImageAnnotationsByLabelController.php | 77 +++++++++++++++++-- ...ilterVideoAnnotationsByLabelController.php | 77 +++++++++++++++++-- ...ilterImageAnnotationsByLabelController.php | 77 +++++++++++++++++-- ...ilterVideoAnnotationsByLabelController.php | 77 +++++++++++++++++-- app/Services/AnnotationSizeCalculator.php | 0 .../js/largo/components/annotationFilter.vue | 12 ++- 6 files changed, 286 insertions(+), 34 deletions(-) create mode 100644 app/Services/AnnotationSizeCalculator.php diff --git a/app/Http/Controllers/Api/Projects/FilterImageAnnotationsByLabelController.php b/app/Http/Controllers/Api/Projects/FilterImageAnnotationsByLabelController.php index 04a9a09b66..6c9db6b2b8 100644 --- a/app/Http/Controllers/Api/Projects/FilterImageAnnotationsByLabelController.php +++ b/app/Http/Controllers/Api/Projects/FilterImageAnnotationsByLabelController.php @@ -23,6 +23,7 @@ class FilterImageAnnotationsByLabelController extends Controller * @apiParam (Optional arguments) {Number} take Number of image annotations to return. If this parameter is present, the most recent annotations will be returned first. Default is unlimited. * @apiParam (Optional arguments) {Array} shape_id Array of shape ids to use to filter images * @apiParam (Optional arguments) {Array} user_id Array of user ids to use to filter values + * @apiParam (Optional arguments) {Array} confidence Array of confidence category ids to use to filter values * @apiParam (Optional arguments) {Boolean} union Whether the filters should be considered inclusive (OR) or exclusive (AND) * @apiPermission projectMember * @apiDescription Returns a map of image annotation IDs to their image UUIDs. @@ -37,19 +38,21 @@ public function index(Request $request, $pid, $lid) $project = Project::findOrFail($pid); $this->authorize('access', $project); - $this->validate($request, [ - 'take' => 'integer', - 'shape_id' => 'array', - 'shape_id.*' => 'integer', - 'user_id' => 'array', - 'user_id.*' => 'integer', - 'union' => 'boolean', + $this->validate($request, [ + 'session_id' => 'filled|exists:annotation_sessions,id', + 'shape_id' => 'filled|array', + 'shape_id.*' => 'exists:shapes,id', + 'user_id' => 'filled|array', + 'user_id.*' => 'exists:users,id', + 'confidence' => 'filled|array', + 'confidence.*' => 'in:0,1,2,3,4', ]); $take = $request->input('take'); $filters = [ 'shape_id' => $request->input('shape_id'), 'user_id' => $request->input('user_id'), + 'confidence' => $request->input('confidence'), ]; $filters = array_filter($filters); $union = $request->input('union', false); @@ -63,10 +66,68 @@ public function index(Request $request, $pid, $lid) }) ->when(!is_null($take), fn ($query) => $query->take($take)) ->where('image_annotation_labels.label_id', $lid) - ->when(!empty($filters), fn ($query) => $this->compileFilterConditions($query, $union, $filters)) + ->when(!empty($filters), function ($query) use ($union, $filters) { + // Handle confidence filtering separately as it requires range conditions + if (isset($filters['confidence'])) { + $confidenceValues = $filters['confidence']; + + if ($union) { + $query->where(function ($q) use ($confidenceValues) { + foreach ($confidenceValues as $value) { + if ($value < 0) { + $q->orWhereRaw($this->getConfidenceQuery(abs($value))); + } else { + $q->orWhereRaw($this->getConfidenceQuery($value)); + } + } + }); + } else { + foreach ($confidenceValues as $value) { + if ($value < 0) { + $query->whereRaw($this->getConfidenceQuery(abs($value))); + } else { + $query->whereRaw($this->getConfidenceQuery($value)); + } + } + } + + // Remove confidence from filters array to avoid double processing + unset($filters['confidence']); + } + + // Process remaining filters normally + if (!empty($filters)) { + $this->compileFilterConditions($query, $union, $filters); + } + }) ->select('images.uuid', 'image_annotations.id') ->distinct() ->orderBy('image_annotations.id', 'desc') ->pluck('images.uuid', 'image_annotations.id'); } + + /** + * Get the SQL query for filtering annotations by confidence category. + * + * @param int $categoryId The confidence category ID + * @param bool $negate Whether to negate the condition + * @return string The SQL condition + */ + private function getConfidenceQuery($confidence) + { + switch ($confidence) { + case 0: // Very low: 0 - 0.25 + return 'image_annotation_labels.confidence BETWEEN 0 AND 0.25'; + case 1: // Low: 0.25 - 0.5 + return 'image_annotation_labels.confidence BETWEEN 0.25 AND 0.5'; + case 2: // Medium: 0.5 - 0.75 + return 'image_annotation_labels.confidence BETWEEN 0.5 AND 0.75'; + case 3: // High: 0.75 - 0.9 + return 'image_annotation_labels.confidence BETWEEN 0.75 AND 0.9'; + case 4: // Very High: 0.9 - 1.0 + return 'image_annotation_labels.confidence BETWEEN 0.9 AND 1.0'; + default: + return '1=1'; // No filter + } + } } diff --git a/app/Http/Controllers/Api/Projects/FilterVideoAnnotationsByLabelController.php b/app/Http/Controllers/Api/Projects/FilterVideoAnnotationsByLabelController.php index 4404e04cf1..5a2cbb0b5e 100644 --- a/app/Http/Controllers/Api/Projects/FilterVideoAnnotationsByLabelController.php +++ b/app/Http/Controllers/Api/Projects/FilterVideoAnnotationsByLabelController.php @@ -23,6 +23,7 @@ class FilterVideoAnnotationsByLabelController extends Controller * @apiParam (Optional arguments) {Number} take Number of video annotations to return. If this parameter is present, the most recent annotations will be returned first. Default is unlimited. * @apiParam (Optional arguments) {Array} shape_id Array of shape ids to use to filter images * @apiParam (Optional arguments) {Array} user_id Array of user ids to use to filter values + * @apiParam (Optional arguments) {Array} confidence Array of confidence category ids to use to filter values * @apiParam (Optional arguments) {Boolean} union Whether the filters should be considered inclusive (OR) or exclusive (AND) * @apiPermission projectMember * @apiDescription Returns a map of video annotation IDs to their video UUIDs. @@ -37,19 +38,21 @@ public function index(Request $request, $pid, $lid) $project = Project::findOrFail($pid); $this->authorize('access', $project); - $this->validate($request, [ - 'take' => 'integer', - 'shape_id' => 'array', - 'shape_id.*' => 'integer', - 'user_id' => 'array', - 'user_id.*' => 'integer', - 'union' => 'boolean', + $this->validate($request, [ + 'session_id' => 'filled|exists:annotation_sessions,id', + 'shape_id' => 'filled|array', + 'shape_id.*' => 'exists:shapes,id', + 'user_id' => 'filled|array', + 'user_id.*' => 'exists:users,id', + 'confidence' => 'filled|array', + 'confidence.*' => 'in:0,1,2,3,4', ]); $take = $request->input('take'); $filters = [ 'shape_id' => $request->input('shape_id'), 'user_id' => $request->input('user_id'), + 'confidence' => $request->input('confidence'), ]; $filters = array_filter($filters); $union = $request->input('union', false); @@ -62,11 +65,69 @@ public function index(Request $request, $pid, $lid) ->where('project_id', $pid); }) ->where('video_annotation_labels.label_id', $lid) - ->when(!empty($filters), fn ($query) => $this->compileFilterConditions($query, $union, $filters)) + ->when(!empty($filters), function ($query) use ($union, $filters) { + // Handle confidence filtering separately as it requires range conditions + if (isset($filters['confidence'])) { + $confidenceValues = $filters['confidence']; + + if ($union) { + $query->where(function ($q) use ($confidenceValues) { + foreach ($confidenceValues as $value) { + if ($value < 0) { + $q->orWhereRaw($this->getConfidenceQuery(abs($value))); + } else { + $q->orWhereRaw($this->getConfidenceQuery($value)); + } + } + }); + } else { + foreach ($confidenceValues as $value) { + if ($value < 0) { + $query->whereRaw($this->getConfidenceQuery(abs($value))); + } else { + $query->whereRaw($this->getConfidenceQuery($value)); + } + } + } + + // Remove confidence from filters array to avoid double processing + unset($filters['confidence']); + } + + // Process remaining filters normally + if (!empty($filters)) { + $this->compileFilterConditions($query, $union, $filters); + } + }) ->when(!is_null($take), fn ($query) => $query->take($take)) ->select('videos.uuid', 'video_annotations.id') ->distinct() ->orderBy('video_annotations.id', 'desc') ->pluck('videos.uuid', 'video_annotations.id'); } + + /** + * Get the SQL query for filtering video annotations by confidence category. + * + * @param int $categoryId The confidence category ID + * @param bool $negate Whether to negate the condition + * @return string The SQL condition + */ + private function getConfidenceQuery($confidence) + { + switch ($confidence) { + case 0: // Very low: 0 - 0.25 + return 'video_annotation_labels.confidence BETWEEN 0 AND 0.25'; + case 1: // Low: 0.25 - 0.5 + return 'video_annotation_labels.confidence BETWEEN 0.25 AND 0.5'; + case 2: // Medium: 0.5 - 0.75 + return 'video_annotation_labels.confidence BETWEEN 0.5 AND 0.75'; + case 3: // High: 0.75 - 0.9 + return 'video_annotation_labels.confidence BETWEEN 0.75 AND 0.9'; + case 4: // Very High: 0.9 - 1.0 + return 'video_annotation_labels.confidence BETWEEN 0.9 AND 1.0'; + default: + return '1=1'; // No filter + } + } } diff --git a/app/Http/Controllers/Api/Volumes/FilterImageAnnotationsByLabelController.php b/app/Http/Controllers/Api/Volumes/FilterImageAnnotationsByLabelController.php index bd2b1804ce..9244fdb716 100644 --- a/app/Http/Controllers/Api/Volumes/FilterImageAnnotationsByLabelController.php +++ b/app/Http/Controllers/Api/Volumes/FilterImageAnnotationsByLabelController.php @@ -22,6 +22,7 @@ class FilterImageAnnotationsByLabelController extends Controller * @apiParam (Optional arguments) {Number} take Number of image annotations to return. If this parameter is present, the most recent annotations will be returned first. Default is unlimited. * @apiParam (Optional arguments) {Array} shape_id Array of shape ids to use to filter images * @apiParam (Optional arguments) {Array} user_id Array of user ids to use to filter values + * @apiParam (Optional arguments) {Array} confidence Array of confidence category ids to use to filter values * @apiParam (Optional arguments) {Boolean} union Whether the filters should be considered inclusive (OR) or exclusive (AND) * @apiPermission projectMember * @apiDescription Returns a map of image annotation IDs to their image UUIDs. If there is an active annotation session, annotations hidden by the session are not returned. Only available for image volumes. @@ -36,13 +37,14 @@ public function index(Request $request, $vid, $lid) $volume = Volume::findOrFail($vid); $this->authorize('access', $volume); - $this->validate($request, [ - 'take' => 'integer', - 'shape_id' => 'array', - 'shape_id.*' => 'integer', - 'user_id' => 'array', - 'user_id.*' => 'integer', - 'union' => 'boolean', + $this->validate($request, [ + 'session_id' => 'filled|exists:annotation_sessions,id', + 'shape_id' => 'filled|array', + 'shape_id.*' => 'exists:shapes,id', + 'user_id' => 'filled|array', + 'user_id.*' => 'exists:users,id', + 'confidence' => 'filled|array', + 'confidence.*' => 'in:0,1,2,3,4', ]); $take = $request->input('take'); @@ -50,6 +52,7 @@ public function index(Request $request, $vid, $lid) $filters = [ 'shape_id' => $request->input('shape_id'), 'user_id' => $request->input('user_id'), + 'confidence' => $request->input('confidence'), ]; $filters = array_filter($filters); $union = $request->input('union', false); @@ -67,7 +70,40 @@ public function index(Request $request, $vid, $lid) ->where('images.volume_id', $vid) ->where('image_annotation_labels.label_id', $lid) ->when(!is_null($take), fn ($query) => $query->take($take)) - ->when(!empty($filters), fn ($query) => $this->compileFilterConditions($query, $union, $filters)) + ->when(!empty($filters), function ($query) use ($union, $filters) { + // Handle confidence filtering separately as it requires range conditions + if (isset($filters['confidence'])) { + $confidenceValues = $filters['confidence']; + + if ($union) { + $query->where(function ($q) use ($confidenceValues) { + foreach ($confidenceValues as $value) { + if ($value < 0) { + $q->orWhereRaw($this->getConfidenceQuery(abs($value))); + } else { + $q->orWhereRaw($this->getConfidenceQuery($value)); + } + } + }); + } else { + foreach ($confidenceValues as $value) { + if ($value < 0) { + $query->whereRaw($this->getConfidenceQuery(abs($value))); + } else { + $query->whereRaw($this->getConfidenceQuery($value)); + } + } + } + + // Remove confidence from filters array to avoid double processing + unset($filters['confidence']); + } + + // Process remaining filters normally + if (!empty($filters)) { + $this->compileFilterConditions($query, $union, $filters); + } + }) ->when($session, function ($query) use ($session, $request) { if ($session->hide_other_users_annotations) { $query->where('image_annotation_labels.user_id', $request->user()->id); @@ -78,4 +114,29 @@ public function index(Request $request, $vid, $lid) ->orderBy('image_annotations.id', 'desc') ->pluck('images.uuid', 'image_annotations.id'); } + + /** + * Get the SQL query for filtering annotations by confidence category. + * + * @param int $categoryId The confidence category ID + * @param bool $negate Whether to negate the condition + * @return string The SQL condition + */ + private function getConfidenceQuery($confidence) + { + switch ($confidence) { + case 0: // Very low: 0 - 0.25 + return 'image_annotation_labels.confidence BETWEEN 0 AND 0.25'; + case 1: // Low: 0.25 - 0.5 + return 'image_annotation_labels.confidence BETWEEN 0.25 AND 0.5'; + case 2: // Medium: 0.5 - 0.75 + return 'image_annotation_labels.confidence BETWEEN 0.5 AND 0.75'; + case 3: // High: 0.75 - 0.9 + return 'image_annotation_labels.confidence BETWEEN 0.75 AND 0.9'; + case 4: // Very High: 0.9 - 1.0 + return 'image_annotation_labels.confidence BETWEEN 0.9 AND 1.0'; + default: + return '1=1'; // No filter + } + } } diff --git a/app/Http/Controllers/Api/Volumes/FilterVideoAnnotationsByLabelController.php b/app/Http/Controllers/Api/Volumes/FilterVideoAnnotationsByLabelController.php index f5707a2253..27b8fa9d06 100644 --- a/app/Http/Controllers/Api/Volumes/FilterVideoAnnotationsByLabelController.php +++ b/app/Http/Controllers/Api/Volumes/FilterVideoAnnotationsByLabelController.php @@ -23,6 +23,7 @@ class FilterVideoAnnotationsByLabelController extends Controller * @apiParam (Optional arguments) {Number} take Number of video annotations to return. If this parameter is present, the most recent annotations will be returned first. Default is unlimited. * @apiParam (Optional arguments) {Array} shape_id Array of shape ids to use to filter images * @apiParam (Optional arguments) {Array} user_id Array of user ids to use to filter values + * @apiParam (Optional arguments) {Array} confidence Array of confidence category ids to use to filter values * @apiParam (Optional arguments) {Boolean} union Whether the filters should be considered inclusive (OR) or exclusive (AND) * @apiPermission projectMember * @apiDescription Returns a map of video annotation IDs to their video UUIDs. If there is an active annotation session, annotations hidden by the session are not returned. Only available for video volumes. @@ -37,19 +38,21 @@ public function index(Request $request, $vid, $lid) $volume = Volume::findOrFail($vid); $this->authorize('access', $volume); - $this->validate($request, [ - 'take' => 'integer', - 'shape_id' => 'array', - 'shape_id.*' => 'integer', - 'user_id' => 'array', - 'user_id.*' => 'integer', - 'union' => 'boolean', + $this->validate($request, [ + 'session_id' => 'filled|exists:annotation_sessions,id', + 'shape_id' => 'filled|array', + 'shape_id.*' => 'exists:shapes,id', + 'user_id' => 'filled|array', + 'user_id.*' => 'exists:users,id', + 'confidence' => 'filled|array', + 'confidence.*' => 'in:0,1,2,3,4', ]); $take = $request->input('take'); $filters = [ 'shape_id' => $request->input('shape_id'), 'user_id' => $request->input('user_id'), + 'confidence' => $request->input('confidence'), ]; $filters = array_filter($filters); $union = $request->input('union', false); @@ -66,7 +69,40 @@ public function index(Request $request, $vid, $lid) ->join('videos', 'video_annotations.video_id', '=', 'videos.id') ->where('videos.volume_id', $vid) ->where('video_annotation_labels.label_id', $lid) - ->when(!empty($filters), fn ($query) => $this->compileFilterConditions($query, $union, $filters)) + ->when(!empty($filters), function ($query) use ($union, $filters) { + // Handle confidence filtering separately as it requires range conditions + if (isset($filters['confidence'])) { + $confidenceValues = $filters['confidence']; + + if ($union) { + $query->where(function ($q) use ($confidenceValues) { + foreach ($confidenceValues as $value) { + if ($value < 0) { + $q->orWhereRaw($this->getConfidenceQuery(abs($value))); + } else { + $q->orWhereRaw($this->getConfidenceQuery($value)); + } + } + }); + } else { + foreach ($confidenceValues as $value) { + if ($value < 0) { + $query->whereRaw($this->getConfidenceQuery(abs($value))); + } else { + $query->whereRaw($this->getConfidenceQuery($value)); + } + } + } + + // Remove confidence from filters array to avoid double processing + unset($filters['confidence']); + } + + // Process remaining filters normally + if (!empty($filters)) { + $this->compileFilterConditions($query, $union, $filters); + } + }) ->when($session, function ($query) use ($session, $request) { if ($session->hide_other_users_annotations) { $query->where('video_annotation_labels.user_id', $request->user()->id); @@ -78,4 +114,29 @@ public function index(Request $request, $vid, $lid) ->orderBy('video_annotations.id', 'desc') ->pluck('videos.uuid', 'video_annotations.id'); } + + /** + * Get the SQL query for filtering video annotations by confidence category. + * + * @param int $categoryId The confidence category ID + * @param bool $negate Whether to negate the condition + * @return string The SQL condition + */ + private function getConfidenceQuery($confidence) + { + switch ($confidence) { + case 0: // Very low: 0 - 0.25 + return 'video_annotation_labels.confidence BETWEEN 0 AND 0.25'; + case 1: // Low: 0.25 - 0.5 + return 'video_annotation_labels.confidence BETWEEN 0.25 AND 0.5'; + case 2: // Medium: 0.5 - 0.75 + return 'video_annotation_labels.confidence BETWEEN 0.5 AND 0.75'; + case 3: // High: 0.75 - 0.9 + return 'video_annotation_labels.confidence BETWEEN 0.75 AND 0.9'; + case 4: // Very High: 0.9 - 1.0 + return 'video_annotation_labels.confidence BETWEEN 0.9 AND 1.0'; + default: + return '1=1'; // No filter + } + } } diff --git a/app/Services/AnnotationSizeCalculator.php b/app/Services/AnnotationSizeCalculator.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/assets/js/largo/components/annotationFilter.vue b/resources/assets/js/largo/components/annotationFilter.vue index 9fc575ae1c..b0af138783 100644 --- a/resources/assets/js/largo/components/annotationFilter.vue +++ b/resources/assets/js/largo/components/annotationFilter.vue @@ -109,11 +109,19 @@ export default { return { filterValues: { Shape: availableShapes, - User: {} + User: {}, + Confidence: { + 0: 'Very low (0 - 0.25)', + 1: 'Low (0.25 - 0.5)', + 2: 'Medium (0.5 - 0.75)', + 3: 'High (0.75 - 0.9)', + 4: 'Very high (0.9 - 1.0)' + } }, filterToKeyMapping: { Shape: "shape_id", - User: "user_id" + User: "user_id", + Confidence: "confidence" }, selectedFilter: "Shape", selectedFilterValue: null,