From bb7ddda8237a62014fefeb7d811ea35523e44409 Mon Sep 17 00:00:00 2001 From: Vaghinak Basentsyan <84702976+VaghinakDev@users.noreply.github.com> Date: Fri, 3 Sep 2021 12:34:39 +0400 Subject: [PATCH 01/10] Added tests trigger --- .github/workflows/python-tests.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/python-tests.yml diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 000000000..033c5d1d1 --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,30 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python application + +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.6 + uses: actions/setup-python@v2 + with: + python-version: 3.6 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest pytest-xdist pytest-parallel + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Test with pytest + run: | + pytest From 4673e31a1203ab56a8b61d1669a63b2bf5ea47e4 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 11 Oct 2021 19:47:34 +0400 Subject: [PATCH 02/10] added assinged annotator and qa columns to the aggregated df --- src/superannotate/lib/app/analytics/common.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/superannotate/lib/app/analytics/common.py b/src/superannotate/lib/app/analytics/common.py index c93b219e8..96d271a0f 100644 --- a/src/superannotate/lib/app/analytics/common.py +++ b/src/superannotate/lib/app/analytics/common.py @@ -212,6 +212,8 @@ def aggregate_annotations_as_df( "updatorRole": [], "updatorEmail": [], "folderName": [], + "imageAnnotator": [], + "imageQA": [], } if include_comments: @@ -255,6 +257,8 @@ def __get_image_metadata(image_name, annotations): image_metadata["imageWidth"] = annotations["metadata"].get("width") image_metadata["imageStatus"] = annotations["metadata"].get("status") image_metadata["imagePinned"] = annotations["metadata"].get("pinned") + image_metadata["imageAnnotator"] = annotations["metadata"].get("annotatorEmail") + image_metadata["imageQA"] = annotations["metadata"].get("qaEmail") return image_metadata def __get_user_metadata(annotation): From 0516cb06d6cb050bd0e01bacf0b3142d0cc69580 Mon Sep 17 00:00:00 2001 From: Vaghinak Basentsyan Date: Thu, 14 Oct 2021 23:07:49 +0400 Subject: [PATCH 03/10] Fixed fill class data function --- src/superannotate/lib/core/helpers.py | 116 ++++++++ src/superannotate/lib/core/usecases/images.py | 250 ++---------------- .../test_annotation_upload_pixel.py | 4 +- .../test_annotation_upload_vector.py | 57 ++++ .../integration/test_preannotation_upload.py | 36 ++- tests/unit/test_class_data_filling.py | 44 +++ 6 files changed, 268 insertions(+), 239 deletions(-) create mode 100644 tests/integration/test_annotation_upload_vector.py create mode 100644 tests/unit/test_class_data_filling.py diff --git a/src/superannotate/lib/core/helpers.py b/src/superannotate/lib/core/helpers.py index e69de29bb..53d3ba480 100644 --- a/src/superannotate/lib/core/helpers.py +++ b/src/superannotate/lib/core/helpers.py @@ -0,0 +1,116 @@ +from collections import defaultdict +from typing import List + + +def map_annotation_classes_name(annotation_classes, logger=None) -> dict: + classes_data = defaultdict(dict) + for annotation_class in annotation_classes: + class_info = {"id": annotation_class.uuid} + if annotation_class.attribute_groups: + for attribute_group in annotation_class.attribute_groups: + attribute_group_data = defaultdict(dict) + for attribute in attribute_group["attributes"]: + if logger and attribute["name"] in attribute_group_data.keys(): + logger.warning( + f"Duplicate annotation class attribute name {attribute['name']}" + f" in attribute group {attribute_group['name']}. " + "Only one of the annotation class attributes will be used. " + "This will result in errors in annotation upload." + ) + attribute_group_data[attribute["name"]] = attribute["id"] + if logger and attribute_group["name"] in class_info.keys(): + logger.warning( + f"Duplicate annotation class attribute group name {attribute_group['name']}." + " Only one of the annotation class attribute groups will be used." + " This will result in errors in annotation upload." + ) + class_info["attribute_groups"] = { + attribute_group["name"]: { + "id": attribute_group["id"], + "attributes": attribute_group_data, + } + } + if logger and annotation_class.name in classes_data.keys(): + logger.warning( + f"Duplicate annotation class name {annotation_class.name}." + f" Only one of the annotation classes will be used." + " This will result in errors in annotation upload.", + ) + classes_data[annotation_class.name] = class_info + return classes_data + + +def fill_annotation_ids(annotations: dict, annotation_classes_name_maps: dict, templates: List[dict], logger=None): + annotation_classes_name_maps = annotation_classes_name_maps + if "instances" not in annotations: + return + missing_classes = set() + missing_attribute_groups = set() + missing_attributes = set() + unknown_classes = dict() + report = { + "missing_classes": missing_classes, + "missing_attribute_groups": missing_attribute_groups, + "missing_attributes": missing_attributes, + } + for annotation in [i for i in annotations["instances"] if "className" in i]: + if "className" not in annotation: + return + annotation_class_name = annotation["className"] + if annotation_class_name not in annotation_classes_name_maps.keys(): + if annotation_class_name not in unknown_classes: + missing_classes.add(annotation_class_name) + unknown_classes[annotation_class_name] = { + "id": -(len(unknown_classes) + 1), + "attribute_groups": {}, + } + annotation_classes_name_maps.update(unknown_classes) + template_name_id_map = {template["name"]: template["id"] for template in templates} + for annotation in ( + i for i in annotations["instances"] if i.get("type", None) == "template" + ): + annotation["templateId"] = template_name_id_map.get( + annotation.get("templateName", ""), -1 + ) + + for annotation in [i for i in annotations["instances"] if "className" in i]: + annotation_class_name = annotation["className"] + if annotation_class_name not in annotation_classes_name_maps.keys(): + if logger: + logger.warning( + f"Couldn't find annotation class {annotation_class_name}" + ) + continue + annotation["classId"] = annotation_classes_name_maps[annotation_class_name]["id"] + for attribute in annotation["attributes"]: + if ( + attribute["groupName"] + not in annotation_classes_name_maps[annotation_class_name]["attribute_groups"] + ): + if logger: + logger.warning( + f"Couldn't find annotation group {attribute['groupName']}." + ) + missing_attribute_groups.add(attribute["groupName"]) + continue + attribute["groupId"] = annotation_classes_name_maps[annotation_class_name][ + "attribute_groups" + ][attribute["groupName"]]["id"] + if ( + attribute["name"] + not in annotation_classes_name_maps[annotation_class_name][ + "attribute_groups" + ][attribute["groupName"]]["attributes"] + ): + del attribute["groupId"] + if logger: + logger.warning( + f"Couldn't find annotation name {attribute['name']} in" + f" annotation group {attribute['groupName']}", + ) + missing_attributes.add(attribute["name"]) + continue + attribute["id"] = annotation_classes_name_maps[annotation_class_name][ + "attribute_groups" + ][attribute["groupName"]]["attributes"][attribute["name"]] + return report diff --git a/src/superannotate/lib/core/usecases/images.py b/src/superannotate/lib/core/usecases/images.py index 6ebfe7296..32b5d3792 100644 --- a/src/superannotate/lib/core/usecases/images.py +++ b/src/superannotate/lib/core/usecases/images.py @@ -33,6 +33,8 @@ from lib.core.exceptions import AppException from lib.core.exceptions import AppValidationException from lib.core.exceptions import ImageProcessingException +from lib.core.helpers import fill_annotation_ids +from lib.core.helpers import map_annotation_classes_name from lib.core.plugin import ImagePlugin from lib.core.plugin import VideoPlugin from lib.core.repositories import BaseManageableRepository @@ -2216,152 +2218,6 @@ def validate_project_type(self): constances.LIMITED_FUNCTIONS[self._project.project_type] ) - @property - def annotation_classes_name_map(self) -> dict: - classes_data = defaultdict(dict) - annotation_classes = self._annotation_classes.get_all() - for annotation_class in annotation_classes: - class_info = {"id": annotation_class.uuid} - if annotation_class.attribute_groups: - for attribute_group in annotation_class.attribute_groups: - attribute_group_data = defaultdict(dict) - for attribute in attribute_group["attributes"]: - if attribute["name"] in attribute_group_data.keys(): - logger.warning( - f"Duplicate annotation class attribute name {attribute['name']}" - f" in attribute group {attribute_group['name']}. " - "Only one of the annotation class attributes will be used. " - "This will result in errors in annotation upload." - ) - attribute_group_data[attribute["name"]] = attribute["id"] - if attribute_group["name"] in class_info.keys(): - logger.warning( - f"Duplicate annotation class attribute group name {attribute_group['name']}." - " Only one of the annotation class attribute groups will be used." - " This will result in errors in annotation upload." - ) - class_info["attribute_groups"] = { - attribute_group["name"]: { - "id": attribute_group["id"], - "attributes": attribute_group_data, - } - } - if annotation_class.name in classes_data.keys(): - logger.warning( - f"Duplicate annotation class name {annotation_class.name}." - f" Only one of the annotation classes will be used." - " This will result in errors in annotation upload.", - ) - classes_data[annotation_class.name] = class_info - return classes_data - - @property - def get_annotation_classes_name_to_id(self): - annotation_classes = self._annotation_classes - annotation_classes_dict = {} - for annotation_class in annotation_classes: - class_id = annotation_class["id"] - class_name = annotation_class["name"] - class_info = {"id": class_id, "attribute_groups": {}} - if "attribute_groups" in annotation_class: - for attribute_group in annotation_class["attribute_groups"]: - attribute_group_info = {} - for attribute in attribute_group["attributes"]: - if attribute["name"] in attribute_group_info: - logger.warning( - "Duplicate annotation class attribute name %s in attribute group %s. Only one of the annotation classe attributes will be used. This will result in errors in annotation upload.", - attribute["name"], - attribute_group["name"], - ) - attribute_group_info[attribute["name"]] = attribute["id"] - if attribute_group["name"] in class_info["attribute_groups"]: - logger.warning( - "Duplicate annotation class attribute group name %s. Only one of the annotation classe attribute groups will be used. This will result in errors in annotation upload.", - attribute_group["name"], - ) - class_info["attribute_groups"][attribute_group["name"]] = { - "id": attribute_group["id"], - "attributes": attribute_group_info, - } - if class_name in annotation_classes_dict: - logger.warning( - "Duplicate annotation class name %s. Only one of the annotation classes will be used. This will result in errors in annotation upload.", - class_name, - ) - annotation_classes_dict[class_name] = class_info - return annotation_classes_dict - - def get_templates_mapping(self): - templates = self._backend_service.get_templates( - team_id=self._project.team_id - ).get("data", []) - templates_map = {} - for template in templates: - templates_map[template["name"]] = template["id"] - return templates_map - - def fill_classes_data(self, annotations: dict): - annotation_classes = self.annotation_classes_name_map - if "instances" not in annotations: - return - - unknown_classes = {} - for annotation in [i for i in annotations["instances"] if "className" in i]: - if "className" not in annotation: - return - annotation_class_name = annotation["className"] - if annotation_class_name not in annotation_classes.keys(): - if annotation_class_name not in unknown_classes: - unknown_classes[annotation_class_name] = { - "id": -(len(unknown_classes) + 1), - "attribute_groups": {}, - } - if unknown_classes: - annotation_classes.update(unknown_classes) - templates = self.get_templates_mapping() - for annotation in ( - i for i in annotations["instances"] if i.get("type", None) == "template" - ): - annotation["templateId"] = templates.get( - annotation.get("templateName", ""), -1 - ) - - for annotation in [i for i in annotations["instances"] if "className" in i]: - annotation_class_name = annotation["className"] - if annotation_class_name not in annotation_classes.keys(): - logger.warning( - f"Couldn't find annotation class {annotation_class_name}" - ) - continue - annotation["classId"] = annotation_classes[annotation_class_name]["id"] - for attribute in annotation["attributes"]: - if ( - attribute["groupName"] - not in annotation_classes[annotation_class_name]["attribute_groups"] - ): - logger.warning( - f"Couldn't find annotation group {attribute['groupName']}." - ) - continue - attribute["groupId"] = annotation_classes[annotation_class_name][ - "attribute_groups" - ][attribute["groupName"]]["id"] - if ( - attribute["name"] - not in annotation_classes[annotation_class_name][ - "attribute_groups" - ][attribute["groupName"]]["attributes"] - ): - del attribute["groupId"] - logger.warning( - f"Couldn't find annotation name {attribute['name']} in" - f" annotation group {attribute['groupName']}", - ) - continue - attribute["id"] = annotation_classes[annotation_class_name][ - "attribute_groups" - ][attribute["groupName"]]["attributes"][attribute["name"]] - def execute(self): if self.is_valid(): image_data = self._backend_service.get_bulk_images( @@ -2388,7 +2244,12 @@ def execute(self): ) resource = session.resource("s3") bucket = resource.Bucket(response.data.bucket) - self.fill_classes_data(self._annotations) + fill_annotation_ids( + annotations=self._annotations, + annotation_classes_name_maps=map_annotation_classes_name(self._annotation_classes.get_all()), + templates=self._backend_service.get_templates(self._project.team_id).get("data", []), + logger=logger + ) bucket.put_object( Key=response.data.images[image_data["id"]]["annotation_json_path"], Body=json.dumps(self._annotations), @@ -2513,26 +2374,6 @@ def __init__( def s3_client(self): return boto3.client("s3") - @property - def annotation_classes_name_map(self) -> dict: - classes_data = defaultdict(dict) - annotation_classes = self._annotation_classes - for annotation_class in annotation_classes: - class_info = {"id": annotation_class.uuid} - if annotation_class.attribute_groups: - for attribute_group in annotation_class.attribute_groups: - attribute_group_data = defaultdict(dict) - for attribute in attribute_group["attributes"]: - attribute_group_data[attribute["name"]] = attribute["id"] - class_info["attribute_groups"] = { - attribute_group["name"]: { - "id": attribute_group["id"], - "attributes": attribute_group_data, - } - } - classes_data[annotation_class.name] = class_info - return classes_data - @property def annotation_postfix(self): return ( @@ -2541,72 +2382,6 @@ def annotation_postfix(self): else constances.PIXEL_ANNOTATION_POSTFIX ) - def get_templates_mapping(self): - templates_map = {} - for template in self._templates: - templates_map[template["name"]] = template["id"] - return templates_map - - def fill_classes_data(self, annotations: dict): - annotation_classes = self.annotation_classes_name_map - if "instances" not in annotations: - return - unknown_classes = {} - for annotation in [i for i in annotations["instances"] if "className" in i]: - if "className" not in annotation: - return - annotation_class_name = annotation["className"] - if annotation_class_name not in annotation_classes: - if annotation_class_name not in unknown_classes: - self.missing_classes.add(annotation_class_name) - unknown_classes[annotation_class_name] = { - "id": -(len(unknown_classes) + 1), - "attribute_groups": {}, - } - annotation_classes.update(unknown_classes) - templates = self.get_templates_mapping() - for annotation in ( - i for i in annotations["instances"] if i.get("type", None) == "template" - ): - annotation["templateId"] = templates.get( - annotation.get("templateName", ""), -1 - ) - - for annotation in [i for i in annotations["instances"] if "className" in i]: - annotation_class_name = annotation["className"] - if annotation_class_name not in annotation_classes: - continue - annotation["classId"] = annotation_classes[annotation_class_name]["id"] - for attribute in annotation["attributes"]: - if annotation_classes[annotation_class_name].get("attribute_groups"): - if ( - attribute["groupName"] - not in annotation_classes[annotation_class_name][ - "attribute_groups" - ] - ): - continue - else: - self.missing_attribute_groups.add(attribute["groupName"]) - continue - - attribute["groupId"] = annotation_classes[annotation_class_name][ - "attribute_groups" - ][attribute["groupName"]]["id"] - - if ( - attribute["name"] - not in annotation_classes[annotation_class_name][ - "attribute_groups" - ][attribute["groupName"]]["attributes"] - ): - del attribute["groupId"] - self.missing_attributes.add(attribute["name"]) - continue - attribute["id"] = annotation_classes[annotation_class_name][ - "attribute_groups" - ][attribute["groupName"]]["attributes"] - @property def annotations_to_upload(self): if not self._annotations_to_upload: @@ -2761,7 +2536,14 @@ def upload_to_s3( annotation_json = json.load(file) else: annotation_json = json.load(open(image_id_name_map[image_id].path)) - self.fill_classes_data(annotation_json) + report = fill_annotation_ids( + annotations=annotation_json, + annotation_classes_name_maps=map_annotation_classes_name(self._annotation_classes), + templates=self._templates + ) + self.missing_classes.update(report["missing_classes"]) + self.missing_attribute_groups.update(report["missing_attribute_groups"]) + self.missing_attributes.update(report["missing_attributes"]) if not self._is_valid_json(annotation_json): logger.warning(f"Invalid json {image_id_name_map[image_id].path}") return image_id_name_map[image_id], False diff --git a/tests/integration/test_annotation_upload_pixel.py b/tests/integration/test_annotation_upload_pixel.py index fda574756..92809324b 100644 --- a/tests/integration/test_annotation_upload_pixel.py +++ b/tests/integration/test_annotation_upload_pixel.py @@ -22,7 +22,7 @@ def test_recursive_annotation_upload_pixel(self): sa.upload_images_from_folder_to_project( self.PROJECT_NAME, self.folder_path, recursive_subfolders=False ) - uploaded_annotations,_,_ = sa.upload_annotations_from_folder_to_project(self.PROJECT_NAME, self.S3_FOLDER_PATH, + uploaded_annotations, _, _ = sa.upload_annotations_from_folder_to_project(self.PROJECT_NAME, self.S3_FOLDER_PATH, from_s3_bucket="superannotate-python-sdk-test", recursive_subfolders=False) - self.assertEqual(len(uploaded_annotations), 3) \ No newline at end of file + self.assertEqual(len(uploaded_annotations), 3) diff --git a/tests/integration/test_annotation_upload_vector.py b/tests/integration/test_annotation_upload_vector.py new file mode 100644 index 000000000..538aa5139 --- /dev/null +++ b/tests/integration/test_annotation_upload_vector.py @@ -0,0 +1,57 @@ +import tempfile +from os.path import dirname +from os.path import join +import json +import pytest + +import src.superannotate as sa +from tests.integration.base import BaseTestCase + + +class TestAnnotationUploadVector(BaseTestCase): + PROJECT_NAME = "TestAnnotationUploadVector" + PROJECT_DESCRIPTION = "Desc" + PROJECT_TYPE = "Vector" + S3_FOLDER_PATH = "sample_project_pixel" + TEST_FOLDER_PATH = "data_set/sample_project_vector" + IMAGE_NAME = "example_image_1.jpg" + + @property + def folder_path(self): + return join(dirname(dirname(__file__)), self.TEST_FOLDER_PATH) + + @pytest.mark.flaky(reruns=2) + def test_annotation_upload(self): + annotation_path = join(self.folder_path, f"{self.IMAGE_NAME}___objects.json") + sa.upload_image_to_project(self.PROJECT_NAME, join(self.folder_path, self.IMAGE_NAME)) + sa.upload_image_annotations(self.PROJECT_NAME, self.IMAGE_NAME, annotation_path) + with tempfile.TemporaryDirectory() as tmp_dir: + sa.download_image_annotations(self.PROJECT_NAME, self.IMAGE_NAME, tmp_dir) + origin_annotation = json.load(open(annotation_path)) + annotation = json.load(open(join(tmp_dir, f"{self.IMAGE_NAME}___objects.json"))) + self.assertEqual( + [i["attributes"]for i in annotation["instances"]], + [i["attributes"]for i in origin_annotation["instances"]] + ) + + def test_pre_annotation_folder_upload_download(self): + sa.upload_images_from_folder_to_project( + self.PROJECT_NAME, self.folder_path, annotation_status="InProgress" + ) + sa.create_annotation_classes_from_classes_json( + self.PROJECT_NAME, f"{self.folder_path}/classes/classes.json" + ) + _, _, _ = sa.upload_preannotations_from_folder_to_project( + self.PROJECT_NAME, self.folder_path + ) + images = sa.search_images(self.PROJECT_NAME) + with tempfile.TemporaryDirectory() as tmp_dir: + for image_name in images: + annotation_path = join(self.folder_path, f"{image_name}___objects.json") + sa.download_image_preannotations(self.PROJECT_NAME, image_name, tmp_dir) + origin_annotation = json.load(open(annotation_path)) + annotation = json.load(open(join(tmp_dir, f"{image_name}___objects.json"))) + self.assertEqual( + len([i["attributes"] for i in annotation["instances"]]), + len([i["attributes"] for i in origin_annotation["instances"]]) + ) \ No newline at end of file diff --git a/tests/integration/test_preannotation_upload.py b/tests/integration/test_preannotation_upload.py index ebfb66230..09624ec0b 100644 --- a/tests/integration/test_preannotation_upload.py +++ b/tests/integration/test_preannotation_upload.py @@ -1,6 +1,5 @@ import os import tempfile -import time from os.path import dirname from pathlib import Path @@ -25,7 +24,7 @@ def test_pre_annotation_folder_upload_download(self): sa.create_annotation_classes_from_classes_json( self.PROJECT_NAME, f"{self.folder_path}/classes/classes.json" ) - sa.upload_preannotations_from_folder_to_project( + _, _, _ = sa.upload_preannotations_from_folder_to_project( self.PROJECT_NAME, self.folder_path ) count_in = len(list(Path(self.folder_path).glob("*.json"))) @@ -36,4 +35,35 @@ def test_pre_annotation_folder_upload_download(self): count_out = len(list(Path(tmp_dir).glob("*.json"))) - self.assertEqual(count_in, count_out) \ No newline at end of file + self.assertEqual(count_in, count_out) + + +class TestVectorAnnotationImage(BaseTestCase): + PROJECT_NAME = "TestVectorAnnotationImage" + PROJECT_DESCRIPTION = "Example Project test vector pre-annotation upload" + PROJECT_TYPE = "Vector" + TEST_FOLDER_PATH = "data_set/sample_project_vector" + + @property + def folder_path(self): + return os.path.join(dirname(dirname(__file__)), self.TEST_FOLDER_PATH) + + def test_pre_annotation_folder_upload_download(self): + sa.upload_images_from_folder_to_project( + self.PROJECT_NAME, self.folder_path, annotation_status="InProgress" + ) + sa.create_annotation_classes_from_classes_json( + self.PROJECT_NAME, f"{self.folder_path}/classes/classes.json" + ) + _, _, _ = sa.upload_annotations_from_folder_to_project( + self.PROJECT_NAME, self.folder_path + ) + count_in = len(list(Path(self.folder_path).glob("*.json"))) + images = sa.search_images(self.PROJECT_NAME) + with tempfile.TemporaryDirectory() as tmp_dir: + for image_name in images: + sa.download_image_annotations(self.PROJECT_NAME, image_name, tmp_dir) + + count_out = len(list(Path(tmp_dir).glob("*.json"))) + + self.assertEqual(count_in, count_out) diff --git a/tests/unit/test_class_data_filling.py b/tests/unit/test_class_data_filling.py new file mode 100644 index 000000000..e8db1e748 --- /dev/null +++ b/tests/unit/test_class_data_filling.py @@ -0,0 +1,44 @@ +from unittest import TestCase + +from src.superannotate.lib.core.helpers import fill_annotation_ids + + +TEST_ANNOTATION = { + "metadata": { + "name": "example_image_1.jpg", + "width": 1024, + "height": 683, + }, + "instances": [ + { + "type": "bbox", + "probability": 100, + "points": { + "x1": 437.16, + "x2": 465.23, + "y1": 341.5, + "y2": 357.09 + }, + "pointLabels": {}, + "attributes": [ + { + "name": "2", + "groupName": "Num doors" + } + ], + "className": "Personal vehicle" + } + ] +} + + +class TestClassData(TestCase): + + def test_map_annotation_classes_name(self): + annotation_classes_name_maps = { + "Personal vehicle": {"id": 72274, "attribute_groups": {"Num doors": {"id": 28230, "attributes": {"2": 117845}}}}} + fill_annotation_ids(TEST_ANNOTATION, annotation_classes_name_maps, []) + attribute = TEST_ANNOTATION["instances"][0]["attributes"][0] + self.assertEqual(TEST_ANNOTATION["instances"][0]["classId"], 72274) + self.assertEqual(attribute["id"], 117845) + self.assertEqual(attribute["groupId"], 28230) From 3bd03e44a0e3794a275d418335976bdb47e654d3 Mon Sep 17 00:00:00 2001 From: shab Date: Fri, 15 Oct 2021 10:53:59 +0400 Subject: [PATCH 04/10] Add test for attributes --- src/superannotate/lib/app/interface/types.py | 2 +- .../test_annotation_upload_pixel.py | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/superannotate/lib/app/interface/types.py b/src/superannotate/lib/app/interface/types.py index bd68684ab..ae48eff51 100644 --- a/src/superannotate/lib/app/interface/types.py +++ b/src/superannotate/lib/app/interface/types.py @@ -80,6 +80,6 @@ def wrapped(*args, **kwargs): field, " " * (48 - len(field)), f"\n {' ' * 48}".join(text) ) ) - raise Exception("\n".join(texts)) + raise Exception("\n".join(texts)) from None return wrapped diff --git a/tests/integration/test_annotation_upload_pixel.py b/tests/integration/test_annotation_upload_pixel.py index 92809324b..1dfa21858 100644 --- a/tests/integration/test_annotation_upload_pixel.py +++ b/tests/integration/test_annotation_upload_pixel.py @@ -1,10 +1,11 @@ import os from os.path import dirname import pytest - import src.superannotate as sa from tests.integration.base import BaseTestCase - +import tempfile +import json +from os.path import join class TestRecursiveFolderPixel(BaseTestCase): PROJECT_NAME = "test_recursive_pixel" @@ -12,6 +13,7 @@ class TestRecursiveFolderPixel(BaseTestCase): PROJECT_TYPE = "Pixel" S3_FOLDER_PATH = "sample_project_pixel" TEST_FOLDER_PATH = "data_set/sample_project_pixel" + IMAGE_NAME = "example_image_1.jpg" @property def folder_path(self): @@ -26,3 +28,16 @@ def test_recursive_annotation_upload_pixel(self): from_s3_bucket="superannotate-python-sdk-test", recursive_subfolders=False) self.assertEqual(len(uploaded_annotations), 3) + + @pytest.mark.flaky(reruns=2) + def test_annotation_upload_pixel(self): + sa.upload_images_from_folder_to_project(self.PROJECT_NAME, self.folder_path) + sa.upload_annotations_from_folder_to_project(self.PROJECT_NAME, self.folder_path) + with tempfile.TemporaryDirectory() as tmp_dir: + sa.download_image_annotations(self.PROJECT_NAME, self.IMAGE_NAME, tmp_dir) + origin_annotation = json.load(open(f"{self.folder_path}/{self.IMAGE_NAME}___pixel.json")) + annotation = json.load(open(join(tmp_dir, f"{self.IMAGE_NAME}___pixel.json"))) + self.assertEqual( + [i["attributes"] for i in annotation["instances"]], + [i["attributes"] for i in origin_annotation["instances"]] + ) From 0b0174631b1857a83198719ae62abf27a99abba8 Mon Sep 17 00:00:00 2001 From: Vaghinak Basentsyan Date: Fri, 15 Oct 2021 14:47:47 +0400 Subject: [PATCH 05/10] Fixed create_folder --- .../lib/app/interface/sdk_interface.py | 4 --- .../lib/core/usecases/folders.py | 6 +++++ tests/integration/test_interface.py | 27 +++++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/superannotate/lib/app/interface/sdk_interface.py b/src/superannotate/lib/app/interface/sdk_interface.py index c83c37b44..b4fe39b3f 100644 --- a/src/superannotate/lib/app/interface/sdk_interface.py +++ b/src/superannotate/lib/app/interface/sdk_interface.py @@ -333,10 +333,6 @@ def create_folder(project: NotEmptyStr, folder_name: NotEmptyStr): res = controller.create_folder(project=project, folder_name=folder_name) if res.data: folder = res.data - if folder and folder.name != folder_name: - logger.warning( - f"Created folder has name {folder.name}, since folder with name {folder_name} already existed.", - ) logger.info(f"Folder {folder.name} created in project {project}") return folder.to_dict() if res.errors: diff --git a/src/superannotate/lib/core/usecases/folders.py b/src/superannotate/lib/core/usecases/folders.py index e1d4e0722..b5c2f2d60 100644 --- a/src/superannotate/lib/core/usecases/folders.py +++ b/src/superannotate/lib/core/usecases/folders.py @@ -27,6 +27,7 @@ def __init__( self._project = project self._folder = folder self._folders = folders + self._origin_name = folder.name def validate_folder(self): if not self._folder.name: @@ -53,6 +54,11 @@ def execute(self): if self.is_valid(): self._folder.project_id = self._project.uuid self._response.data = self._folders.insert(self._folder) + if self._response.data.name not in (self._origin_name, self._folder.name): + logger.warning( + f"Created folder has name {self._response.data.name}," + f" since folder with name {self._folder.name} already existed." + ) return self._response diff --git a/tests/integration/test_interface.py b/tests/integration/test_interface.py index 4e0851f97..f199f4a5c 100644 --- a/tests/integration/test_interface.py +++ b/tests/integration/test_interface.py @@ -3,6 +3,7 @@ import tempfile import pytest + import src.superannotate as sa from src.superannotate.lib.app.exceptions import AppException from tests.integration.base import BaseTestCase @@ -224,3 +225,29 @@ def test_export_annotation(self): ) pass + def test_create_folder_with_special_character(self): + with self.assertLogs() as logs: + folder_1 = sa.create_folder(self.PROJECT_NAME, "**abc") + folder_2 = sa.create_folder(self.PROJECT_NAME, "**abc") + self.assertEqual(folder_1["name"], "__abc") + self.assertEqual(folder_2["name"], "__abc (1)") + self.assertIn( + 'New folder name has special characters. Special characters will be replaced by underscores.', + logs.output[0] + ) + self.assertIn( + 'Folder __abc created in project Interface Pixel test', + logs.output[1] + ) + self.assertIn( + 'New folder name has special characters. Special characters will be replaced by underscores.', + logs.output[2] + ) + self.assertIn( + 'Created folder has name __abc (1), since folder with name __abc already existed.', + logs.output[3] + ) + self.assertIn( + 'Folder __abc (1) created in project Interface Pixel test', + logs.output[4] + ) From ffac18a70fefb2e7a83194c7cf9f8bec1992a840 Mon Sep 17 00:00:00 2001 From: Vaghinak Basentsyan Date: Mon, 18 Oct 2021 18:28:13 +0400 Subject: [PATCH 06/10] Added type AnnotationStatuses --- .../lib/app/interface/sdk_interface.py | 19 +++++++++---------- src/superannotate/lib/app/interface/types.py | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/superannotate/lib/app/interface/sdk_interface.py b/src/superannotate/lib/app/interface/sdk_interface.py index c83c37b44..739196514 100644 --- a/src/superannotate/lib/app/interface/sdk_interface.py +++ b/src/superannotate/lib/app/interface/sdk_interface.py @@ -30,7 +30,6 @@ from lib.app.interface.types import AnnotationType from lib.app.interface.types import ImageQualityChoices from lib.app.interface.types import NotEmptyStr -from lib.app.interface.types import Status from lib.app.interface.types import validate_arguments from lib.app.mixp.decorators import Trackable from lib.app.serializers import BaseSerializers @@ -280,7 +279,7 @@ def clone_project( def search_images( project: Union[NotEmptyStr, dict], image_name_prefix: Optional[NotEmptyStr] = None, - annotation_status: Optional[Status] = None, + annotation_status: Optional[AnnotationStatuses] = None, return_metadata: Optional[StrictBool] = False, ): """Search images by name_prefix (case-insensitive) and annotation status @@ -603,7 +602,7 @@ def upload_images_from_public_urls_to_project( project: Union[NotEmptyStr, dict], img_urls: List[NotEmptyStr], img_names: Optional[List[NotEmptyStr]] = None, - annotation_status: Optional[Status] = "NotStarted", + annotation_status: Optional[AnnotationStatuses] = "NotStarted", image_quality_in_editor: Optional[NotEmptyStr] = None, ): """Uploads all images given in the list of URL strings in img_urls to the project. @@ -1641,7 +1640,7 @@ def upload_videos_from_folder_to_project( target_fps: Optional[int] = None, start_time: Optional[float] = 0.0, end_time: Optional[float] = None, - annotation_status: Optional[Status] = "NotStarted", + annotation_status: Optional[AnnotationStatuses] = "NotStarted", image_quality_in_editor: Optional[str] = None, ): """Uploads image frames from all videos with given extensions from folder_path to the project. @@ -1778,7 +1777,7 @@ def upload_video_to_project( target_fps: Optional[int] = None, start_time: Optional[float] = 0.0, end_time: Optional[float] = None, - annotation_status: Optional[Status] = "NotStarted", + annotation_status: Optional[AnnotationStatuses] = "NotStarted", image_quality_in_editor: Optional[NotEmptyStr] = None, ): """Uploads image frames from video to platform. Uploaded images will have @@ -2235,7 +2234,7 @@ def download_image( def attach_image_urls_to_project( project: Union[NotEmptyStr, dict], attachments: Union[str, Path], - annotation_status: Optional[Status] = "NotStarted", + annotation_status: Optional[AnnotationStatuses] = "NotStarted", ): """Link images on external storage to SuperAnnotate. @@ -2298,7 +2297,7 @@ def attach_image_urls_to_project( def attach_video_urls_to_project( project: Union[NotEmptyStr, dict], attachments: Union[str, Path], - annotation_status: Optional[Status] = "NotStarted", + annotation_status: Optional[AnnotationStatuses] = "NotStarted", ): """Link videos on external storage to SuperAnnotate. @@ -3314,7 +3313,7 @@ def upload_image_to_project( project: NotEmptyStr, img, image_name: Optional[NotEmptyStr] = None, - annotation_status: Optional[Status] = "NotStarted", + annotation_status: Optional[AnnotationStatuses] = "NotStarted", from_s3_bucket=None, image_quality_in_editor: Optional[NotEmptyStr] = None, ): @@ -3389,7 +3388,7 @@ def search_models( def upload_images_to_project( project: NotEmptyStr, img_paths: List[NotEmptyStr], - annotation_status: Optional[Status] = "NotStarted", + annotation_status: Optional[AnnotationStatuses] = "NotStarted", from_s3_bucket=None, image_quality_in_editor: Optional[ImageQualityChoices] = None, ): @@ -3523,7 +3522,7 @@ def delete_annotations( def attach_document_urls_to_project( project: Union[NotEmptyStr, dict], attachments: Union[Path, NotEmptyStr], - annotation_status: Optional[Status] = "NotStarted", + annotation_status: Optional[AnnotationStatuses] = "NotStarted", ): """Link documents on external storage to SuperAnnotate. diff --git a/src/superannotate/lib/app/interface/types.py b/src/superannotate/lib/app/interface/types.py index ae48eff51..a6cbfb258 100644 --- a/src/superannotate/lib/app/interface/types.py +++ b/src/superannotate/lib/app/interface/types.py @@ -17,7 +17,7 @@ def validate(cls, value: Union[str]) -> Union[str]: if cls.curtail_length and len(value) > cls.curtail_length: value = value[: cls.curtail_length] if value.lower() not in AnnotationStatus.values(): - raise TypeError(f"Available statuses is {', '.join(AnnotationStatus)}. ") + raise TypeError(f"Available statuses is {', '.join(AnnotationStatus.titles())}. ") return value From 2d0ec47a1fa72dad91164ef489351f541c78490c Mon Sep 17 00:00:00 2001 From: shab Date: Tue, 19 Oct 2021 15:24:04 +0400 Subject: [PATCH 07/10] Fix excluded extns log --- src/superannotate/lib/app/interface/sdk_interface.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/superannotate/lib/app/interface/sdk_interface.py b/src/superannotate/lib/app/interface/sdk_interface.py index 739196514..7c2eb2505 100644 --- a/src/superannotate/lib/app/interface/sdk_interface.py +++ b/src/superannotate/lib/app/interface/sdk_interface.py @@ -1704,11 +1704,7 @@ def upload_videos_from_folder_to_project( project_folder_name = project_name + (f"/{folder_name}" if folder_name else "") logger.info( - "Uploading all videos with extensions %s from %s to project %s. Excluded file patterns are: %s.", - extensions, - str(folder_path), - project_name, - exclude_file_patterns, + f"Uploading all videos with extensions {extensions} from {str(folder_path)} to project {project_name}. Excluded file patterns are: {[*exclude_file_patterns]}.", ) uploaded_paths = [] for path in video_paths: From 40fdbf353c3319ab6c5a1d26fe44c2dfa83951ef Mon Sep 17 00:00:00 2001 From: Vaghinak Basentsyan Date: Tue, 19 Oct 2021 15:48:27 +0400 Subject: [PATCH 08/10] Fix upload from public url. --- .github/workflows/python-tests.yml | 30 ------------------- src/superannotate/lib/core/usecases/images.py | 4 ++- .../lib/infrastructure/controller.py | 2 +- 3 files changed, 4 insertions(+), 32 deletions(-) delete mode 100644 .github/workflows/python-tests.yml diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml deleted file mode 100644 index 033c5d1d1..000000000 --- a/.github/workflows/python-tests.yml +++ /dev/null @@ -1,30 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Python application - -on: - push: - branches: [ develop ] - pull_request: - branches: [ develop ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.6 - uses: actions/setup-python@v2 - with: - python-version: 3.6 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest pytest-xdist pytest-parallel - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Test with pytest - run: | - pytest diff --git a/src/superannotate/lib/core/usecases/images.py b/src/superannotate/lib/core/usecases/images.py index 6ebfe7296..c31f902d9 100644 --- a/src/superannotate/lib/core/usecases/images.py +++ b/src/superannotate/lib/core/usecases/images.py @@ -1579,7 +1579,7 @@ def __init__( project: ProjectEntity, folder: FolderEntity, backend_service: SuerannotateServiceProvider, - settings: BaseManageableRepository, + settings: List[ProjectSettingEntity], s3_repo, image_urls: List[str], image_names: List[str] = None, @@ -1598,6 +1598,7 @@ def __init__( self._settings = settings self._auth_data = None + @property def auth_data(self): if not self._auth_data: @@ -1743,6 +1744,7 @@ def execute(self): image.entity for image in images_to_upload[i : i + 100] ], annotation_status=self._annotation_status, + upload_state_code=constances.UploadState.BASIC.value ).execute() if response.errors: continue diff --git a/src/superannotate/lib/infrastructure/controller.py b/src/superannotate/lib/infrastructure/controller.py index 0337d592e..9fe010287 100644 --- a/src/superannotate/lib/infrastructure/controller.py +++ b/src/superannotate/lib/infrastructure/controller.py @@ -438,7 +438,7 @@ def upload_images_from_public_urls_to_project( backend_service=self._backend_client, settings=ProjectSettingsRepository( service=self._backend_client, project=project - ), + ).get_all(), s3_repo=self.s3_repo, image_quality_in_editor=image_quality_in_editor, annotation_status=annotation_status, From 7b9da480ebe81d094c4d8a3d6f0f85e23ed29aac Mon Sep 17 00:00:00 2001 From: Vaghinak Basentsyan Date: Wed, 20 Oct 2021 12:36:10 +0400 Subject: [PATCH 09/10] Removed traceback limit --- src/superannotate/__init__.py | 1 - src/superannotate/lib/app/interface/types.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/superannotate/__init__.py b/src/superannotate/__init__.py index 996105ac5..5dc58156a 100644 --- a/src/superannotate/__init__.py +++ b/src/superannotate/__init__.py @@ -302,4 +302,3 @@ logging.config.fileConfig( os.path.join(WORKING_DIR, "logging.conf"), disable_existing_loggers=False ) -sys.tracebacklimit = 0 diff --git a/src/superannotate/lib/app/interface/types.py b/src/superannotate/lib/app/interface/types.py index a6cbfb258..2ea6366df 100644 --- a/src/superannotate/lib/app/interface/types.py +++ b/src/superannotate/lib/app/interface/types.py @@ -81,5 +81,4 @@ def wrapped(*args, **kwargs): ) ) raise Exception("\n".join(texts)) from None - return wrapped From 502225d6166da0871e1f1d1b89e74936e1869a01 Mon Sep 17 00:00:00 2001 From: Shabin Dilanchian Date: Wed, 20 Oct 2021 12:37:22 +0400 Subject: [PATCH 10/10] Update version.py --- src/superannotate/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/superannotate/version.py b/src/superannotate/version.py index aef46acb4..ed045069b 100644 --- a/src/superannotate/version.py +++ b/src/superannotate/version.py @@ -1 +1 @@ -__version__ = "4.2.1" +__version__ = "4.2.2b1"