4040from lib .app .serializers import TeamSerializer
4141from lib .core .enums import ImageQuality
4242from lib .core .exceptions import AppException
43+ from lib .core .exceptions import AppValidationException
44+ from lib .core .types import AttributeGroup
4345from lib .core .types import ClassesJson
46+ from lib .core .types import Project
4447from lib .infrastructure .controller import Controller
4548from plotly .subplots import make_subplots
4649from 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
17781860def 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
32923396def 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