From 82f3fa239ab796cba22d8a5a949a2f0ec041c234 Mon Sep 17 00:00:00 2001 From: shab Date: Thu, 8 Apr 2021 12:31:46 +0400 Subject: [PATCH] Video upload cleanup --- superannotate/__init__.py | 2 +- superannotate/db/project_images.py | 4 +- superannotate/db/projects.py | 214 ++++++++++++++++------------- 3 files changed, 125 insertions(+), 95 deletions(-) diff --git a/superannotate/__init__.py b/superannotate/__init__.py index d5483db84..ecefca8f5 100644 --- a/superannotate/__init__.py +++ b/superannotate/__init__.py @@ -50,7 +50,7 @@ def consensus(*args, **kwargs): download_image_preannotations, get_image_annotations, get_image_bytes, get_image_metadata, get_image_preannotations, search_images, search_images_all_folders, set_image_annotation_status, - set_images_annotation_statuses, upload_image_annotations + set_images_annotation_statuses, upload_image_annotations, get_project_root_folder_id ) from .db.project_api import ( create_folder, delete_folders, get_folder_metadata, diff --git a/superannotate/db/project_images.py b/superannotate/db/project_images.py index 437856820..c6273e7b1 100644 --- a/superannotate/db/project_images.py +++ b/superannotate/db/project_images.py @@ -213,6 +213,8 @@ def copy_images( :type copy_annotation_status: bool :param copy_pin: enables image pin status copy :type copy_pin: bool + :return: list of skipped image names + :rtype: list of strs """ source_project, source_project_folder = get_project_and_folder_metadata( source_project @@ -232,7 +234,7 @@ def copy_images( image_names, source_project["name"] + "" if source_project_folder is None else "/" + source_project_folder["name"], destination_project["name"] + - "" if destination_project_folder is None else "/" + + "" if destination_project_folder is None else destination_project["name"] + "/" + destination_project_folder["name"], len(res["skipped"]) ) return res["skipped"] diff --git a/superannotate/db/projects.py b/superannotate/db/projects.py index 1c664440f..5178ca09e 100644 --- a/superannotate/db/projects.py +++ b/superannotate/db/projects.py @@ -205,57 +205,63 @@ def get_project_image_count(project, with_all_subfolders=False): return len(search_images_all_folders(project)) -def upload_video_to_project( - project, - video_path, - target_fps=None, - start_time=0.0, - end_time=None, - annotation_status="NotStarted", - image_quality_in_editor=None -): - """Uploads image frames from video to platform. Uploaded images will have - names "_.jpg". +def _get_video_frames_count(video_path): + """ + Get video frames count + """ + video = cv2.VideoCapture(str(video_path), cv2.CAP_FFMPEG) + total_num_of_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) + if total_num_of_frames < 0: + total_num_of_frames = 0 + flag = True + while flag: + flag, _ = video.read() + if flag: + total_num_of_frames += 1 + else: + break + return total_num_of_frames - :param project: project name or folder path (e.g., "project1/folder1") - :type project: str - :param video_path: video to upload - :type video_path: Pathlike (str or Path) - :param target_fps: how many frames per second need to extract from the video (approximate). - If None, all frames will be uploaded - :type target_fps: float - :param start_time: Time (in seconds) from which to start extracting frames - :type start_time: float - :param end_time: Time (in seconds) up to which to extract frames. If None up to end - :type end_time: float - :param annotation_status: value to set the annotation statuses of the uploaded - video frames NotStarted InProgress QualityCheck Returned Completed Skipped - :type annotation_status: str - :param image_quality_in_editor: image quality be seen in SuperAnnotate web annotation editor. - Can be either "compressed" or "original". If None then the default value in project settings will be used. - :type image_quality_in_editor: str - :return: filenames of uploaded images - :rtype: list of strs +def _get_video_fps_ration(target_fps,video,ratio): """ - project, project_folder = get_project_and_folder_metadata(project) - upload_state = common.upload_state_int_to_str(project.get("upload_state")) - if upload_state == "External": - raise SABaseException( - 0, - "The function does not support projects containing images attached with URLs" + Get video fps / target fps ratio + """ + video_fps = float(video.get(cv2.CAP_PROP_FPS)) + if target_fps >= video_fps: + logger.warning( + "Video frame rate %s smaller than target frame rate %s. Cannot change frame rate.", + video_fps, target_fps + ) + else: + logger.info( + "Changing video frame rate from %s to target frame rate %s.", + video_fps, target_fps ) - logger.info("Uploading from video %s.", str(video_path)) + ratio = video_fps / target_fps + return ratio + +def _get_available_image_counts(project,folder): + if folder: + folder_id = folder["id"] + else: + folder_id = get_project_root_folder_id(project) + params = {'team_id': project['team_id'] , 'folder_id' : folder_id } + res = _get_upload_auth_token(params=params,project_id=project['id']) + return res['availableImageCount'] + +def _get_video_rotate_code(video_path): rotate_code = None try: + cv2_rotations = { + 90 : cv2.ROTATE_90_CLOCKWISE, + 180 : cv2.ROTATE_180, + 270 :cv2.ROTATE_90_COUNTERCLOCKWISE, + } + meta_dict = ffmpeg.probe(str(video_path)) rot = int(meta_dict['streams'][0]['tags']['rotate']) - if rot == 90: - rotate_code = cv2.ROTATE_90_CLOCKWISE - elif rot == 180: - rotate_code = cv2.ROTATE_180 - elif rot == 270: - rotate_code = cv2.ROTATE_90_COUNTERCLOCKWISE + rotate_code = cv2_rotations[rot] if rot != 0: logger.info( "Frame rotation of %s found. Output images will be rotated accordingly.", @@ -269,61 +275,30 @@ def upload_video_to_project( "Couldn't read video metadata to determine rotation. %s", warning_str ) + return rotate_code - video = cv2.VideoCapture(str(video_path), cv2.CAP_FFMPEG) - if not video.isOpened(): - raise SABaseException(0, "Couldn't open video file " + str(video_path)) - - total_num_of_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) - if total_num_of_frames < 0: - total_num_of_frames = 0 - flag = True - while flag: - flag, frame = video.read() - if flag: - total_num_of_frames += 1 - else: - break - video = cv2.VideoCapture(str(video_path), cv2.CAP_FFMPEG) - logger.info("Video frame count is %s.", total_num_of_frames) - - r = 1.0 - if target_fps is not None: - video_fps = float(video.get(cv2.CAP_PROP_FPS)) - if target_fps >= video_fps: - logger.warning( - "Video frame rate %s smaller than target frame rate %s. Cannot change frame rate.", - video_fps, target_fps - ) - else: - logger.info( - "Changing video frame rate from %s to target frame rate %s.", - video_fps, target_fps - ) - r = video_fps / target_fps - - zero_fill_count = len(str(total_num_of_frames)) - tempdir = tempfile.TemporaryDirectory() +def _extract_frames_from_video(start_time,end_time,ratio,video,video_path,tempdir,limit,rotate_code,total_num_of_frames): video_name = Path(video_path).stem frame_no = 0 frame_no_with_change = 1.0 extracted_frame_no = 1 logger.info("Extracting frames from video to %s.", tempdir.name) - while True: + zero_fill_count = len(str(total_num_of_frames)) + while extracted_frame_no < (limit + 1) : success, frame = video.read() if not success: break frame_no += 1 if round(frame_no_with_change) != frame_no: continue - frame_no_with_change += r + frame_no_with_change += ratio frame_time = video.get(cv2.CAP_PROP_POS_MSEC) / 1000.0 - if end_time is not None and frame_time > end_time: + if end_time and frame_time > end_time: break if frame_time < start_time: continue - if rotate_code is not None: + if rotate_code: frame = cv2.rotate(frame, rotate_code) cv2.imwrite( str( @@ -334,26 +309,79 @@ def upload_video_to_project( ), frame ) extracted_frame_no += 1 + return extracted_frame_no - 1 + + +def upload_video_to_project( + project, + video_path, + target_fps=None, + start_time=0.0, + end_time=None, + annotation_status="NotStarted", + image_quality_in_editor=None +): + """Uploads image frames from video to platform. Uploaded images will have + names "_.jpg". + + :param project: project name or folder path (e.g., "project1/folder1") + :type project: str + :param video_path: video to upload + :type video_path: Pathlike (str or Path) + :param target_fps: how many frames per second need to extract from the video (approximate). + If None, all frames will be uploaded + :type target_fps: float + :param start_time: Time (in seconds) from which to start extracting frames + :type start_time: float + :param end_time: Time (in seconds) up to which to extract frames. If None up to end + :type end_time: float + :param annotation_status: value to set the annotation statuses of the uploaded + video frames NotStarted InProgress QualityCheck Returned Completed Skipped + :type annotation_status: str + :param image_quality_in_editor: image quality be seen in SuperAnnotate web annotation editor. + Can be either "compressed" or "original". If None then the default value in project settings will be used. + :type image_quality_in_editor: str + + :return: filenames of uploaded images + :rtype: list of strs + """ + + project, folder = get_project_and_folder_metadata(project) + limit = _get_available_image_counts(project,folder) + + upload_state = common.upload_state_int_to_str(project.get("upload_state")) + if upload_state == "External": + raise SABaseException( + 0, + "The function does not support projects containing images attached with URLs" + ) + logger.info("Uploading from video %s.", str(video_path)) + rotate_code = _get_video_rotate_code(video_path) + video = cv2.VideoCapture(str(video_path), cv2.CAP_FFMPEG) + if not video.isOpened(): + raise SABaseException(0, "Couldn't open video file " + str(video_path)) + total_num_of_frames = _get_video_frames_count(video_path) + logger.info("Video frame count is %s.", total_num_of_frames) + ratio = 1.0 + if target_fps: + ratio = _get_video_fps_ration(target_fps,video,ratio) + tempdir = tempfile.TemporaryDirectory() + extracted_frame_no = _extract_frames_from_video(start_time,end_time,ratio, + video,video_path,tempdir, + limit,rotate_code,total_num_of_frames) logger.info( "Extracted %s frames from video. Now uploading to platform.", - extracted_frame_no - 1 + extracted_frame_no ) - filenames = upload_images_from_folder_to_project( - (project, project_folder), + (project, folder), tempdir.name, extensions=["jpg"], annotation_status=annotation_status, image_quality_in_editor=image_quality_in_editor ) - - assert len(filenames[1]) == 0 - - filenames_base = [] - for file in filenames[0]: - filenames_base.append(Path(file).name) - + filenames_base = [Path(f).name for f in filenames[0]] return filenames_base @@ -398,7 +426,7 @@ def upload_videos_from_folder_to_project( :return: uploaded and not-uploaded video frame images' filenames :rtype: tuple of list of strs """ - project, project_folder = get_project_and_folder_metadata(project) + project, folder = get_project_and_folder_metadata(project) upload_state = common.upload_state_int_to_str(project.get("upload_state")) if upload_state == "External": raise SABaseException( @@ -440,7 +468,7 @@ def upload_videos_from_folder_to_project( filenames = [] for path in filtered_paths: filenames += upload_video_to_project( - (project, project_folder), + (project, folder), path, target_fps=target_fps, start_time=start_time,