Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
minversion = 3.0
log_cli=true
python_files = test_*.py
;addopts = -n32 --dist=loadscope
addopts = -n32 --dist=loadscope
4 changes: 2 additions & 2 deletions src/superannotate/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os
import logging.config
import os

from superannotate.version import __version__
from superannotate.lib.app.analytics.class_analytics import attribute_distribution
from superannotate.lib.app.analytics.class_analytics import class_distribution
from superannotate.lib.app.annotation_helpers import add_annotation_bbox_to_json
Expand Down Expand Up @@ -155,6 +154,7 @@
from superannotate.lib.app.interface.sdk_interface import (
upload_videos_from_folder_to_project,
)
from superannotate.version import __version__


__all__ = [
Expand Down
78 changes: 61 additions & 17 deletions src/superannotate/lib/app/interface/sdk_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,17 +179,13 @@ def create_project(project_name: str, project_description: str, project_type: st
:return: dict object metadata the new project
:rtype: dict
"""
projects = controller.search_project(name=project_name).data
if projects:
raise AppException(
f"Project with name {project_name} already exists."
f" Please use unique names for projects to use with SDK."
)

result = controller.create_project(
response = controller.create_project(
name=project_name, description=project_description, project_type=project_type
).data
return ProjectSerializer(result).serialize()
)
if response.errors:
raise Exception(response.errors)

return ProjectSerializer(response.data).serialize()


@Trackable
Expand Down Expand Up @@ -672,6 +668,8 @@ def _upload_image(image_url, image_path) -> ProcessedImage:
else:
failed_images.append(processed_image)

logger.info("Downloading %s images", len(images_to_upload))

for i in range(0, len(images_to_upload), 500):
controller.upload_images(
project_name=project_name,
Expand Down Expand Up @@ -886,7 +884,9 @@ def get_project_settings(project: Union[str, dict]):
"""
project_name, folder_name = extract_project_folder(project)
settings = controller.get_project_settings(project_name=project_name)
settings = [BaseSerializers(attribute).serialize() for attribute in settings.data]
settings = [
SettingsSerializer(attribute).serialize() for attribute in settings.data
]
return settings


Expand Down Expand Up @@ -1099,6 +1099,9 @@ def delete_images(project: Union[str, dict], image_names: Optional[List[str]] =
"""
project_name, folder_name = extract_project_folder(project)

if not isinstance(image_names, list) and image_names is not None:
raise AppValidationException("Image_names should be a list of strs or None.")

response = controller.delete_images(
project_name=project_name, folder_name=folder_name, image_names=image_names
)
Expand Down Expand Up @@ -1143,9 +1146,6 @@ def assign_images(project: Union[str, dict], image_names: List[str], user: str):
logger.warning(
f"Skipping {user}. {user} is not a verified contributor for the {project_name}"
)



return

controller.assign_images(project_name, folder_name, image_names, user)
Expand Down Expand Up @@ -1204,9 +1204,27 @@ def assign_folder(project_name: str, folder_name: str, users: List[str]):
:param users: list of user emails
:type users: list of str
"""

contributors = (
controller.get_project_metadata(
project_name=project_name, include_contributors=True
)
.data["project"]
.users
)
verified_users = [i["user_id"] for i in contributors]
verified_users = set(users).intersection(set(verified_users))
unverified_contributor = set(users) - verified_users

for user in unverified_contributor:
logger.warning(
f"Skipping {user} from assignees. {user} is not a verified contributor for the {project_name}"
)

response = controller.assign_folder(
project_name=project_name, folder_name=folder_name, users=users
project_name=project_name, folder_name=folder_name, users=list(verified_users)
)

if response.errors:
raise AppException(response.errors)

Expand All @@ -1226,10 +1244,15 @@ def share_project(project_name: str, user: Union[str, dict], user_role: str):
if isinstance(user, dict):
user_id = user["id"]
else:
user_id = controller.search_team_contributors(email=user).data[0]["id"]
controller.share_project(
response = controller.search_team_contributors(email=user)
if not response.data:
raise AppException(f"User {user} not found.")
user_id = response.data[0]["id"]
response = controller.share_project(
project_name=project_name, user_id=user_id, user_role=user_role
)
if response.errors:
raise AppException(response.errors)


@Trackable
Expand Down Expand Up @@ -1814,6 +1837,9 @@ def upload_videos_from_folder_to_project(
if os.name != "nt":
video_paths += list(Path(folder_path).glob(f"*.{extension.upper()}"))
else:
logger.warning(
"When using recursive subfolder parsing same name videos in different subfolders will overwrite each other."
)
video_paths += list(Path(folder_path).rglob(f"*.{extension.lower()}"))
if os.name != "nt":
video_paths += list(Path(folder_path).rglob(f"*.{extension.upper()}"))
Expand All @@ -1824,6 +1850,14 @@ def upload_videos_from_folder_to_project(
if all(not_in_exclude_list):
filtered_paths.append(path)

logger.info(
"Uploading all videos with extensions %s from %s to project %s. Excluded file patterns are: %s.",
extensions,
str(folder_path),
project_name,
exclude_file_patterns,
)

uploaded_images, failed_images = [], []
for path in tqdm(video_paths):
with tempfile.TemporaryDirectory() as temp_path:
Expand Down Expand Up @@ -2173,6 +2207,7 @@ def _upload_file_to_s3(to_s3_bucket, path, s3_key) -> None:

for future in concurrent.futures.as_completed(results):
future.result()
logger.info("Exported to AWS %s/%s", to_s3_bucket, str(path))


@Trackable
Expand Down Expand Up @@ -2569,6 +2604,7 @@ def upload_image_annotations(
image_name=image_name,
annotations=annotation_json,
mask=mask,
verbose=verbose,
)
if response.errors:
raise AppValidationException(response.errors)
Expand Down Expand Up @@ -3485,6 +3521,9 @@ def _upload_s3_image(image_path: str):
progress_bar.update(1)
uploaded = []
duplicates = []

logger.info("Uploading %s images to project.", len(images_to_upload))

for i in range(0, len(uploaded_image_entities), 500):
response = controller.upload_images(
project_name=project_name,
Expand All @@ -3496,6 +3535,11 @@ def _upload_s3_image(image_path: str):
uploaded.extend(attachments)
duplicates.extend(duplications)

if len(duplicates):
logger.warning(
"%s already existing images found that won't be uploaded.", len(duplicates)
)

return uploaded, failed_images, duplicates


Expand Down
36 changes: 34 additions & 2 deletions src/superannotate/lib/core/plugin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import io
import logging
from pathlib import Path
from typing import List
from typing import Tuple
Expand All @@ -10,6 +11,8 @@
from PIL import ImageDraw
from PIL import ImageOps

logger = logging.getLogger()


class ImagePlugin:
def __init__(self, image_bytes: io.BytesIO, max_resolution: int = 4096):
Expand Down Expand Up @@ -185,8 +188,18 @@ def get_video_rotate_code(video_path):
meta_dict = ffmpeg.probe(str(video_path))
rot = int(meta_dict["streams"][0]["tags"]["rotate"])
if rot:
logger.info(
"Frame rotation of %s found. Output images will be rotated accordingly.",
rot,
)
return cv2_rotations[rot]
except Exception:
except Exception as e:
warning_str = ""
if "ffprobe" in str(e):
warning_str = "This could be because ffmpeg package is not installed. To install it, run: sudo apt install ffmpeg"
logger.warning(
"Couldn't read video metadata to determine rotation. %s", warning_str
)
return

@staticmethod
Expand All @@ -202,8 +215,25 @@ def extract_frames(
if not video.isOpened():
return []
frames_count = VideoPlugin.get_frames_count(video_path)
logger.info("Video frame count is %s.", frames_count)

fps = video.get(cv2.CAP_PROP_FPS)
ratio = fps / target_fps if target_fps else 1
if target_fps > fps:
logger.warning(
"Video frame rate %s smaller than target frame rate %s. Cannot change frame rate.",
fps,
target_fps,
)
target_fps = fps

else:
logger.info(
"Changing video frame rate from %s to target frame rate %s.",
fps,
target_fps,
)

ratio = fps / target_fps
extracted_frames_paths = []
zero_fill_count = len(str(frames_count))

Expand All @@ -212,6 +242,8 @@ def extract_frames(
frame_number = 0
extracted_frame_number = 0
extracted_frame_ratio = ratio
logger.info("Extracting frames from video to %s.", extracted_frames_paths)

while len(extracted_frames_paths) < limit:
success, frame = video.read()
if success:
Expand Down
Loading