Skip to content

Commit cdf13d7

Browse files
authored
Handle annotation hole in filters and validation (#983)
1 parent f9fa289 commit cdf13d7

File tree

5 files changed

+97
-11
lines changed

5 files changed

+97
-11
lines changed

interactive_ai/services/director/app/coordination/dataset_manager/dataset_counter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def on_dataset_update(
154154
task_node_id=task_node.id_,
155155
include_empty=False,
156156
)
157-
task_label_ids = [label.id_ for label in task_labels]
157+
task_label_ids = [label.id_ for label in task_labels if not label.is_background]
158158

159159
dataset_item_count_repo = DatasetItemCountRepo(
160160
dataset_storage_identifier=DatasetStorageIdentifier(

interactive_ai/services/resource/app/communication/rest_data_validator/annotation_rest_validator.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -611,14 +611,6 @@ def __validate_global_annotation(
611611
f"{next(iter(non_empty_labels_in_annotation)).id_} for task {task.id_}"
612612
)
613613

614-
# Validate that if a background annotation is present, it is not the only annotation in the scene
615-
background_labels_in_annotation = {label for label in annotation_labels_current_task if label.is_background}
616-
if len(background_labels_in_annotation) > 0 and len(non_empty_labels_in_annotation) == 0:
617-
raise BadRequestException(
618-
f"Cannot create annotation that has a background label with ID "
619-
f"{next(iter(background_labels_in_annotation)).id_} without any other labels for task {task.id_}"
620-
)
621-
622614
# Validate that global annotations for the first task use a full box shape
623615
if previous_task is None and annotation_rest[SHAPE] != AnnotationRESTViews.generate_full_box_rest(
624616
media_height=media_height, media_width=media_width
@@ -636,7 +628,7 @@ def __validate_global_annotation(
636628
raise BadRequestException("Annotation for a global task is missing a label for the preceding task.")
637629

638630
@staticmethod
639-
def __validate_local_annotation( # noqa: C901, PLR0913
631+
def __validate_local_annotation( # noqa: C901, PLR0912, PLR0913
640632
annotation_rest: dict,
641633
task: TaskNode,
642634
previous_task: TaskNode | None,
@@ -653,6 +645,7 @@ def __validate_local_annotation( # noqa: C901, PLR0913
653645
be present
654646
- If the annotation contains an empty label, no other annotation for this task may intersect with the empty
655647
label.
648+
- If the annotation contains a background label, it must also contain a label for the current task
656649
- If the task is (rotated) detection, shape must be (rotated) rectangle
657650
658651
:param annotation_rest: REST view of the annotation
@@ -669,6 +662,17 @@ def __validate_local_annotation( # noqa: C901, PLR0913
669662
# If the annotation contains no labels for this task, return without doing validation for this task
670663
return
671664

665+
# Validate that a background label is not the only label in the annotation scene
666+
background_label_ids = {label.id_ for label in task_labels if label.is_background}
667+
only_background_labels = True
668+
for annotation in annotation_scene_rest[ANNOTATIONS]:
669+
label_ids = {ID(label_rest[ID_]) for label_rest in annotation[LABELS]}
670+
if background_label_ids != label_ids:
671+
only_background_labels = False
672+
break
673+
if only_background_labels:
674+
raise BadRequestException("It is not allowed to create an annotation with only background labels.")
675+
672676
# Validate that if the task is a local anomaly task, it contains only one of the labels (it's not allowed to
673677
# have both the normal and the anomalous label).
674678
if task.task_properties.is_anomaly and not task.task_properties.is_global:

interactive_ai/services/resource/app/usecases/statistics.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,13 +391,15 @@ def get_annotation_stats_for_task(
391391
- number of annotations including certain labels
392392
- number of shapes that have certain labels
393393
394+
If the task is semantic segmentation, then the background label is not included in the statistics.
395+
394396
:param task_node_label_schema: label schema of the task
395397
:param dataset_storage_identifier: identifier of the dataset storage of interest
396398
:param include_empty: whether to include the empty label in the stats
397399
"""
398400
labels = task_node_label_schema.get_labels(include_empty=include_empty)
399401
ann_scene_repo = AnnotationSceneRepo(dataset_storage_identifier)
400-
label_ids = [label.id_ for label in labels]
402+
label_ids = [label.id_ for label in labels if not label.is_background]
401403
(
402404
obj_sizes_per_label,
403405
label_count_per_annotation,
@@ -416,6 +418,8 @@ def get_annotation_stats_for_task(
416418
object_size_distribution_per_label = []
417419

418420
for label in labels:
421+
if label.is_background:
422+
continue
419423
count_per_shape = label_count_per_shape.get(label.id_, 0)
420424
count_per_annotation = label_count_per_annotation.get(label.id_, 0)
421425
object_size_distribution = computed_size_distribution_per_label[label.id_]

interactive_ai/services/resource/tests/fixtures/label.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,19 @@ def fxt_empty_segmentation_label(fxt_mongo_id):
190190
)
191191

192192

193+
@pytest.fixture
194+
def fxt_background_segmentation_label(fxt_mongo_id):
195+
yield Label(
196+
name="Empty segmentation label",
197+
domain=DummyValues.SEGMENTATION_DOMAIN,
198+
color=Color.from_hex_str("#ff0000"),
199+
hotkey=DummyValues.LABEL_HOTKEY,
200+
is_empty=False,
201+
is_background=True,
202+
id_=ID(fxt_mongo_id(103)),
203+
)
204+
205+
193206
@pytest.fixture
194207
def fxt_label(fxt_mongo_id):
195208
yield Label(
@@ -352,6 +365,18 @@ def _build_schema(num_labels: int = 2, **kwargs) -> LabelSchema:
352365
yield _build_schema
353366

354367

368+
@pytest.fixture
369+
def fxt_segmentation_label_schema_with_background(
370+
fxt_segmentation_label_factory, fxt_empty_segmentation_label, fxt_background_segmentation_label
371+
):
372+
"""
373+
Create a label schema for a segmentation task with a background label
374+
"""
375+
yield label_schema_from_labels(
376+
[fxt_segmentation_label_factory(0), fxt_empty_segmentation_label, fxt_background_segmentation_label]
377+
)
378+
379+
355380
@pytest.fixture
356381
def fxt_anomaly_label_schema(fxt_anomaly_labels):
357382
yield LabelSchemaView.from_labels(fxt_anomaly_labels)

interactive_ai/services/resource/tests/unit/communication/rest_data_validator/test_annotation_rest_validator.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,31 @@ def fxt_annotation_scene_empty_non_empty_label_coincide(fxt_detection_label, fxt
298298
}
299299

300300

301+
@pytest.fixture
302+
def fxt_annotation_scene_background_label(fxt_background_segmentation_label):
303+
yield {
304+
"annotations": [
305+
{
306+
"shape": {
307+
"type": "RECTANGLE",
308+
"x": 0.1 * DummyValues.MEDIA_WIDTH,
309+
"y": 0.1 * DummyValues.MEDIA_HEIGHT,
310+
"width": 0.2 * DummyValues.MEDIA_WIDTH,
311+
"height": 0.2 * DummyValues.MEDIA_HEIGHT,
312+
},
313+
"labels": [
314+
{
315+
"id": str(fxt_background_segmentation_label.id_),
316+
"name": DummyValues.LABEL_NAME,
317+
"probability": DummyValues.LABEL_PROBABILITY,
318+
"color": "#ff0000ff",
319+
}
320+
],
321+
},
322+
]
323+
}
324+
325+
301326
@pytest.fixture
302327
def fxt_annotation_scene_empty_detection_not_full_box(fxt_empty_detection_label):
303328
yield {
@@ -992,6 +1017,34 @@ def test_validate_annotation_scene_empty_non_empty_labels_overlap(
9921017
label_schema_by_task=label_schema_by_task,
9931018
)
9941019

1020+
def test_validate_annotation_scene_background_label(
1021+
self,
1022+
fxt_project_with_segmentation_task,
1023+
fxt_segmentation_label_schema_with_background,
1024+
fxt_annotation_scene_background_label,
1025+
fxt_image_identifier_1,
1026+
) -> None:
1027+
"""
1028+
Test validate_annotation_scene on a detection project that contains two
1029+
annotations, one of which is the empty one.
1030+
"""
1031+
label_schema: LabelSchema = fxt_segmentation_label_schema_with_background
1032+
label_schema_by_task = label_schema_by_task_for_single_task_project(
1033+
fxt_project_with_segmentation_task, label_schema
1034+
)
1035+
with pytest.raises(
1036+
BadRequestException,
1037+
match="It is not allowed to create an annotation with only background labels.",
1038+
):
1039+
AnnotationRestValidator().validate_annotation_scene(
1040+
annotation_scene_rest=fxt_annotation_scene_background_label,
1041+
project=fxt_project_with_segmentation_task,
1042+
media_identifier=fxt_image_identifier_1,
1043+
media_width=DummyValues.MEDIA_WIDTH,
1044+
media_height=DummyValues.MEDIA_HEIGHT,
1045+
label_schema_by_task=label_schema_by_task,
1046+
)
1047+
9951048
def test_validate_annotation_scene_duplicate_annotation_id(
9961049
self,
9971050
fxt_project_with_detection_task,

0 commit comments

Comments
 (0)