Skip to content

Commit 3421a15

Browse files
committed
Fix cli
1 parent 15aa09d commit 3421a15

File tree

6 files changed

+80
-160
lines changed

6 files changed

+80
-160
lines changed

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

Lines changed: 44 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import concurrent.futures
21
import json
32
import logging
43
import os
54
import sys
65
import tempfile
76
import uuid
8-
from collections import Counter
9-
from collections import namedtuple
10-
from io import BytesIO
11-
from pathlib import Path
127
from typing import Any
138
from typing import Optional
149

@@ -19,12 +14,21 @@
1914
from lib.app.helpers import split_project_path
2015
from lib.app.input_converters.conversion import import_annotation
2116
from lib.app.interface.base_interface import BaseInterfaceFacade
17+
from lib.app.interface.sdk_interface import attach_image_urls_to_project
18+
from lib.app.interface.sdk_interface import attach_video_urls_to_project
19+
from lib.app.interface.sdk_interface import create_folder
20+
from lib.app.interface.sdk_interface import create_project
21+
from lib.app.interface.sdk_interface import upload_images_from_folder_to_project
22+
from lib.app.interface.sdk_interface import upload_videos_from_folder_to_project
2223
from lib.app.serializers import ImageSerializer
2324
from lib.core.entities import ConfigEntity
25+
from lib.infrastructure.controller import Controller
2426
from lib.infrastructure.repositories import ConfigRepository
2527
from tqdm import tqdm
2628

29+
2730
logger = logging.getLogger()
31+
controller = Controller(logger)
2832

2933

3034
class CLIFacade(BaseInterfaceFacade):
@@ -72,18 +76,14 @@ def create_project(self, name: str, description: str, type: str):
7276
"""
7377
To create a new project
7478
"""
75-
response = self.controller.create_project(name, description, type)
76-
if response.errors:
77-
return response.errors
78-
return response.data
79+
create_project(name, description, type)
80+
sys.exit(0)
7981

8082
def create_folder(self, project: str, name: str):
8183
"""
8284
To create a new folder
8385
"""
84-
response = self.controller.create_folder(project=project, folder_name=name)
85-
if response.errors:
86-
logger.critical(response.errors)
86+
create_folder(project, name)
8787
sys.exit(0)
8888

