From fc636d2512b47e5439e467cf943ca27c215bc515 Mon Sep 17 00:00:00 2001 From: "kirill.sizov" Date: Thu, 15 Apr 2021 10:28:14 +0300 Subject: [PATCH 01/13] fix match dm item for no image frame --- cvat/apps/dataset_manager/bindings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index a8d2fcb98da3..a48cb28cdb04 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -611,7 +611,7 @@ def match_dm_item(item, task_data, root_hint=None): if frame_number is None and item.has_image: frame_number = task_data.match_frame(item.id + item.image.ext, root_hint) if frame_number is None: - frame_number = task_data.match_frame(item.id, root_hint) + frame_number = task_data.match_frame(item.id + '.', root_hint) if frame_number is None: frame_number = cast(item.attributes.get('frame', item.id), int) if frame_number is None and is_video: From 6366b992488022c578a78839a63dd9a8f671cef0 Mon Sep 17 00:00:00 2001 From: "kirill.sizov" Date: Thu, 15 Apr 2021 10:41:13 +0300 Subject: [PATCH 02/13] test --- .../dataset_manager/tests/test_formats.py | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/cvat/apps/dataset_manager/tests/test_formats.py b/cvat/apps/dataset_manager/tests/test_formats.py index f4589feedc05..588f2ce63545 100644 --- a/cvat/apps/dataset_manager/tests/test_formats.py +++ b/cvat/apps/dataset_manager/tests/test_formats.py @@ -217,9 +217,9 @@ def _generate_annotations(self, task): } return self._generate_custom_annotations(annotations, task) - def _generate_task_images(self, count): # pylint: disable=no-self-use + def _generate_task_images(self, count, name="image"): # pylint: disable=no-self-use images = { - "client_files[%d]" % i: generate_image_file("image_%d.jpg" % i) + "client_files[%d]" % i: generate_image_file(name + "_%d.jpg" % i) for i in range(count) } images["image_quality"] = 75 @@ -264,6 +264,21 @@ def _test_export(check, task, format_name, **export_args): check(file_path) + def _test_can_import_annotations(self, task, format_name): + with tempfile.TemporaryDirectory() as temp_dir: + file_path = osp.join(temp_dir, format_name) + + dm.task.export_task(task["id"], file_path, format_name) + expected_ann = TaskAnnotation(task["id"]) + expected_ann.init_from_db() + + dm.task.import_task_annotations(task["id"], + file_path, format_name) + actual_ann = TaskAnnotation(task["id"]) + actual_ann.init_from_db() + + self.assertEqual(len(expected_ann.data), len(actual_ann.data)) + def test_export_formats_query(self): formats = dm.views.get_export_formats() @@ -496,6 +511,21 @@ def test_frames_outside_are_not_generated(self): self.assertTrue(frame.frame in range(6, 10)) self.assertEqual(i + 1, 4) + def test_can_import_annotations_for_image_with_dots_in_filename(self): + export_formats = [f.DISPLAY_NAME for f in dm.views.get_export_formats()] + formats = [f.DISPLAY_NAME for f in dm.views.get_import_formats() if + f.DISPLAY_NAME in export_formats] + for format_name in formats: + if format_name == "VGGFace2 1.0": + self.skipTest("Format is disabled") + + images = self._generate_task_images(2, "img0.0.0.") + task = self._generate_task(images) + self._generate_annotations(task) + + with self.subTest(format=format_name): + self._test_can_import_annotations(task, format_name) + class FrameMatchingTest(_DbTestBase): def _generate_task_images(self, paths): # pylint: disable=no-self-use f = BytesIO() From d7be0429fd3ff4e9928f6221e1a1905c56afbe7f Mon Sep 17 00:00:00 2001 From: "kirill.sizov" Date: Thu, 15 Apr 2021 13:31:38 +0300 Subject: [PATCH 03/13] fix import dm annotations --- cvat/apps/dataset_manager/bindings.py | 5 +++-- cvat/apps/dataset_manager/formats/mots.py | 4 ++-- cvat/apps/dataset_manager/formats/yolo.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index a48cb28cdb04..a03adb2110db 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -14,6 +14,7 @@ from cvat.apps.engine.models import AttributeType, ShapeType from datumaro.util import cast from datumaro.util.image import ByteImage, Image +from datumaro.components.extractor import DatasetItem from .annotation import AnnotationManager, TrackManager @@ -611,7 +612,7 @@ def match_dm_item(item, task_data, root_hint=None): if frame_number is None and item.has_image: frame_number = task_data.match_frame(item.id + item.image.ext, root_hint) if frame_number is None: - frame_number = task_data.match_frame(item.id + '.', root_hint) + frame_number = task_data.match_frame(item.id, root_hint) if frame_number is None: frame_number = cast(item.attributes.get('frame', item.id), int) if frame_number is None and is_video: @@ -651,7 +652,7 @@ def import_dm_annotations(dm_dataset, task_data): for item in dm_dataset: frame_number = task_data.abs_frame_id( - match_dm_item(item, task_data, root_hint=root_hint)) + match_dm_item(DatasetItem(id=item.id + '.'), task_data, root_hint=root_hint)) # do not store one-item groups group_map = {0: 0} diff --git a/cvat/apps/dataset_manager/formats/mots.py b/cvat/apps/dataset_manager/formats/mots.py index 22b9dd08c7ea..a4a14e44527e 100644 --- a/cvat/apps/dataset_manager/formats/mots.py +++ b/cvat/apps/dataset_manager/formats/mots.py @@ -5,7 +5,7 @@ from tempfile import TemporaryDirectory from datumaro.components.dataset import Dataset -from datumaro.components.extractor import AnnotationType, Transform +from datumaro.components.extractor import AnnotationType, Transform, DatasetItem from pyunpack import Archive from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, @@ -48,7 +48,7 @@ def _import(src_file, task_data): for item in dataset: frame_number = task_data.abs_frame_id( - match_dm_item(item, task_data, root_hint=root_hint)) + match_dm_item(DatasetItem(id=item.id + '.'), task_data, root_hint=root_hint)) for ann in item.annotations: if ann.type != AnnotationType.polygon: diff --git a/cvat/apps/dataset_manager/formats/yolo.py b/cvat/apps/dataset_manager/formats/yolo.py index 0df6f5fe27a1..c0994774393d 100644 --- a/cvat/apps/dataset_manager/formats/yolo.py +++ b/cvat/apps/dataset_manager/formats/yolo.py @@ -40,7 +40,7 @@ def _import(src_file, task_data): for frame in frames: frame_info = None try: - frame_id = match_dm_item(DatasetItem(id=frame), task_data, + frame_id = match_dm_item(DatasetItem(id=frame + '.'), task_data, root_hint=root_hint) frame_info = task_data.frame_info[frame_id] except Exception: # nosec From cbcb62048a16ef076d61ceb01148915c84affdb8 Mon Sep 17 00:00:00 2001 From: "kirill.sizov" Date: Fri, 16 Apr 2021 10:24:16 +0300 Subject: [PATCH 04/13] fix incorrect solution --- cvat/apps/dataset_manager/bindings.py | 10 +++++----- cvat/apps/dataset_manager/formats/cvat.py | 2 +- cvat/apps/dataset_manager/formats/mots.py | 4 ++-- cvat/apps/dataset_manager/formats/yolo.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index a03adb2110db..b800e18ca3cf 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -14,7 +14,6 @@ from cvat.apps.engine.models import AttributeType, ShapeType from datumaro.util import cast from datumaro.util.image import ByteImage, Image -from datumaro.components.extractor import DatasetItem from .annotation import AnnotationManager, TrackManager @@ -436,8 +435,9 @@ def db_task(self): def _get_filename(path): return osp.splitext(path)[0] - def match_frame(self, path, root_hint=None): - path = self._get_filename(path) + def match_frame(self, path, root_hint=None, path_has_ext=True): + if path_has_ext: + path = self._get_filename(path) match = self._frame_mapping.get(path) if not match and root_hint and not path.startswith(root_hint): path = osp.join(root_hint, path) @@ -612,7 +612,7 @@ def match_dm_item(item, task_data, root_hint=None): if frame_number is None and item.has_image: frame_number = task_data.match_frame(item.id + item.image.ext, root_hint) if frame_number is None: - frame_number = task_data.match_frame(item.id, root_hint) + frame_number = task_data.match_frame(item.id, root_hint, path_has_ext=False) if frame_number is None: frame_number = cast(item.attributes.get('frame', item.id), int) if frame_number is None and is_video: @@ -652,7 +652,7 @@ def import_dm_annotations(dm_dataset, task_data): for item in dm_dataset: frame_number = task_data.abs_frame_id( - match_dm_item(DatasetItem(id=item.id + '.'), task_data, root_hint=root_hint)) + match_dm_item(item, task_data, root_hint=root_hint)) # do not store one-item groups group_map = {0: 0} diff --git a/cvat/apps/dataset_manager/formats/cvat.py b/cvat/apps/dataset_manager/formats/cvat.py index 02025afc750a..aa1a0a5af4cd 100644 --- a/cvat/apps/dataset_manager/formats/cvat.py +++ b/cvat/apps/dataset_manager/formats/cvat.py @@ -441,7 +441,7 @@ def load(file_object, annotations): elif el.tag == 'image': image_is_opened = True frame_id = annotations.abs_frame_id(match_dm_item( - DatasetItem(id=el.attrib['name'], + DatasetItem(id=osp.splitext(el.attrib['name'])[0], attributes={'frame': el.attrib['id']} ), task_data=annotations diff --git a/cvat/apps/dataset_manager/formats/mots.py b/cvat/apps/dataset_manager/formats/mots.py index a4a14e44527e..22b9dd08c7ea 100644 --- a/cvat/apps/dataset_manager/formats/mots.py +++ b/cvat/apps/dataset_manager/formats/mots.py @@ -5,7 +5,7 @@ from tempfile import TemporaryDirectory from datumaro.components.dataset import Dataset -from datumaro.components.extractor import AnnotationType, Transform, DatasetItem +from datumaro.components.extractor import AnnotationType, Transform from pyunpack import Archive from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, @@ -48,7 +48,7 @@ def _import(src_file, task_data): for item in dataset: frame_number = task_data.abs_frame_id( - match_dm_item(DatasetItem(id=item.id + '.'), task_data, root_hint=root_hint)) + match_dm_item(item, task_data, root_hint=root_hint)) for ann in item.annotations: if ann.type != AnnotationType.polygon: diff --git a/cvat/apps/dataset_manager/formats/yolo.py b/cvat/apps/dataset_manager/formats/yolo.py index c0994774393d..0df6f5fe27a1 100644 --- a/cvat/apps/dataset_manager/formats/yolo.py +++ b/cvat/apps/dataset_manager/formats/yolo.py @@ -40,7 +40,7 @@ def _import(src_file, task_data): for frame in frames: frame_info = None try: - frame_id = match_dm_item(DatasetItem(id=frame + '.'), task_data, + frame_id = match_dm_item(DatasetItem(id=frame), task_data, root_hint=root_hint) frame_info = task_data.frame_info[frame_id] except Exception: # nosec From 43fe33ff9ff1bf130a8c2cf5d0e8855ce0dfecfa Mon Sep 17 00:00:00 2001 From: "kirill.sizov" Date: Fri, 16 Apr 2021 15:12:06 +0300 Subject: [PATCH 05/13] remove incorrect tests --- .../dataset_manager/tests/test_formats.py | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/cvat/apps/dataset_manager/tests/test_formats.py b/cvat/apps/dataset_manager/tests/test_formats.py index 588f2ce63545..44c0cda7822d 100644 --- a/cvat/apps/dataset_manager/tests/test_formats.py +++ b/cvat/apps/dataset_manager/tests/test_formats.py @@ -264,21 +264,6 @@ def _test_export(check, task, format_name, **export_args): check(file_path) - def _test_can_import_annotations(self, task, format_name): - with tempfile.TemporaryDirectory() as temp_dir: - file_path = osp.join(temp_dir, format_name) - - dm.task.export_task(task["id"], file_path, format_name) - expected_ann = TaskAnnotation(task["id"]) - expected_ann.init_from_db() - - dm.task.import_task_annotations(task["id"], - file_path, format_name) - actual_ann = TaskAnnotation(task["id"]) - actual_ann.init_from_db() - - self.assertEqual(len(expected_ann.data), len(actual_ann.data)) - def test_export_formats_query(self): formats = dm.views.get_export_formats() @@ -511,21 +496,6 @@ def test_frames_outside_are_not_generated(self): self.assertTrue(frame.frame in range(6, 10)) self.assertEqual(i + 1, 4) - def test_can_import_annotations_for_image_with_dots_in_filename(self): - export_formats = [f.DISPLAY_NAME for f in dm.views.get_export_formats()] - formats = [f.DISPLAY_NAME for f in dm.views.get_import_formats() if - f.DISPLAY_NAME in export_formats] - for format_name in formats: - if format_name == "VGGFace2 1.0": - self.skipTest("Format is disabled") - - images = self._generate_task_images(2, "img0.0.0.") - task = self._generate_task(images) - self._generate_annotations(task) - - with self.subTest(format=format_name): - self._test_can_import_annotations(task, format_name) - class FrameMatchingTest(_DbTestBase): def _generate_task_images(self, paths): # pylint: disable=no-self-use f = BytesIO() From aad580b9e080e630c9d1d3b4fe1ec7015e251e67 Mon Sep 17 00:00:00 2001 From: "kirill.sizov" Date: Fri, 16 Apr 2021 15:16:23 +0300 Subject: [PATCH 06/13] add image path in load annotations for cvat format --- cvat/apps/dataset_manager/formats/cvat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cvat/apps/dataset_manager/formats/cvat.py b/cvat/apps/dataset_manager/formats/cvat.py index aa1a0a5af4cd..786a5025e7c0 100644 --- a/cvat/apps/dataset_manager/formats/cvat.py +++ b/cvat/apps/dataset_manager/formats/cvat.py @@ -442,7 +442,8 @@ def load(file_object, annotations): image_is_opened = True frame_id = annotations.abs_frame_id(match_dm_item( DatasetItem(id=osp.splitext(el.attrib['name'])[0], - attributes={'frame': el.attrib['id']} + attributes={'frame': el.attrib['id']}, + image=el.attrib['name'] ), task_data=annotations )) From 5d5a33068e1408d98aad32ad1f5f85b833c73a99 Mon Sep 17 00:00:00 2001 From: "kirill.sizov" Date: Fri, 16 Apr 2021 15:18:59 +0300 Subject: [PATCH 07/13] add image with dots in filename for test rest api --- cvat/apps/engine/tests/test_rest_api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py index 01b85641ecbc..2469f7f980e0 100644 --- a/cvat/apps/engine/tests/test_rest_api.py +++ b/cvat/apps/engine/tests/test_rest_api.py @@ -2967,11 +2967,11 @@ def _create_task(self, owner, assignee, annotation_format=""): "client_files[1]": generate_image_file("test_2.jpg")[1], "client_files[2]": generate_image_file("test_3.jpg")[1], "client_files[4]": generate_image_file("test_4.jpg")[1], - "client_files[5]": generate_image_file("test_5.jpg")[1], - "client_files[6]": generate_image_file("test_6.jpg")[1], - "client_files[7]": generate_image_file("test_7.jpg")[1], - "client_files[8]": generate_image_file("test_8.jpg")[1], - "client_files[9]": generate_image_file("test_9.jpg")[1], + "client_files[5]": generate_image_file("test_5.0.jpg")[1], + "client_files[6]": generate_image_file("test_6.0.jpg")[1], + "client_files[7]": generate_image_file("test_7.0.jpg")[1], + "client_files[8]": generate_image_file("test_8.0.jpg")[1], + "client_files[9]": generate_image_file("test_9.0.jpg")[1], "image_quality": 75, "frame_filter": "step=3", } From 54b7cc37e840db06f34e33ac3e74549da4755ed9 Mon Sep 17 00:00:00 2001 From: "kirill.sizov" Date: Mon, 19 Apr 2021 12:53:57 +0300 Subject: [PATCH 08/13] delete filenames with dots in test rest api --- cvat/apps/engine/tests/test_rest_api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py index 2469f7f980e0..01b85641ecbc 100644 --- a/cvat/apps/engine/tests/test_rest_api.py +++ b/cvat/apps/engine/tests/test_rest_api.py @@ -2967,11 +2967,11 @@ def _create_task(self, owner, assignee, annotation_format=""): "client_files[1]": generate_image_file("test_2.jpg")[1], "client_files[2]": generate_image_file("test_3.jpg")[1], "client_files[4]": generate_image_file("test_4.jpg")[1], - "client_files[5]": generate_image_file("test_5.0.jpg")[1], - "client_files[6]": generate_image_file("test_6.0.jpg")[1], - "client_files[7]": generate_image_file("test_7.0.jpg")[1], - "client_files[8]": generate_image_file("test_8.0.jpg")[1], - "client_files[9]": generate_image_file("test_9.0.jpg")[1], + "client_files[5]": generate_image_file("test_5.jpg")[1], + "client_files[6]": generate_image_file("test_6.jpg")[1], + "client_files[7]": generate_image_file("test_7.jpg")[1], + "client_files[8]": generate_image_file("test_8.jpg")[1], + "client_files[9]": generate_image_file("test_9.jpg")[1], "image_quality": 75, "frame_filter": "step=3", } From be35a52ecbadc3b68c3b2f1b459aac343bbaeeca Mon Sep 17 00:00:00 2001 From: "kirill.sizov" Date: Mon, 19 Apr 2021 12:54:40 +0300 Subject: [PATCH 09/13] add test --- .../dataset_manager/tests/test_formats.py | 329 +++++++++++++++++- 1 file changed, 327 insertions(+), 2 deletions(-) diff --git a/cvat/apps/dataset_manager/tests/test_formats.py b/cvat/apps/dataset_manager/tests/test_formats.py index 44c0cda7822d..2a7c40cdad6e 100644 --- a/cvat/apps/dataset_manager/tests/test_formats.py +++ b/cvat/apps/dataset_manager/tests/test_formats.py @@ -217,9 +217,9 @@ def _generate_annotations(self, task): } return self._generate_custom_annotations(annotations, task) - def _generate_task_images(self, count, name="image"): # pylint: disable=no-self-use + def _generate_task_images(self, count): # pylint: disable=no-self-use images = { - "client_files[%d]" % i: generate_image_file(name + "_%d.jpg" % i) + "client_files[%d]" % i: generate_image_file("image_%d.jpg" % i) for i in range(count) } images["image_quality"] = 75 @@ -496,6 +496,7 @@ def test_frames_outside_are_not_generated(self): self.assertTrue(frame.frame in range(6, 10)) self.assertEqual(i + 1, 4) + class FrameMatchingTest(_DbTestBase): def _generate_task_images(self, paths): # pylint: disable=no-self-use f = BytesIO() @@ -586,3 +587,327 @@ def test_dataset_root(self): root = find_dataset_root(dataset, task_data) self.assertEqual(expected, root) + +class TaskAnnotationsImportTest(_DbTestBase): + def _generate_custom_annotations(self, annotations, task): + self._put_api_v1_task_id_annotations(task["id"], annotations) + return annotations + + def _generate_task_images(self, count, name="image"): + images = { + "client_files[%d]" % i: generate_image_file("image_%d.jpg" % i) + for i in range(count) + } + images["image_quality"] = 75 + return images + + def _generate_task(self, images, annotation_format, **overrides): + labels = [] + if annotation_format in ["ICDAR Recognition 1.0", + "ICDAR Localization 1.0"]: + labels = [{ + "name": "icdar", + "attributes": [{ + "name": "text", + "mutable": False, + "input_type": "text", + "values": ["word1", "word2"] + }] + }] + elif annotation_format == "ICDAR Segmentation 1.0": + labels = [{ + "name": "icdar", + "attributes": [ + { + "name": "text", + "mutable": False, + "input_type": "text", + "values": ["word_1", "word_2", "word_3"] + }, + { + "name": "index", + "mutable": False, + "input_type": "number", + "values": ["0", "1", "2"] + }, + { + "name": "color", + "mutable": False, + "input_type": "text", + "values": ["100 110 240", "10 15 20", "120 128 64"] + }, + { + "name": "center", + "mutable": False, + "input_type": "text", + "values": ["1 2", "2 4", "10 45"] + }, + ] + }] + elif annotation_format == "Market-1501 1.0": + labels = [{ + "name": "market-1501", + "attributes": [ + { + "name": "query", + "mutable": False, + "input_type": "select", + "values": ["True", "False"] + }, + { + "name": "camera_id", + "mutable": False, + "input_type": "number", + "values": ["0", "1", "2", "3"] + }, + { + "name": "person_id", + "mutable": False, + "input_type": "number", + "values": ["1", "2", "3"] + }, + ] + }] + else: + labels = [ + { + "name": "car", + "attributes": [ + { + "name": "model", + "mutable": False, + "input_type": "select", + "default_value": "mazda", + "values": ["bmw", "mazda", "renault"] + }, + { + "name": "parked", + "mutable": True, + "input_type": "checkbox", + "default_value": False + } + ] + }, + {"name": "person"} + ] + + task = { + "name": "my task #1", + "overlap": 0, + "segment_size": 100, + "labels": labels + } + task.update(overrides) + return self._create_task(task, images) + + def _generate_annotations(self, task, annotation_format): + shapes = [] + tracks = [] + tags = [] + + if annotation_format in ["ICDAR Recognition 1.0", + "ICDAR Localization 1.0"]: + shapes = [{ + "frame": 0, + "label_id": task["labels"][0]["id"], + "group": 0, + "source": "manual", + "attributes": [ + { + "spec_id": task["labels"][0]["attributes"][0]["id"], + "value": task["labels"][0]["attributes"][0]["values"][0] + }, + ], + "points": [1.0, 2.1, 10.6, 53.22], + "type": "rectangle", + "occluded": False, + }] + elif annotation_format == "Market-1501 1.0": + tags = [{ + "frame": 1, + "label_id": task["labels"][0]["id"], + "group": 0, + "source": "manual", + "attributes": [ + { + "spec_id": task["labels"][0]["attributes"][0]["id"], + "value": task["labels"][0]["attributes"][0]["values"][1] + }, + { + "spec_id": task["labels"][0]["attributes"][1]["id"], + "value": task["labels"][0]["attributes"][1]["values"][2] + }, + { + "spec_id": task["labels"][0]["attributes"][2]["id"], + "value": task["labels"][0]["attributes"][2]["values"][0] + } + ], + }] + elif annotation_format == "ICDAR Segmentation 1.0": + shapes = [{ + "frame": 0, + "label_id": task["labels"][0]["id"], + "group": 0, + "source": "manual", + "attributes": [ + { + "spec_id": task["labels"][0]["attributes"][0]["id"], + "value": task["labels"][0]["attributes"][0]["values"][0] + }, + { + "spec_id": task["labels"][0]["attributes"][1]["id"], + "value": task["labels"][0]["attributes"][1]["values"][0] + }, + { + "spec_id": task["labels"][0]["attributes"][2]["id"], + "value": task["labels"][0]["attributes"][2]["values"][1] + }, + { + "spec_id": task["labels"][0]["attributes"][3]["id"], + "value": task["labels"][0]["attributes"][3]["values"][2] + } + ], + "points": [1.0, 2.1, 10.6, 53.22], + "type": "rectangle", + "occluded": False, + }] + elif annotation_format == "VGGFace2 1.0": + shapes = [{ + "frame": 1, + "label_id": task["labels"][1]["id"], + "group": None, + "source": "manual", + "attributes": [], + "points": [2.0, 2.1, 40, 50.7], + "type": "rectangle", + "occluded": False + }] + else: + rectangle_shape_wo_attrs = { + "frame": 1, + "label_id": task["labels"][1]["id"], + "group": 0, + "source": "manual", + "attributes": [], + "points": [2.0, 2.1, 40, 50.7], + "type": "rectangle", + "occluded": False, + } + + rectangle_shape_with_attrs = { + "frame": 0, + "label_id": task["labels"][0]["id"], + "group": 0, + "source": "manual", + "attributes": [ + { + "spec_id": task["labels"][0]["attributes"][0]["id"], + "value": task["labels"][0]["attributes"][0]["values"][0] + }, + { + "spec_id": task["labels"][0]["attributes"][1]["id"], + "value": task["labels"][0]["attributes"][1]["default_value"] + } + ], + "points": [1.0, 2.1, 10.6, 53.22], + "type": "rectangle", + "occluded": False, + } + + track_wo_attrs = { + "frame": 0, + "label_id": task["labels"][1]["id"], + "group": 0, + "source": "manual", + "attributes": [], + "shapes": [ + { + "frame": 0, + "attributes": [], + "points": [1.0, 2.1, 100, 300.222], + "type": "polygon", + "occluded": False, + "outside": False + } + ] + } + + tag_wo_attrs = { + "frame": 0, + "label_id": task["labels"][0]["id"], + "group": None, + "attributes": [] + } + + tag_with_attrs = { + "frame": 1, + "label_id": task["labels"][0]["id"], + "group": 3, + "source": "manual", + "attributes": [ + { + "spec_id": task["labels"][0]["attributes"][0]["id"], + "value": task["labels"][0]["attributes"][0]["values"][1] + }, + { + "spec_id": task["labels"][0]["attributes"][1]["id"], + "value": task["labels"][0]["attributes"][1]["default_value"] + } + ], + } + + if annotation_format == "VGGFace2 1.0": + shapes = rectangle_shape_wo_attrs + elif annotation_format == "CVAT 1.1": + shapes = [rectangle_shape_wo_attrs, + rectangle_shape_with_attrs] + tags = [tag_with_attrs, tag_wo_attrs] + elif annotation_format == "MOTS PNG 1.0": + tracks = [track_wo_attrs] + else: + shapes = [rectangle_shape_wo_attrs, + rectangle_shape_with_attrs] + tags = tag_wo_attrs + tracks = track_wo_attrs + + annotations = { + "version": 0, + "tags": tags, + "shapes": shapes, + "tracks": tracks + } + + return self._generate_custom_annotations(annotations, task) + + def _test_can_import_annotations(self, task, import_format): + with tempfile.TemporaryDirectory() as temp_dir: + file_path = osp.join(temp_dir, import_format) + + export_format = import_format + if import_format == "CVAT 1.1": + export_format = "CVAT for images 1.1" + + dm.task.export_task(task["id"], file_path, export_format) + expected_ann = TaskAnnotation(task["id"]) + expected_ann.init_from_db() + + dm.task.import_task_annotations(task["id"], + file_path, import_format) + actual_ann = TaskAnnotation(task["id"]) + actual_ann.init_from_db() + + self.assertEqual(len(expected_ann.data), len(actual_ann.data)) + + def test_can_import_annotations_for_image_with_dots_in_filename(self): + for f in dm.views.get_import_formats(): + format_name = f.DISPLAY_NAME + + images = self._generate_task_images(3, "img0.0.0") + task = self._generate_task(images, format_name) + self._generate_annotations(task, format_name) + + with self.subTest(format=format_name): + if not f.ENABLED: + self.skipTest("Format is disabled") + + self._test_can_import_annotations(task, format_name) \ No newline at end of file From 8a635b11fb6f40f635c0da2881684e4b1f80bb6a Mon Sep 17 00:00:00 2001 From: Max Wang Date: Tue, 20 Apr 2021 04:30:22 -0400 Subject: [PATCH 10/13] Fix bug with hung web worker (#3096) * Fix bug with hung web worker * Added upper limit to stop property of active chunk request * Add web worker fix to changelog * Bump to version 3.12.1 --- CHANGELOG.md | 1 + cvat-core/package-lock.json | 2 +- cvat-core/package.json | 2 +- cvat-core/src/frames.js | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6a6eaa280bf..3ad808be0e68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Export of instance masks with holes () - Changing a label on canvas does not work when 'Show object details' enabled () +- Make sure frame unzip web worker correctly terminates after unzipping all images in a requested chunk () ### Security diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json index 644895f51d0b..f9c5f1c1ad9a 100644 --- a/cvat-core/package-lock.json +++ b/cvat-core/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "3.12.0", + "version": "3.12.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-core/package.json b/cvat-core/package.json index e0453e8ed457..dcd6aaf9d954 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "3.12.0", + "version": "3.12.1", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "babel.config.js", "scripts": { diff --git a/cvat-core/src/frames.js b/cvat-core/src/frames.js index 4f029a3fd147..bd1009ef7200 100644 --- a/cvat-core/src/frames.js +++ b/cvat-core/src/frames.js @@ -286,7 +286,7 @@ if (nextChunkNumber * chunkSize < this.stopFrame) { provider.setReadyToLoading(nextChunkNumber); const nextStart = nextChunkNumber * chunkSize; - const nextStop = (nextChunkNumber + 1) * chunkSize - 1; + const nextStop = Math.min(this.stopFrame, (nextChunkNumber + 1) * chunkSize - 1); if (!provider.isChunkCached(nextStart, nextStop)) { if (!frameDataCache[this.tid].activeChunkRequest) { frameDataCache[this.tid].activeChunkRequest = { From f267f8aed33163bde6cbaa1c16c23d24029019b6 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 20 Apr 2021 11:45:16 +0300 Subject: [PATCH 11/13] Added DICOM conversion script (#3095) * Added DICOM conversion script * Updated changelog * Fixed strip to rstrip * Fixed some detected issues * Removed extra variable, updated README.md --- CHANGELOG.md | 1 + utils/dicom_converter/README.md | 21 +++++ utils/dicom_converter/requirements.txt | 4 + utils/dicom_converter/script.py | 113 +++++++++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 utils/dicom_converter/README.md create mode 100644 utils/dicom_converter/requirements.txt create mode 100644 utils/dicom_converter/script.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ad808be0e68..62042ab38a58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Documentation on mask annotation () - Hotkeys to switch a label of existing object or to change default label (for objects created with N) () +- A script to convert some kinds of DICOM files to regular images () ### Changed diff --git a/utils/dicom_converter/README.md b/utils/dicom_converter/README.md new file mode 100644 index 000000000000..e4c5ed800659 --- /dev/null +++ b/utils/dicom_converter/README.md @@ -0,0 +1,21 @@ +# Description + +The script is used to convert some kinds of DICOM data to regular images. +Then you can annotate these images on CVAT and get a segmentation mask. +The conversion script was tested on CT, MT and some multi-frame DICOM data. +DICOM files with series (multi-frame) are saved under the same name with a number postfix: 001, 002, 003, etc. + +# Installation + +```bash +python3 -m venv .env +. .env/bin/activate +pip install -r requirements.txt +``` + +# Running + +``` +. .env/bin/activate # if not activated +python script.py input_data output_data +``` diff --git a/utils/dicom_converter/requirements.txt b/utils/dicom_converter/requirements.txt new file mode 100644 index 000000000000..9ed6b39b6afd --- /dev/null +++ b/utils/dicom_converter/requirements.txt @@ -0,0 +1,4 @@ +numpy==1.20.2 +Pillow==8.2.0 +pydicom==2.1.2 +tqdm==4.60.0 diff --git a/utils/dicom_converter/script.py b/utils/dicom_converter/script.py new file mode 100644 index 000000000000..5bfbba3af120 --- /dev/null +++ b/utils/dicom_converter/script.py @@ -0,0 +1,113 @@ +# Copyright (C) 2021 Intel Corporation +# +# SPDX-License-Identifier: MIT + + +import os +import argparse +import logging +from glob import glob + +import numpy as np +from tqdm import tqdm +from PIL import Image +from pydicom import dcmread +from pydicom.pixel_data_handlers.util import convert_color_space + + +# Script configuration +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s') +parser = argparse.ArgumentParser(description='The script is used to convert some kinds of DICOM (.dcm) files to regular image files (.png)') +parser.add_argument('input', type=str, help='A root directory with medical data files in DICOM format. The script finds all these files based on their extension') +parser.add_argument('output', type=str, help='Where to save converted files. The script repeats internal directories structure of the input root directory') +args = parser.parse_args() + + +class Converter: + def __init__(self, filename): + with dcmread(filename) as ds: + self._pixel_array = ds.pixel_array + self._photometric_interpretation = ds.PhotometricInterpretation + self._min_value = ds.pixel_array.min() + self._max_value = ds.pixel_array.max() + self._depth = ds.BitsStored + + logging.debug('File: {}'.format(filename)) + logging.debug('Photometric interpretation: {}'.format(self._photometric_interpretation)) + logging.debug('Min value: {}'.format(self._min_value)) + logging.debug('Max value: {}'.format(self._max_value)) + logging.debug('Depth: {}'.format(self._depth)) + + try: + self._length = ds["NumberOfFrames"].value + except KeyError: + self._length = 1 + + def __len__(self): + return self._length + + def __iter__(self): + if self._length == 1: + self._pixel_array = np.expand_dims(self._pixel_array, axis=0) + + for pixel_array in self._pixel_array: + # Normalization to an output range 0..255, 0..65535 + pixel_array = pixel_array - self._min_value + pixel_array = pixel_array.astype(int) * (2 ** self._depth - 1) + pixel_array = pixel_array // (self._max_value - self._min_value) + + # In some cases we need to convert colors additionally + if 'YBR' in self._photometric_interpretation: + pixel_array = convert_color_space(pixel_array, self._photometric_interpretation, 'RGB') + + if self._depth == 8: + image = Image.fromarray(pixel_array.astype(np.uint8)) + elif self._depth == 16: + image = Image.fromarray(pixel_array.astype(np.uint16)) + else: + raise Exception('Not supported depth {}'.format(self._depth)) + + yield image + + +def main(root_dir, output_root_dir): + dicom_files = glob(os.path.join(root_dir, '**', '*.dcm'), recursive = True) + if not len(dicom_files): + logging.info('DICOM files are not found under the specified path') + else: + logging.info('Number of found DICOM files: ' + str(len(dicom_files))) + + pbar = tqdm(dicom_files) + for input_filename in pbar: + pbar.set_description('Conversion: ' + input_filename) + input_basename = os.path.basename(input_filename) + + output_subpath = os.path.relpath(os.path.dirname(input_filename), root_dir) + output_path = os.path.join(output_root_dir, output_subpath) + output_basename = '{}.png'.format(os.path.splitext(input_basename)[0]) + output_filename = os.path.join(output_path, output_basename) + + if not os.path.exists(output_path): + os.makedirs(output_path) + + try: + iterated_converter = Converter(input_filename) + length = len(iterated_converter) + for i, image in enumerate(iterated_converter): + if length == 1: + image.save(output_filename) + else: + filename_index = str(i).zfill(len(str(length))) + list_output_filename = '{}_{}.png'.format(os.path.splitext(output_filename)[0], filename_index) + image.save(list_output_filename) + except Exception as ex: + logging.error('Error while processing ' + input_filename) + logging.error(ex) + +if __name__ == '__main__': + input_root_path = os.path.abspath(args.input.rstrip(os.sep)) + output_root_path = os.path.abspath(args.output.rstrip(os.sep)) + + logging.info('From: {}'.format(input_root_path)) + logging.info('To: {}'.format(output_root_path)) + main(input_root_path, output_root_path) From d9f1798684f441b5de93c456898768985786aad5 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 20 Apr 2021 13:54:44 +0200 Subject: [PATCH 12/13] add --project_id argument for create task (#3090) --- utils/cli/core/core.py | 4 ++++ utils/cli/core/definition.py | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/utils/cli/core/core.py b/utils/cli/core/core.py index 57b39d1f8601..facd17d407f1 100644 --- a/utils/cli/core/core.py +++ b/utils/cli/core/core.py @@ -66,16 +66,20 @@ def tasks_create(self, name, labels, overlap, segment_size, bug, resource_type, completion_verification_period=20, git_completion_verification_period=2, dataset_repository_url='', + project_id=None, lfs=False, **kwargs): """ Create a new task with the given name and labels JSON and add the files to it. """ url = self.api.tasks + labels = [] if project_id is not None else labels data = {'name': name, 'labels': labels, 'overlap': overlap, 'segment_size': segment_size, 'bug_tracker': bug, } + if project_id: + data.update({'project_id': project_id}) response = self.session.post(url, json=data) response.raise_for_status() response_json = response.json() diff --git a/utils/cli/core/definition.py b/utils/cli/core/definition.py index c15dbafee43f..48e11456916e 100644 --- a/utils/cli/core/definition.py +++ b/utils/cli/core/definition.py @@ -112,6 +112,12 @@ def argparse(s): type=parse_label_arg, help='string or file containing JSON labels specification' ) +task_create_parser.add_argument( + '--project', + default=None, + type=int, + help='project ID if project exists' +) task_create_parser.add_argument( '--overlap', default=0, @@ -175,6 +181,7 @@ def argparse(s): action='store_true', help='using lfs for dataset repository (default: %(default)s)' ) + ####################################################################### # Delete ####################################################################### From e45018bc43a1506631a54b968bea194f174cb7c6 Mon Sep 17 00:00:00 2001 From: Dmitry Kruchinin <33020454+dvkruchinin@users.noreply.github.com> Date: Tue, 20 Apr 2021 20:25:10 +0300 Subject: [PATCH 13/13] CI. Adding file validation by hadolint/stylelint/remark linters. (#3105) * hadolint * Hadolint config. Add some echos for help * some fix * add some dockerfiles for check * Add python script to convert jsom to html. hadolint.yml adaptation * hadolint report if level "error" exist * Revert Dockerfiles * Add stylelint checking worklow * Add remark checking for md files * Remark. Remove --silent --- .github/workflows/hadolint.yml | 49 +++++++++++++++++++++++++++++++++ .github/workflows/remark.yml | 46 +++++++++++++++++++++++++++++++ .github/workflows/stylelint.yml | 42 ++++++++++++++++++++++++++++ tests/json_to_html.py | 22 +++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 .github/workflows/hadolint.yml create mode 100644 .github/workflows/remark.yml create mode 100644 .github/workflows/stylelint.yml create mode 100644 tests/json_to_html.py diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml new file mode 100644 index 000000000000..3ba99d5a069c --- /dev/null +++ b/.github/workflows/hadolint.yml @@ -0,0 +1,49 @@ +name: Linter +on: pull_request +jobs: + HadoLint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Run checks + env: + HADOLINT: "${{ github.workspace }}/hadolint" + HADOLINT_VER: "2.1.0" + VERIFICATION_LEVEL: "error" + run: | + URL="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" + PR_FILES=$(curl -s -X GET -G $URL | jq -r '.[] | select(.status != "removed") | .filename') + for file in $PR_FILES; do + if [[ ${file} =~ 'Dockerfile' ]]; then + changed_dockerfiles+=" ${file}" + fi + done + + if [[ ! -z ${changed_dockerfiles} ]]; then + curl -sL -o ${HADOLINT} "https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VER}/hadolint-Linux-x86_64" && chmod 700 ${HADOLINT} + echo "HadoLint version: "`${HADOLINT} --version` + echo "The files will be checked: "`echo ${changed_dockerfiles}` + mkdir -p hadolint_report + + ${HADOLINT} --no-fail --format json ${changed_dockerfiles} > ./hadolint_report/hadolint_report.json + get_verification_level=`cat ./hadolint_report/hadolint_report.json | jq -r '.[] | .level'` + for line in ${get_verification_level}; do + if [[ ${line} =~ ${VERIFICATION_LEVEL} ]]; then + pip install json2html + python ./tests/json_to_html.py ./hadolint_report/hadolint_report.json + exit 1 + else + exit 0 + fi + done + else + echo "No files with the \"Dockerfile*\" name found" + fi + + - name: Upload artifacts + if: failure() + uses: actions/upload-artifact@v2 + with: + name: hadolint_report + path: hadolint_report diff --git a/.github/workflows/remark.yml b/.github/workflows/remark.yml new file mode 100644 index 000000000000..3550e5227087 --- /dev/null +++ b/.github/workflows/remark.yml @@ -0,0 +1,46 @@ +name: Linter +on: pull_request +jobs: + Remark: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 12 + + - name: Run checks + run: | + URL="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" + PR_FILES=$(curl -s -X GET -G $URL | jq -r '.[] | select(.status != "removed") | .filename') + for files in $PR_FILES; do + extension="${files##*.}" + if [[ $extension == 'md' ]]; then + changed_files_remark+=" ${files}" + fi + done + + if [[ ! -z ${changed_files_remark} ]]; then + npm ci + npm install remark-cli vfile-reporter-json + mkdir -p remark_report + + echo "Remark version: "`npx remark --version` + echo "The files will be checked: "`echo ${changed_files_remark}` + npx remark --report json --no-stdout ${changed_files_remark} 2> ./remark_report/remark_report.json + get_report=`cat ./remark_report/remark_report.json | jq -r '.[]'` + if [[ ! -z ${get_report} ]]; then + pip install json2html + python ./tests/json_to_html.py ./remark_report/remark_report.json + exit 1 + fi + else + echo "No files with the \"md\" extension found" + fi + + - name: Upload artifacts + if: failure() + uses: actions/upload-artifact@v2 + with: + name: remark_report + path: remark_report diff --git a/.github/workflows/stylelint.yml b/.github/workflows/stylelint.yml new file mode 100644 index 000000000000..76634447c915 --- /dev/null +++ b/.github/workflows/stylelint.yml @@ -0,0 +1,42 @@ +name: Linter +on: pull_request +jobs: + StyleLint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 12 + + - name: Run checks + run: | + URL="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" + PR_FILES=$(curl -s -X GET -G $URL | jq -r '.[] | select(.status != "removed") | .filename') + for files in $PR_FILES; do + extension="${files##*.}" + if [[ $extension == 'css' || $extension == 'scss' ]]; then + changed_files_stylelint+=" ${files}" + fi + done + + if [[ ! -z ${changed_files_stylelint} ]]; then + npm ci + mkdir -p stylelint_report + + echo "StyleLint version: "`npx stylelint --version` + echo "The files will be checked: "`echo ${changed_files_stylelint}` + npx stylelint --formatter json --output-file ./stylelint_report/stylelint_report.json ${changed_files_stylelint} || exit_code=`echo $?` || true + pip install json2html + python ./tests/json_to_html.py ./stylelint_report/stylelint_report.json + exit ${exit_code} + else + echo "No files with the \"css|scss\" extension found" + fi + + - name: Upload artifacts + if: failure() + uses: actions/upload-artifact@v2 + with: + name: stylelint_report + path: stylelint_report diff --git a/tests/json_to_html.py b/tests/json_to_html.py new file mode 100644 index 000000000000..901179559c14 --- /dev/null +++ b/tests/json_to_html.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +# Copyright (C) 2021 Intel Corporation +# +# SPDX-License-Identifier: MIT + +from json2html import * +import sys +import os +import json + +def json_to_html(path_to_json): + with open(path_to_json) as json_file: + data = json.load(json_file) + hadolint_html_report = json2html.convert(json = data) + + with open(os.path.splitext(path_to_json)[0] + '.html', 'w') as html_file: + html_file.write(hadolint_html_report) + + +if __name__ == '__main__': + json_to_html(sys.argv[1])