From f6836ff9d283d40ffacd471fa11fa29087c36516 Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 22 Sep 2020 18:22:33 +0300 Subject: [PATCH 01/10] Added ability to upload meta information with video & some fixes --- cvat/apps/engine/prepare.py | 110 +++++++++++++++++++++++++++++++----- cvat/apps/engine/task.py | 58 +++++++++++++------ 2 files changed, 136 insertions(+), 32 deletions(-) diff --git a/cvat/apps/engine/prepare.py b/cvat/apps/engine/prepare.py index 3d4ca7dabd9b..5bfffb13cbbd 100644 --- a/cvat/apps/engine/prepare.py +++ b/cvat/apps/engine/prepare.py @@ -3,7 +3,9 @@ # SPDX-License-Identifier: MIT import av +from collections import OrderedDict import hashlib +import os class WorkWithVideo: def __init__(self, **kwargs): @@ -72,27 +74,31 @@ def __init__(self, **kwargs): def get_task_size(self): return self.frames + @property + def frame_sizes(self): + frame = self.key_frames.get(next(iter(self.key_frames))) + return (frame.width, frame.height) + + def check_key_frame(self, container, video_stream, key_frame): + for packet in container.demux(video_stream): + for frame in packet.decode(): + if md5_hash(frame) != md5_hash(key_frame[1]) or frame.pts != key_frame[1].pts: + self.key_frames.pop(key_frame[0]) + return + def check_seek_key_frames(self): container = self._open_video_container(self.source_path, mode='r') video_stream = self._get_video_stream(container) key_frames_copy = self.key_frames.copy() - for index, key_frame in key_frames_copy.items(): - container.seek(offset=key_frame.pts, stream=video_stream) - flag = True - for packet in container.demux(video_stream): - for frame in packet.decode(): - if md5_hash(frame) != md5_hash(key_frame) or frame.pts != key_frame.pts: - self.key_frames.pop(index) - flag = False - break - if not flag: - break + for key_frame in key_frames_copy.items(): + container.seek(offset=key_frame[1].pts, stream=video_stream) + self.check_key_frame(container, video_stream, key_frame) - #TODO: correct ratio of number of frames to keyframes - if len(self.key_frames) == 0: - raise Exception('Too few keyframes') + def check_frames_ratio(self, chunk_size): + if not len(self.key_frames) or (len(self.key_frames) and (self.frames // len(self.key_frames)) > chunk_size): + raise Exception('Too few keyframes for smooth video decoding') def save_key_frames(self): container = self._open_video_container(self.source_path, mode='r') @@ -152,4 +158,78 @@ def decode_needed_frames(self, chunk_number, db_data): self._close_video_container(container) return - self._close_video_container(container) \ No newline at end of file + self._close_video_container(container) + +class UploadedMeta(PrepareInfo): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + with open(self.meta_path, 'r') as meta_file: + lines = meta_file.read().strip().split('\n') + self.frames = int(lines.pop()) + + key_frames = {int(line.split()[0]): int(line.split()[1]) for line in lines} + self.key_frames = OrderedDict(sorted(key_frames.items(), key=lambda x: x[0])) + + @property + def frame_sizes(self): + container = self._open_video_container(self.source_path, 'r') + video_stream = self._get_video_stream(container) + container.seek(offset=self.key_frames.get(next(iter(self.key_frames))), stream=video_stream) + for packet in container.demux(video_stream): + for frame in packet.decode(): + self._close_video_container(container) + return (frame.width, frame.height) + + def save_meta_info(self): + with open(self.meta_path, 'w') as meta_file: + for index, pts in self.key_frames.items(): + meta_file.write('{} {}\n'.format(index, pts)) + + def check_key_frame(self, container, video_stream, key_frame): + for packet in container.demux(video_stream): + for frame in packet.decode(): + assert frame.pts == key_frame[1], "Uploaded meta information does not match the video" + return + + def check_seek_key_frames(self): + container = self._open_video_container(self.source_path, mode='r') + video_stream = self._get_video_stream(container) + + for key_frame in self.key_frames.items(): + container.seek(offset=key_frame[1], stream=video_stream) + self.check_key_frame(container, video_stream, key_frame) + + self._close_video_container(container) + + def check_frames_numbers(self): + container = self._open_video_container(self.source_path, mode='r') + video_stream = self._get_video_stream(container) + # not all videos contain information about numbers of frames + if video_stream.frames: + self._close_video_container(container) + assert video_stream.frames == self.frames, "Uploaded meta information does not match the video" + return + self._close_video_container(container) + +def prepare_meta(media_file, upload_dir=None, meta_dir=None): + paths = { + 'source_path': os.path.join(upload_dir, media_file) if upload_dir else media_file, + 'meta_path': os.path.join(meta_dir, 'meta_info.txt') if meta_dir else os.path.join(upload_dir, 'meta_info.txt'), + } + analyzer = AnalyzeVideo(source_path=paths.get('source_path')) + analyzer.check_type_first_frame() + analyzer.check_video_timestamps_sequences() + + meta_info = PrepareInfo(source_path=paths.get('source_path'), + meta_path=paths.get('meta_path')) + meta_info.save_key_frames() + meta_info.check_seek_key_frames() + meta_info.save_meta_info() + + return meta_info + +def prepare_meta_for_upload(func, *args): + meta_info = func(*args) + with open(meta_info.meta_path, 'a') as meta_file: + meta_file.write(str(meta_info.get_task_size())) diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index e89877295088..33a17b4f960b 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -6,6 +6,7 @@ import itertools import os import sys +from re import findall import rq import shutil from traceback import print_exception @@ -16,6 +17,7 @@ from cvat.apps.engine.media_extractors import get_mime, MEDIA_TYPES, Mpeg4ChunkWriter, ZipChunkWriter, Mpeg4CompressedChunkWriter, ZipCompressedChunkWriter from cvat.apps.engine.models import DataChoice, StorageMethodChoice from cvat.apps.engine.utils import av_scan_paths +from cvat.apps.engine.prepare import prepare_meta import django_rq from django.conf import settings @@ -24,7 +26,6 @@ from . import models from .log import slogger -from .prepare import PrepareInfo, AnalyzeVideo ############################# Low Level server API @@ -105,7 +106,7 @@ def _save_task_to_db(db_task): db_task.data.save() db_task.save() -def _count_files(data): +def _count_files(data, meta_info_file=None): share_root = settings.SHARE_ROOT server_files = [] @@ -132,11 +133,12 @@ def count_files(file_mapping, counter): mime = get_mime(full_path) if mime in counter: counter[mime].append(rel_path) + elif findall('meta_info.txt$', rel_path): + meta_info_file.append(rel_path) else: slogger.glob.warn("Skip '{}' file (its mime type doesn't " "correspond to a video or an image file)".format(full_path)) - counter = { media_type: [] for media_type in MEDIA_TYPES.keys() } count_files( @@ -151,7 +153,7 @@ def count_files(file_mapping, counter): return counter -def _validate_data(counter): +def _validate_data(counter, meta_info_file=None): unique_entries = 0 multiple_entries = 0 for media_type, media_config in MEDIA_TYPES.items(): @@ -161,6 +163,9 @@ def _validate_data(counter): else: multiple_entries += len(counter[media_type]) + if meta_info_file and media_type != 'video': + raise Exception('File with meta information can only be uploaded with video file') + if unique_entries == 1 and multiple_entries > 0 or unique_entries > 1: unique_types = ', '.join([k for k, v in MEDIA_TYPES.items() if v['unique']]) multiply_types = ', '.join([k for k, v in MEDIA_TYPES.items() if not v['unique']]) @@ -219,8 +224,12 @@ def _create_thread(tid, data): if data['remote_files']: data['remote_files'] = _download_data(data['remote_files'], upload_dir) - media = _count_files(data) - media, task_mode = _validate_data(media) + meta_info_file = [] + media = _count_files(data, meta_info_file) + media, task_mode = _validate_data(media, meta_info_file) + if meta_info_file: + assert settings.USE_CACHE and db_data.storage_method == StorageMethodChoice.CACHE, \ + "File with meta information can be uploaded if 'Use cache' option is also selected" if data['server_files']: _copy_data_from_share(data['server_files'], upload_dir) @@ -288,24 +297,39 @@ def update_progress(progress): if media_files: if task_mode == MEDIA_TYPES['video']['mode']: try: - analyzer = AnalyzeVideo(source_path=os.path.join(upload_dir, media_files[0])) - analyzer.check_type_first_frame() - analyzer.check_video_timestamps_sequences() - - meta_info = PrepareInfo(source_path=os.path.join(upload_dir, media_files[0]), - meta_path=os.path.join(upload_dir, 'meta_info.txt')) - meta_info.save_key_frames() - meta_info.check_seek_key_frames() - meta_info.save_meta_info() + if meta_info_file: + try: + from cvat.apps.engine.prepare import UploadedMeta + meta_info = UploadedMeta(source_path=os.path.join(upload_dir, media_files[0]), + meta_path=os.path.join(upload_dir, meta_info_file[0])) + meta_info.check_seek_key_frames() + meta_info.check_frames_ratio(db_data.chunk_size) + meta_info.check_frames_numbers() + meta_info.save_meta_info() + except AssertionError as ex: + job.meta['status'] = str(ex) + job.save_meta() + meta_info = prepare_meta(media_file=media_files[0], upload_dir=upload_dir) + meta_info.check_frames_ratio(db_data.chunk_size) + except Exception as ex: + job.meta['status'] = 'Invalid meta information was upload. Start prepare valid meta information.' + job.save_meta() + meta_info = prepare_meta(media_file=media_files[0], upload_dir=upload_dir) + meta_info.check_frames_ratio(db_data.chunk_size) + else: + meta_info = prepare_meta(media_file=media_files[0], upload_dir=upload_dir) + meta_info.check_frames_ratio(db_data.chunk_size) all_frames = meta_info.get_task_size() + video_size = meta_info.frame_sizes + db_data.size = len(range(db_data.start_frame, min(data['stop_frame'] + 1 if data['stop_frame'] else all_frames, all_frames), db_data.get_frame_step())) video_path = os.path.join(upload_dir, media_files[0]) - frame = meta_info.key_frames.get(next(iter(meta_info.key_frames))) - video_size = (frame.width, frame.height) except Exception: db_data.storage_method = StorageMethodChoice.FILE_SYSTEM + if os.path.exists(db_data.get_meta_path()): + os.remove(db_data.get_meta_path()) else:#images,archive counter_ = itertools.count() From 89cb669a863f86012715f2321af9f54844e28774 Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 22 Sep 2020 18:26:36 +0300 Subject: [PATCH 02/10] Added documentation for data on the fly preparation --- cvat/apps/documentation/data_on_fly.md | 30 ++++++++++++++++++ .../static/documentation/images/image128.jpg | Bin 43218 -> 0 bytes .../images/image128_use_cache.jpg | Bin 0 -> 38770 bytes cvat/apps/documentation/user_guide.md | 9 +++++- 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 cvat/apps/documentation/data_on_fly.md delete mode 100644 cvat/apps/documentation/static/documentation/images/image128.jpg create mode 100644 cvat/apps/documentation/static/documentation/images/image128_use_cache.jpg diff --git a/cvat/apps/documentation/data_on_fly.md b/cvat/apps/documentation/data_on_fly.md new file mode 100644 index 000000000000..208af1aed76c --- /dev/null +++ b/cvat/apps/documentation/data_on_fly.md @@ -0,0 +1,30 @@ + +# Data preparation on the fly + +## Description +Data on the fly processing is a way of working with data, the main idea of which is as follows: +Minimum necessary meta information is collected, when task is created. This meta information allows in the future to create a necessary chunks when receiving a request from a client. + +Generated chunks are stored in a cache of limited size with a policy of evicting less popular items. + +When a request received from a client, the required chunk is searched for in the cache. If the chunk does not exist yet, it is created using a prepared meta information and then put into the cache. + +This method of working with data allows: + - reduce the task creation time. + - store data in a cache of limited size with a policy of evicting less popular items. + +## Prepare meta information +Different meta information is collected for different types of uploaded data. +### Video +For video, this is a valid mapping of key frame numbers and their timestamps. This information is saved to `meta_info.txt`. + +Unfortunately, this method will not work for all videos with valid meta information. If there are not enough keyframes in the video for smooth video decoding, the task will be created in the old way. + +#### Uploading meta information along with data + +When creating a task, you can upload a file with meta information along with the video, which will further reduce the time for creating a task. You can see how to prepare meta information [here](/utils/prepare_meta_information/README.md). + +It is worth noting that the generated file also contains information about the number of frames in the video at the end. + +### Images +Mapping of chunk number and paths to images that should enter the chunk is saved at the time of creating a task in a files `dummy_{chunk_number}.txt` diff --git a/cvat/apps/documentation/static/documentation/images/image128.jpg b/cvat/apps/documentation/static/documentation/images/image128.jpg deleted file mode 100644 index e1e196d5bc508718f3abf79263412a1fe26633ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43218 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 Date: Tue, 22 Sep 2020 18:28:04 +0300 Subject: [PATCH 03/10] Added ability to prepare meta information for video manually --- utils/prepare_meta_information/README.md | 19 ++++++++++++++++ utils/prepare_meta_information/prepare.py | 27 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 utils/prepare_meta_information/README.md create mode 100644 utils/prepare_meta_information/prepare.py diff --git a/utils/prepare_meta_information/README.md b/utils/prepare_meta_information/README.md new file mode 100644 index 000000000000..dab81ae1a5c9 --- /dev/null +++ b/utils/prepare_meta_information/README.md @@ -0,0 +1,19 @@ +# Simple command line for prepare meta information for video data + +**Usage** +```bash +usage: prepare.py [-h] video_file meta_directory + +positional arguments: + video_file Path to video file + meta_directory Directory where the file with meta information will be saved + +optional arguments: + -h, --help show this help message and exit +``` + +**Examples** + +```bash +python prepare.py ~/Documents/some_video.mp4 ~/Documents +``` diff --git a/utils/prepare_meta_information/prepare.py b/utils/prepare_meta_information/prepare.py new file mode 100644 index 000000000000..de4ccfbf532e --- /dev/null +++ b/utils/prepare_meta_information/prepare.py @@ -0,0 +1,27 @@ +import argparse +import sys +import os + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument('video_file', + type=str, + help='Path to video file') + parser.add_argument('meta_directory', + type=str, + help='Directory where the file with meta information will be saved') + + return parser.parse_args() + +def main(): + args = get_args() + try: + prepare_meta_for_upload(prepare_meta, args.video_file, None, args.meta_directory) + except Exception: + print('Impossible to prepare meta information') + +if __name__ == "__main__": + base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + sys.path.append(base_dir) + from cvat.apps.engine.prepare import prepare_meta, prepare_meta_for_upload + main() \ No newline at end of file From c88f6450e3423e8551c2f56203676d0b950a3a65 Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 22 Sep 2020 18:29:49 +0300 Subject: [PATCH 04/10] fix --- cvat/settings/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cvat/settings/base.py b/cvat/settings/base.py index d05ca3900861..780485a7cb07 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -427,14 +427,14 @@ def generate_ssh_keys(): ), } +# http://www.grantjenks.com/docs/diskcache/tutorial.html#djangocache CACHES = { 'default' : { 'BACKEND' : 'diskcache.DjangoCache', 'LOCATION' : CACHE_ROOT, 'TIMEOUT' : None, 'OPTIONS' : { - #'statistics' :True, - 'size_limit' : 2 ** 40, # 1 тб + 'size_limit' : 2 ** 40, # 1 Tb } } } From 0de46c4e338c08cd3d0135b9d54c1d8f2fc3ba8e Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 29 Sep 2020 22:27:40 +0300 Subject: [PATCH 05/10] style: fix codacy issues --- cvat/apps/documentation/data_on_fly.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/cvat/apps/documentation/data_on_fly.md b/cvat/apps/documentation/data_on_fly.md index 208af1aed76c..87c9926447a0 100644 --- a/cvat/apps/documentation/data_on_fly.md +++ b/cvat/apps/documentation/data_on_fly.md @@ -1,30 +1,35 @@ - # Data preparation on the fly ## Description Data on the fly processing is a way of working with data, the main idea of which is as follows: -Minimum necessary meta information is collected, when task is created. This meta information allows in the future to create a necessary chunks when receiving a request from a client. +Minimum necessary meta information is collected, when task is created. +This meta information allows in the future to create a necessary chunks when receiving a request from a client. Generated chunks are stored in a cache of limited size with a policy of evicting less popular items. -When a request received from a client, the required chunk is searched for in the cache. If the chunk does not exist yet, it is created using a prepared meta information and then put into the cache. +When a request received from a client, the required chunk is searched for in the cache. +If the chunk does not exist yet, it is created using a prepared meta information and then put into the cache. This method of working with data allows: - - reduce the task creation time. - - store data in a cache of limited size with a policy of evicting less popular items. +- reduce the task creation time. +- store data in a cache of limited size with a policy of evicting less popular items. ## Prepare meta information Different meta information is collected for different types of uploaded data. ### Video For video, this is a valid mapping of key frame numbers and their timestamps. This information is saved to `meta_info.txt`. -Unfortunately, this method will not work for all videos with valid meta information. If there are not enough keyframes in the video for smooth video decoding, the task will be created in the old way. +Unfortunately, this method will not work for all videos with valid meta information. +If there are not enough keyframes in the video for smooth video decoding, the task will be created in the old way. #### Uploading meta information along with data -When creating a task, you can upload a file with meta information along with the video, which will further reduce the time for creating a task. You can see how to prepare meta information [here](/utils/prepare_meta_information/README.md). +When creating a task, you can upload a file with meta information along with the video, +which will further reduce the time for creating a task. +You can see how to prepare meta information [here](/utils/prepare_meta_information/README.md). It is worth noting that the generated file also contains information about the number of frames in the video at the end. ### Images -Mapping of chunk number and paths to images that should enter the chunk is saved at the time of creating a task in a files `dummy_{chunk_number}.txt` +Mapping of chunk number and paths to images that should enter the chunk +is saved at the time of creating a task in a files `dummy_{chunk_number}.txt` From 17a22be2ef8947a256d7e62a53c740ba1f41b4d0 Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 29 Sep 2020 22:49:18 +0300 Subject: [PATCH 06/10] Refactoring --- cvat/apps/engine/prepare.py | 16 ++++----- cvat/apps/engine/task.py | 40 +++++++++++++++-------- utils/prepare_meta_information/prepare.py | 9 ++++- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/cvat/apps/engine/prepare.py b/cvat/apps/engine/prepare.py index 5bfffb13cbbd..9465b680f3a0 100644 --- a/cvat/apps/engine/prepare.py +++ b/cvat/apps/engine/prepare.py @@ -76,7 +76,7 @@ def get_task_size(self): @property def frame_sizes(self): - frame = self.key_frames.get(next(iter(self.key_frames))) + frame = next(iter(self.key_frames.values())) return (frame.width, frame.height) def check_key_frame(self, container, video_stream, key_frame): @@ -97,8 +97,7 @@ def check_seek_key_frames(self): self.check_key_frame(container, video_stream, key_frame) def check_frames_ratio(self, chunk_size): - if not len(self.key_frames) or (len(self.key_frames) and (self.frames // len(self.key_frames)) > chunk_size): - raise Exception('Too few keyframes for smooth video decoding') + return (len(self.key_frames) and (self.frames // len(self.key_frames)) <= 2 * chunk_size) def save_key_frames(self): container = self._open_video_container(self.source_path, mode='r') @@ -175,7 +174,7 @@ def __init__(self, **kwargs): def frame_sizes(self): container = self._open_video_container(self.source_path, 'r') video_stream = self._get_video_stream(container) - container.seek(offset=self.key_frames.get(next(iter(self.key_frames))), stream=video_stream) + container.seek(offset=next(iter(self.key_frames.values())), stream=video_stream) for packet in container.demux(video_stream): for frame in packet.decode(): self._close_video_container(container) @@ -212,7 +211,7 @@ def check_frames_numbers(self): return self._close_video_container(container) -def prepare_meta(media_file, upload_dir=None, meta_dir=None): +def prepare_meta(media_file, upload_dir=None, meta_dir=None, chunk_size=None): paths = { 'source_path': os.path.join(upload_dir, media_file) if upload_dir else media_file, 'meta_path': os.path.join(meta_dir, 'meta_info.txt') if meta_dir else os.path.join(upload_dir, 'meta_info.txt'), @@ -226,10 +225,11 @@ def prepare_meta(media_file, upload_dir=None, meta_dir=None): meta_info.save_key_frames() meta_info.check_seek_key_frames() meta_info.save_meta_info() - - return meta_info + smooth_decoding = meta_info.check_frames_ratio(chunk_size) if chunk_size else None + return (meta_info, smooth_decoding) def prepare_meta_for_upload(func, *args): - meta_info = func(*args) + meta_info, smooth_decoding = func(*args) with open(meta_info.meta_path, 'a') as meta_file: meta_file.write(str(meta_info.get_task_size())) + return smooth_decoding diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 33a17b4f960b..ed493cb5cbf4 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -300,36 +300,48 @@ def update_progress(progress): if meta_info_file: try: from cvat.apps.engine.prepare import UploadedMeta + if os.path.split(meta_info_file[0])[0]: + os.replace( + os.path.join(upload_dir, meta_info_file[0]), + db_data.get_meta_path() + ) meta_info = UploadedMeta(source_path=os.path.join(upload_dir, media_files[0]), - meta_path=os.path.join(upload_dir, meta_info_file[0])) + meta_path=db_data.get_meta_path()) meta_info.check_seek_key_frames() - meta_info.check_frames_ratio(db_data.chunk_size) meta_info.check_frames_numbers() meta_info.save_meta_info() - except AssertionError as ex: - job.meta['status'] = str(ex) - job.save_meta() - meta_info = prepare_meta(media_file=media_files[0], upload_dir=upload_dir) - meta_info.check_frames_ratio(db_data.chunk_size) + assert len(meta_info.key_frames) > 0, 'No key frames.' except Exception as ex: - job.meta['status'] = 'Invalid meta information was upload. Start prepare valid meta information.' + base_msg = str(ex) if isinstance(ex, AssertionError) else \ + 'Invalid meta information was upload.' + job.meta['status'] = '{} Start prepare valid meta information.'.format(base_msg) job.save_meta() - meta_info = prepare_meta(media_file=media_files[0], upload_dir=upload_dir) - meta_info.check_frames_ratio(db_data.chunk_size) + meta_info, smooth_decoding = prepare_meta( + media_file=media_files[0], + upload_dir=upload_dir, + chunk_size=db_data.chunk_size + ) + assert smooth_decoding == True, 'Too few keyframes for smooth video decoding.' else: - meta_info = prepare_meta(media_file=media_files[0], upload_dir=upload_dir) - meta_info.check_frames_ratio(db_data.chunk_size) + meta_info, smooth_decoding = prepare_meta( + media_file=media_files[0], + upload_dir=upload_dir, + chunk_size=db_data.chunk_size + ) + assert smooth_decoding == True, 'Too few keyframes for smooth video decoding.' all_frames = meta_info.get_task_size() video_size = meta_info.frame_sizes db_data.size = len(range(db_data.start_frame, min(data['stop_frame'] + 1 if data['stop_frame'] else all_frames, all_frames), db_data.get_frame_step())) video_path = os.path.join(upload_dir, media_files[0]) - - except Exception: + except Exception as ex: db_data.storage_method = StorageMethodChoice.FILE_SYSTEM if os.path.exists(db_data.get_meta_path()): os.remove(db_data.get_meta_path()) + base_msg = str(ex) if isinstance(ex, AssertionError) else "Uploaded video does not support a quick way of task creating." + job.meta['status'] = "{} The task will be created using the old method".format(base_msg) + job.save_meta() else:#images,archive counter_ = itertools.count() diff --git a/utils/prepare_meta_information/prepare.py b/utils/prepare_meta_information/prepare.py index de4ccfbf532e..20727d511a0f 100644 --- a/utils/prepare_meta_information/prepare.py +++ b/utils/prepare_meta_information/prepare.py @@ -10,13 +10,20 @@ def get_args(): parser.add_argument('meta_directory', type=str, help='Directory where the file with meta information will be saved') + parser.add_argument('-chunk_size', + type=int, + help='Chunk size that will be specified when creating the task with specified video and generated meta information') return parser.parse_args() def main(): args = get_args() try: - prepare_meta_for_upload(prepare_meta, args.video_file, None, args.meta_directory) + smooth_decoding = prepare_meta_for_upload(prepare_meta, args.video_file, None, args.meta_directory, args.chunk_size) + print('Meta information for video has been prepared') + + if smooth_decoding != None and not smooth_decoding: + print('NOTE: prepared meta information contains too few key frames for smooth decoding.') except Exception: print('Impossible to prepare meta information') From 313b2208e5447d0dd70ff0ec6b5a59a6ad622168 Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 29 Sep 2020 22:51:13 +0300 Subject: [PATCH 07/10] docs: add optional parameter --- utils/prepare_meta_information/README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/utils/prepare_meta_information/README.md b/utils/prepare_meta_information/README.md index dab81ae1a5c9..92d587747627 100644 --- a/utils/prepare_meta_information/README.md +++ b/utils/prepare_meta_information/README.md @@ -2,16 +2,26 @@ **Usage** ```bash -usage: prepare.py [-h] video_file meta_directory +usage: prepare.py [-h] [-chunk_size CHUNK_SIZE] video_file meta_directory positional arguments: - video_file Path to video file - meta_directory Directory where the file with meta information will be saved + video_file Path to video file + meta_directory Directory where the file with meta information will be saved optional arguments: - -h, --help show this help message and exit + -h, --help show this help message and exit + -chunk_size CHUNK_SIZE + Chunk size that will be specified when creating the task with specified video and generated meta information ``` +**NOTE**: For smooth video decoding, the `chunk size` must be greater than or equal to the ratio of number of frames +to a number of key frames. +You can understand the approximate `chunk size` by preparing and looking at the file with meta information. + +**NOTE**: If ratio of number of frames to number of key frames is small compared to the `chunk size`, +then when creating a task with prepared meta information, you should expect that the waiting time for some chunks +will be longer than the waiting time for other chunks. (At the first iteration, when there is no chunk in the cache) + **Examples** ```bash From 066d1ce6b47ae6c1798240691e25de7fd6ccd461 Mon Sep 17 00:00:00 2001 From: Maya Date: Wed, 30 Sep 2020 08:26:57 +0300 Subject: [PATCH 08/10] Add test --- cvat/apps/engine/tests/_test_rest_api.py | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/cvat/apps/engine/tests/_test_rest_api.py b/cvat/apps/engine/tests/_test_rest_api.py index 374a4cd2e213..67367063e6b2 100644 --- a/cvat/apps/engine/tests/_test_rest_api.py +++ b/cvat/apps/engine/tests/_test_rest_api.py @@ -80,6 +80,7 @@ def _setUpModule(): from cvat.apps.engine.models import (AttributeType, Data, Job, Project, Segment, StatusChoice, Task, StorageMethodChoice) +from cvat.apps.engine.prepare import prepare_meta, prepare_meta_for_upload _setUpModule() @@ -1619,6 +1620,8 @@ def tearDownClass(cls): path = os.path.join(settings.SHARE_ROOT, "videos", "test_video_1.mp4") os.remove(path) + path = os.path.join(settings.SHARE_ROOT, "videos", "meta_info.txt") + os.remove(path) def _run_api_v1_tasks_id_data_post(self, tid, user, data): with ForceLogin(user, self.client): @@ -1986,6 +1989,31 @@ def _test_api_v1_tasks_id_data(self, user): self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.IMAGESET, image_sizes, StorageMethodChoice.CACHE) + prepare_meta_for_upload( + prepare_meta, + os.path.join(settings.SHARE_ROOT, "videos", "test_video_1.mp4"), + os.path.join(settings.SHARE_ROOT, "videos") + ) + task_spec = { + "name": "my video with meta info task #11", + "overlap": 0, + "segment_size": 0, + "labels": [ + {"name": "car"}, + {"name": "person"}, + ] + } + task_data = { + "server_files[0]": os.path.join("videos", "test_video_1.mp4"), + "server_files[1]": os.path.join("videos", "meta_info.txt"), + "image_quality": 70, + "use_cache": True + } + image_sizes = self._image_sizes[task_data['server_files[0]']] + + self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.VIDEO, + self.ChunkType.VIDEO, image_sizes, StorageMethodChoice.CACHE) + def test_api_v1_tasks_id_data_admin(self): self._test_api_v1_tasks_id_data(self.admin) From 1ead5704bef8f544cab5042f23c5b9d531779538 Mon Sep 17 00:00:00 2001 From: Maya Date: Wed, 30 Sep 2020 09:27:15 +0300 Subject: [PATCH 09/10] Add license header --- utils/prepare_meta_information/prepare.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utils/prepare_meta_information/prepare.py b/utils/prepare_meta_information/prepare.py index 20727d511a0f..0cd200a0c866 100644 --- a/utils/prepare_meta_information/prepare.py +++ b/utils/prepare_meta_information/prepare.py @@ -1,3 +1,6 @@ +# Copyright (C) 2020 Intel Corporation +# +# SPDX-License-Identifier: MIT import argparse import sys import os From 3608fa85feeca7f5a76d9b56c3697aeb60787f28 Mon Sep 17 00:00:00 2001 From: Maya Date: Wed, 30 Sep 2020 12:17:59 +0300 Subject: [PATCH 10/10] Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8c70e3d4235..a69698501221 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 It supports regular navigation, searching a frame according to annotations filters and searching the nearest frame without any annotations () - MacOS users notes in CONTRIBUTING.md +- Ability to prepare meta information manually () +- Ability to upload prepared meta information along with a video when creating a task () ### Changed - UI models (like DEXTR) were redesigned to be more interactive () @@ -44,6 +46,7 @@ filters and searching the nearest frame without any annotations () - Fixed use case when UI throws exception: Cannot read property 'objectType' of undefined #2053 () - Fixed use case when logs could be saved twice or more times #2202 () +- Fixed issues from #2112 () ### Security -