8989
def upload_images(
@@ -104,79 +104,16 @@ def upload_images(
104104
Optional argument extensions accepts comma separated list of image extensions to look for.
105105
If the argument is not given then value jpg,jpeg,png,tif,tiff,webp,bmp is assumed.
106106
"""
107-
uploaded_image_entities = []
108-
failed_images = []
109-
project_name, folder_name = split_project_path(project)
110-
ProcessedImage = namedtuple("ProcessedImage", ["uploaded", "path", "entity"])
111-
112-
def upload_image(image_path: str):
113-
with open(image_path, "rb") as image:
114-
image_bytes = BytesIO(image.read())
115-
upload_response = self.controller.upload_image_to_s3(
116-
project_name=project_name,
117-
image_path=image_path,
118-
image_bytes=image_bytes,
119-
folder_name=folder_name,
120-
image_quality_in_editor=image_quality_in_editor,
121-
)
122-
123-
if not upload_response.errors and upload_response.data:
124-
entity = upload_response.data
125-
return ProcessedImage(
126-
uploaded=True, path=entity.path, entity=entity
127-
)
128-
else:
129-
return ProcessedImage(uploaded=False, path=image_path, entity=None)
130-
131-
paths = []
132-
133-
if isinstance(extensions, str):
134-
extensions = extensions.strip().split(",")
135-
136-
for extension in extensions:
137-
if recursive_subfolders:
138-
paths += list(Path(folder).rglob(f"*.{extension.lower()}"))
139-
if os.name != "nt":
140-
paths += list(Path(folder).rglob(f"*.{extension.upper()}"))
141-
else:
142-
paths += list(Path(folder).glob(f"*.{extension.lower()}"))
143-
if os.name != "nt":
144-
paths += list(Path(folder).glob(f"*.{extension.upper()}"))
145-
146-
filtered_paths = []
147-
for path in paths:
148-
not_in_exclude_list = [
149-
x not in Path(path).name for x in exclude_file_patterns
150-
]
151-
if all(not_in_exclude_list):
152-
filtered_paths.append(path)
153-
154-
duplication_counter = Counter(filtered_paths)
155-
images_to_upload, duplicated_images = (
156-
set(filtered_paths),
157-
[item for item in duplication_counter if duplication_counter[item] > 1],
107+
upload_images_from_folder_to_project(
108+
project,
109+
folder_path=folder,
110+
extensions=extensions,
111+
annotation_status=set_annotation_status,
112+
from_s3_bucket=None,
113+
exclude_file_patterns=exclude_file_patterns,
114+
recursive_subfolders=recursive_subfolders,
115+
image_quality_in_editor=image_quality_in_editor,
158116
)
159-
with tqdm(total=len(images_to_upload)) as progress_bar:
160-
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
161-
results = [
162-
executor.submit(upload_image, image_path)
163-
for image_path in images_to_upload
164-
]
165-
for future in concurrent.futures.as_completed(results):
166-
processed_image = future.result()
167-
if processed_image.uploaded and processed_image.entity:
168-
uploaded_image_entities.append(processed_image.entity)
169-
else:
170-
failed_images.append(processed_image.path)
171-
progress_bar.update(1)
172-
173-
for i in range(0, len(uploaded_image_entities), 500):
174-
self.controller.upload_images(
175-
project_name=project_name,
176-
folder_name=folder_name,
177-
images=uploaded_image_entities[i : i + 500], # noqa: E203
178-
annotation_status=set_annotation_status,
179-
)
180117
sys.exit(0)
181118

182119
def export_project(
@@ -305,13 +242,22 @@ def attach_image_urls(
305242
"""
306243
To attach image URLs to project use:
307244
"""
308-
self._attach_urls(project, attachments, annotation_status)
245+
246+
attach_image_urls_to_project(
247+
project=project,
248+
attachments=attachments,
249+
annotation_status=annotation_status,
250+
)
309251
sys.exit(0)
310252

311253
def attach_video_urls(
312254
self, project: str, attachments: str, annotation_status: Optional[Any] = None
313255
):
314-
self._attach_urls(project, attachments, annotation_status)
256+
attach_video_urls_to_project(
257+
project=project,
258+
attachments=attachments,
259+
annotation_status=annotation_status,
260+
)
315261
sys.exit(0)
316262

317263
def _attach_urls(
@@ -372,58 +318,17 @@ def upload_videos(
372318
start-time specifies time (in seconds) from which to start extracting frames, default is 0.0.
373319
end-time specifies time (in seconds) up to which to extract frames. If it is not specified, then up to end is assumed.
374320
"""
375-
project_name, folder_name = split_project_path(project)
376-
377-
uploaded_image_entities = []
378-
failed_images = []
379-
380-
def _upload_image(image_path: str) -> str:
381-
with open(image_path, "rb") as image:
382-
image_bytes = BytesIO(image.read())
383-
upload_response = self.controller.upload_image_to_s3(
384-
project_name=project_name,
385-
image_path=image_path,
386-
image_bytes=image_bytes,
387-
folder_name=folder_name,
388-
)
389-
if not upload_response.errors:
390-
uploaded_image_entities.append(upload_response.data)
391-
else:
392-
return image_path
393321

394-
video_paths = []
395-
for extension in extensions:
396-
if not recursive:
397-
video_paths += list(Path(folder).glob(f"*.{extension.lower()}"))
398-
if os.name != "nt":
399-
video_paths += list(Path(folder).glob(f"*.{extension.upper()}"))
400-
else:
401-
video_paths += list(Path(folder).rglob(f"*.{extension.lower()}"))
402-
if os.name != "nt":
403-
video_paths += list(Path(folder).rglob(f"*.{extension.upper()}"))
404-
video_paths = [str(path) for path in video_paths]
405-
406-
for path in video_paths:
407-
with tempfile.TemporaryDirectory() as temp_path:
408-
res = self.controller.extract_video_frames(
409-
project_name=project_name,
410-
folder_name=folder_name,
411-
video_path=path,
412-
extract_path=temp_path,
413-
target_fps=int(target_fps),
414-
start_time=float(start_time),
415-
end_time=end_time if not end_time else float(end_time),
416-
annotation_status=set_annotation_status,
417-
)
418-
if not res.errors:
419-
extracted_frame_paths = res.data
420-
for image_path in extracted_frame_paths:
421-
failed_images.append(_upload_image(image_path))
422-
for i in range(0, len(uploaded_image_entities), 500):
423-
self.controller.upload_images(
424-
project_name=project_name,
425-
folder_name=folder_name,
426-
images=uploaded_image_entities[i : i + 500], # noqa: E203
427-
annotation_status=set_annotation_status,
428-
)
322+
upload_videos_from_folder_to_project(
323+
project=project,
324+
folder_path=folder,
325+
extensions=extensions,
326+
exclude_file_patterns=(),
327+
recursive_subfolders=recursive,
328+
target_fps=target_fps,
329+
start_time=start_time,
330+
end_time=end_time,
331+
annotation_status=set_annotation_status,
332+
image_quality_in_editor=None,
333+
)
429334
sys.exit(0)

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

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,7 +1148,9 @@ def assign_images(project: Union[str, dict], image_names: List[str], user: str):
11481148
)
11491149
return
11501150

1151-
controller.assign_images(project_name, folder_name, image_names, user)
1151+
response = controller.assign_images(project_name, folder_name, image_names, user)
1152+
if not response.errors:
1153+
logger.info(f"Assign images to user {user}")
11521154

11531155

11541156
@Trackable
@@ -1556,9 +1558,7 @@ def upload_images_from_folder_to_project(
15561558
"extensions should be a list or a tuple in upload_images_from_folder_to_project"
15571559
)
15581560

1559-
project_folder_name = project_name + (
1560-
f"/{folder_name}" if folder_name != "root" else ""
1561-
)
1561+
project_folder_name = project_name + (f"/{folder_name}" if folder_name else "")
15621562

15631563
logger.info(
15641564
"Uploading all images with extensions %s from %s to project %s. Excluded file patterns are: %s.",
@@ -1884,6 +1884,8 @@ def upload_videos_from_folder_to_project(
18841884
if all(not_in_exclude_list):
18851885
filtered_paths.append(path)
18861886

1887+
project_folder_name = project_name + (f"/{folder_name}" if folder_name else "")
1888+
18871889
logger.info(
18881890
"Uploading all videos with extensions %s from %s to project %s. Excluded file patterns are: %s.",
18891891
extensions,
@@ -1892,8 +1894,7 @@ def upload_videos_from_folder_to_project(
18921894
exclude_file_patterns,
18931895
)
18941896

1895-
uploaded_images, failed_images = [], []
1896-
for path in tqdm(video_paths, desc="Uploading videos"):
1897+
for path in video_paths:
18971898
with tempfile.TemporaryDirectory() as temp_path:
18981899
res = controller.extract_video_frames(
18991900
project_name=project_name,
@@ -1915,17 +1916,31 @@ def upload_videos_from_folder_to_project(
19151916
annotation_status=annotation_status,
19161917
image_quality_in_editor=image_quality_in_editor,
19171918
)
1918-
images_to_upload, _ = use_case.images_to_upload
1919+
images_to_upload, duplicates = use_case.images_to_upload
1920+
logger.info(
1921+
"Extracted %s frames from video. Now uploading to platform.",
1922+
len(res.data),
1923+
)
1924+
logger.info(
1925+
"Uploading %s images to project %s.",
1926+
len(images_to_upload),
1927+
str(project_folder_name),
1928+
)
1929+
if len(duplicates):
1930+
logger.warning(
1931+
"%s already existing images found that won't be uploaded.",
1932+
len(duplicates),
1933+
)
19191934
if use_case.is_valid():
1920-
for _ in use_case.execute():
1921-
pass
1922-
uploaded, failed_images, _ = use_case.data
1923-
uploaded_images.append(uploaded)
1924-
failed_images.append(failed_images)
1935+
with tqdm(
1936+
total=len(images_to_upload), desc="Uploading images"
1937+
) as progress_bar:
1938+
for _ in use_case.execute():
1939+
progress_bar.update(1)
19251940
else:
19261941
raise AppValidationException(use_case.response.errors)
19271942

1928-
return uploaded_images, failed_images
1943+
return
19291944

19301945

19311946
@Trackable

src/superannotate/lib/core/plugin.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import io
22
import logging
3+
import os
34
from pathlib import Path
45
from typing import List
56
from typing import Tuple
@@ -234,17 +235,17 @@ def extract_frames(
234235
)
235236

236237
ratio = fps / target_fps
237-
extracted_frames_paths = []
238238
zero_fill_count = len(str(frames_count))
239239

240240
rotate_code = VideoPlugin.get_video_rotate_code(video_path)
241241

242242
frame_number = 0
243243
extracted_frame_number = 0
244244
extracted_frame_ratio = ratio
245-
logger.info("Extracting frames from video to %s.", extracted_frames_paths)
245+
logger.info("Extracting frames from video to %s.", extract_path)
246+
extracted_frames_paths = []
246247

247-
while len(extracted_frames_paths) < limit:
248+
while len(os.listdir(extract_path)) < limit:
248249
success, frame = video.read()
249250
if success:
250251
frame_time = video.get(cv2.CAP_PROP_POS_MSEC) / 1000.0

src/superannotate/lib/core/usecases.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1689,8 +1689,6 @@ def execute(self):
16891689
f"Cant assign {', '.join(self._image_names[i: i + self.CHUNK_SIZE])}"
16901690
)
16911691
continue
1692-
logger.info(f"Assign images to user {self._user}")
1693-
16941692
return self._response
16951693

16961694

src/superannotate/lib/infrastructure/controller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ def search_annotation_classes(self, project_name: str, name_prefix: str = None):
726726
classes=AnnotationClassRepository(
727727
service=self._backend_client, project=project_entity
728728
),
729-
condition=condition
729+
condition=condition,
730730
)
731731
return use_case.execute()
732732

src/superannotate/lib/infrastructure/services.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import os
21
from contextlib import contextmanager
32
from datetime import datetime
43
from typing import Dict
@@ -27,7 +26,9 @@ class BaseBackendService(SuerannotateServiceProvider):
2726
Base service class
2827
"""
2928

30-
def __init__(self, api_url: str, auth_token: str, logger, paginate_by=None, verify_ssl=True):
29+
def __init__(
30+
self, api_url: str, auth_token: str, logger, paginate_by=None, verify_ssl=True
31+
):
3132
self.api_url = api_url
3233
self._auth_token = auth_token
3334
self.logger = logger

0 commit comments

Comments
 (0)