Skip to content

Commit 1f59527

Browse files
Vaghinak BasentsyanVaghinak Basentsyan
authored andcommitted
Merge remote-tracking branch 'origin/develop' into sdk_limitations
# Conflicts: # src/superannotate/lib/app/interface/sdk_interface.py
2 parents deb7c9e + 9ebb3c1 commit 1f59527

File tree

8 files changed

+275
-62
lines changed

8 files changed

+275
-62
lines changed

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
minversion = 3.0
33
log_cli=true
44
python_files = test_*.py
5-
;addopts = -n 32 --dist=loadscope
5+
addopts = -n 32 --dist=loadscope

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

Lines changed: 220 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@
4040
from lib.app.serializers import TeamSerializer
4141
from lib.core.enums import ImageQuality
4242
from lib.core.exceptions import AppException
43+
from lib.core.exceptions import AppValidationException
44+
from lib.core.types import AttributeGroup
4345
from lib.core.types import ClassesJson
46+
from lib.core.types import Project
4447
from lib.infrastructure.controller import Controller
4548
from plotly.subplots import make_subplots
4649
from pydantic import EmailStr
@@ -199,14 +202,15 @@ def create_project(
199202

200203
@Trackable
201204
@validate_arguments
202-
def create_project_from_metadata(project_metadata: dict):
205+
def create_project_from_metadata(project_metadata: Project):
203206
"""Create a new project in the team using project metadata object dict.
204207
Mandatory keys in project_metadata are "name", "description" and "type" (Vector or Pixel)
205208
Non-mandatory keys: "workflow", "contributors", "settings" and "annotation_classes".
206209
207210
:return: dict object metadata the new project
208211
:rtype: dict
209212
"""
213+
project_metadata = project_metadata.dict()
210214
response = controller.create_project(
211215
name=project_metadata["name"],
212216
description=project_metadata["description"],
@@ -622,22 +626,100 @@ def upload_images_from_public_urls_to_project(
622626
:rtype: tuple of list of strs
623627
"""
624628

629+
if img_names is not None and len(img_names) != len(img_urls):
630+
raise AppException("Not all image URLs have corresponding names.")
631+
625632
project_name, folder_name = extract_project_folder(project)
626633

627-
use_case = controller.upload_images_from_public_urls_to_project(
628-
project_name=project_name,
629-
folder_name=folder_name,
630-
image_urls=img_urls,
631-
image_names=img_names,
632-
annotation_status=annotation_status,
633-
image_quality_in_editor=image_quality_in_editor,
634-
)
635-
if use_case.is_valid():
636-
with tqdm(total=len(img_urls), desc="Uploading images") as progress_bar:
637-
for _ in use_case.execute():
634+
images_to_upload = []
635+
ProcessedImage = namedtuple("ProcessedImage", ["url", "uploaded", "path", "entity"])
636+
637+
def _upload_image(image_url, image_name=None) -> ProcessedImage:
638+
download_response = controller.download_image_from_public_url(
639+
project_name=project_name, image_url=image_url
640+
)
641+
if not download_response.errors:
642+
content, content_name = download_response.data
643+
image_name = image_name if image_name else content_name
644+
duplicated_images = [
645+
image.name
646+
for image in controller.get_duplicated_images(
647+
project_name=project_name,
648+
folder_name=folder_name,
649+
images=[image_name],
650+
)
651+
]
652+
if image_name not in duplicated_images:
653+
upload_response = controller.upload_image_to_s3(
654+
project_name=project_name,
655+
image_path=image_name,
656+
image_bytes=content,
657+
folder_name=folder_name,
658+
image_quality_in_editor=image_quality_in_editor,
659+
)
660+
if upload_response.errors:
661+
logger.warning(upload_response.errors)
662+
else:
663+
return ProcessedImage(
664+
url=image_url,
665+
uploaded=True,
666+
path=image_url,
667+
entity=upload_response.data,
668+
)
669+
logger.warning(download_response.errors)
670+
return ProcessedImage(
671+
url=image_url, uploaded=False, path=image_name, entity=None
672+
)
673+
674+
logger.info("Downloading %s images", len(img_urls))
675+
with tqdm(total=len(img_urls), desc="Downloading") as progress_bar:
676+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
677+
failed_images = []
678+
if img_names:
679+
results = [
680+
executor.submit(_upload_image, url, img_urls[idx])
681+
for idx, url in enumerate(img_urls)
682+
]
683+
else:
684+
results = [executor.submit(_upload_image, url) for url in img_urls]
685+
for future in concurrent.futures.as_completed(results):
686+
processed_image = future.result()
687+
if processed_image.uploaded and processed_image.entity:
688+
images_to_upload.append(processed_image)
689+
else:
690+
failed_images.append(processed_image)
638691
progress_bar.update(1)
639-
return use_case.data
640-
raise AppException(use_case.response.errors)
692+
693+
uploaded = []
694+
duplicates = []
695+
for i in range(0, len(images_to_upload), 500):
696+
response = controller.upload_images(
697+
project_name=project_name,
698+
folder_name=folder_name,
699+
images=[
700+
image.entity for image in images_to_upload[i : i + 500] # noqa: E203
701+
],
702+
annotation_status=annotation_status,
703+
)
704+
705+
attachments, duplications = response.data
706+
uploaded.extend([attachment["name"] for attachment in attachments])
707+
duplicates.extend([duplication["name"] for duplication in duplications])
708+
uploaded_image_urls = list(
709+
{
710+
image.entity.name
711+
for image in images_to_upload
712+
if image.entity.name in uploaded
713+
}
714+
)
715+
failed_image_urls = [image.url for image in failed_images]
716+
717+
return (
718+
uploaded_image_urls,
719+
uploaded,
720+
duplicates,
721+
failed_image_urls,
722+
)
641723

642724

643725
@Trackable
@@ -1776,10 +1858,10 @@ def upload_video_to_project(
17761858
@Trackable
17771859
@validate_arguments
17781860
def create_annotation_class(
1779-
project: Union[dict, str],
1780-
name: str,
1781-
color: str,
1782-
attribute_groups: Optional[List[dict]] = None,
1861+
project: Union[Project, NotEmptyStr],
1862+
name: NotEmptyStr,
1863+
color: NotEmptyStr,
1864+
attribute_groups: Optional[List[AttributeGroup]] = None,
17831865
):
17841866
"""Create annotation class in project
17851867
@@ -1797,6 +1879,11 @@ def create_annotation_class(
17971879
:return: new class metadata
17981880
:rtype: dict
17991881
"""
1882+
if isinstance(project, Project):
1883+
project = project.dict()
1884+
attribute_groups = (
1885+
list(map(lambda x: x.dict(), attribute_groups)) if attribute_groups else None
1886+
)
18001887
response = controller.create_annotation_class(
18011888
project_name=project, name=name, color=color, attribute_groups=attribute_groups
18021889
)
@@ -1853,6 +1940,8 @@ def download_annotation_classes_json(project: str, folder: Union[str, Path]):
18531940
response = controller.download_annotation_classes(
18541941
project_name=project, download_path=folder
18551942
)
1943+
if response.errors:
1944+
raise AppException(response.errors)
18561945
return response.data
18571946

18581947

@@ -3275,20 +3364,35 @@ def upload_image_to_project(
32753364
:type image_quality_in_editor: str
32763365
"""
32773366
project_name, folder_name = extract_project_folder(project)
3278-
response = controller.upload_image_to_project(
3367+
3368+
project = controller.get_project_metadata(project_name).data
3369+
if project["project"].project_type == constances.ProjectType.VIDEO.value:
3370+
raise AppException(
3371+
"The function does not support projects containing videos attached with URLs"
3372+
)
3373+
3374+
if not isinstance(img, io.BytesIO):
3375+
if from_s3_bucket:
3376+
image_bytes = controller.get_image_from_s3(from_s3_bucket, image_name)
3377+
else:
3378+
image_bytes = io.BytesIO(open(img, "rb").read())
3379+
else:
3380+
image_bytes = img
3381+
upload_response = controller.upload_image_to_s3(
32793382
project_name=project_name,
3383+
image_path=image_name if image_name else Path(img).name,
3384+
image_bytes=image_bytes,
32803385
folder_name=folder_name,
3281-
image_name=image_name,
3282-
image=img,
3283-
annotation_status=annotation_status,
32843386
image_quality_in_editor=image_quality_in_editor,
3285-
from_s3_bucket=from_s3_bucket,
32863387
)
3287-
if response.errors:
3288-
raise AppException(response.errors)
3388+
controller.upload_images(
3389+
project_name=project_name,
3390+
folder_name=folder_name,
3391+
images=[upload_response.data], # noqa: E203
3392+
annotation_status=annotation_status,
3393+
)
32893394

32903395

3291-
@validate_arguments
32923396
def search_models(
32933397
name: Optional[NotEmptyStr] = None,
32943398
type_: Optional[NotEmptyStr] = None,
@@ -3353,31 +3457,103 @@ def upload_images_to_project(
33533457
:return: uploaded, could-not-upload, existing-images filepaths
33543458
:rtype: tuple (3 members) of list of strs
33553459
"""
3460+
uploaded_image_entities = []
3461+
failed_images = []
33563462
project_name, folder_name = extract_project_folder(project)
3463+
project = controller.get_project_metadata(project_name).data
3464+
if project["project"].project_type == constances.ProjectType.VIDEO.value:
3465+
raise AppException(
3466+
"The function does not support projects containing videos attached with URLs"
3467+
)
33573468

3358-
project_folder_name = project_name + (f"/{folder_name}" if folder_name else "")
3359-
use_case = controller.upload_images_to_project(
3360-
project_name=project_name,
3361-
folder_name=folder_name,
3362-
annotation_status=annotation_status,
3363-
image_quality_in_editor=image_quality_in_editor,
3364-
paths=img_paths,
3365-
from_s3_bucket=from_s3_bucket,
3469+
ProcessedImage = namedtuple("ProcessedImage", ["uploaded", "path", "entity"])
3470+
3471+
def _upload_local_image(image_path: str):
3472+
try:
3473+
with open(image_path, "rb") as image:
3474+
image_bytes = BytesIO(image.read())
3475+
upload_response = controller.upload_image_to_s3(
3476+
project_name=project_name,
3477+
image_path=image_path,
3478+
image_bytes=image_bytes,
3479+
folder_name=folder_name,
3480+
image_quality_in_editor=image_quality_in_editor,
3481+
)
3482+
3483+
if not upload_response.errors and upload_response.data:
3484+
entity = upload_response.data
3485+
return ProcessedImage(
3486+
uploaded=True, path=entity.path, entity=entity
3487+
)
3488+
else:
3489+
return ProcessedImage(uploaded=False, path=image_path, entity=None)
3490+
except FileNotFoundError:
3491+
return ProcessedImage(uploaded=False, path=image_path, entity=None)
3492+
3493+
def _upload_s3_image(image_path: str):
3494+
try:
3495+
image_bytes = controller.get_image_from_s3(
3496+
s3_bucket=from_s3_bucket, image_path=image_path
3497+
).data
3498+
except AppValidationException as e:
3499+
logger.warning(e)
3500+
return image_path
3501+
upload_response = controller.upload_image_to_s3(
3502+
project_name=project_name,
3503+
image_path=image_path,
3504+
image_bytes=image_bytes,
3505+
folder_name=folder_name,
3506+
image_quality_in_editor=image_quality_in_editor,
3507+
)
3508+
if not upload_response.errors and upload_response.data:
3509+
entity = upload_response.data
3510+
return ProcessedImage(uploaded=True, path=entity.path, entity=entity)
3511+
else:
3512+
return ProcessedImage(uploaded=False, path=image_path, entity=None)
3513+
3514+
filtered_paths = img_paths
3515+
duplication_counter = Counter(filtered_paths)
3516+
images_to_upload, duplicated_images = (
3517+
set(filtered_paths),
3518+
[item for item in duplication_counter if duplication_counter[item] > 1],
33663519
)
3367-
images_to_upload, duplicates = use_case.images_to_upload
3520+
upload_method = _upload_s3_image if from_s3_bucket else _upload_local_image
3521+
3522+
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
3523+
results = [
3524+
executor.submit(upload_method, image_path)
3525+
for image_path in images_to_upload
3526+
]
3527+
with tqdm(total=len(images_to_upload), desc="Uploading images") as progress_bar:
3528+
for future in concurrent.futures.as_completed(results):
3529+
processed_image = future.result()
3530+
if processed_image.uploaded and processed_image.entity:
3531+
uploaded_image_entities.append(processed_image.entity)
3532+
else:
3533+
failed_images.append(processed_image.path)
3534+
progress_bar.update(1)
3535+
uploaded = []
3536+
duplicates = []
3537+
3538+
logger.info("Uploading %s images to project.", len(images_to_upload))
3539+
3540+
for i in range(0, len(uploaded_image_entities), 500):
3541+
response = controller.upload_images(
3542+
project_name=project_name,
3543+
folder_name=folder_name,
3544+
images=uploaded_image_entities[i : i + 500], # noqa: E203
3545+
annotation_status=annotation_status,
3546+
)
3547+
attachments, duplications = response.data
3548+
uploaded.extend(attachments)
3549+
duplicates.extend(duplications)
3550+
33683551
if len(duplicates):
33693552
logger.warning(
33703553
"%s already existing images found that won't be uploaded.", len(duplicates)
33713554
)
3372-
logger.info(
3373-
"Uploading %s images to project %s.", len(images_to_upload), project_folder_name
3374-
)
3375-
if use_case.is_valid():
3376-
with tqdm(total=len(images_to_upload), desc="Uploading images") as progress_bar:
3377-
for _ in use_case.execute():
3378-
progress_bar.update(1)
3379-
return use_case.data
3380-
raise AppException(use_case.response.errors)
3555+
3556+
return uploaded, failed_images, duplicates
33813557

33823558

33833559
@Trackable

0 commit comments

Comments
 (0)