Skip to content

Commit 37ecbde

Browse files
committed
merge
2 parents 0ff3361 + 8f4313d commit 37ecbde

File tree

4 files changed

+107
-30
lines changed

4 files changed

+107
-30
lines changed

src/superannotate/lib/app/interface/sdk_interface.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from lib.app.helpers import reformat_metrics_json
3131
from lib.app.interface.types import AnnotationStatuses
3232
from lib.app.interface.types import AnnotationType
33+
from lib.app.interface.types import ImageQualityChoices
3334
from lib.app.interface.types import NotEmptyStr
3435
from lib.app.interface.types import Status
3536
from lib.app.interface.types import validate_arguments
@@ -3471,7 +3472,7 @@ def upload_images_to_project(
34713472
img_paths: List[NotEmptyStr],
34723473
annotation_status: Optional[Status] = "NotStarted",
34733474
from_s3_bucket=None,
3474-
image_quality_in_editor: Optional[NotEmptyStr] = None,
3475+
image_quality_in_editor: Optional[ImageQualityChoices] = None,
34753476
):
34763477
"""Uploads all images given in list of path objects in img_paths to the project.
34773478
Sets status of all the uploaded images to set_status if it is not None.

src/superannotate/lib/app/interface/types.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ def validate(cls, value: Union[str]) -> Union[str]:
3434
return value
3535

3636

37+
class ImageQualityChoices(StrictStr):
38+
VALID_CHOICES = ["compressed", "original"]
39+
40+
@classmethod
41+
def validate(cls, value: Union[str]) -> Union[str]:
42+
super().validate(value)
43+
if value.lower() not in cls.VALID_CHOICES:
44+
raise TypeError(
45+
f"Image quality should be on of {', '.join(cls.VALID_CHOICES)}."
46+
)
47+
return value.lower()
48+
49+
3750
class AnnotationStatuses(StrictStr):
3851
@classmethod
3952
def validate(cls, value: Union[str]) -> Union[str]:

src/superannotate/lib/core/usecases.py

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
from typing import Optional
1818

1919
import boto3
20-
import botocore.exceptions
2120
import cv2
2221
import lib.core as constances
2322
import numpy as np
2423
import pandas as pd
2524
import requests
25+
from botocore.exceptions import ClientError
2626
from lib.app.analytics.common import aggregate_annotations_as_df
2727
from lib.app.analytics.common import consensus_plot
2828
from lib.app.analytics.common import image_consensus
@@ -805,7 +805,7 @@ def __init__(
805805
):
806806
super().__init__(),
807807
self._project = project
808-
self._folder_names = folder_names
808+
self._folder_names = list(folder_names)
809809
self._backend_service = backend_service_provider
810810
self._annotation_statuses = annotation_statuses
811811
self._include_fuse = include_fuse
@@ -855,12 +855,12 @@ def execute(self):
855855
if "error" in response:
856856
raise AppException(response["error"])
857857

858-
report_message = self._project.name
858+
report_message = ""
859859
if self._folder_names:
860-
report_message = f"[{', '.join(self._folder_names)}]"
860+
report_message = f"[{', '.join(self._folder_names)}] "
861861
logger.info(
862-
f"Prepared export {response['name']} for project "
863-
f"{report_message} (project ID {self._project.uuid})."
862+
f"Prepared export {response['name']} for project {self._project.name} "
863+
f"{report_message}(project ID {self._project.uuid})."
864864
)
865865
self._response.data = response
866866

@@ -2156,14 +2156,19 @@ def __init__(
21562156
self._image_path = image_path
21572157

21582158
def execute(self):
2159-
image = io.BytesIO()
2160-
session = boto3.Session()
2161-
resource = session.resource("s3")
2162-
image_object = resource.Object(self._s3_bucket, self._image_path)
2163-
if image_object.content_length > constances.MAX_IMAGE_SIZE:
2164-
raise AppValidationException(f"File size is {image_object.content_length}")
2165-
image_object.download_fileobj(image)
2166-
self._response.data = image
2159+
try:
2160+
image = io.BytesIO()
2161+
session = boto3.Session()
2162+
resource = session.resource("s3")
2163+
image_object = resource.Object(self._s3_bucket, self._image_path)
2164+
if image_object.content_length > constances.MAX_IMAGE_SIZE:
2165+
raise AppValidationException(
2166+
f"File size is {image_object.content_length}"
2167+
)
2168+
image_object.download_fileobj(image)
2169+
self._response.data = image
2170+
except ClientError as e:
2171+
self._response.errors = str(e)
21672172
return self._response
21682173

21692174

@@ -3431,19 +3436,21 @@ def execute(self):
34313436
)
34323437
if self._project.project_type == constances.ProjectType.PIXEL.value:
34333438
mask_path = None
3434-
if os.path.exists(self._annotation_path) and not self._mask:
3435-
mask_path = self._annotation_path
3439+
png_path = self._annotation_path.replace(
3440+
"___pixel.json", "___save.png"
3441+
)
3442+
if os.path.exists(png_path) and not self._mask:
3443+
mask_path = png_path
34363444
elif self._mask:
34373445
mask_path = self._mask
34383446

34393447
if mask_path:
34403448
with open(mask_path, "rb") as descriptor:
3441-
file = io.BytesIO(descriptor.read())
34423449
bucket.put_object(
34433450
Key=response.data.images[image_data["id"]][
34443451
"annotation_bluemap_path"
34453452
],
3446-
Body=file,
3453+
Body=descriptor.read(),
34473454
)
34483455
if self._verbose:
34493456
logger.info(
@@ -4162,7 +4169,7 @@ def execute(self):
41624169
mapper_path,
41634170
os.path.join(self._download_path, "classes_mapper.json"),
41644171
)
4165-
except botocore.exceptions.ClientError:
4172+
except ClientError:
41664173
logger.info(
41674174
"The specified model does not contain a classes_mapper and/or a metrics file."
41684175
)
@@ -4853,11 +4860,20 @@ def _upload_image(self, image_path: str):
48534860
"ProcessedImage", ["uploaded", "path", "entity", "name"]
48544861
)
48554862
if self._from_s3_bucket:
4856-
image_bytes = (
4857-
GetS3ImageUseCase(s3_bucket=self._from_s3_bucket, image_path=image_path)
4858-
.execute()
4859-
.data
4860-
)
4863+
response = GetS3ImageUseCase(
4864+
s3_bucket=self._from_s3_bucket, image_path=image_path
4865+
).execute()
4866+
if response.errors:
4867+
logger.warning(
4868+
f"Unable to upload image {image_path} \n{response.errors}"
4869+
)
4870+
return ProcessedImage(
4871+
uploaded=False,
4872+
path=image_path,
4873+
entity=None,
4874+
name=Path(image_path).name,
4875+
)
4876+
image_bytes = response.data
48614877
else:
48624878
try:
48634879
image_bytes = io.BytesIO(open(image_path, "rb").read())
@@ -4934,7 +4950,6 @@ def images_to_upload(self):
49344950
images_to_upload.append(path)
49354951
else:
49364952
duplicated_paths.append(path)
4937-
49384953
self._images_to_upload = list(set(images_to_upload)), duplicated_paths
49394954
return self._images_to_upload
49404955

@@ -4979,7 +4994,8 @@ def execute(self):
49794994
duplications.extend(attach_duplications)
49804995
uploaded = [image["name"] for image in uploaded]
49814996
failed_images = [image.split("/")[-1] for image in failed_images]
4982-
4997+
if duplications:
4998+
logger.info(f"Duplicated images {', '.join(duplications)}")
49834999
self._response.data = uploaded, failed_images, duplications
49845000
return self._response
49855001

tests/integration/test_interface.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
from os.path import dirname
33
import tempfile
4-
import unittest
4+
import boto3
55

66
import src.superannotate as sa
77
from src.superannotate.lib.app.exceptions import AppException
@@ -10,17 +10,21 @@
1010

1111
class TestInterface(BaseTestCase):
1212
PROJECT_NAME = "Interface test"
13-
TEST_FOLDER_PATH = "data_set/sample_project_vector"
13+
TEST_FOLDER_PATH = "sample_project_vector"
1414
TEST_FOLDER_PATH_WITH_MULTIPLE_IMAGERS = "data_set/sample_project_vector"
1515
PROJECT_DESCRIPTION = "desc"
1616
PROJECT_TYPE = "Vector"
1717
TEST_FOLDER_NAME = "folder"
1818
EXAMPLE_IMAGE_1 = "example_image_1.jpg"
1919
EXAMPLE_IMAGE_2 = "example_image_2.jpg"
2020

21+
@property
22+
def data_set_path(self):
23+
return os.path.join(dirname(dirname(__file__)), "data_set")
24+
2125
@property
2226
def folder_path(self):
23-
return os.path.join(dirname(dirname(__file__)), self.TEST_FOLDER_PATH)
27+
return os.path.join(self.data_set_path, self.TEST_FOLDER_PATH)
2428

2529
@property
2630
def folder_path_with_multiple_images(self):
@@ -133,3 +137,46 @@ def test_overlay_fuse(self):
133137
include_overlay=True,
134138
)
135139
self.assertIsNotNone(paths)
140+
141+
def test_upload_images_to_project_image_quality_in_editor(self):
142+
self.assertRaises(
143+
AppException,
144+
sa.upload_images_to_project,
145+
self.PROJECT_NAME,
146+
[self.EXAMPLE_IMAGE_1],
147+
image_quality_in_editor='random_string'
148+
)
149+
150+
151+
class TestPixelInterface(BaseTestCase):
152+
PROJECT_NAME = "Interface test"
153+
TEST_FOLDER_PATH = "sample_project_pixel"
154+
PROJECT_DESCRIPTION = "desc"
155+
PROJECT_TYPE = "Pixel"
156+
TEST_FOLDER_NAME = "folder"
157+
EXAMPLE_IMAGE_1 = "example_image_1.jpg"
158+
159+
@property
160+
def data_set_path(self):
161+
return os.path.join(dirname(dirname(__file__)), "data_set")
162+
163+
@property
164+
def folder_path(self):
165+
return os.path.join(self.data_set_path, self.TEST_FOLDER_PATH)
166+
167+
def test_export_annotation(self):
168+
sa.upload_image_to_project(self.PROJECT_NAME, f"{self.folder_path}/{self.EXAMPLE_IMAGE_1}")
169+
sa.create_annotation_classes_from_classes_json(self.PROJECT_NAME, f"{self.folder_path}/classes/classes.json")
170+
sa.upload_image_annotations(
171+
self.PROJECT_NAME, self.EXAMPLE_IMAGE_1, f"{self.folder_path}/{self.EXAMPLE_IMAGE_1}___pixel.json"
172+
)
173+
with tempfile.TemporaryDirectory() as export_dir:
174+
result = sa.prepare_export(
175+
self.PROJECT_NAME,
176+
)
177+
sa.download_export(self.PROJECT_NAME, result, export_dir, True)
178+
with tempfile.TemporaryDirectory() as convert_path:
179+
sa.export_annotation(
180+
export_dir, convert_path, "COCO", "data_set_name", "Pixel", "panoptic_segmentation"
181+
)
182+
pass

0 commit comments

Comments
 (0)