diff --git a/cvat/apps/annotation/README.md b/cvat/apps/annotation/README.md index e7cc3f7d38d4..a99fbcea4d64 100644 --- a/cvat/apps/annotation/README.md +++ b/cvat/apps/annotation/README.md @@ -315,7 +315,8 @@ It may take some time. #### COCO loader description - uploaded file: single unpacked `*.json`. -- supported shapes: Polygons (the `segmentation` must not be empty) +- supported shapes: object is interpreted as Polygon if the `segmentation` field of annotation is not empty + else as Rectangle with coordinates from `bbox` field. - additional comments: the CVAT task should be created with the full label set that may be in the annotation files #### How to create a task from MS COCO dataset diff --git a/cvat/apps/annotation/coco.py b/cvat/apps/annotation/coco.py index f95a34d1fcbe..ec80b399d1cf 100644 --- a/cvat/apps/annotation/coco.py +++ b/cvat/apps/annotation/coco.py @@ -344,11 +344,12 @@ def load(file_object, annotations): for ann in anns: group = 0 label_name = labels[ann['category_id']] + polygons = [] if 'segmentation' in ann: - polygons = [] # polygon if ann['iscrowd'] == 0: - polygons = ann['segmentation'] + # filter non-empty polygons + polygons = [polygon for polygon in ann['segmentation'] if polygon] # mask else: if isinstance(ann['segmentation']['counts'], list): @@ -375,3 +376,18 @@ def load(file_object, annotations): attributes=[], group=group, )) + + if not polygons and 'bbox' in ann and isinstance(ann['bbox'], list): + xtl = ann['bbox'][0] + ytl = ann['bbox'][1] + xbr = xtl + ann['bbox'][2] + ybr = ytl + ann['bbox'][3] + annotations.add_shape(annotations.LabeledShape( + type='rectangle', + frame=frame_number, + label=label_name, + points=[xtl, ytl, xbr, ybr], + occluded=False, + attributes=[], + group=group, + )) diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index e55dcd24f215..079d5b9b05d9 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -280,9 +280,9 @@ class FloatArrayField(models.TextField): separator = "," def from_db_value(self, value, expression, connection): - if value is None: - return value - return [float(v) for v in value.split(self.separator)] + if not value: + return value + return [float(v) for v in value.split(self.separator)] def to_python(self, value): if isinstance(value, list): diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 53729a0615b7..9ef3556d4c2e 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -366,7 +366,8 @@ class ShapeSerializer(serializers.Serializer): occluded = serializers.BooleanField() z_order = serializers.IntegerField(default=0) points = serializers.ListField( - child=serializers.FloatField(min_value=0) + child=serializers.FloatField(min_value=0), + allow_empty=False, ) class LabeledShapeSerializer(ShapeSerializer, AnnotationSerializer): diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py index cea29e33079d..ae109bc72723 100644 --- a/cvat/apps/engine/tests/test_rest_api.py +++ b/cvat/apps/engine/tests/test_rest_api.py @@ -2779,6 +2779,84 @@ def etree_to_dict(t): elif annotation_format_name == "MASK": self.assertTrue(zipfile.is_zipfile(content)) + + def _run_coco_annotation_upload_test(self, user): + def generate_coco_anno(): + return b"""{ + "categories": [ + { + "id": 1, + "name": "car", + "supercategory": "" + }, + { + "id": 2, + "name": "person", + "supercategory": "" + } + ], + "images": [ + { + "coco_url": "", + "date_captured": "", + "flickr_url": "", + "license": 0, + "id": 0, + "file_name": "test_1.jpg", + "height": 720, + "width": 1280 + } + ], + "annotations": [ + { + "category_id": 1, + "id": 1, + "image_id": 0, + "iscrowd": 0, + "segmentation": [ + [] + ], + "area": 17702.0, + "bbox": [ + 574.0, + 407.0, + 167.0, + 106.0 + ] + } + ] + }""" + + response = self._get_annotation_formats(user) + self.assertEqual(response.status_code, status.HTTP_200_OK) + supported_formats = response.data + self.assertTrue(isinstance(supported_formats, list) and supported_formats) + + coco_format = None + for f in response.data: + if f["name"] == "COCO": + coco_format = f + break + self.assertTrue(coco_format) + loader = coco_format["loaders"][0] + + task, _ = self._create_task(user, user) + + content = io.BytesIO(generate_coco_anno()) + content.seek(0) + + uploaded_data = { + "annotation_file": content, + } + response = self._upload_api_v1_tasks_id_annotations(task["id"], user, uploaded_data, "format={}".format(loader["display_name"])) + self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + + response = self._upload_api_v1_tasks_id_annotations(task["id"], user, {}, "format={}".format(loader["display_name"])) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self._get_api_v1_tasks_id_annotations(task["id"], user) + self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_api_v1_tasks_id_annotations_admin(self): self._run_api_v1_tasks_id_annotations(self.admin, self.assignee, self.assignee) @@ -2801,6 +2879,9 @@ def test_api_v1_tasks_id_annotations_dump_load_user(self): def test_api_v1_tasks_id_annotations_dump_load_no_auth(self): self._run_api_v1_tasks_id_annotations_dump_load(self.user, self.assignee, None) + def test_api_v1_tasks_id_annotations_upload_coco_user(self): + self._run_coco_annotation_upload_test(self.user) + class ServerShareAPITestCase(APITestCase): def setUp(self): self.client = APIClient()