Skip to content

Commit 0ff8921

Browse files
authored
Merge pull request #20 from superannotateai/folder-limitations
Video upload cleanup - limitations
2 parents 8f24510 + 82f3fa2 commit 0ff8921

File tree

3 files changed

+125
-95
lines changed

3 files changed

+125
-95
lines changed

superannotate/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def consensus(*args, **kwargs):
5050
download_image_preannotations, get_image_annotations, get_image_bytes,
5151
get_image_metadata, get_image_preannotations, search_images,
5252
search_images_all_folders, set_image_annotation_status,
53-
set_images_annotation_statuses, upload_image_annotations
53+
set_images_annotation_statuses, upload_image_annotations, get_project_root_folder_id
5454
)
5555
from .db.project_api import (
5656
create_folder, delete_folders, get_folder_metadata,

superannotate/db/project_images.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ def copy_images(
213213
:type copy_annotation_status: bool
214214
:param copy_pin: enables image pin status copy
215215
:type copy_pin: bool
216+
:return: list of skipped image names
217+
:rtype: list of strs
216218
"""
217219
source_project, source_project_folder = get_project_and_folder_metadata(
218220
source_project
@@ -232,7 +234,7 @@ def copy_images(
232234
image_names,
233235
source_project["name"] + "" if source_project_folder is None else "/" +
234236
source_project_folder["name"], destination_project["name"] +
235-
"" if destination_project_folder is None else "/" +
237+
"" if destination_project_folder is None else destination_project["name"] + "/" +
236238
destination_project_folder["name"], len(res["skipped"])
237239
)
238240
return res["skipped"]

superannotate/db/projects.py

Lines changed: 121 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -205,57 +205,63 @@ def get_project_image_count(project, with_all_subfolders=False):
205205
return len(search_images_all_folders(project))
206206

207207

208-
def upload_video_to_project(
209-
project,
210-
video_path,
211-
target_fps=None,
212-
start_time=0.0,
213-
end_time=None,
214-
annotation_status="NotStarted",
215-
image_quality_in_editor=None
216-
):
217-
"""Uploads image frames from video to platform. Uploaded images will have
218-
names "<video_name>_<frame_no>.jpg".
208+
def _get_video_frames_count(video_path):
209+
"""
210+
Get video frames count
211+
"""
212+
video = cv2.VideoCapture(str(video_path), cv2.CAP_FFMPEG)
213+
total_num_of_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
214+
if total_num_of_frames < 0:
215+
total_num_of_frames = 0
216+
flag = True
217+
while flag:
218+
flag, _ = video.read()
219+
if flag:
220+
total_num_of_frames += 1
221+
else:
222+
break
223+
return total_num_of_frames
219224

220-
:param project: project name or folder path (e.g., "project1/folder1")
221-
:type project: str
222-
:param video_path: video to upload
223-
:type video_path: Pathlike (str or Path)
224-
:param target_fps: how many frames per second need to extract from the video (approximate).
225-
If None, all frames will be uploaded
226-
:type target_fps: float
227-
:param start_time: Time (in seconds) from which to start extracting frames
228-
:type start_time: float
229-
:param end_time: Time (in seconds) up to which to extract frames. If None up to end
230-
:type end_time: float
231-
:param annotation_status: value to set the annotation statuses of the uploaded
232-
video frames NotStarted InProgress QualityCheck Returned Completed Skipped
233-
:type annotation_status: str
234-
:param image_quality_in_editor: image quality be seen in SuperAnnotate web annotation editor.
235-
Can be either "compressed" or "original". If None then the default value in project settings will be used.
236-
:type image_quality_in_editor: str
237225

238-
:return: filenames of uploaded images
239-
:rtype: list of strs
226+
def _get_video_fps_ration(target_fps,video,ratio):
240227
"""
241-
project, project_folder = get_project_and_folder_metadata(project)
242-
upload_state = common.upload_state_int_to_str(project.get("upload_state"))
243-
if upload_state == "External":
244-
raise SABaseException(
245-
0,
246-
"The function does not support projects containing images attached with URLs"
228+
Get video fps / target fps ratio
229+
"""
230+
video_fps = float(video.get(cv2.CAP_PROP_FPS))
231+
if target_fps >= video_fps:
232+
logger.warning(
233+
"Video frame rate %s smaller than target frame rate %s. Cannot change frame rate.",
234+
video_fps, target_fps
235+
)
236+
else:
237+
logger.info(
238+
"Changing video frame rate from %s to target frame rate %s.",
239+
video_fps, target_fps
247240
)
248-
logger.info("Uploading from video %s.", str(video_path))
241+
ratio = video_fps / target_fps
242+
return ratio
243+
244+
def _get_available_image_counts(project,folder):
245+
if folder:
246+
folder_id = folder["id"]
247+
else:
248+
folder_id = get_project_root_folder_id(project)
249+
params = {'team_id': project['team_id'] , 'folder_id' : folder_id }
250+
res = _get_upload_auth_token(params=params,project_id=project['id'])
251+
return res['availableImageCount']
252+
253+
def _get_video_rotate_code(video_path):
249254
rotate_code = None
250255
try:
256+
cv2_rotations = {
257+
90 : cv2.ROTATE_90_CLOCKWISE,
258+
180 : cv2.ROTATE_180,
259+
270 :cv2.ROTATE_90_COUNTERCLOCKWISE,
260+
}
261+
251262
meta_dict = ffmpeg.probe(str(video_path))
252263
rot = int(meta_dict['streams'][0]['tags']['rotate'])
253-
if rot == 90:
254-
rotate_code = cv2.ROTATE_90_CLOCKWISE
255-
elif rot == 180:
256-
rotate_code = cv2.ROTATE_180
257-
elif rot == 270:
258-
rotate_code = cv2.ROTATE_90_COUNTERCLOCKWISE
264+
rotate_code = cv2_rotations[rot]
259265
if rot != 0:
260266
logger.info(
261267
"Frame rotation of %s found. Output images will be rotated accordingly.",
@@ -269,61 +275,30 @@ def upload_video_to_project(
269275
"Couldn't read video metadata to determine rotation. %s",
270276
warning_str
271277
)
278+
return rotate_code
272279

273-
video = cv2.VideoCapture(str(video_path), cv2.CAP_FFMPEG)
274-
if not video.isOpened():
275-
raise SABaseException(0, "Couldn't open video file " + str(video_path))
276-
277-
total_num_of_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
278-
if total_num_of_frames < 0:
279-
total_num_of_frames = 0
280-
flag = True
281-
while flag:
282-
flag, frame = video.read()
283-
if flag:
284-
total_num_of_frames += 1
285-
else:
286-
break
287-
video = cv2.VideoCapture(str(video_path), cv2.CAP_FFMPEG)
288-
logger.info("Video frame count is %s.", total_num_of_frames)
289-
290-
r = 1.0
291-
if target_fps is not None:
292-
video_fps = float(video.get(cv2.CAP_PROP_FPS))
293-
if target_fps >= video_fps:
294-
logger.warning(
295-
"Video frame rate %s smaller than target frame rate %s. Cannot change frame rate.",
296-
video_fps, target_fps
297-
)
298-
else:
299-
logger.info(
300-
"Changing video frame rate from %s to target frame rate %s.",
301-
video_fps, target_fps
302-
)
303-
r = video_fps / target_fps
304-
305-
zero_fill_count = len(str(total_num_of_frames))
306-
tempdir = tempfile.TemporaryDirectory()
307280

281+
def _extract_frames_from_video(start_time,end_time,ratio,video,video_path,tempdir,limit,rotate_code,total_num_of_frames):
308282
video_name = Path(video_path).stem
309283
frame_no = 0
310284
frame_no_with_change = 1.0
311285
extracted_frame_no = 1
312286
logger.info("Extracting frames from video to %s.", tempdir.name)
313-
while True:
287+
zero_fill_count = len(str(total_num_of_frames))
288+
while extracted_frame_no < (limit + 1) :
314289
success, frame = video.read()
315290
if not success:
316291
break
317292
frame_no += 1
318293
if round(frame_no_with_change) != frame_no:
319294
continue
320-
frame_no_with_change += r
295+
frame_no_with_change += ratio
321296
frame_time = video.get(cv2.CAP_PROP_POS_MSEC) / 1000.0
322-
if end_time is not None and frame_time > end_time:
297+
if end_time and frame_time > end_time:
323298
break
324299
if frame_time < start_time:
325300
continue
326-
if rotate_code is not None:
301+
if rotate_code:
327302
frame = cv2.rotate(frame, rotate_code)
328303
cv2.imwrite(
329304
str(
@@ -334,26 +309,79 @@ def upload_video_to_project(
334309
), frame
335310
)
336311
extracted_frame_no += 1
312+
return extracted_frame_no - 1
313+
314+
315+
def upload_video_to_project(
316+
project,
317+
video_path,
318+
target_fps=None,
319+
start_time=0.0,
320+
end_time=None,
321+
annotation_status="NotStarted",
322+
image_quality_in_editor=None
323+
):
324+
"""Uploads image frames from video to platform. Uploaded images will have
325+
names "<video_name>_<frame_no>.jpg".
326+
327+
:param project: project name or folder path (e.g., "project1/folder1")
328+
:type project: str
329+
:param video_path: video to upload
330+
:type video_path: Pathlike (str or Path)
331+
:param target_fps: how many frames per second need to extract from the video (approximate).
332+
If None, all frames will be uploaded
333+
:type target_fps: float
334+
:param start_time: Time (in seconds) from which to start extracting frames
335+
:type start_time: float
336+
:param end_time: Time (in seconds) up to which to extract frames. If None up to end
337+
:type end_time: float
338+
:param annotation_status: value to set the annotation statuses of the uploaded
339+
video frames NotStarted InProgress QualityCheck Returned Completed Skipped
340+
:type annotation_status: str
341+
:param image_quality_in_editor: image quality be seen in SuperAnnotate web annotation editor.
342+
Can be either "compressed" or "original". If None then the default value in project settings will be used.
343+
:type image_quality_in_editor: str
344+
345+
:return: filenames of uploaded images
346+
:rtype: list of strs
347+
"""
348+
349+
project, folder = get_project_and_folder_metadata(project)
350+
limit = _get_available_image_counts(project,folder)
351+
352+
upload_state = common.upload_state_int_to_str(project.get("upload_state"))
353+
if upload_state == "External":
354+
raise SABaseException(
355+
0,
356+
"The function does not support projects containing images attached with URLs"
357+
)
358+
logger.info("Uploading from video %s.", str(video_path))
359+
rotate_code = _get_video_rotate_code(video_path)
360+
video = cv2.VideoCapture(str(video_path), cv2.CAP_FFMPEG)
361+
if not video.isOpened():
362+
raise SABaseException(0, "Couldn't open video file " + str(video_path))
337363

364+
total_num_of_frames = _get_video_frames_count(video_path)
365+
logger.info("Video frame count is %s.", total_num_of_frames)
366+
ratio = 1.0
367+
if target_fps:
368+
ratio = _get_video_fps_ration(target_fps,video,ratio)
369+
tempdir = tempfile.TemporaryDirectory()
370+
extracted_frame_no = _extract_frames_from_video(start_time,end_time,ratio,
371+
video,video_path,tempdir,
372+
limit,rotate_code,total_num_of_frames)
338373
logger.info(
339374
"Extracted %s frames from video. Now uploading to platform.",
340-
extracted_frame_no - 1
375+
extracted_frame_no
341376
)
342-
343377
filenames = upload_images_from_folder_to_project(
344-
(project, project_folder),
378+
(project, folder),
345379
tempdir.name,
346380
extensions=["jpg"],
347381
annotation_status=annotation_status,
348382
image_quality_in_editor=image_quality_in_editor
349383
)
350-
351-
assert len(filenames[1]) == 0
352-
353-
filenames_base = []
354-
for file in filenames[0]:
355-
filenames_base.append(Path(file).name)
356-
384+
filenames_base = [Path(f).name for f in filenames[0]]
357385
return filenames_base
358386

359387

@@ -398,7 +426,7 @@ def upload_videos_from_folder_to_project(
398426
:return: uploaded and not-uploaded video frame images' filenames
399427
:rtype: tuple of list of strs
400428
"""
401-
project, project_folder = get_project_and_folder_metadata(project)
429+
project, folder = get_project_and_folder_metadata(project)
402430
upload_state = common.upload_state_int_to_str(project.get("upload_state"))
403431
if upload_state == "External":
404432
raise SABaseException(
@@ -440,7 +468,7 @@ def upload_videos_from_folder_to_project(
440468
filenames = []
441469
for path in filtered_paths:
442470
filenames += upload_video_to_project(
443-
(project, project_folder),
471+
(project, folder),
444472
path,
445473
target_fps=target_fps,
446474
start_time=start_time,

0 commit comments

Comments
 (0)