Skip to content

Commit 5b0a25b

Browse files
authored
Merge pull request #187 from superannotateai/fix_cli
Fix cli
2 parents d9511b0 + 8fba83a commit 5b0a25b

File tree

4 files changed

+78
-156
lines changed

4 files changed

+78
-156
lines changed

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

Lines changed: 46 additions & 138 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,19 @@ 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(",")
107+
if not isinstance(extensions, list):
108+
extensions = extensions.split(",")
135109

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],
110+
upload_images_from_folder_to_project(
111+
project,
112+
folder_path=folder,
113+
extensions=extensions,
114+
annotation_status=set_annotation_status,
115+
from_s3_bucket=None,
116+
exclude_file_patterns=exclude_file_patterns,
117+
recursive_subfolders=recursive_subfolders,
118+
image_quality_in_editor=image_quality_in_editor,
158119
)
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-
)
180120
sys.exit(0)
181121

182122
def export_project(
@@ -305,13 +245,22 @@ def attach_image_urls(
305245
"""
306246
To attach image URLs to project use:
307247
"""
308-
self._attach_urls(project, attachments, annotation_status)
248+
249+
attach_image_urls_to_project(
250+
project=project,
251+
attachments=attachments,
252+
annotation_status=annotation_status,
253+
)
309254
sys.exit(0)
310255

311256
def attach_video_urls(
312257
self, project: str, attachments: str, annotation_status: Optional[Any] = None
313258
):
314-
self._attach_urls(project, attachments, annotation_status)
259+
attach_video_urls_to_project(
260+
project=project,
261+
attachments=attachments,
262+
annotation_status=annotation_status,
263+
)
315264
sys.exit(0)
316265

317266
def _attach_urls(
@@ -372,58 +321,17 @@ def upload_videos(
372321
start-time specifies time (in seconds) from which to start extracting frames, default is 0.0.
373322
end-time specifies time (in seconds) up to which to extract frames. If it is not specified, then up to end is assumed.
374323
"""
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
393324

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-
)
325+
upload_videos_from_folder_to_project(
326+
project=project,
327+
folder_path=folder,
328+
extensions=extensions,
329+
exclude_file_patterns=(),
330+
recursive_subfolders=recursive,
331+
target_fps=target_fps,
332+
start_time=start_time,
333+
end_time=end_time,
334+
annotation_status=set_annotation_status,
335+
image_quality_in_editor=None,
336+
)
429337
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
@@ -1150,7 +1150,9 @@ def assign_images(project: Union[str, dict], image_names: List[str], user: str):
11501150
)
11511151
return
11521152

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

11551157

11561158
@Trackable
@@ -1558,9 +1560,7 @@ def upload_images_from_folder_to_project(
15581560
"extensions should be a list or a tuple in upload_images_from_folder_to_project"
15591561
)
15601562

1561-
project_folder_name = project_name + (
1562-
f"/{folder_name}" if folder_name != "root" else ""
1563-
)
1563+
project_folder_name = project_name + (f"/{folder_name}" if folder_name else "")
15641564

15651565
logger.info(
15661566
"Uploading all images with extensions %s from %s to project %s. Excluded file patterns are: %s.",
@@ -1886,6 +1886,8 @@ def upload_videos_from_folder_to_project(
18861886
if all(not_in_exclude_list):
18871887
filtered_paths.append(path)
18881888

1889+
project_folder_name = project_name + (f"/{folder_name}" if folder_name else "")
1890+
18891891
logger.info(
18901892
"Uploading all videos with extensions %s from %s to project %s. Excluded file patterns are: %s.",
18911893
extensions,
@@ -1894,8 +1896,7 @@ def upload_videos_from_folder_to_project(
18941896
exclude_file_patterns,
18951897
)
18961898

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

1930-
return uploaded_images, failed_images
1945+
return
19311946

19321947

19331948
@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
@@ -1705,8 +1705,6 @@ def execute(self):
17051705
f"Cant assign {', '.join(self._image_names[i: i + self.CHUNK_SIZE])}"
17061706
)
17071707
continue
1708-
logger.info(f"Assign images to user {self._user}")
1709-
17101708
return self._response
17111709

17121710

0 commit comments

Comments
 (0)