From 8abadaff31fa921c3c0261cf40abc3d31ef73725 Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 28 Jul 2020 14:44:21 +0300 Subject: [PATCH 001/467] Added cache integration settings --- cvat/settings/base.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 9155f2b3..3f161fbb 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -333,6 +333,9 @@ os.makedirs(DATA_ROOT, exist_ok=True) MEDIA_DATA_ROOT = os.path.join(DATA_ROOT, 'data') os.makedirs(MEDIA_DATA_ROOT, exist_ok=True) +CACHE_ROOT = os.path.join(DATA_ROOT, 'cache') +os.makedirs(CACHE_ROOT, exist_ok=True) + TASKS_ROOT = os.path.join(DATA_ROOT, 'tasks') os.makedirs(TASKS_ROOT, exist_ok=True) @@ -431,3 +434,17 @@ RESTRICTIONS = { 'engine.role.admin', ), } + +CACHES = { + 'default' : { + 'BACKEND' : 'diskcache.DjangoCache', + 'LOCATION' : CACHE_ROOT, + 'TIMEOUT' : None, + 'OPTIONS' : { + #'statistics' :True, + 'size_limit' : 2 ** 40, # 1 тб + } + } +} + +USE_CACHE = True From 08b195f05f9ec0df2cb834174f92a3a4bebe85cb Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 28 Jul 2020 14:47:24 +0300 Subject: [PATCH 002/467] Added preparation of meta information --- cvat/apps/engine/prepare.py | 76 +++++++++++++++++++++++++++++++++++++ cvat/apps/engine/task.py | 18 +++++++++ 2 files changed, 94 insertions(+) create mode 100644 cvat/apps/engine/prepare.py diff --git a/cvat/apps/engine/prepare.py b/cvat/apps/engine/prepare.py new file mode 100644 index 00000000..18beaa80 --- /dev/null +++ b/cvat/apps/engine/prepare.py @@ -0,0 +1,76 @@ +import av + +class PrepareInfo: + + def __init__(self, source_path, meta_path): + self.source_path = source_path + self.meta_path = meta_path + + def _open_video_container(self, sourse_path, mode, options=None): + return av.open(sourse_path, mode=mode, options=options) + + def _close_video_container(self, container): + container.close() + + def _get_video_stream(self, container): + video_stream = next(stream for stream in container.streams if stream.type == 'video') + video_stream.thread_type = 'AUTO' + return video_stream + + #@get_execution_time + def save_meta_info(self): + container = self._open_video_container(self.source_path, mode='r') + video_stream = self._get_video_stream(container) + + with open(self.meta_path, 'w') as file: + frame_number = 0 + + for packet in container.demux(video_stream): + for frame in packet.decode(): + frame_number += 1 + if frame.key_frame: + file.write('{} {}\n'.format(frame_number, frame.pts)) + + self._close_video_container(container) + return frame_number# == task_size + + def get_nearest_left_key_frame(self, start_chunk_frame_number): + start_decode_frame_number = 0 + start_decode_timestamp = 0 + + with open(self.meta_path, 'r') as file: + for line in file: + frame_number, timestamp = line.strip().split(' ') + + #TODO: исправить если вдруг ключевой кадр окажется не первым + if int(frame_number) <= start_chunk_frame_number: + start_decode_frame_number = frame_number + start_decode_timestamp = timestamp + else: + break + + return int(start_decode_frame_number), int(start_decode_timestamp) + + def decode_needed_frames(self, chunk_number, chunk_size): + start_chunk_frame_number = (chunk_number - 1) * chunk_size + 1 + end_chunk_frame_number = start_chunk_frame_number + chunk_size #- 1 + start_decode_frame_number, start_decode_timestamp = self.get_nearest_left_key_frame(start_chunk_frame_number) + extra_frames = start_chunk_frame_number - start_decode_frame_number + + container = self._open_video_container(self.source_path, mode='r') + video_stream = self._get_video_stream(container) + container.seek(offset=start_decode_timestamp, stream=video_stream) + + frame_number = start_decode_frame_number - 1 + for packet in container.demux(video_stream): + for frame in packet.decode(): + frame_number += 1 + if frame_number < start_chunk_frame_number: + continue + elif frame_number < end_chunk_frame_number: + yield frame + else: + self._close_video_container(container) + return + + self._close_video_container(container) diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 998bc4af..804ab1b4 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -24,6 +24,8 @@ from distutils.dir_util import copy_tree from . import models from .log import slogger +from .prepare import PrepareInfo +from diskcache import Cache ############################# Low Level server API @@ -230,6 +232,22 @@ def _create_thread(tid, data): job.meta['status'] = 'Media files are being extracted...' job.save_meta() + if settings.USE_CACHE: + for media_type, media_files in media.items(): + if media_files: + if task_mode == MEDIA_TYPES['video']['mode']: + 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_meta_info() + # else: + # with Cache(settings.CACHE_ROOT) as cache: + # counter_ = itertools.count(start=1) + + #TODO: chunk size + # for chunk_number, media_paths in itertools.groupby(media_files, lambda x: next(counter_) // db_data.chunk_size): + # cache.set('{}_{}'.format(tid, chunk_number), media_paths, tag='dummy') + #else: + db_images = [] extractor = None From 5fa50be1fad0b42c3d86ff6fd96ef21bd3810a72 Mon Sep 17 00:00:00 2001 From: Maya Date: Mon, 10 Aug 2020 09:54:56 +0300 Subject: [PATCH 003/467] Added most of video processing & cache implementation --- cvat/apps/engine/media_extractors.py | 62 ++++++++++++- cvat/apps/engine/models.py | 3 + cvat/apps/engine/prepare.py | 128 +++++++++++++++++++++++---- cvat/apps/engine/task.py | 128 +++++++++++++++++---------- cvat/apps/engine/views.py | 60 ++++++++++++- cvat/requirements/base.txt | 1 + 6 files changed, 315 insertions(+), 67 deletions(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index dea14183..c2bc020f 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -285,6 +285,19 @@ class ZipChunkWriter(IChunkWriter): # and does not decode it to know img size. return [] + def save_as_chunk_to_buff(self, images, format_='jpeg'): + buff = io.BytesIO() + + with zipfile.ZipFile(buff, 'w') as zip_file: + for idx, image in enumerate(images): + arcname = '{:06d}.{}'.format(idx, format_) + if isinstance(image, av.VideoFrame): + zip_file.writestr(arcname, image.to_image().tobytes().getvalue()) + else: + zip_file.write(filename=image, arcname=arcname) + buff.seek(0) + return buff + class ZipCompressedChunkWriter(IChunkWriter): def save_as_chunk(self, images, chunk_path): image_sizes = [] @@ -297,20 +310,30 @@ class ZipCompressedChunkWriter(IChunkWriter): return image_sizes + def save_as_chunk_to_buff(self, images, format_='jpeg'): + buff = io.BytesIO() + with zipfile.ZipFile(buff, 'w') as zip_file: + for idx, image in enumerate(images): + (_, _, image_buf) = self._compress_image(image, self._image_quality) + arcname = '{:06d}.{}'.format(idx, format_) + zip_file.writestr(arcname, image_buf.getvalue()) + buff.seek(0) + return buff + class Mpeg4ChunkWriter(IChunkWriter): def __init__(self, _): super().__init__(17) self._output_fps = 25 @staticmethod - def _create_av_container(path, w, h, rate, options): + def _create_av_container(path, w, h, rate, options, f=None): # x264 requires width and height must be divisible by 2 for yuv420p if h % 2: h += 1 if w % 2: w += 1 - container = av.open(path, 'w') + container = av.open(path, 'w',format=f) video_stream = container.add_stream('libx264', rate=rate) video_stream.pix_fmt = "yuv420p" video_stream.width = w @@ -341,6 +364,41 @@ class Mpeg4ChunkWriter(IChunkWriter): output_container.close() return [(input_w, input_h)] + def save_as_chunk_to_buff(self, frames, format_): + if not frames: + raise Exception('no images to save') + + buff = io.BytesIO() + input_w = frames[0].width + input_h = frames[0].height + + output_container, output_v_stream = self._create_av_container( + path=buff, + w=input_w, + h=input_h, + rate=self._output_fps, + options={ + "crf": str(self._image_quality), + "preset": "ultrafast", + }, + f=format_, + ) + + for frame in frames: + # let libav set the correct pts and time_base + frame.pts = None + frame.time_base = None + + for packet in output_v_stream.encode(frame): + output_container.mux(packet) + + # Flush streams + for packet in output_v_stream.encode(): + output_container.mux(packet) + output_container.close() + buff.seek(0) + return buff + @staticmethod def _encode_images(images, container, stream): for frame, _, _ in images: diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index d4c46eb3..d2f8e26e 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -102,6 +102,9 @@ class Data(models.Model): def get_preview_path(self): return os.path.join(self.get_data_dirname(), 'preview.jpeg') + def get_meta_path(self): + return os.path.join(self.get_upload_dirname(), 'meta_info.txt') + class Video(models.Model): data = models.OneToOneField(Data, on_delete=models.CASCADE, related_name="video", null=True) path = models.CharField(max_length=1024, default='') diff --git a/cvat/apps/engine/prepare.py b/cvat/apps/engine/prepare.py index 18beaa80..01d1c367 100644 --- a/cvat/apps/engine/prepare.py +++ b/cvat/apps/engine/prepare.py @@ -1,10 +1,11 @@ import av +import hashlib -class PrepareInfo: - - def __init__(self, source_path, meta_path): - self.source_path = source_path - self.meta_path = meta_path +class WorkWithVideo: + def __init__(self, **kwargs): + if not kwargs.get('source_path'): + raise Exeption('No sourse path') + self.source_path = kwargs.get('source_path') def _open_video_container(self, sourse_path, mode, options=None): return av.open(sourse_path, mode=mode, options=options) @@ -17,22 +18,114 @@ class PrepareInfo: video_stream.thread_type = 'AUTO' return video_stream - #@get_execution_time - def save_meta_info(self): + +class AnalyzeVideo(WorkWithVideo): + def check_type_first_frame(self): + container = self._open_video_container(self.source_path, mode='r') + video_stream = self._get_video_stream(container) + + for packet in container.demux(video_stream): + for frame in packet.decode(): + self._close_video_container(container) + assert frame.pict_type.name == 'I', 'First frame is not key frame' + return + + def check_video_timestamps_sequences(self): container = self._open_video_container(self.source_path, mode='r') video_stream = self._get_video_stream(container) - with open(self.meta_path, 'w') as file: - frame_number = 0 + frame_pts = -1 + frame_dts = -1 + for packet in container.demux(video_stream): + for frame in packet.decode(): + + if None not in [frame.pts, frame_pts] and frame.pts <= frame_pts: + self._close_video_container(container) + raise Exception('Invalid pts sequences') + + if None not in [frame.dts, frame_dts] and frame.dts <= frame_dts: + self._close_video_container(container) + raise Exception('Invalid dts sequences') + + frame_pts, frame_dts = frame.pts, frame.dts + self._close_video_container(container) + +# class Frame: +# def __init__(self, frame, frame_number=None): +# self.frame = frame +# if frame_number: +# self.frame_number = frame_number + +# def md5_hash(self): +# return hashlib.md5(self.frame.to_image().tobytes()).hexdigest() + +# def __eq__(self, image): +# return self.md5_hash(self) == image.md5_hash(image) and self.frame.pts == image.frame.pts + +# def __ne__(self, image): +# return md5_hash(self) != md5_hash(image) or self.frame.pts != image.frame.pts + +# def __len__(self): +# return (self.frame.width, self.frame.height) + - for packet in container.demux(video_stream): +def md5_hash(frame): + return hashlib.md5(frame.to_image().tobytes()).hexdigest() + +class PrepareInfo(WorkWithVideo): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not kwargs.get('meta_path'): + raise Exception('No meta path') + + self.meta_path = kwargs.get('meta_path') + self.key_frames = {} + self.frames = 0 + + def get_task_size(self): + return self.frames + + 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 _, 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(): - frame_number += 1 - if frame.key_frame: - file.write('{} {}\n'.format(frame_number, frame.pts)) + 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 + + if len(self.key_frames) == 0: #or self.frames // len(self.key_frames) > 300: + raise Exception('Too few keyframes') + + def save_key_frames(self): + container = self._open_video_container(self.source_path, mode='r') + video_stream = self._get_video_stream(container) + frame_number = 0 + for packet in container.demux(video_stream): + for frame in packet.decode(): + if frame.key_frame: + self.key_frames[frame_number] = frame + frame_number += 1 + + self.frames = frame_number self._close_video_container(container) - return frame_number# == task_size + + def save_meta_info(self): + with open(self.meta_path, 'w') as meta_file: + for index, frame in self.key_frames.items(): + meta_file.write('{} {}\n'.format(index, frame.pts)) def get_nearest_left_key_frame(self, start_chunk_frame_number): start_decode_frame_number = 0 @@ -52,10 +145,9 @@ class PrepareInfo: return int(start_decode_frame_number), int(start_decode_timestamp) def decode_needed_frames(self, chunk_number, chunk_size): - start_chunk_frame_number = (chunk_number - 1) * chunk_size + 1 - end_chunk_frame_number = start_chunk_frame_number + chunk_size #- 1 + start_chunk_frame_number = chunk_number * chunk_size + end_chunk_frame_number = start_chunk_frame_number + chunk_size start_decode_frame_number, start_decode_timestamp = self.get_nearest_left_key_frame(start_chunk_frame_number) - extra_frames = start_chunk_frame_number - start_decode_frame_number container = self._open_video_container(self.source_path, mode='r') video_stream = self._get_video_stream(container) @@ -73,4 +165,4 @@ class PrepareInfo: self._close_video_container(container) return - self._close_video_container(container) + self._close_video_container(container) \ No newline at end of file diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 804ab1b4..b468d4ef 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -24,7 +24,7 @@ from distutils.dir_util import copy_tree from . import models from .log import slogger -from .prepare import PrepareInfo +from .prepare import PrepareInfo, AnalyzeVideo from diskcache import Cache ############################# Low Level server API @@ -232,22 +232,6 @@ def _create_thread(tid, data): job.meta['status'] = 'Media files are being extracted...' job.save_meta() - if settings.USE_CACHE: - for media_type, media_files in media.items(): - if media_files: - if task_mode == MEDIA_TYPES['video']['mode']: - 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_meta_info() - # else: - # with Cache(settings.CACHE_ROOT) as cache: - # counter_ = itertools.count(start=1) - - #TODO: chunk size - # for chunk_number, media_paths in itertools.groupby(media_files, lambda x: next(counter_) // db_data.chunk_size): - # cache.set('{}_{}'.format(tid, chunk_number), media_paths, tag='dummy') - #else: - db_images = [] extractor = None @@ -294,37 +278,91 @@ def _create_thread(tid, data): else: db_data.chunk_size = 36 + #it's better to add the field to the Task model + video_suitable_on_the_fly_processing = True + video_path = "" video_size = (0, 0) - counter = itertools.count() - generator = itertools.groupby(extractor, lambda x: next(counter) // db_data.chunk_size) - for chunk_idx, chunk_data in generator: - chunk_data = list(chunk_data) - original_chunk_path = db_data.get_original_chunk_path(chunk_idx) - original_chunk_writer.save_as_chunk(chunk_data, original_chunk_path) - - compressed_chunk_path = db_data.get_compressed_chunk_path(chunk_idx) - img_sizes = compressed_chunk_writer.save_as_chunk(chunk_data, compressed_chunk_path) - - if db_task.mode == 'annotation': - db_images.extend([ - models.Image( - data=db_data, - path=os.path.relpath(data[1], upload_dir), - frame=data[2], - width=size[0], - height=size[1]) - - for data, size in zip(chunk_data, img_sizes) - ]) - else: - video_size = img_sizes[0] - video_path = chunk_data[0][1] + if settings.USE_CACHE: + for media_type, media_files in media.items(): + if media_files: + if task_mode == MEDIA_TYPES['video']['mode']: + try: + analizer = AnalyzeVideo(source_path=os.path.join(upload_dir, media_files[0])) + analizer.check_type_first_frame() + analizer.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.test_seek() + meta_info.check_seek_key_frames() + meta_info.save_meta_info() + + db_data.size = meta_info.get_task_size() + 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 AssertionError as ex: + video_suitable_on_the_fly_processing = False + except Exception as ex: + video_suitable_on_the_fly_processing = False + + else:#images, TODO:archive + with Cache(settings.CACHE_ROOT) as cache: + counter_ = itertools.count() + + for chunk_number, media_paths in itertools.groupby(media_files, lambda x: next(counter_) // db_data.chunk_size): + media_paths = list(media_paths) + cache.set('{}_{}'.format(tid, chunk_number), [os.path.join(upload_dir, file_name) for file_name in media_paths], tag='dummy') + + img_sizes = [] + from PIL import Image + for media_path in media_paths: + img_sizes += [Image.open(os.path.join(upload_dir, media_path)).size] + db_data.size += len(media_paths) + db_images.extend([ + models.Image( + data=db_data, + path=data[1], + frame=data[0], + width=size[0], + height=size[1]) + for data, size in zip(enumerate(media_paths), img_sizes) + ]) + + + if db_task.mode == 'interpolation' and not video_suitable_on_the_fly_processing or not settings.USE_CACHE: + counter = itertools.count() + generator = itertools.groupby(extractor, lambda x: next(counter) // db_data.chunk_size) + for chunk_idx, chunk_data in generator: + chunk_data = list(chunk_data) + original_chunk_path = db_data.get_original_chunk_path(chunk_idx) + original_chunk_writer.save_as_chunk(chunk_data, original_chunk_path) + + compressed_chunk_path = db_data.get_compressed_chunk_path(chunk_idx) + img_sizes = compressed_chunk_writer.save_as_chunk(chunk_data, compressed_chunk_path) + + if db_task.mode == 'annotation': + db_images.extend([ + models.Image( + data=db_data, + path=os.path.relpath(data[1], upload_dir), + frame=data[2], + width=size[0], + height=size[1]) + + for data, size in zip(chunk_data, img_sizes) + ]) + else: + video_size = img_sizes[0] + video_path = chunk_data[0][1] - db_data.size += len(chunk_data) - progress = extractor.get_progress(chunk_data[-1][2]) - update_progress(progress) + db_data.size += len(chunk_data) + progress = extractor.get_progress(chunk_data[-1][2]) + update_progress(progress) if db_task.mode == 'annotation': models.Image.objects.bulk_create(db_images) @@ -342,4 +380,4 @@ def _create_thread(tid, data): preview.save(db_data.get_preview_path()) slogger.glob.info("Founded frames {} for Data #{}".format(db_data.size, db_data.id)) - _save_task_to_db(db_task) + _save_task_to_db(db_task) \ No newline at end of file diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 120125ea..621f85e8 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -29,7 +29,7 @@ from rest_framework.exceptions import APIException from rest_framework.permissions import SAFE_METHODS, IsAuthenticated from rest_framework.renderers import JSONRenderer from rest_framework.response import Response -from sendfile import sendfile +from sendfile import sendfile#убрать import cvat.apps.dataset_manager as dm import cvat.apps.dataset_manager.views # pylint: disable=unused-import @@ -37,7 +37,7 @@ from cvat.apps.authentication import auth from cvat.apps.authentication.decorators import login_required from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer from cvat.apps.engine.frame_provider import FrameProvider -from cvat.apps.engine.models import Job, Plugin, StatusChoice, Task +from cvat.apps.engine.models import Job, Plugin, StatusChoice, Task, DataChoice from cvat.apps.engine.serializers import ( AboutSerializer, AnnotationFileSerializer, BasicUserSerializer, DataMetaSerializer, DataSerializer, ExceptionSerializer, @@ -49,6 +49,12 @@ from cvat.apps.engine.utils import av_scan_paths from . import models, task from .log import clogger, slogger +from .media_extractors import ( + Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter, + ZipCompressedChunkWriter, ZipChunkWriter) +from .prepare import PrepareInfo +from diskcache import Cache +#from cvat.apps.engine.mime_types import mimetypes # drf-yasg component doesn't handle correctly URL_FORMAT_OVERRIDE and @@ -437,13 +443,63 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet): try: db_task = self.get_object() + db_data = db_task.data frame_provider = FrameProvider(db_task.data) if data_type == 'chunk': data_id = int(data_id) + quality = data_quality + data_quality = FrameProvider.Quality.COMPRESSED \ if data_quality == 'compressed' else FrameProvider.Quality.ORIGINAL + path = os.path.realpath(frame_provider.get_chunk(data_id, data_quality)) + #TODO: av.FFmpegError processing + if settings.USE_CACHE: + with Cache(settings.CACHE_ROOT) as cache: + buff = None + chunk, tag = cache.get('{}_{}_{}'.format(db_task.id, data_id, quality), tag=True) + + if not chunk: + extractor_classes = { + 'compressed' : Mpeg4CompressedChunkWriter if db_data.compressed_chunk_type == DataChoice.VIDEO else ZipCompressedChunkWriter, + 'original' : Mpeg4ChunkWriter if db_data.original_chunk_type == DataChoice.VIDEO else ZipChunkWriter, + } + + image_quality = 100 if extractor_classes[quality] in [Mpeg4ChunkWriter, ZipChunkWriter] else db_data.image_quality + file_extension = 'mp4' if extractor_classes[quality] in [Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter] else 'jpeg' + mime_type = 'video/mp4' if extractor_classes[quality] in [Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter] else 'application/zip' + + extractor = extractor_classes[quality](image_quality) + + if 'interpolation' == db_task.mode: + + meta = PrepareInfo(source_path=os.path.join(db_data.get_upload_dirname(), db_data.video.path), + meta_path=db_data.get_meta_path()) + + frames = [] + for frame in meta.decode_needed_frames(data_id,db_data.chunk_size): + frames.append(frame) + + + buff = extractor.save_as_chunk_to_buff(frames, + format_=file_extension) + cache.set('{}_{}_{}'.format(db_task.id, data_id, quality), buff, tag=mime_type) + + else: + img_paths = cache.get('{}_{}'.format(db_task.id, data_id)) + buff = extractor.save_as_chunk_to_buff(img_paths, + format_=file_extension) + cache.set('{}_{}_{}'.format(db_task.id, data_id, quality), buff, tag=mime_type) + + + + elif 'process_creating' == tag: + pass + else: + buff, mime_type = cache.get('{}_{}_{}'.format(db_task.id, data_id, quality), tag=True) + + return HttpResponse(buff.getvalue(), content_type=mime_type) # Follow symbol links if the chunk is a link on a real image otherwise # mimetype detection inside sendfile will work incorrectly. diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index b2847cbd..cc650f3f 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -49,3 +49,4 @@ av==6.2.0 # The package is used by pyunpack as a command line tool to support multiple # archives. Don't use as a python module because it has GPL license. patool==1.12 +diskcache==4.1.0 \ No newline at end of file From e947a8252c37fba957500c9014b80592d5588b9d Mon Sep 17 00:00:00 2001 From: Maya Date: Sat, 15 Aug 2020 11:01:56 +0300 Subject: [PATCH 004/467] Added ability to select using cache or no in task creating form & some fixes --- cvat-core/src/session.js | 20 ++++++ cvat-ui/src/actions/tasks-actions.ts | 1 + .../advanced-configuration-form.tsx | 25 ++++++++ .../create-task-page/create-task-content.tsx | 1 + cvat/apps/engine/media_extractors.py | 61 +++++++++++++++++-- .../migrations/0028_data_storage_method.py | 19 ++++++ cvat/apps/engine/models.py | 12 ++++ cvat/apps/engine/prepare.py | 2 +- cvat/apps/engine/serializers.py | 5 +- cvat/apps/engine/task.py | 32 +++++----- cvat/apps/engine/views.py | 9 ++- 11 files changed, 162 insertions(+), 25 deletions(-) create mode 100644 cvat/apps/engine/migrations/0028_data_storage_method.py diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 5d5bd7b2..ccaff82c 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -814,6 +814,7 @@ data_compressed_chunk_type: undefined, data_original_chunk_type: undefined, use_zip_chunks: undefined, + use_cache: undefined, }; for (const property in data) { @@ -1069,6 +1070,24 @@ data.use_zip_chunks = useZipChunks; }, }, + /** + * @name useCache + * @type {boolean} + * @memberof module:API.cvat.classes.Task + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + useCache: { + get: () => data.use_cache, + set: (useCache) => { + if (typeof (useCache) !== 'boolean') { + throw new ArgumentError( + 'Value must be a boolean', + ); + } + data.use_cache = useCache; + }, + }, /** * After task has been created value can be appended only. * @name labels @@ -1639,6 +1658,7 @@ remote_files: this.remoteFiles, image_quality: this.imageQuality, use_zip_chunks: this.useZipChunks, + use_cache: this.useCache, }; if (typeof (this.startFrame) !== 'undefined') { diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index fd1bfbc4..9fc4c1e8 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -390,6 +390,7 @@ ThunkAction, {}, {}, AnyAction> { z_order: data.advanced.zOrder, image_quality: 70, use_zip_chunks: data.advanced.useZipChunks, + use_cache: data.advanced.useCache, }; if (data.advanced.bugTracker) { diff --git a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx index b038408b..89a760e0 100644 --- a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx +++ b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx @@ -26,6 +26,7 @@ export interface AdvancedConfiguration { repository?: string; useZipChunks: boolean; dataChunkSize?: number; + useCache: boolean; } type Props = FormComponentProps & { @@ -380,6 +381,24 @@ class AdvancedConfigurationForm extends React.PureComponent { ); } + private renderCreateTaskMethod(): JSX.Element { + const { form } = this.props; + return ( + + {form.getFieldDecorator('useCache', { + initialValue: true, + valuePropName: 'checked', + })( + + + Use cache + + , + )} + + ); + } + private renderChunkSize(): JSX.Element { const { form } = this.props; @@ -433,6 +452,12 @@ class AdvancedConfigurationForm extends React.PureComponent { + + + {this.renderCreateTaskMethod()} + + + {this.renderImageQuality()} diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index 2bd32807..229015ef 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -39,6 +39,7 @@ const defaultState = { zOrder: false, lfs: false, useZipChunks: true, + useCache: true, }, labels: [], files: { diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index c2bc020f..0099b349 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -125,22 +125,21 @@ class DirectoryReader(ImageListReader): class ArchiveReader(DirectoryReader): def __init__(self, source_path, step=1, start=0, stop=None): - self._tmp_dir = create_tmp_dir() self._archive_source = source_path[0] - Archive(self._archive_source).extractall(self._tmp_dir) + Archive(self._archive_source).extractall(os.path.dirname(source_path[0])) super().__init__( - source_path=[self._tmp_dir], + source_path=[os.path.dirname(source_path[0])], step=step, start=start, stop=stop, ) def __del__(self): - delete_tmp_dir(self._tmp_dir) + os.remove(self._archive_source) def get_path(self, i): base_dir = os.path.dirname(self._archive_source) - return os.path.join(base_dir, os.path.relpath(self._source_path[i], self._tmp_dir)) + return os.path.join(base_dir, os.path.relpath(self._source_path[i], base_dir)) class PdfReader(DirectoryReader): def __init__(self, source_path, step=1, start=0, stop=None): @@ -193,6 +192,10 @@ class ZipReader(ImageListReader): def get_path(self, i): return os.path.join(os.path.dirname(self._zip_source.filename), self._source_path[i]) + def extract(self): + self._zip_source.extractall(os.path.dirname(self._zip_source.filename)) + os.remove(self._zip_source.filename) + class VideoReader(IMediaReader): def __init__(self, source_path, step=1, start=0, stop=None): super().__init__( @@ -312,7 +315,7 @@ class ZipCompressedChunkWriter(IChunkWriter): def save_as_chunk_to_buff(self, images, format_='jpeg'): buff = io.BytesIO() - with zipfile.ZipFile(buff, 'w') as zip_file: + with zipfile.ZipFile(buff, 'x') as zip_file: for idx, image in enumerate(images): (_, _, image_buf) = self._compress_image(image, self._image_quality) arcname = '{:06d}.{}'.format(idx, format_) @@ -452,6 +455,52 @@ class Mpeg4CompressedChunkWriter(Mpeg4ChunkWriter): output_container.close() return [(input_w, input_h)] + def save_as_chunk_to_buff(self, frames, format_): + if not frames: + raise Exception('no images to save') + + buff = io.BytesIO() + input_w = frames[0].width + input_h = frames[0].height + + downscale_factor = 1 + while input_h / downscale_factor >= 1080: + downscale_factor *= 2 + + output_h = input_h // downscale_factor + output_w = input_w // downscale_factor + + + output_container, output_v_stream = self._create_av_container( + path=buff, + w=output_w, + h=output_h, + rate=self._output_fps, + options={ + 'profile': 'baseline', + 'coder': '0', + 'crf': str(self._image_quality), + 'wpredp': '0', + 'flags': '-loop' + }, + f=format_, + ) + + for frame in frames: + # let libav set the correct pts and time_base + frame.pts = None + frame.time_base = None + + for packet in output_v_stream.encode(frame): + output_container.mux(packet) + + # Flush streams + for packet in output_v_stream.encode(): + output_container.mux(packet) + output_container.close() + buff.seek(0) + return buff + def _is_archive(path): mime = mimetypes.guess_type(path) mime_type = mime[0] diff --git a/cvat/apps/engine/migrations/0028_data_storage_method.py b/cvat/apps/engine/migrations/0028_data_storage_method.py new file mode 100644 index 00000000..6490f5bf --- /dev/null +++ b/cvat/apps/engine/migrations/0028_data_storage_method.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.13 on 2020-08-13 05:49 + +import cvat.apps.engine.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('engine', '0027_auto_20200719_1552'), + ] + + operations = [ + migrations.AddField( + model_name='data', + name='storage_method', + field=models.CharField(choices=[('cache', 'CACHE'), ('file_system', 'FILE_SYSTEM')], default=cvat.apps.engine.models.StorageMethodChoice('file_system'), max_length=15), + ), + ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index d2f8e26e..1abec5a8 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -43,6 +43,17 @@ class DataChoice(str, Enum): def __str__(self): return self.value +class StorageMethodChoice(str, Enum): + CACHE = 'cache' + FILE_SYSTEM = 'file_system' + + @classmethod + def choices(cls): + return tuple((x.value, x.name) for x in cls) + + def __str__(self): + return self.value + class Data(models.Model): chunk_size = models.PositiveIntegerField(null=True) size = models.PositiveIntegerField(default=0) @@ -54,6 +65,7 @@ class Data(models.Model): default=DataChoice.IMAGESET) original_chunk_type = models.CharField(max_length=32, choices=DataChoice.choices(), default=DataChoice.IMAGESET) + storage_method = models.CharField(max_length=15, choices=StorageMethodChoice.choices(), default=StorageMethodChoice.FILE_SYSTEM) class Meta: default_permissions = () diff --git a/cvat/apps/engine/prepare.py b/cvat/apps/engine/prepare.py index 01d1c367..69d74b5a 100644 --- a/cvat/apps/engine/prepare.py +++ b/cvat/apps/engine/prepare.py @@ -4,7 +4,7 @@ import hashlib class WorkWithVideo: def __init__(self, **kwargs): if not kwargs.get('source_path'): - raise Exeption('No sourse path') + raise Exception('No sourse path') self.source_path = kwargs.get('source_path') def _open_video_container(self, sourse_path, mode, options=None): diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 90e24714..e789ae94 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -167,11 +167,13 @@ class DataSerializer(serializers.ModelSerializer): client_files = ClientFileSerializer(many=True, default=[]) server_files = ServerFileSerializer(many=True, default=[]) remote_files = RemoteFileSerializer(many=True, default=[]) + use_cache = serializers.BooleanField(default=False) class Meta: model = models.Data fields = ('chunk_size', 'size', 'image_quality', 'start_frame', 'stop_frame', 'frame_filter', - 'compressed_chunk_type', 'original_chunk_type', 'client_files', 'server_files', 'remote_files', 'use_zip_chunks') + 'compressed_chunk_type', 'original_chunk_type', 'client_files', 'server_files', 'remote_files', 'use_zip_chunks', + 'use_cache') # pylint: disable=no-self-use def validate_frame_filter(self, value): @@ -199,6 +201,7 @@ class DataSerializer(serializers.ModelSerializer): server_files = validated_data.pop('server_files') remote_files = validated_data.pop('remote_files') validated_data.pop('use_zip_chunks') + validated_data.pop('use_cache') db_data = models.Data.objects.create(**validated_data) data_path = db_data.get_data_dirname() diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index b468d4ef..fc2f3759 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -14,7 +14,7 @@ from urllib import parse as urlparse from urllib import request as urlrequest from cvat.apps.engine.media_extractors import get_mime, MEDIA_TYPES, Mpeg4ChunkWriter, ZipChunkWriter, Mpeg4CompressedChunkWriter, ZipCompressedChunkWriter -from cvat.apps.engine.models import DataChoice +from cvat.apps.engine.models import DataChoice, StorageMethodChoice from cvat.apps.engine.utils import av_scan_paths import django_rq @@ -245,6 +245,8 @@ def _create_thread(tid, data): start=db_data.start_frame, stop=data['stop_frame'], ) + if extractor.__class__ == MEDIA_TYPES['zip']['extractor']: + extractor.extract() db_task.mode = task_mode db_data.compressed_chunk_type = models.DataChoice.VIDEO if task_mode == 'interpolation' and not data['use_zip_chunks'] else models.DataChoice.IMAGESET db_data.original_chunk_type = models.DataChoice.VIDEO if task_mode == 'interpolation' else models.DataChoice.IMAGESET @@ -278,25 +280,22 @@ def _create_thread(tid, data): else: db_data.chunk_size = 36 - #it's better to add the field to the Task model - video_suitable_on_the_fly_processing = True video_path = "" video_size = (0, 0) - if settings.USE_CACHE: + if settings.USE_CACHE and db_data.storage_method == StorageMethodChoice.CACHE: for media_type, media_files in media.items(): if media_files: if task_mode == MEDIA_TYPES['video']['mode']: try: - analizer = AnalyzeVideo(source_path=os.path.join(upload_dir, media_files[0])) - analizer.check_type_first_frame() - analizer.check_video_timestamps_sequences() + 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.test_seek() meta_info.check_seek_key_frames() meta_info.save_meta_info() @@ -306,14 +305,17 @@ def _create_thread(tid, data): video_size = (frame.width, frame.height) except AssertionError as ex: - video_suitable_on_the_fly_processing = False + db_data.storage_method = StorageMethodChoice.FILE_SYSTEM except Exception as ex: - video_suitable_on_the_fly_processing = False + db_data.storage_method = StorageMethodChoice.FILE_SYSTEM - else:#images, TODO:archive + else:#images,archive with Cache(settings.CACHE_ROOT) as cache: counter_ = itertools.count() + if extractor.__class__ in [MEDIA_TYPES['archive']['extractor'], MEDIA_TYPES['zip']['extractor']]: + media_files = [os.path.join(upload_dir, f) for f in extractor._source_path] + for chunk_number, media_paths in itertools.groupby(media_files, lambda x: next(counter_) // db_data.chunk_size): media_paths = list(media_paths) cache.set('{}_{}'.format(tid, chunk_number), [os.path.join(upload_dir, file_name) for file_name in media_paths], tag='dummy') @@ -321,20 +323,20 @@ def _create_thread(tid, data): img_sizes = [] from PIL import Image for media_path in media_paths: - img_sizes += [Image.open(os.path.join(upload_dir, media_path)).size] + img_sizes += [Image.open(media_path).size] db_data.size += len(media_paths) db_images.extend([ models.Image( data=db_data, - path=data[1], + path=os.path.basename(data[1]), frame=data[0], width=size[0], height=size[1]) - for data, size in zip(enumerate(media_paths), img_sizes) + for data, size in zip(enumerate(media_paths, start=len(db_images)), img_sizes) ]) - if db_task.mode == 'interpolation' and not video_suitable_on_the_fly_processing or not settings.USE_CACHE: + if db_data.storage_method == StorageMethodChoice.FILE_SYSTEM or not settings.USE_CACHE: counter = itertools.count() generator = itertools.groupby(extractor, lambda x: next(counter) // db_data.chunk_size) for chunk_idx, chunk_data in generator: diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 621f85e8..f06f6d4b 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -37,7 +37,7 @@ from cvat.apps.authentication import auth from cvat.apps.authentication.decorators import login_required from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer from cvat.apps.engine.frame_provider import FrameProvider -from cvat.apps.engine.models import Job, Plugin, StatusChoice, Task, DataChoice +from cvat.apps.engine.models import Job, Plugin, StatusChoice, Task, DataChoice, StorageMethodChoice from cvat.apps.engine.serializers import ( AboutSerializer, AnnotationFileSerializer, BasicUserSerializer, DataMetaSerializer, DataSerializer, ExceptionSerializer, @@ -419,6 +419,11 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet): db_task.save() data = {k:v for k, v in serializer.data.items()} data['use_zip_chunks'] = serializer.validated_data['use_zip_chunks'] + data['use_cache'] = serializer.validated_data['use_cache'] + if data['use_cache']: + db_task.data.storage_method = StorageMethodChoice.CACHE + db_task.data.save(update_fields=['storage_method']) + # if the value of stop_frame is 0, then inside the function we cannot know # the value specified by the user or it's default value from the database if 'stop_frame' not in serializer.validated_data: @@ -455,7 +460,7 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet): path = os.path.realpath(frame_provider.get_chunk(data_id, data_quality)) #TODO: av.FFmpegError processing - if settings.USE_CACHE: + if settings.USE_CACHE and db_data.storage_method == StorageMethodChoice.CACHE: with Cache(settings.CACHE_ROOT) as cache: buff = None chunk, tag = cache.get('{}_{}_{}'.format(db_task.id, data_id, quality), tag=True) From 94bb3659c91630f3066d677ebafe953af32be6f6 Mon Sep 17 00:00:00 2001 From: Maya Date: Sat, 15 Aug 2020 11:10:25 +0300 Subject: [PATCH 005/467] Added data migration --- .../migrations/0029_auto_20200814_2153.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 cvat/apps/engine/migrations/0029_auto_20200814_2153.py diff --git a/cvat/apps/engine/migrations/0029_auto_20200814_2153.py b/cvat/apps/engine/migrations/0029_auto_20200814_2153.py new file mode 100644 index 00000000..f81c73c8 --- /dev/null +++ b/cvat/apps/engine/migrations/0029_auto_20200814_2153.py @@ -0,0 +1,53 @@ +from django.db import migrations +from django.conf import settings +import os +from cvat.apps.engine.mime_types import mimetypes +from pyunpack import Archive + +def unzip(apps, schema_editor): + Data = apps.get_model("engine", "Data") + data_q_set = Data.objects.all() + archive_paths = [] + archive_mimes = [ + 'application/gzip', + 'application/rar' + 'application/x-7z-compressed', + 'application/x-bzip', + 'application/x-bzip-compressed-tar', + 'application/x-compress', + 'application/x-compressed-tar', + 'application/x-cpio', + 'application/x-gtar-compressed', + 'application/x-lha', + 'application/x-lhz', + 'application/x-lrzip-compressed-tar', + 'application/x-lz4', + 'application/x-lzip', + 'application/x-lzip-compressed-tar', + 'application/x-lzma', + 'application/x-lzma-compressed-tar', + 'application/x-lzop', + 'application/x-tar', + 'application/x-tarz', + 'application/x-tzo', + 'application/x-xz-compressed-tar', + 'application/zip', + ] + + for data_instance in data_q_set: + for root, _, files in os.walk(os.path.join(settings.MEDIA_DATA_ROOT, '{}/raw/'.format(data_instance.id))): + archive_paths.extend([os.path.join(root, file) for file in files if mimetypes.guess_type(file)[0] in archive_mimes]) + + for path in archive_paths: + Archive(path).extractall(os.path.dirname(path)) + os.remove(path) + +class Migration(migrations.Migration): + + dependencies = [ + ('engine', '0028_data_storage_method'), + ] + + operations = [ + migrations.RunPython(unzip) + ] \ No newline at end of file From f3630aa4b513c7a8bfcf48d7d13e66768d7ab7c0 Mon Sep 17 00:00:00 2001 From: Maya Date: Wed, 19 Aug 2020 08:09:34 +0300 Subject: [PATCH 006/467] Some additions: * moved cache implementation * fixed start, stop, step for tasks that created on the fly * changed frame provider --- cvat/apps/engine/cache.py | 61 ++++++++++++++++++++++++++++ cvat/apps/engine/frame_provider.py | 49 ++++++++++++++++++---- cvat/apps/engine/media_extractors.py | 5 ++- cvat/apps/engine/models.py | 3 ++ cvat/apps/engine/prepare.py | 12 +++--- cvat/apps/engine/task.py | 56 +++++++++++++------------ cvat/apps/engine/views.py | 57 ++------------------------ 7 files changed, 149 insertions(+), 94 deletions(-) create mode 100644 cvat/apps/engine/cache.py diff --git a/cvat/apps/engine/cache.py b/cvat/apps/engine/cache.py new file mode 100644 index 00000000..c79bf4f2 --- /dev/null +++ b/cvat/apps/engine/cache.py @@ -0,0 +1,61 @@ +from diskcache import Cache +from django.conf import settings +from cvat.apps.engine.media_extractors import (Mpeg4ChunkWriter, ZipChunkWriter, + Mpeg4CompressedChunkWriter, ZipCompressedChunkWriter) +from cvat.apps.engine.models import DataChoice +from .prepare import PrepareInfo +import os + +class CacheInteraction: + def __init__(self): + self._cache = Cache(settings.CACHE_ROOT) + + def __del__(self): + self._cache.close() + + def get_buff_mime(self, chunk_number, quality, db_data): + chunk, tag = self._cache.get('{}_{}_{}'.format(db_data.id, chunk_number, quality), tag=True) + + if not chunk: + chunk, tag = self.prepare_chunk_buff(db_data, quality, chunk_number) + self.save_chunk(db_data.id, chunk_number, quality, chunk, tag) + return chunk, tag + + def get_buff(self, chunk_number, quality, db_data): + chunk, tag = self._cache.get('{}_{}_{}'.format(db_data.id, chunk_number, quality), tag=True) + + if not chunk: + chunk, tag = self.prepare_chunk_buff(db_data, quality, chunk_number) + self.save_chunk(db_data.id, chunk_number, quality, chunk, tag) + return chunk + + def prepare_chunk_buff(self, db_data, quality, chunk_number): + from cvat.apps.engine.frame_provider import FrameProvider + extractor_classes = { + FrameProvider.Quality.COMPRESSED : Mpeg4CompressedChunkWriter if db_data.compressed_chunk_type == DataChoice.VIDEO else ZipCompressedChunkWriter, + FrameProvider.Quality.ORIGINAL : Mpeg4ChunkWriter if db_data.original_chunk_type == DataChoice.VIDEO else ZipChunkWriter, + } + + image_quality = 100 if extractor_classes[quality] in [Mpeg4ChunkWriter, ZipChunkWriter] else db_data.image_quality + file_extension = 'mp4' if extractor_classes[quality] in [Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter] else 'jpeg' + mime_type = 'video/mp4' if extractor_classes[quality] in [Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter] else 'application/zip' + + extractor = extractor_classes[quality](image_quality) + + #if 'interpolation' == task_mode: + if os.path.exists(db_data.get_meta_path()): + meta = PrepareInfo(source_path=os.path.join(db_data.get_upload_dirname(), db_data.video.path), + meta_path=db_data.get_meta_path()) + frames = [] + for frame in meta.decode_needed_frames(chunk_number, db_data):#db_data.chunk_size + frames.append(frame) + buff = extractor.save_as_chunk_to_buff(frames, file_extension) + else: + img_paths = None + with open(db_data.get_dummy_chunk_path(chunk_number), 'r') as dummy_file: + img_paths = [line.strip() for line in dummy_file] + buff = extractor.save_as_chunk_to_buff(img_paths, file_extension) + return buff, mime_type + + def save_chunk(self, db_data_id, chunk_number, quality, buff, mime_type): + self._cache.set('{}_{}_{}'.format(db_data_id, chunk_number, quality), buff, tag=mime_type) \ No newline at end of file diff --git a/cvat/apps/engine/frame_provider.py b/cvat/apps/engine/frame_provider.py index 25575ea5..10e6b9e9 100644 --- a/cvat/apps/engine/frame_provider.py +++ b/cvat/apps/engine/frame_provider.py @@ -11,8 +11,8 @@ from PIL import Image from cvat.apps.engine.media_extractors import VideoReader, ZipReader from cvat.apps.engine.mime_types import mimetypes -from cvat.apps.engine.models import DataChoice - +from cvat.apps.engine.models import DataChoice, StorageMethodChoice +from .cache import CacheInteraction class RandomAccessIterator: def __init__(self, iterable): @@ -65,6 +65,20 @@ class FrameProvider: self.reader_class([self.get_chunk_path(chunk_id)])) return self.chunk_reader + class BuffChunkLoader(ChunkLoader): + def __init__(self, reader_class, path_getter, buff_mime_getter, quality, db_data): + super().__init__(reader_class, path_getter) + self.get_chunk = buff_mime_getter + self.quality = quality + self.db_data = db_data + + def load(self, chunk_id): + if self.chunk_id != chunk_id: + self.chunk_id = chunk_id + self.chunk_reader = RandomAccessIterator( + self.reader_class([self.get_chunk_path(chunk_id, self.quality, self.db_data)])) + return self.chunk_reader + def __init__(self, db_data): self._db_data = db_data self._loaders = {} @@ -73,12 +87,29 @@ class FrameProvider: DataChoice.IMAGESET: ZipReader, DataChoice.VIDEO: VideoReader, } - self._loaders[self.Quality.COMPRESSED] = self.ChunkLoader( - reader_class[db_data.compressed_chunk_type], - db_data.get_compressed_chunk_path) - self._loaders[self.Quality.ORIGINAL] = self.ChunkLoader( - reader_class[db_data.original_chunk_type], - db_data.get_original_chunk_path) + + if db_data.storage_method == StorageMethodChoice.CACHE: + cache = CacheInteraction() + + self._loaders[self.Quality.COMPRESSED] = self.BuffChunkLoader( + reader_class[db_data.compressed_chunk_type], + cache.get_buff, + cache.get_buff_mime, + self.Quality.COMPRESSED, + self._db_data) + self._loaders[self.Quality.ORIGINAL] = self.BuffChunkLoader( + reader_class[db_data.original_chunk_type], + cache.get_buff, + cache.get_buff_mime, + self.Quality.ORIGINAL, + self._db_data) + else: + self._loaders[self.Quality.COMPRESSED] = self.ChunkLoader( + reader_class[db_data.compressed_chunk_type], + db_data.get_compressed_chunk_path) + self._loaders[self.Quality.ORIGINAL] = self.ChunkLoader( + reader_class[db_data.original_chunk_type], + db_data.get_original_chunk_path) def __len__(self): return self._db_data.size @@ -129,6 +160,8 @@ class FrameProvider: def get_chunk(self, chunk_number, quality=Quality.ORIGINAL): chunk_number = self._validate_chunk_number(chunk_number) + if self._db_data.storage_method == StorageMethodChoice.CACHE: + return self._loaders[quality].get_chunk(chunk_number, quality, self._db_data) return self._loaders[quality].get_chunk_path(chunk_number) def get_frame(self, frame_number, quality=Quality.ORIGINAL, diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 0099b349..f2a59df4 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -190,7 +190,10 @@ class ZipReader(ImageListReader): return io.BytesIO(self._zip_source.read(self._source_path[i])) def get_path(self, i): - return os.path.join(os.path.dirname(self._zip_source.filename), self._source_path[i]) + if self._zip_source.filename: + return os.path.join(os.path.dirname(self._zip_source.filename), self._source_path[i]) + else: #для определения mime_type + return self._source_path[i] def extract(self): self._zip_source.extractall(os.path.dirname(self._zip_source.filename)) diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 1abec5a8..690ab216 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -117,6 +117,9 @@ class Data(models.Model): def get_meta_path(self): return os.path.join(self.get_upload_dirname(), 'meta_info.txt') + def get_dummy_chunk_path(self, chunk_number): + return os.path.join(self.get_upload_dirname(), 'dummy_{}.txt'.format(chunk_number)) + class Video(models.Model): data = models.OneToOneField(Data, on_delete=models.CASCADE, related_name="video", null=True) path = models.CharField(max_length=1024, default='') diff --git a/cvat/apps/engine/prepare.py b/cvat/apps/engine/prepare.py index 69d74b5a..f8d1ab4d 100644 --- a/cvat/apps/engine/prepare.py +++ b/cvat/apps/engine/prepare.py @@ -144,11 +144,11 @@ class PrepareInfo(WorkWithVideo): return int(start_decode_frame_number), int(start_decode_timestamp) - def decode_needed_frames(self, chunk_number, chunk_size): - start_chunk_frame_number = chunk_number * chunk_size - end_chunk_frame_number = start_chunk_frame_number + chunk_size + def decode_needed_frames(self, chunk_number, db_data): + step = db_data.get_frame_step() + start_chunk_frame_number = db_data.start_frame + chunk_number * db_data.chunk_size * step + end_chunk_frame_number = min(start_chunk_frame_number + (db_data.chunk_size - 1) * step + 1, db_data.stop_frame + 1) start_decode_frame_number, start_decode_timestamp = self.get_nearest_left_key_frame(start_chunk_frame_number) - container = self._open_video_container(self.source_path, mode='r') video_stream = self._get_video_stream(container) container.seek(offset=start_decode_timestamp, stream=video_stream) @@ -159,8 +159,10 @@ class PrepareInfo(WorkWithVideo): frame_number += 1 if frame_number < start_chunk_frame_number: continue - elif frame_number < end_chunk_frame_number: + elif frame_number < end_chunk_frame_number and not (frame_number % step): yield frame + elif frame_number % step: + continue else: self._close_video_container(container) return diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index fc2f3759..546edb6e 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -25,7 +25,6 @@ from distutils.dir_util import copy_tree from . import models from .log import slogger from .prepare import PrepareInfo, AnalyzeVideo -from diskcache import Cache ############################# Low Level server API @@ -299,7 +298,8 @@ def _create_thread(tid, data): meta_info.check_seek_key_frames() meta_info.save_meta_info() - db_data.size = meta_info.get_task_size() + all_frames = meta_info.get_task_size() + 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) @@ -310,31 +310,33 @@ def _create_thread(tid, data): db_data.storage_method = StorageMethodChoice.FILE_SYSTEM else:#images,archive - with Cache(settings.CACHE_ROOT) as cache: - counter_ = itertools.count() - - if extractor.__class__ in [MEDIA_TYPES['archive']['extractor'], MEDIA_TYPES['zip']['extractor']]: - media_files = [os.path.join(upload_dir, f) for f in extractor._source_path] - - for chunk_number, media_paths in itertools.groupby(media_files, lambda x: next(counter_) // db_data.chunk_size): - media_paths = list(media_paths) - cache.set('{}_{}'.format(tid, chunk_number), [os.path.join(upload_dir, file_name) for file_name in media_paths], tag='dummy') - - img_sizes = [] - from PIL import Image - for media_path in media_paths: - img_sizes += [Image.open(media_path).size] - db_data.size += len(media_paths) - db_images.extend([ - models.Image( - data=db_data, - path=os.path.basename(data[1]), - frame=data[0], - width=size[0], - height=size[1]) - for data, size in zip(enumerate(media_paths, start=len(db_images)), img_sizes) - ]) - + counter_ = itertools.count() + if extractor.__class__ in [MEDIA_TYPES['archive']['extractor'], MEDIA_TYPES['zip']['extractor']]: + media_files = [os.path.join(upload_dir, f) for f in extractor._source_path] + + numbers_sequence = range(db_data.start_frame, min(data['stop_frame'] if data['stop_frame'] else len(media_files), len(media_files)), db_data.get_frame_step()) + m_paths = [] + m_paths = [(path, numb) for numb, path in enumerate(media_files) if numb in numbers_sequence] + + for chunk_number, media_paths in itertools.groupby(m_paths, lambda x: next(counter_) // db_data.chunk_size): + media_paths = list(media_paths) + img_sizes = [] + from PIL import Image + with open(db_data.get_dummy_chunk_path(chunk_number), 'w') as dummy_chunk: + for path, _ in media_paths: + dummy_chunk.write(os.path.join(upload_dir, path)+'\n') + img_sizes += [Image.open(os.path.join(upload_dir, path)).size] + + db_data.size += len(media_paths) + db_images.extend([ + models.Image( + data=db_data, + path=os.path.basename(data[0]), + frame=data[1], + width=size[0], + height=size[1]) + for data, size in zip(media_paths, img_sizes) + ]) if db_data.storage_method == StorageMethodChoice.FILE_SYSTEM or not settings.USE_CACHE: counter = itertools.count() diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index f06f6d4b..542339b9 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -37,7 +37,7 @@ from cvat.apps.authentication import auth from cvat.apps.authentication.decorators import login_required from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer from cvat.apps.engine.frame_provider import FrameProvider -from cvat.apps.engine.models import Job, Plugin, StatusChoice, Task, DataChoice, StorageMethodChoice +from cvat.apps.engine.models import Job, Plugin, StatusChoice, Task, StorageMethodChoice from cvat.apps.engine.serializers import ( AboutSerializer, AnnotationFileSerializer, BasicUserSerializer, DataMetaSerializer, DataSerializer, ExceptionSerializer, @@ -49,13 +49,6 @@ from cvat.apps.engine.utils import av_scan_paths from . import models, task from .log import clogger, slogger -from .media_extractors import ( - Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter, - ZipCompressedChunkWriter, ZipChunkWriter) -from .prepare import PrepareInfo -from diskcache import Cache -#from cvat.apps.engine.mime_types import mimetypes - # drf-yasg component doesn't handle correctly URL_FORMAT_OVERRIDE and # send requests with ?format=openapi suffix instead of ?scheme=openapi. @@ -458,56 +451,14 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet): data_quality = FrameProvider.Quality.COMPRESSED \ if data_quality == 'compressed' else FrameProvider.Quality.ORIGINAL - path = os.path.realpath(frame_provider.get_chunk(data_id, data_quality)) #TODO: av.FFmpegError processing if settings.USE_CACHE and db_data.storage_method == StorageMethodChoice.CACHE: - with Cache(settings.CACHE_ROOT) as cache: - buff = None - chunk, tag = cache.get('{}_{}_{}'.format(db_task.id, data_id, quality), tag=True) - - if not chunk: - extractor_classes = { - 'compressed' : Mpeg4CompressedChunkWriter if db_data.compressed_chunk_type == DataChoice.VIDEO else ZipCompressedChunkWriter, - 'original' : Mpeg4ChunkWriter if db_data.original_chunk_type == DataChoice.VIDEO else ZipChunkWriter, - } - - image_quality = 100 if extractor_classes[quality] in [Mpeg4ChunkWriter, ZipChunkWriter] else db_data.image_quality - file_extension = 'mp4' if extractor_classes[quality] in [Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter] else 'jpeg' - mime_type = 'video/mp4' if extractor_classes[quality] in [Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter] else 'application/zip' - - extractor = extractor_classes[quality](image_quality) - - if 'interpolation' == db_task.mode: - - meta = PrepareInfo(source_path=os.path.join(db_data.get_upload_dirname(), db_data.video.path), - meta_path=db_data.get_meta_path()) - - frames = [] - for frame in meta.decode_needed_frames(data_id,db_data.chunk_size): - frames.append(frame) - - - buff = extractor.save_as_chunk_to_buff(frames, - format_=file_extension) - cache.set('{}_{}_{}'.format(db_task.id, data_id, quality), buff, tag=mime_type) - - else: - img_paths = cache.get('{}_{}'.format(db_task.id, data_id)) - buff = extractor.save_as_chunk_to_buff(img_paths, - format_=file_extension) - cache.set('{}_{}_{}'.format(db_task.id, data_id, quality), buff, tag=mime_type) - - - - elif 'process_creating' == tag: - pass - else: - buff, mime_type = cache.get('{}_{}_{}'.format(db_task.id, data_id, quality), tag=True) - - return HttpResponse(buff.getvalue(), content_type=mime_type) + buff, mime_type = frame_provider.get_chunk(data_id, data_quality) + return HttpResponse(buff.getvalue(), content_type=mime_type) # Follow symbol links if the chunk is a link on a real image otherwise # mimetype detection inside sendfile will work incorrectly. + path = os.path.realpath(frame_provider.get_chunk(data_id, data_quality)) return sendfile(request, path) elif data_type == 'frame': From ade215399c0dca208afcd8a5516ed30a8f8aaa2b Mon Sep 17 00:00:00 2001 From: Maya Date: Wed, 19 Aug 2020 10:25:33 +0300 Subject: [PATCH 007/467] Fix --- cvat/apps/engine/prepare.py | 4 ++-- cvat/apps/engine/task.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cvat/apps/engine/prepare.py b/cvat/apps/engine/prepare.py index f8d1ab4d..83a11f93 100644 --- a/cvat/apps/engine/prepare.py +++ b/cvat/apps/engine/prepare.py @@ -159,9 +159,9 @@ class PrepareInfo(WorkWithVideo): frame_number += 1 if frame_number < start_chunk_frame_number: continue - elif frame_number < end_chunk_frame_number and not (frame_number % step): + elif frame_number < end_chunk_frame_number and not ((frame_number - start_chunk_frame_number) % step): yield frame - elif frame_number % step: + elif (frame_number - start_chunk_frame_number) % step: continue else: self._close_video_container(container) diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 546edb6e..08ecb275 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -316,7 +316,7 @@ def _create_thread(tid, data): numbers_sequence = range(db_data.start_frame, min(data['stop_frame'] if data['stop_frame'] else len(media_files), len(media_files)), db_data.get_frame_step()) m_paths = [] - m_paths = [(path, numb) for numb, path in enumerate(media_files) if numb in numbers_sequence] + m_paths = [(path, numb) for numb, path in enumerate(sorted(media_files)) if numb in numbers_sequence] for chunk_number, media_paths in itertools.groupby(m_paths, lambda x: next(counter_) // db_data.chunk_size): media_paths = list(media_paths) From 1520dcc605f608562d6f3115e04b1b42e306938a Mon Sep 17 00:00:00 2001 From: Maya Date: Thu, 20 Aug 2020 20:27:54 +0300 Subject: [PATCH 008/467] Deleted ArchiveReader/get_path --- cvat/apps/engine/media_extractors.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index f2a59df4..97bd0249 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -137,10 +137,6 @@ class ArchiveReader(DirectoryReader): def __del__(self): os.remove(self._archive_source) - def get_path(self, i): - base_dir = os.path.dirname(self._archive_source) - return os.path.join(base_dir, os.path.relpath(self._source_path[i], base_dir)) - class PdfReader(DirectoryReader): def __init__(self, source_path, step=1, start=0, stop=None): if not source_path: From ff62baab99e3f905b055621eb93ab07022f2f8c8 Mon Sep 17 00:00:00 2001 From: Maya Date: Thu, 20 Aug 2020 20:34:51 +0300 Subject: [PATCH 009/467] Changed paths --- cvat/apps/engine/cache.py | 2 +- cvat/apps/engine/task.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cvat/apps/engine/cache.py b/cvat/apps/engine/cache.py index c79bf4f2..c922f73e 100644 --- a/cvat/apps/engine/cache.py +++ b/cvat/apps/engine/cache.py @@ -53,7 +53,7 @@ class CacheInteraction: else: img_paths = None with open(db_data.get_dummy_chunk_path(chunk_number), 'r') as dummy_file: - img_paths = [line.strip() for line in dummy_file] + img_paths = [os.path.join(db_data.get_upload_dirname(), line.strip()) for line in dummy_file] buff = extractor.save_as_chunk_to_buff(img_paths, file_extension) return buff, mime_type diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 08ecb275..881bbed2 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -311,8 +311,10 @@ def _create_thread(tid, data): else:#images,archive counter_ = itertools.count() - if extractor.__class__ in [MEDIA_TYPES['archive']['extractor'], MEDIA_TYPES['zip']['extractor']]: - media_files = [os.path.join(upload_dir, f) for f in extractor._source_path] + if isinstance(extractor, MEDIA_TYPES['archive']['extractor']): + media_files = [os.path.relpath(path, upload_dir) for path in extractor._source_path] + elif isinstance(extractor, MEDIA_TYPES['zip']['extractor']): + media_files = extractor._source_path numbers_sequence = range(db_data.start_frame, min(data['stop_frame'] if data['stop_frame'] else len(media_files), len(media_files)), db_data.get_frame_step()) m_paths = [] @@ -324,14 +326,14 @@ def _create_thread(tid, data): from PIL import Image with open(db_data.get_dummy_chunk_path(chunk_number), 'w') as dummy_chunk: for path, _ in media_paths: - dummy_chunk.write(os.path.join(upload_dir, path)+'\n') + dummy_chunk.write(path+'\n') img_sizes += [Image.open(os.path.join(upload_dir, path)).size] db_data.size += len(media_paths) db_images.extend([ models.Image( data=db_data, - path=os.path.basename(data[0]), + path=data[0], frame=data[1], width=size[0], height=size[1]) From 3b9b1c686f735153f162479d73a011c1b5b98aea Mon Sep 17 00:00:00 2001 From: Maya Date: Mon, 24 Aug 2020 22:47:42 +0300 Subject: [PATCH 010/467] Disabled use cache default --- .../components/create-task-page/advanced-configuration-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx index c2dd2552..727eccd9 100644 --- a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx +++ b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx @@ -386,7 +386,7 @@ class AdvancedConfigurationForm extends React.PureComponent { return ( {form.getFieldDecorator('useCache', { - initialValue: true, + initialValue: false, valuePropName: 'checked', })( From 43c2c6ea8775883fa85b42efa79d4e2dcae74134 Mon Sep 17 00:00:00 2001 From: Maya Date: Mon, 24 Aug 2020 22:51:26 +0300 Subject: [PATCH 011/467] Merged migrations into one file --- .../migrations/0028_data_storage_method.py | 17 ++++++ .../migrations/0029_auto_20200814_2153.py | 53 ------------------- 2 files changed, 17 insertions(+), 53 deletions(-) delete mode 100644 cvat/apps/engine/migrations/0029_auto_20200814_2153.py diff --git a/cvat/apps/engine/migrations/0028_data_storage_method.py b/cvat/apps/engine/migrations/0028_data_storage_method.py index 6490f5bf..4bd8f757 100644 --- a/cvat/apps/engine/migrations/0028_data_storage_method.py +++ b/cvat/apps/engine/migrations/0028_data_storage_method.py @@ -1,8 +1,24 @@ # Generated by Django 2.2.13 on 2020-08-13 05:49 +from cvat.apps.engine.media_extractors import _is_archive, _is_zip import cvat.apps.engine.models +from django.conf import settings from django.db import migrations, models +import os +from pyunpack import Archive +def unzip(apps, schema_editor): + Data = apps.get_model("engine", "Data") + data_q_set = Data.objects.all() + archive_paths = [] + + for data_instance in data_q_set: + for root, _, files in os.walk(os.path.join(settings.MEDIA_DATA_ROOT, '{}/raw/'.format(data_instance.id))): + archive_paths.extend([os.path.join(root, file) for file in files if _is_archive(file) or _is_zip(file)]) + + for path in archive_paths: + Archive(path).extractall(os.path.dirname(path)) + os.remove(path) class Migration(migrations.Migration): @@ -16,4 +32,5 @@ class Migration(migrations.Migration): name='storage_method', field=models.CharField(choices=[('cache', 'CACHE'), ('file_system', 'FILE_SYSTEM')], default=cvat.apps.engine.models.StorageMethodChoice('file_system'), max_length=15), ), + migrations.RunPython(unzip), ] diff --git a/cvat/apps/engine/migrations/0029_auto_20200814_2153.py b/cvat/apps/engine/migrations/0029_auto_20200814_2153.py deleted file mode 100644 index f81c73c8..00000000 --- a/cvat/apps/engine/migrations/0029_auto_20200814_2153.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.db import migrations -from django.conf import settings -import os -from cvat.apps.engine.mime_types import mimetypes -from pyunpack import Archive - -def unzip(apps, schema_editor): - Data = apps.get_model("engine", "Data") - data_q_set = Data.objects.all() - archive_paths = [] - archive_mimes = [ - 'application/gzip', - 'application/rar' - 'application/x-7z-compressed', - 'application/x-bzip', - 'application/x-bzip-compressed-tar', - 'application/x-compress', - 'application/x-compressed-tar', - 'application/x-cpio', - 'application/x-gtar-compressed', - 'application/x-lha', - 'application/x-lhz', - 'application/x-lrzip-compressed-tar', - 'application/x-lz4', - 'application/x-lzip', - 'application/x-lzip-compressed-tar', - 'application/x-lzma', - 'application/x-lzma-compressed-tar', - 'application/x-lzop', - 'application/x-tar', - 'application/x-tarz', - 'application/x-tzo', - 'application/x-xz-compressed-tar', - 'application/zip', - ] - - for data_instance in data_q_set: - for root, _, files in os.walk(os.path.join(settings.MEDIA_DATA_ROOT, '{}/raw/'.format(data_instance.id))): - archive_paths.extend([os.path.join(root, file) for file in files if mimetypes.guess_type(file)[0] in archive_mimes]) - - for path in archive_paths: - Archive(path).extractall(os.path.dirname(path)) - os.remove(path) - -class Migration(migrations.Migration): - - dependencies = [ - ('engine', '0028_data_storage_method'), - ] - - operations = [ - migrations.RunPython(unzip) - ] \ No newline at end of file From 2144c4aadd65d1072fdb5f67471576c3cb9a85bc Mon Sep 17 00:00:00 2001 From: Maya Date: Mon, 24 Aug 2020 23:07:49 +0300 Subject: [PATCH 012/467] Refactoring --- cvat/apps/engine/cache.py | 16 +++------------- cvat/apps/engine/frame_provider.py | 9 +++------ cvat/apps/engine/media_extractors.py | 16 ++++++++-------- cvat/apps/engine/prepare.py | 2 +- cvat/apps/engine/task.py | 4 +--- cvat/apps/engine/views.py | 1 - 6 files changed, 16 insertions(+), 32 deletions(-) diff --git a/cvat/apps/engine/cache.py b/cvat/apps/engine/cache.py index c922f73e..e4d19f59 100644 --- a/cvat/apps/engine/cache.py +++ b/cvat/apps/engine/cache.py @@ -21,14 +21,6 @@ class CacheInteraction: self.save_chunk(db_data.id, chunk_number, quality, chunk, tag) return chunk, tag - def get_buff(self, chunk_number, quality, db_data): - chunk, tag = self._cache.get('{}_{}_{}'.format(db_data.id, chunk_number, quality), tag=True) - - if not chunk: - chunk, tag = self.prepare_chunk_buff(db_data, quality, chunk_number) - self.save_chunk(db_data.id, chunk_number, quality, chunk, tag) - return chunk - def prepare_chunk_buff(self, db_data, quality, chunk_number): from cvat.apps.engine.frame_provider import FrameProvider extractor_classes = { @@ -37,24 +29,22 @@ class CacheInteraction: } image_quality = 100 if extractor_classes[quality] in [Mpeg4ChunkWriter, ZipChunkWriter] else db_data.image_quality - file_extension = 'mp4' if extractor_classes[quality] in [Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter] else 'jpeg' mime_type = 'video/mp4' if extractor_classes[quality] in [Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter] else 'application/zip' extractor = extractor_classes[quality](image_quality) - #if 'interpolation' == task_mode: if os.path.exists(db_data.get_meta_path()): meta = PrepareInfo(source_path=os.path.join(db_data.get_upload_dirname(), db_data.video.path), meta_path=db_data.get_meta_path()) frames = [] - for frame in meta.decode_needed_frames(chunk_number, db_data):#db_data.chunk_size + for frame in meta.decode_needed_frames(chunk_number, db_data): frames.append(frame) - buff = extractor.save_as_chunk_to_buff(frames, file_extension) + buff = extractor.save_as_chunk_to_buff(frames) else: img_paths = None with open(db_data.get_dummy_chunk_path(chunk_number), 'r') as dummy_file: img_paths = [os.path.join(db_data.get_upload_dirname(), line.strip()) for line in dummy_file] - buff = extractor.save_as_chunk_to_buff(img_paths, file_extension) + buff = extractor.save_as_chunk_to_buff(img_paths) return buff, mime_type def save_chunk(self, db_data_id, chunk_number, quality, buff, mime_type): diff --git a/cvat/apps/engine/frame_provider.py b/cvat/apps/engine/frame_provider.py index 10e6b9e9..8f56463a 100644 --- a/cvat/apps/engine/frame_provider.py +++ b/cvat/apps/engine/frame_provider.py @@ -66,9 +66,8 @@ class FrameProvider: return self.chunk_reader class BuffChunkLoader(ChunkLoader): - def __init__(self, reader_class, path_getter, buff_mime_getter, quality, db_data): + def __init__(self, reader_class, path_getter, quality, db_data): super().__init__(reader_class, path_getter) - self.get_chunk = buff_mime_getter self.quality = quality self.db_data = db_data @@ -76,7 +75,7 @@ class FrameProvider: if self.chunk_id != chunk_id: self.chunk_id = chunk_id self.chunk_reader = RandomAccessIterator( - self.reader_class([self.get_chunk_path(chunk_id, self.quality, self.db_data)])) + self.reader_class([self.get_chunk_path(chunk_id, self.quality, self.db_data)[0]])) return self.chunk_reader def __init__(self, db_data): @@ -93,13 +92,11 @@ class FrameProvider: self._loaders[self.Quality.COMPRESSED] = self.BuffChunkLoader( reader_class[db_data.compressed_chunk_type], - cache.get_buff, cache.get_buff_mime, self.Quality.COMPRESSED, self._db_data) self._loaders[self.Quality.ORIGINAL] = self.BuffChunkLoader( reader_class[db_data.original_chunk_type], - cache.get_buff, cache.get_buff_mime, self.Quality.ORIGINAL, self._db_data) @@ -161,7 +158,7 @@ class FrameProvider: def get_chunk(self, chunk_number, quality=Quality.ORIGINAL): chunk_number = self._validate_chunk_number(chunk_number) if self._db_data.storage_method == StorageMethodChoice.CACHE: - return self._loaders[quality].get_chunk(chunk_number, quality, self._db_data) + return self._loaders[quality].get_chunk_path(chunk_number, quality, self._db_data) return self._loaders[quality].get_chunk_path(chunk_number) def get_frame(self, frame_number, quality=Quality.ORIGINAL, diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 97bd0249..0ea98941 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -287,12 +287,12 @@ class ZipChunkWriter(IChunkWriter): # and does not decode it to know img size. return [] - def save_as_chunk_to_buff(self, images, format_='jpeg'): + def save_as_chunk_to_buff(self, images): buff = io.BytesIO() with zipfile.ZipFile(buff, 'w') as zip_file: for idx, image in enumerate(images): - arcname = '{:06d}.{}'.format(idx, format_) + arcname = '{:06d}.{}'.format(idx, os.path.splitext(image)[1]) if isinstance(image, av.VideoFrame): zip_file.writestr(arcname, image.to_image().tobytes().getvalue()) else: @@ -312,12 +312,12 @@ class ZipCompressedChunkWriter(IChunkWriter): return image_sizes - def save_as_chunk_to_buff(self, images, format_='jpeg'): + def save_as_chunk_to_buff(self, images): buff = io.BytesIO() with zipfile.ZipFile(buff, 'x') as zip_file: for idx, image in enumerate(images): (_, _, image_buf) = self._compress_image(image, self._image_quality) - arcname = '{:06d}.{}'.format(idx, format_) + arcname = '{:06d}.jpeg'.format(idx) zip_file.writestr(arcname, image_buf.getvalue()) buff.seek(0) return buff @@ -366,7 +366,7 @@ class Mpeg4ChunkWriter(IChunkWriter): output_container.close() return [(input_w, input_h)] - def save_as_chunk_to_buff(self, frames, format_): + def save_as_chunk_to_buff(self, frames): if not frames: raise Exception('no images to save') @@ -383,7 +383,7 @@ class Mpeg4ChunkWriter(IChunkWriter): "crf": str(self._image_quality), "preset": "ultrafast", }, - f=format_, + f='mp4', ) for frame in frames: @@ -454,7 +454,7 @@ class Mpeg4CompressedChunkWriter(Mpeg4ChunkWriter): output_container.close() return [(input_w, input_h)] - def save_as_chunk_to_buff(self, frames, format_): + def save_as_chunk_to_buff(self, frames): if not frames: raise Exception('no images to save') @@ -482,7 +482,7 @@ class Mpeg4CompressedChunkWriter(Mpeg4ChunkWriter): 'wpredp': '0', 'flags': '-loop' }, - f=format_, + f='mp4', ) for frame in frames: diff --git a/cvat/apps/engine/prepare.py b/cvat/apps/engine/prepare.py index 83a11f93..7e0fe395 100644 --- a/cvat/apps/engine/prepare.py +++ b/cvat/apps/engine/prepare.py @@ -93,7 +93,7 @@ class PrepareInfo(WorkWithVideo): key_frames_copy = self.key_frames.copy() - for _, key_frame in key_frames_copy.items(): + 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): diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 881bbed2..1056e2f3 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -304,9 +304,7 @@ def _create_thread(tid, data): frame = meta_info.key_frames.get(next(iter(meta_info.key_frames))) video_size = (frame.width, frame.height) - except AssertionError as ex: - db_data.storage_method = StorageMethodChoice.FILE_SYSTEM - except Exception as ex: + except Exception: db_data.storage_method = StorageMethodChoice.FILE_SYSTEM else:#images,archive diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index e5800d15..94db4f39 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -408,7 +408,6 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet): if data_type == 'chunk': data_id = int(data_id) - quality = data_quality data_quality = FrameProvider.Quality.COMPRESSED \ if data_quality == 'compressed' else FrameProvider.Quality.ORIGINAL From 7bb5e02ff1f8bfea6bcee9591fc594ef1194b2a4 Mon Sep 17 00:00:00 2001 From: Maya Date: Mon, 24 Aug 2020 23:10:08 +0300 Subject: [PATCH 013/467] Added license headers --- cvat/apps/engine/cache.py | 4 ++++ cvat/apps/engine/prepare.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/cvat/apps/engine/cache.py b/cvat/apps/engine/cache.py index e4d19f59..34753557 100644 --- a/cvat/apps/engine/cache.py +++ b/cvat/apps/engine/cache.py @@ -1,3 +1,7 @@ +# Copyright (C) 2020 Intel Corporation +# +# SPDX-License-Identifier: MIT + from diskcache import Cache from django.conf import settings from cvat.apps.engine.media_extractors import (Mpeg4ChunkWriter, ZipChunkWriter, diff --git a/cvat/apps/engine/prepare.py b/cvat/apps/engine/prepare.py index 7e0fe395..97c07a84 100644 --- a/cvat/apps/engine/prepare.py +++ b/cvat/apps/engine/prepare.py @@ -1,3 +1,7 @@ +# Copyright (C) 2020 Intel Corporation +# +# SPDX-License-Identifier: MIT + import av import hashlib From e0461574ba6ffe62c3cd8d6d97ef4fc055849e8b Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 25 Aug 2020 08:36:20 +0300 Subject: [PATCH 014/467] Updated version --- cvat-core/package-lock.json | 2 +- cvat-core/package.json | 2 +- cvat-ui/package-lock.json | 2 +- cvat-ui/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json index 0bc2c787..91ac8a0f 100644 --- a/cvat-core/package-lock.json +++ b/cvat-core/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "3.5.0", + "version": "3.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-core/package.json b/cvat-core/package.json index ca65f045..4418f157 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "3.5.0", + "version": "3.6.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "babel.config.js", "scripts": { diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 4f97ca1e..4cfa9ddf 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 64353225..e587666f 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.8.1", + "version": "1.9.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { From 761c83f3ab0d3f990010e95c09b187c82861b3ba Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 25 Aug 2020 09:20:38 +0300 Subject: [PATCH 015/467] Fix migration --- ...{0028_data_storage_method.py => 0029_data_storage_method.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename cvat/apps/engine/migrations/{0028_data_storage_method.py => 0029_data_storage_method.py} (96%) diff --git a/cvat/apps/engine/migrations/0028_data_storage_method.py b/cvat/apps/engine/migrations/0029_data_storage_method.py similarity index 96% rename from cvat/apps/engine/migrations/0028_data_storage_method.py rename to cvat/apps/engine/migrations/0029_data_storage_method.py index 4bd8f757..1c1aa814 100644 --- a/cvat/apps/engine/migrations/0028_data_storage_method.py +++ b/cvat/apps/engine/migrations/0029_data_storage_method.py @@ -23,7 +23,7 @@ def unzip(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('engine', '0027_auto_20200719_1552'), + ('engine', '0028_labelcolor'), ] operations = [ From b47d3d5683bc2a5b74f80794367a831674f038fd Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 25 Aug 2020 10:29:47 +0300 Subject: [PATCH 016/467] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca2dea56..3e5310b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [Datumaro] Multi-dataset merge (https://github.com/opencv/cvat/pull/1695) - Link to django admin page from UI () - Notification message when users use wrong browser () +- Ability to work with data on the fly (https://github.com/opencv/cvat/pull/2007) ### Changed - Shape coordinates are rounded to 2 digits in dumped annotations () From d29f4cb6e0fa8b374602c869229a24be56a47c9e Mon Sep 17 00:00:00 2001 From: Maya Date: Thu, 27 Aug 2020 09:07:03 +0300 Subject: [PATCH 017/467] Added tests --- cvat/apps/engine/tests/_test_rest_api.py | 85 ++++++++++++++++++++++-- cvat/settings/testing.py | 2 + 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/cvat/apps/engine/tests/_test_rest_api.py b/cvat/apps/engine/tests/_test_rest_api.py index 73c81141..1e773b2f 100644 --- a/cvat/apps/engine/tests/_test_rest_api.py +++ b/cvat/apps/engine/tests/_test_rest_api.py @@ -72,13 +72,14 @@ import av import numpy as np from django.conf import settings from django.contrib.auth.models import Group, User +from django.http import HttpResponse from PIL import Image from pycocotools import coco as coco_loader from rest_framework import status from rest_framework.test import APIClient, APITestCase from cvat.apps.engine.models import (AttributeType, Data, Job, Project, - Segment, StatusChoice, Task) + Segment, StatusChoice, Task, StorageMethodChoice) _setUpModule() @@ -1670,7 +1671,8 @@ class TaskDataAPITestCase(APITestCase): stream = container.streams.video[0] return [f.to_image() for f in container.decode(stream)] - def _test_api_v1_tasks_id_data_spec(self, user, spec, data, expected_compressed_type, expected_original_type, image_sizes): + def _test_api_v1_tasks_id_data_spec(self, user, spec, data, expected_compressed_type, expected_original_type, image_sizes, + expected_storage_method=StorageMethodChoice.FILE_SYSTEM): # create task response = self._create_task(user, spec) self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -1694,6 +1696,7 @@ class TaskDataAPITestCase(APITestCase): self.assertEqual(expected_compressed_type, task["data_compressed_chunk_type"]) self.assertEqual(expected_original_type, task["data_original_chunk_type"]) self.assertEqual(len(image_sizes), task["size"]) + self.assertEqual(expected_storage_method, Task.objects.get(pk=task_id).data.storage_method) # check preview response = self._get_preview(task_id, user) @@ -1706,7 +1709,10 @@ class TaskDataAPITestCase(APITestCase): response = self._get_compressed_chunk(task_id, user, 0) self.assertEqual(response.status_code, expected_status_code) if expected_status_code == status.HTTP_200_OK: - compressed_chunk = io.BytesIO(b"".join(response.streaming_content)) + if isinstance(response, HttpResponse): + compressed_chunk = io.BytesIO(response.content) + else: + compressed_chunk = io.BytesIO(b"".join(response.streaming_content)) if task["data_compressed_chunk_type"] == self.ChunkType.IMAGESET: images = self._extract_zip_chunk(compressed_chunk) else: @@ -1721,7 +1727,10 @@ class TaskDataAPITestCase(APITestCase): response = self._get_original_chunk(task_id, user, 0) self.assertEqual(response.status_code, expected_status_code) if expected_status_code == status.HTTP_200_OK: - original_chunk = io.BytesIO(b"".join(response.streaming_content)) + if isinstance(response, HttpResponse): + original_chunk = io.BytesIO(response.getvalue()) + else: + original_chunk = io.BytesIO(b"".join(response.streaming_content)) if task["data_original_chunk_type"] == self.ChunkType.IMAGESET: images = self._extract_zip_chunk(original_chunk) else: @@ -1909,6 +1918,74 @@ class TaskDataAPITestCase(APITestCase): self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.IMAGESET, image_sizes) + task_spec = { + "name": "use_cache video task #8", + "overlap": 0, + "segment_size": 0, + "labels": [ + {"name": "car"}, + {"name": "person"}, + ] + } + + task_data = { + "server_files[0]": 'test_video_1.mp4', + "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) + + task_spec = { + "name": "use_cache images task #9", + "overlap": 0, + "segment_size": 0, + "labels": [ + {"name": "car"}, + {"name": "person"}, + ] + } + + task_data = { + "server_files[0]": "test_1.jpg", + "server_files[1]": "test_2.jpg", + "server_files[2]": "test_3.jpg", + "image_quality": 70, + "use_cache": True, + } + image_sizes = [ + self._image_sizes[task_data["server_files[0]"]], + self._image_sizes[task_data["server_files[1]"]], + self._image_sizes[task_data["server_files[2]"]], + ] + + self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, + self.ChunkType.IMAGESET, image_sizes, StorageMethodChoice.CACHE) + + task_spec = { + "name": "my zip archive task #10", + "overlap": 0, + "segment_size": 0, + "labels": [ + {"name": "car"}, + {"name": "person"}, + ] + } + + task_data = { + "server_files[0]": "test_archive_1.zip", + "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.IMAGESET, + self.ChunkType.IMAGESET, image_sizes, StorageMethodChoice.CACHE) + def test_api_v1_tasks_id_data_admin(self): self._test_api_v1_tasks_id_data(self.admin) diff --git a/cvat/settings/testing.py b/cvat/settings/testing.py index 35bf5eaf..9825349f 100644 --- a/cvat/settings/testing.py +++ b/cvat/settings/testing.py @@ -22,6 +22,8 @@ os.makedirs(TASKS_ROOT, exist_ok=True) MODELS_ROOT = os.path.join(DATA_ROOT, 'models') os.makedirs(MODELS_ROOT, exist_ok=True) +CACHE_ROOT = os.path.join(DATA_ROOT, 'cache') +os.makedirs(CACHE_ROOT, exist_ok=True) # To avoid ERROR django.security.SuspiciousFileOperation: # The joined path (...) is located outside of the base path component From fb383c71e2a7e4e05bfd26e9c9c721e72cb01559 Mon Sep 17 00:00:00 2001 From: Maya Date: Thu, 27 Aug 2020 09:19:06 +0300 Subject: [PATCH 018/467] Deleted unnecessary --- cvat/apps/engine/media_extractors.py | 2 +- cvat/apps/engine/prepare.py | 23 ++--------------------- cvat/apps/engine/views.py | 2 +- 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 0ea98941..49ee9c9f 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -188,7 +188,7 @@ class ZipReader(ImageListReader): def get_path(self, i): if self._zip_source.filename: return os.path.join(os.path.dirname(self._zip_source.filename), self._source_path[i]) - else: #для определения mime_type + else: # necessary for mime_type definition return self._source_path[i] def extract(self): diff --git a/cvat/apps/engine/prepare.py b/cvat/apps/engine/prepare.py index 97c07a84..3d4ca7da 100644 --- a/cvat/apps/engine/prepare.py +++ b/cvat/apps/engine/prepare.py @@ -54,25 +54,6 @@ class AnalyzeVideo(WorkWithVideo): frame_pts, frame_dts = frame.pts, frame.dts self._close_video_container(container) -# class Frame: -# def __init__(self, frame, frame_number=None): -# self.frame = frame -# if frame_number: -# self.frame_number = frame_number - -# def md5_hash(self): -# return hashlib.md5(self.frame.to_image().tobytes()).hexdigest() - -# def __eq__(self, image): -# return self.md5_hash(self) == image.md5_hash(image) and self.frame.pts == image.frame.pts - -# def __ne__(self, image): -# return md5_hash(self) != md5_hash(image) or self.frame.pts != image.frame.pts - -# def __len__(self): -# return (self.frame.width, self.frame.height) - - def md5_hash(frame): return hashlib.md5(frame.to_image().tobytes()).hexdigest() @@ -109,7 +90,8 @@ class PrepareInfo(WorkWithVideo): if not flag: break - if len(self.key_frames) == 0: #or self.frames // len(self.key_frames) > 300: + #TODO: correct ratio of number of frames to keyframes + if len(self.key_frames) == 0: raise Exception('Too few keyframes') def save_key_frames(self): @@ -139,7 +121,6 @@ class PrepareInfo(WorkWithVideo): for line in file: frame_number, timestamp = line.strip().split(' ') - #TODO: исправить если вдруг ключевой кадр окажется не первым if int(frame_number) <= start_chunk_frame_number: start_decode_frame_number = frame_number start_decode_timestamp = timestamp diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 94db4f39..7e46d836 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -27,7 +27,7 @@ from rest_framework.exceptions import APIException from rest_framework.permissions import SAFE_METHODS, IsAuthenticated from rest_framework.renderers import JSONRenderer from rest_framework.response import Response -from sendfile import sendfile#убрать +from sendfile import sendfile import cvat.apps.dataset_manager as dm import cvat.apps.dataset_manager.views # pylint: disable=unused-import From e7df942f6daa9a13b6b8e04aa0fea23ae738cba3 Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 28 Aug 2020 09:03:09 +0300 Subject: [PATCH 019/467] Refactoring --- cvat/apps/engine/cache.py | 18 +++-- cvat/apps/engine/media_extractors.py | 106 +-------------------------- 2 files changed, 11 insertions(+), 113 deletions(-) diff --git a/cvat/apps/engine/cache.py b/cvat/apps/engine/cache.py index 34753557..2f8b0a62 100644 --- a/cvat/apps/engine/cache.py +++ b/cvat/apps/engine/cache.py @@ -9,6 +9,7 @@ from cvat.apps.engine.media_extractors import (Mpeg4ChunkWriter, ZipChunkWriter, from cvat.apps.engine.models import DataChoice from .prepare import PrepareInfo import os +from io import BytesIO class CacheInteraction: def __init__(self): @@ -37,18 +38,19 @@ class CacheInteraction: extractor = extractor_classes[quality](image_quality) + images = [] + buff = BytesIO() if os.path.exists(db_data.get_meta_path()): - meta = PrepareInfo(source_path=os.path.join(db_data.get_upload_dirname(), db_data.video.path), - meta_path=db_data.get_meta_path()) - frames = [] + source_path = os.path.join(db_data.get_upload_dirname(), db_data.video.path) + meta = PrepareInfo(source_path=source_path, meta_path=db_data.get_meta_path()) for frame in meta.decode_needed_frames(chunk_number, db_data): - frames.append(frame) - buff = extractor.save_as_chunk_to_buff(frames) + images.append(frame) + extractor.save_as_chunk([(image, source_path, None) for image in images], buff) else: - img_paths = None with open(db_data.get_dummy_chunk_path(chunk_number), 'r') as dummy_file: - img_paths = [os.path.join(db_data.get_upload_dirname(), line.strip()) for line in dummy_file] - buff = extractor.save_as_chunk_to_buff(img_paths) + images = [os.path.join(db_data.get_upload_dirname(), line.strip()) for line in dummy_file] + extractor.save_as_chunk([(image, image, None) for image in images], buff) + buff.seek(0) return buff, mime_type def save_chunk(self, db_data_id, chunk_number, quality, buff, mime_type): diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 49ee9c9f..08a660c5 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -287,19 +287,6 @@ class ZipChunkWriter(IChunkWriter): # and does not decode it to know img size. return [] - def save_as_chunk_to_buff(self, images): - buff = io.BytesIO() - - with zipfile.ZipFile(buff, 'w') as zip_file: - for idx, image in enumerate(images): - arcname = '{:06d}.{}'.format(idx, os.path.splitext(image)[1]) - if isinstance(image, av.VideoFrame): - zip_file.writestr(arcname, image.to_image().tobytes().getvalue()) - else: - zip_file.write(filename=image, arcname=arcname) - buff.seek(0) - return buff - class ZipCompressedChunkWriter(IChunkWriter): def save_as_chunk(self, images, chunk_path): image_sizes = [] @@ -312,23 +299,13 @@ class ZipCompressedChunkWriter(IChunkWriter): return image_sizes - def save_as_chunk_to_buff(self, images): - buff = io.BytesIO() - with zipfile.ZipFile(buff, 'x') as zip_file: - for idx, image in enumerate(images): - (_, _, image_buf) = self._compress_image(image, self._image_quality) - arcname = '{:06d}.jpeg'.format(idx) - zip_file.writestr(arcname, image_buf.getvalue()) - buff.seek(0) - return buff - class Mpeg4ChunkWriter(IChunkWriter): def __init__(self, _): super().__init__(17) self._output_fps = 25 @staticmethod - def _create_av_container(path, w, h, rate, options, f=None): + def _create_av_container(path, w, h, rate, options, f='mp4'): # x264 requires width and height must be divisible by 2 for yuv420p if h % 2: h += 1 @@ -366,41 +343,6 @@ class Mpeg4ChunkWriter(IChunkWriter): output_container.close() return [(input_w, input_h)] - def save_as_chunk_to_buff(self, frames): - if not frames: - raise Exception('no images to save') - - buff = io.BytesIO() - input_w = frames[0].width - input_h = frames[0].height - - output_container, output_v_stream = self._create_av_container( - path=buff, - w=input_w, - h=input_h, - rate=self._output_fps, - options={ - "crf": str(self._image_quality), - "preset": "ultrafast", - }, - f='mp4', - ) - - for frame in frames: - # let libav set the correct pts and time_base - frame.pts = None - frame.time_base = None - - for packet in output_v_stream.encode(frame): - output_container.mux(packet) - - # Flush streams - for packet in output_v_stream.encode(): - output_container.mux(packet) - output_container.close() - buff.seek(0) - return buff - @staticmethod def _encode_images(images, container, stream): for frame, _, _ in images: @@ -454,52 +396,6 @@ class Mpeg4CompressedChunkWriter(Mpeg4ChunkWriter): output_container.close() return [(input_w, input_h)] - def save_as_chunk_to_buff(self, frames): - if not frames: - raise Exception('no images to save') - - buff = io.BytesIO() - input_w = frames[0].width - input_h = frames[0].height - - downscale_factor = 1 - while input_h / downscale_factor >= 1080: - downscale_factor *= 2 - - output_h = input_h // downscale_factor - output_w = input_w // downscale_factor - - - output_container, output_v_stream = self._create_av_container( - path=buff, - w=output_w, - h=output_h, - rate=self._output_fps, - options={ - 'profile': 'baseline', - 'coder': '0', - 'crf': str(self._image_quality), - 'wpredp': '0', - 'flags': '-loop' - }, - f='mp4', - ) - - for frame in frames: - # let libav set the correct pts and time_base - frame.pts = None - frame.time_base = None - - for packet in output_v_stream.encode(frame): - output_container.mux(packet) - - # Flush streams - for packet in output_v_stream.encode(): - output_container.mux(packet) - output_container.close() - buff.seek(0) - return buff - def _is_archive(path): mime = mimetypes.guess_type(path) mime_type = mime[0] From 174fe1690b17fef3ea7c4af21110364fa535555d Mon Sep 17 00:00:00 2001 From: Nikita Manovich Date: Mon, 31 Aug 2020 18:12:29 +0300 Subject: [PATCH 020/467] Update CHANGELOG and version. --- CHANGELOG.md | 19 +++++++++++++++++++ cvat/__init__.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb1d1dd3..e35e24d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.0] - Unreleased +### Added +- + +### Changed +- + +### Deprecated +- + +### Removed +- + +### Fixed +- + +### Security +- + ## [1.1.0] - 2020-08-31 ### Added - Siammask tracker as DL serverless function () diff --git a/cvat/__init__.py b/cvat/__init__.py index 61d81f44..f96a1724 100644 --- a/cvat/__init__.py +++ b/cvat/__init__.py @@ -4,6 +4,6 @@ from cvat.utils.version import get_version -VERSION = (1, 1, 0, 'final', 0) +VERSION = (1, 2, 0, 'alpha', 0) __version__ = get_version(VERSION) From 908e0569d80cdebce7d0bf70230c6d34e3d3d85d Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 31 Aug 2020 22:20:50 +0300 Subject: [PATCH 021/467] Improved interface of interactors on UI (#2054) --- CHANGELOG.md | 2 +- cvat-canvas/README.md | 53 +- cvat-canvas/package-lock.json | 2 +- cvat-canvas/package.json | 2 +- cvat-canvas/src/typescript/canvas.ts | 26 +- .../src/typescript/canvasController.ts | 11 + cvat-canvas/src/typescript/canvasModel.ts | 49 +- cvat-canvas/src/typescript/canvasView.ts | 67 +++ cvat-canvas/src/typescript/crosshair.ts | 70 +++ cvat-canvas/src/typescript/drawHandler.ts | 33 +- cvat-canvas/src/typescript/editHandler.ts | 12 +- .../src/typescript/interactionHandler.ts | 281 +++++++++++ cvat-core/src/annotations-history.js | 6 + cvat-core/src/annotations.js | 14 + cvat-core/src/session.js | 26 + cvat-ui/src/actions/annotation-actions.ts | 27 +- cvat-ui/src/actions/models-actions.ts | 9 +- cvat-ui/src/assets/ai-tools-icon.svg | 18 + .../controls-side-bar/controls-side-bar.tsx | 11 +- .../controls-side-bar/dextr-plugin.tsx | 90 ---- .../controls-side-bar/draw-shape-popover.tsx | 2 - .../controls-side-bar/tools-control.tsx | 472 ++++++++++++++++++ .../standard-workspace/styles.scss | 24 +- .../model-runner-modal/model-runner-modal.tsx | 46 +- .../models-page/built-model-item.tsx | 49 -- .../models-page/deployed-models-list.tsx | 1 - .../components/models-page/models-page.tsx | 14 +- .../model-runner-dialog.tsx | 6 +- .../containers/models-page/models-page.tsx | 16 +- cvat-ui/src/cvat-canvas-wrapper.ts | 5 + cvat-ui/src/icons.tsx | 5 + cvat-ui/src/reducers/annotation-reducer.ts | 23 + cvat-ui/src/reducers/interfaces.ts | 10 +- cvat-ui/src/reducers/models-reducer.ts | 12 +- cvat-ui/src/reducers/notifications-reducer.ts | 17 - cvat-ui/src/reducers/plugins-reducer.ts | 6 - cvat-ui/src/utils/dextr-utils.ts | 214 -------- cvat-ui/src/utils/plugin-checker.ts | 8 - 38 files changed, 1223 insertions(+), 516 deletions(-) create mode 100644 cvat-canvas/src/typescript/crosshair.ts create mode 100644 cvat-canvas/src/typescript/interactionHandler.ts create mode 100644 cvat-ui/src/assets/ai-tools-icon.svg delete mode 100644 cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/dextr-plugin.tsx create mode 100644 cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx delete mode 100644 cvat-ui/src/components/models-page/built-model-item.tsx delete mode 100644 cvat-ui/src/utils/dextr-utils.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e35e24d8..d1534bca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - ### Changed -- +- UI models (like DEXTR) were redesigned to be more interactive () ### Deprecated - diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md index 808aa5ac..d6a5698b 100644 --- a/cvat-canvas/README.md +++ b/cvat-canvas/README.md @@ -46,6 +46,7 @@ Canvas itself handles: IDLE = 'idle', DRAG = 'drag', RESIZE = 'resize', + INTERACT = 'interact', DRAW = 'draw', EDIT = 'edit', MERGE = 'merge', @@ -70,6 +71,11 @@ Canvas itself handles: crosshair?: boolean; } + interface InteractionData { + shapeType: string; + minVertices?: number; + } + interface GroupData { enabled: boolean; resetGroup?: boolean; @@ -83,6 +89,12 @@ Canvas itself handles: enabled: boolean; } + interface InteractionResult { + points: number[]; + shapeType: string; + button: number; + }; + interface DrawnData { shapeType: string; points: number[]; @@ -104,6 +116,7 @@ Canvas itself handles: grid(stepX: number, stepY: number): void; draw(drawData: DrawData): void; + interact(interactionData: InteractionData): void; group(groupData: GroupData): void; split(splitData: SplitData): void; merge(mergeData: MergeData): void; @@ -146,6 +159,7 @@ Standard JS events are used. - canvas.moved => {states: ObjectState[], x: number, y: number} - canvas.find => {states: ObjectState[], x: number, y: number} - canvas.drawn => {state: DrawnData} + - canvas.interacted => {shapes: InteractionResult[]} - canvas.editstart - canvas.edited => {state: ObjectState, points: number[]} - canvas.splitted => {state: ObjectState} @@ -187,25 +201,26 @@ Standard JS events are used. ## API Reaction -| | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS | -|--------------|------|-------|-------|------|-------|------|------|--------|-------------|-------------| -| setup() | + | + | + | +/- | + | +/- | +/- | +/- | + | + | -| activate() | + | - | - | - | - | - | - | - | - | - | -| rotate() | + | + | + | + | + | + | + | + | + | + | -| focus() | + | + | + | + | + | + | + | + | + | + | -| fit() | + | + | + | + | + | + | + | + | + | + | -| grid() | + | + | + | + | + | + | + | + | + | + | -| draw() | + | - | - | - | - | - | - | - | - | - | -| split() | + | - | + | - | - | - | - | - | - | - | -| group() | + | + | - | - | - | - | - | - | - | - | -| merge() | + | - | - | - | + | - | - | - | - | - | -| fitCanvas() | + | + | + | + | + | + | + | + | + | + | -| dragCanvas() | + | - | - | - | - | - | + | - | - | + | -| zoomCanvas() | + | - | - | - | - | - | - | + | + | - | -| cancel() | - | + | + | + | + | + | + | + | + | + | -| configure() | + | + | + | + | + | + | + | + | + | + | -| bitmap() | + | + | + | + | + | + | + | + | + | + | -| setZLayer() | + | + | + | + | + | + | + | + | + | + | +| | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS | INTERACT | +|--------------|------|-------|-------|------|-------|------|------|--------|-------------|-------------|----------| +| setup() | + | + | + | +/- | + | +/- | +/- | +/- | + | + | + | +| activate() | + | - | - | - | - | - | - | - | - | - | - | +| rotate() | + | + | + | + | + | + | + | + | + | + | + | +| focus() | + | + | + | + | + | + | + | + | + | + | + | +| fit() | + | + | + | + | + | + | + | + | + | + | + | +| grid() | + | + | + | + | + | + | + | + | + | + | + | +| draw() | + | - | - | + | - | - | - | - | - | - | - | +| interact() | + | - | - | - | - | - | - | - | - | - | + | +| split() | + | - | + | - | - | - | - | - | - | - | - | +| group() | + | + | - | - | - | - | - | - | - | - | - | +| merge() | + | - | - | - | + | - | - | - | - | - | - | +| fitCanvas() | + | + | + | + | + | + | + | + | + | + | + | +| dragCanvas() | + | - | - | - | - | - | + | - | - | + | - | +| zoomCanvas() | + | - | - | - | - | - | - | + | + | - | - | +| cancel() | - | + | + | + | + | + | + | + | + | + | + | +| configure() | + | + | + | + | + | + | + | + | + | + | + | +| bitmap() | + | + | + | + | + | + | + | + | + | + | + | +| setZLayer() | + | + | + | + | + | + | + | + | + | + | + | You can call setup() during editing, dragging, and resizing only to update objects, not to change a frame. You can change frame during draw only when you do not redraw an existing object diff --git a/cvat-canvas/package-lock.json b/cvat-canvas/package-lock.json index b50b6223..5ee19968 100644 --- a/cvat-canvas/package-lock.json +++ b/cvat-canvas/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.0.2", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index 8792ce08..ba7f0180 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.0.2", + "version": "2.1.0", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", "scripts": { diff --git a/cvat-canvas/src/typescript/canvas.ts b/cvat-canvas/src/typescript/canvas.ts index 35699766..508e5564 100644 --- a/cvat-canvas/src/typescript/canvas.ts +++ b/cvat-canvas/src/typescript/canvas.ts @@ -8,26 +8,17 @@ import { MergeData, SplitData, GroupData, + InteractionData, + InteractionResult, CanvasModel, CanvasModelImpl, RectDrawingMethod, CuboidDrawingMethod, Configuration, } from './canvasModel'; - -import { - Master, -} from './master'; - -import { - CanvasController, - CanvasControllerImpl, -} from './canvasController'; - -import { - CanvasView, - CanvasViewImpl, -} from './canvasView'; +import { Master } from './master'; +import { CanvasController, CanvasControllerImpl } from './canvasController'; +import { CanvasView, CanvasViewImpl } from './canvasView'; import '../scss/canvas.scss'; import pjson from '../../package.json'; @@ -43,6 +34,7 @@ interface Canvas { fit(): void; grid(stepX: number, stepY: number): void; + interact(interactionData: InteractionData): void; draw(drawData: DrawData): void; group(groupData: GroupData): void; split(splitData: SplitData): void; @@ -118,6 +110,10 @@ class CanvasImpl implements Canvas { this.model.grid(stepX, stepY); } + public interact(interactionData: InteractionData): void { + this.model.interact(interactionData); + } + public draw(drawData: DrawData): void { this.model.draw(drawData); } @@ -162,4 +158,6 @@ export { RectDrawingMethod, CuboidDrawingMethod, Mode as CanvasMode, + InteractionData, + InteractionResult, }; diff --git a/cvat-canvas/src/typescript/canvasController.ts b/cvat-canvas/src/typescript/canvasController.ts index 179f9b32..786836d8 100644 --- a/cvat-canvas/src/typescript/canvasController.ts +++ b/cvat-canvas/src/typescript/canvasController.ts @@ -13,6 +13,7 @@ import { SplitData, GroupData, Mode, + InteractionData, } from './canvasModel'; export interface CanvasController { @@ -21,6 +22,7 @@ export interface CanvasController { readonly focusData: FocusData; readonly activeElement: ActiveElement; readonly drawData: DrawData; + readonly interactionData: InteractionData; readonly mergeData: MergeData; readonly splitData: SplitData; readonly groupData: GroupData; @@ -30,6 +32,7 @@ export interface CanvasController { zoom(x: number, y: number, direction: number): void; draw(drawData: DrawData): void; + interact(interactionData: InteractionData): void; merge(mergeData: MergeData): void; split(splitData: SplitData): void; group(groupData: GroupData): void; @@ -84,6 +87,10 @@ export class CanvasControllerImpl implements CanvasController { this.model.draw(drawData); } + public interact(interactionData: InteractionData): void { + this.model.interact(interactionData); + } + public merge(mergeData: MergeData): void { this.model.merge(mergeData); } @@ -124,6 +131,10 @@ export class CanvasControllerImpl implements CanvasController { return this.model.drawData; } + public get interactionData(): InteractionData { + return this.model.interactionData; + } + public get mergeData(): MergeData { return this.model.mergeData; } diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index ec860e77..74d9f49c 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -69,6 +69,20 @@ export interface DrawData { redraw?: number; } +export interface InteractionData { + enabled: boolean; + shapeType?: string; + crosshair?: boolean; + minPosVertices?: number; + minNegVertices?: number; +} + +export interface InteractionResult { + points: number[]; + shapeType: string; + button: number; +} + export interface EditData { enabled: boolean; state: any; @@ -105,6 +119,7 @@ export enum UpdateReasons { FITTED_CANVAS = 'fitted_canvas', + INTERACT = 'interact', DRAW = 'draw', MERGE = 'merge', SPLIT = 'split', @@ -126,6 +141,7 @@ export enum Mode { MERGE = 'merge', SPLIT = 'split', GROUP = 'group', + INTERACT = 'interact', DRAG_CANVAS = 'drag_canvas', ZOOM_CANVAS = 'zoom_canvas', } @@ -139,6 +155,7 @@ export interface CanvasModel { readonly focusData: FocusData; readonly activeElement: ActiveElement; readonly drawData: DrawData; + readonly interactionData: InteractionData; readonly mergeData: MergeData; readonly splitData: SplitData; readonly groupData: GroupData; @@ -162,6 +179,7 @@ export interface CanvasModel { split(splitData: SplitData): void; merge(mergeData: MergeData): void; select(objectState: any): void; + interact(interactionData: InteractionData): void; fitCanvas(width: number, height: number): void; bitmap(enabled: boolean): void; @@ -192,6 +210,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { top: number; zLayer: number | null; drawData: DrawData; + interactionData: InteractionData; mergeData: MergeData; groupData: GroupData; splitData: SplitData; @@ -242,6 +261,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { enabled: false, initialState: null, }, + interactionData: { + enabled: false, + }, mergeData: { enabled: false, }, @@ -490,6 +512,27 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { this.notify(UpdateReasons.DRAW); } + public interact(interactionData: InteractionData): void { + if (![Mode.IDLE, Mode.INTERACT].includes(this.data.mode)) { + throw Error(`Canvas is busy. Action: ${this.data.mode}`); + } + + if (interactionData.enabled) { + if (this.data.interactionData.enabled) { + throw new Error('Interaction has been already started'); + } else if (!interactionData.shapeType) { + throw new Error('A shape type was not specified'); + } + } + + this.data.interactionData = interactionData; + if (typeof (this.data.interactionData.crosshair) !== 'boolean') { + this.data.interactionData.crosshair = true; + } + + this.notify(UpdateReasons.INTERACT); + } + public split(splitData: SplitData): void { if (![Mode.IDLE, Mode.SPLIT].includes(this.data.mode)) { throw Error(`Canvas is busy. Action: ${this.data.mode}`); @@ -567,7 +610,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { } public isAbleToChangeFrame(): boolean { - const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE].includes(this.data.mode) + const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE, Mode.INTERACT].includes(this.data.mode) || (this.data.mode === Mode.DRAW && typeof (this.data.drawData.redraw) === 'number'); return !isUnable; @@ -647,6 +690,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { return { ...this.data.drawData }; } + public get interactionData(): InteractionData { + return { ...this.data.interactionData }; + } + public get mergeData(): MergeData { return { ...this.data.mergeData }; } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 4eb95a8f..3cf37f04 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -16,6 +16,7 @@ import { MergeHandler, MergeHandlerImpl } from './mergeHandler'; import { SplitHandler, SplitHandlerImpl } from './splitHandler'; import { GroupHandler, GroupHandlerImpl } from './groupHandler'; import { ZoomHandler, ZoomHandlerImpl } from './zoomHandler'; +import { InteractionHandler, InteractionHandlerImpl } from './interactionHandler'; import { AutoborderHandler, AutoborderHandlerImpl } from './autoborderHandler'; import consts from './consts'; import { @@ -42,6 +43,8 @@ import { Mode, Size, Configuration, + InteractionResult, + InteractionData, } from './canvasModel'; export interface CanvasView { @@ -72,6 +75,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private groupHandler: GroupHandler; private zoomHandler: ZoomHandler; private autoborderHandler: AutoborderHandler; + private interactionHandler: InteractionHandler; private activeElement: ActiveElement; private configuration: Configuration; private serviceFlags: { @@ -127,6 +131,41 @@ export class CanvasViewImpl implements CanvasView, Listener { } } + private onInteraction( + shapes: InteractionResult[] | null, + shapesUpdated: boolean = true, + isDone: boolean = false, + ): void { + const { zLayer } = this.controller; + if (Array.isArray(shapes)) { + const event: CustomEvent = new CustomEvent('canvas.interacted', { + bubbles: false, + cancelable: true, + detail: { + shapesUpdated, + isDone, + shapes, + zOrder: zLayer || 0, + }, + }); + + this.canvas.dispatchEvent(event); + } + + if (shapes === null || isDone) { + const event: CustomEvent = new CustomEvent('canvas.canceled', { + bubbles: false, + cancelable: true, + }); + + this.canvas.dispatchEvent(event); + this.mode = Mode.IDLE; + this.controller.interact({ + enabled: false, + }); + } + } + private onDrawDone(data: object | null, duration: number, continueDraw?: boolean): void { const hiddenBecauseOfDraw = Object.keys(this.serviceFlags.drawHidden) .map((_clientID): number => +_clientID); @@ -373,6 +412,8 @@ export class CanvasViewImpl implements CanvasView, Listener { this.drawHandler.transform(this.geometry); this.editHandler.transform(this.geometry); this.zoomHandler.transform(this.geometry); + this.autoborderHandler.transform(this.geometry); + this.interactionHandler.transform(this.geometry); } private transformCanvas(): void { @@ -438,7 +479,9 @@ export class CanvasViewImpl implements CanvasView, Listener { // Transform handlers this.drawHandler.transform(this.geometry); this.editHandler.transform(this.geometry); + this.zoomHandler.transform(this.geometry); this.autoborderHandler.transform(this.geometry); + this.interactionHandler.transform(this.geometry); } private resizeCanvas(): void { @@ -846,6 +889,11 @@ export class CanvasViewImpl implements CanvasView, Listener { this.adoptedContent, this.geometry, ); + this.interactionHandler = new InteractionHandlerImpl( + this.onInteraction.bind(this), + this.adoptedContent, + this.geometry, + ); // Setup event handlers this.content.addEventListener('dblclick', (e: MouseEvent): void => { @@ -1063,6 +1111,18 @@ export class CanvasViewImpl implements CanvasView, Listener { this.drawHandler.draw(data, this.geometry); } } + } else if (reason === UpdateReasons.INTERACT) { + const data: InteractionData = this.controller.interactionData; + if (data.enabled && this.mode === Mode.IDLE) { + this.canvas.style.cursor = 'crosshair'; + this.mode = Mode.INTERACT; + this.interactionHandler.interact(data); + } else { + this.canvas.style.cursor = ''; + if (this.mode !== Mode.IDLE) { + this.interactionHandler.interact(data); + } + } } else if (reason === UpdateReasons.MERGE) { const data: MergeData = this.controller.mergeData; if (data.enabled) { @@ -1101,6 +1161,8 @@ export class CanvasViewImpl implements CanvasView, Listener { } else if (reason === UpdateReasons.CANCEL) { if (this.mode === Mode.DRAW) { this.drawHandler.cancel(); + } else if (this.mode === Mode.INTERACT) { + this.interactionHandler.cancel(); } else if (this.mode === Mode.MERGE) { this.mergeHandler.cancel(); } else if (this.mode === Mode.SPLIT) { @@ -1405,6 +1467,11 @@ export class CanvasViewImpl implements CanvasView, Listener { [state, +state.getAttribute('data-z-order')] )); + const crosshair = Array.from(this.content.getElementsByClassName('cvat_canvas_crosshair')); + crosshair.forEach((line: SVGLineElement): void => this.content.append(line)); + const interaction = Array.from(this.content.getElementsByClassName('cvat_interaction_point')); + interaction.forEach((circle: SVGCircleElement): void => this.content.append(circle)); + const needSort = states.some((pair): boolean => pair[1] !== states[0][1]); if (!states.length || !needSort) { return; diff --git a/cvat-canvas/src/typescript/crosshair.ts b/cvat-canvas/src/typescript/crosshair.ts new file mode 100644 index 00000000..27d25569 --- /dev/null +++ b/cvat-canvas/src/typescript/crosshair.ts @@ -0,0 +1,70 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import * as SVG from 'svg.js'; +import consts from './consts'; + +export default class Crosshair { + private x: SVG.Line | null; + private y: SVG.Line | null; + private canvas: SVG.Container | null; + + public constructor() { + this.x = null; + this.y = null; + this.canvas = null; + } + + public show(canvas: SVG.Container, x: number, y: number, scale: number): void { + if (this.canvas && this.canvas !== canvas) { + if (this.x) this.x.remove(); + if (this.y) this.y.remove(); + this.x = null; + this.y = null; + } + + this.canvas = canvas; + this.x = this.canvas.line(0, y, this.canvas.node.clientWidth, y).attr({ + 'stroke-width': consts.BASE_STROKE_WIDTH / (2 * scale), + }).addClass('cvat_canvas_crosshair'); + + this.y = this.canvas.line(x, 0, x, this.canvas.node.clientHeight).attr({ + 'stroke-width': consts.BASE_STROKE_WIDTH / (2 * scale), + }).addClass('cvat_canvas_crosshair'); + } + + public hide(): void { + if (this.x) { + this.x.remove(); + this.x = null; + } + + if (this.y) { + this.y.remove(); + this.y = null; + } + + this.canvas = null; + } + + public move(x: number, y: number): void { + if (this.x) { + this.x.attr({ y1: y, y2: y }); + } + + if (this.y) { + this.y.attr({ x1: x, x2: x }); + } + } + + public scale(scale: number): void { + if (this.x) { + this.x.attr('stroke-width', consts.BASE_STROKE_WIDTH / (2 * scale)); + } + + if (this.y) { + this.y.attr('stroke-width', consts.BASE_STROKE_WIDTH / (2 * scale)); + } + } +} diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index 404157bd..6716ead0 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -16,6 +16,7 @@ import { BBox, Box, } from './shared'; +import Crosshair from './crosshair'; import consts from './consts'; import { DrawData, @@ -44,10 +45,7 @@ export class DrawHandlerImpl implements DrawHandler { x: number; y: number; }; - private crosshair: { - x: SVG.Line; - y: SVG.Line; - }; + private crosshair: Crosshair; private drawData: DrawData; private geometry: Geometry; private autoborderHandler: AutoborderHandler; @@ -188,22 +186,11 @@ export class DrawHandlerImpl implements DrawHandler { private addCrosshair(): void { const { x, y } = this.cursorPosition; - this.crosshair = { - x: this.canvas.line(0, y, this.canvas.node.clientWidth, y).attr({ - 'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale), - zOrder: Number.MAX_SAFE_INTEGER, - }).addClass('cvat_canvas_crosshair'), - y: this.canvas.line(x, 0, x, this.canvas.node.clientHeight).attr({ - 'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale), - zOrder: Number.MAX_SAFE_INTEGER, - }).addClass('cvat_canvas_crosshair'), - }; + this.crosshair.show(this.canvas, x, y, this.geometry.scale); } private removeCrosshair(): void { - this.crosshair.x.remove(); - this.crosshair.y.remove(); - this.crosshair = null; + this.crosshair.hide(); } private release(): void { @@ -741,7 +728,7 @@ export class DrawHandlerImpl implements DrawHandler { this.canceled = false; this.drawData = null; this.geometry = null; - this.crosshair = null; + this.crosshair = new Crosshair(); this.drawInstance = null; this.pointsGroup = null; this.cursorPosition = { @@ -756,8 +743,7 @@ export class DrawHandlerImpl implements DrawHandler { ); this.cursorPosition = { x, y }; if (this.crosshair) { - this.crosshair.x.attr({ y1: y, y2: y }); - this.crosshair.y.attr({ x1: x, x2: x }); + this.crosshair.move(x, y); } }); } @@ -787,12 +773,7 @@ export class DrawHandlerImpl implements DrawHandler { } if (this.crosshair) { - this.crosshair.x.attr({ - 'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale), - }); - this.crosshair.y.attr({ - 'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale), - }); + this.crosshair.scale(this.geometry.scale); } if (this.pointsGroup) { diff --git a/cvat-canvas/src/typescript/editHandler.ts b/cvat-canvas/src/typescript/editHandler.ts index 8cea079d..413beda7 100644 --- a/cvat-canvas/src/typescript/editHandler.ts +++ b/cvat-canvas/src/typescript/editHandler.ts @@ -115,7 +115,7 @@ export class EditHandlerImpl implements EditHandler { (this.editLine as any).addClass('cvat_canvas_shape_drawing').style({ 'pointer-events': 'none', 'fill-opacity': 0, - 'stroke': strokeColor, + stroke: strokeColor, }).attr({ 'data-origin-client-id': this.editData.state.clientID, }).on('drawstart drawpoint', (e: CustomEvent): void => { @@ -213,20 +213,20 @@ export class EditHandlerImpl implements EditHandler { const cutIndexes2 = oldPoints.reduce((acc: string[], _: string, i: number) => i <= stop && i >= start ? [...acc, i] : acc, []); - const curveLength = (indexes: number[]) => { + const curveLength = (indexes: number[]): number => { const points = indexes.map((index: number): string => oldPoints[index]) .map((point: string): string[] => point.split(',')) .map((point: string[]): number[] => [+point[0], +point[1]]); let length = 0; for (let i = 1; i < points.length; i++) { length += Math.sqrt( - (points[i][0] - points[i - 1][0]) ** 2 - + (points[i][1] - points[i - 1][1]) ** 2, + ((points[i][0] - points[i - 1][0]) ** 2) + + ((points[i][1] - points[i - 1][1]) ** 2), ); } return length; - } + }; const pointsCriteria = cutIndexes1.length > cutIndexes2.length; const lengthCriteria = curveLength(cutIndexes1) > curveLength(cutIndexes2); @@ -278,8 +278,6 @@ export class EditHandlerImpl implements EditHandler { }); } } - - return; } private setupPoints(enabled: boolean): void { diff --git a/cvat-canvas/src/typescript/interactionHandler.ts b/cvat-canvas/src/typescript/interactionHandler.ts new file mode 100644 index 00000000..76237cf0 --- /dev/null +++ b/cvat-canvas/src/typescript/interactionHandler.ts @@ -0,0 +1,281 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import * as SVG from 'svg.js'; +import consts from './consts'; +import Crosshair from './crosshair'; +import { translateToSVG } from './shared'; +import { InteractionData, InteractionResult, Geometry } from './canvasModel'; + +export interface InteractionHandler { + transform(geometry: Geometry): void; + interact(interactData: InteractionData): void; + cancel(): void; +} + +export class InteractionHandlerImpl implements InteractionHandler { + private onInteraction: ( + shapes: InteractionResult[] | null, + shapesUpdated?: boolean, + isDone?: boolean, + ) => void; + private geometry: Geometry; + private canvas: SVG.Container; + private interactionData: InteractionData; + private cursorPosition: { x: number; y: number }; + private shapesWereUpdated: boolean; + private interactionShapes: SVG.Shape[]; + private currentInteractionShape: SVG.Shape | null; + private crosshair: Crosshair; + + private prepareResult(): InteractionResult[] { + return this.interactionShapes.map((shape: SVG.Shape): InteractionResult => { + if (shape.type === 'circle') { + const points = [(shape as SVG.Circle).cx(), (shape as SVG.Circle).cy()]; + return { + points: points.map((coord: number): number => coord - this.geometry.offset), + shapeType: 'points', + button: shape.attr('stroke') === 'green' ? 0 : 2, + }; + } + + const bbox = (shape.node as any as SVGRectElement).getBBox(); + const points = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height]; + return { + points: points.map((coord: number): number => coord - this.geometry.offset), + shapeType: 'rectangle', + button: 0, + }; + }); + } + + private shouldRaiseEvent(ctrlKey: boolean): boolean { + const { interactionData, interactionShapes, shapesWereUpdated } = this; + const { minPosVertices, minNegVertices, enabled } = interactionData; + + const positiveShapes = interactionShapes + .filter((shape: SVG.Shape): boolean => (shape as any).attr('stroke') === 'green'); + const negativeShapes = interactionShapes + .filter((shape: SVG.Shape): boolean => (shape as any).attr('stroke') !== 'green'); + + if (interactionData.shapeType === 'rectangle') { + return enabled && !ctrlKey && !!interactionShapes.length; + } + + const minimumVerticesAchieved = (typeof (minPosVertices) === 'undefined' + || minPosVertices <= positiveShapes.length) && (typeof (minNegVertices) === 'undefined' + || minPosVertices <= negativeShapes.length); + return enabled && !ctrlKey && minimumVerticesAchieved && shapesWereUpdated; + } + + private addCrosshair(): void { + const { x, y } = this.cursorPosition; + this.crosshair.show(this.canvas, x, y, this.geometry.scale); + } + + private removeCrosshair(): void { + this.crosshair.hide(); + } + + private interactPoints(): void { + const eventListener = (e: MouseEvent): void => { + if ((e.button === 0 || e.button === 2) && !e.altKey) { + e.preventDefault(); + const [cx, cy] = translateToSVG( + this.canvas.node as any as SVGSVGElement, + [e.clientX, e.clientY], + ); + this.currentInteractionShape = this.canvas + .circle(consts.BASE_POINT_SIZE * 2 / this.geometry.scale).center(cx, cy) + .fill('white') + .stroke(e.button === 0 ? 'green' : 'red') + .addClass('cvat_interaction_point') + .attr({ + 'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale, + }); + + this.interactionShapes.push(this.currentInteractionShape); + this.shapesWereUpdated = true; + if (this.shouldRaiseEvent(e.ctrlKey)) { + this.onInteraction(this.prepareResult(), true, false); + } + + const self = this.currentInteractionShape; + self.on('mouseenter', (): void => { + self.attr({ + 'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale, + }); + + self.on('mousedown', (_e: MouseEvent): void => { + _e.preventDefault(); + _e.stopPropagation(); + self.remove(); + this.interactionShapes = this.interactionShapes.filter( + (shape: SVG.Shape): boolean => shape !== self, + ); + this.shapesWereUpdated = true; + if (this.shouldRaiseEvent(_e.ctrlKey)) { + this.onInteraction(this.prepareResult(), true, false); + } + }); + }); + + self.on('mouseleave', (): void => { + self.attr({ + 'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale, + }); + + self.off('mousedown'); + }); + } + }; + + // clear this listener in relese() + this.canvas.on('mousedown.interaction', eventListener); + } + + private interactRectangle(): void { + let initialized = false; + const eventListener = (e: MouseEvent): void => { + if (e.button === 0 && !e.altKey) { + if (!initialized) { + (this.currentInteractionShape as any).draw(e, { snapToGrid: 0.1 }); + initialized = true; + } else { + (this.currentInteractionShape as any).draw(e); + } + } + }; + + this.currentInteractionShape = this.canvas.rect(); + this.canvas.on('mousedown.interaction', eventListener); + this.currentInteractionShape.on('drawstop', (): void => { + this.interactionShapes.push(this.currentInteractionShape); + this.shapesWereUpdated = true; + + this.canvas.off('mousedown.interaction', eventListener); + if (this.shouldRaiseEvent(false)) { + this.onInteraction(this.prepareResult(), true, false); + } + + this.interact({ enabled: false }); + }).addClass('cvat_canvas_shape_drawing').attr({ + 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, + }); + } + + private initInteraction(): void { + if (this.interactionData.crosshair) { + this.addCrosshair(); + } + } + + private startInteraction(): void { + if (this.interactionData.shapeType === 'rectangle') { + this.interactRectangle(); + } else if (this.interactionData.shapeType === 'points') { + this.interactPoints(); + } else { + throw new Error('Interactor implementation supports only rectangle and points'); + } + } + + private release(): void { + if (this.crosshair) { + this.removeCrosshair(); + } + + this.canvas.off('mousedown.interaction'); + this.interactionShapes.forEach((shape: SVG.Shape): SVG.Shape => shape.remove()); + this.interactionShapes = []; + if (this.currentInteractionShape) { + this.currentInteractionShape.remove(); + this.currentInteractionShape = null; + } + } + + public constructor( + onInteraction: ( + shapes: InteractionResult[] | null, + shapesUpdated?: boolean, + isDone?: boolean, + ) => void, + canvas: SVG.Container, + geometry: Geometry, + ) { + this.onInteraction = ( + shapes: InteractionResult[] | null, + shapesUpdated?: boolean, + isDone?: boolean, + ): void => { + this.shapesWereUpdated = false; + onInteraction(shapes, shapesUpdated, isDone); + }; + this.canvas = canvas; + this.geometry = geometry; + this.shapesWereUpdated = false; + this.interactionShapes = []; + this.interactionData = { enabled: false }; + this.currentInteractionShape = null; + this.crosshair = new Crosshair(); + this.cursorPosition = { + x: 0, + y: 0, + }; + + this.canvas.on('mousemove.interaction', (e: MouseEvent): void => { + const [x, y] = translateToSVG( + this.canvas.node as any as SVGSVGElement, + [e.clientX, e.clientY], + ); + this.cursorPosition = { x, y }; + if (this.crosshair) { + this.crosshair.move(x, y); + } + }); + + document.body.addEventListener('keyup', (e: KeyboardEvent): void => { + if (e.keyCode === 17 && this.shouldRaiseEvent(false)) { // 17 is ctrl + this.onInteraction(this.prepareResult(), true, false); + } + }); + } + + public transform(geometry: Geometry): void { + this.geometry = geometry; + + if (this.crosshair) { + this.crosshair.scale(this.geometry.scale); + } + + const shapesToBeScaled = this.currentInteractionShape + ? [...this.interactionShapes, this.currentInteractionShape] + : [...this.interactionShapes]; + for (const shape of shapesToBeScaled) { + if (shape.type === 'circle') { + (shape as SVG.Circle).radius(consts.BASE_POINT_SIZE / this.geometry.scale); + shape.attr('stroke-width', consts.POINTS_STROKE_WIDTH / this.geometry.scale); + } else { + shape.attr('stroke-width', consts.BASE_STROKE_WIDTH / this.geometry.scale); + } + } + } + + public interact(interactionData: InteractionData): void { + if (interactionData.enabled) { + this.interactionData = interactionData; + this.initInteraction(); + this.startInteraction(); + } else { + this.onInteraction(this.prepareResult(), this.shouldRaiseEvent(false), true); + this.release(); + this.interactionData = interactionData; + } + } + + public cancel(): void { + this.release(); + this.onInteraction(null); + } +} diff --git a/cvat-core/src/annotations-history.js b/cvat-core/src/annotations-history.js index 4fdbf34c..ee84fddc 100644 --- a/cvat-core/src/annotations-history.js +++ b/cvat-core/src/annotations-history.js @@ -7,9 +7,14 @@ const MAX_HISTORY_LENGTH = 128; class AnnotationHistory { constructor() { + this.frozen = false; this.clear(); } + freeze(frozen) { + this.frozen = frozen; + } + get() { return { undo: this._undo.map((undo) => [undo.action, undo.frame]), @@ -18,6 +23,7 @@ class AnnotationHistory { } do(action, undo, redo, clientIDs, frame) { + if (this.frozen) return; const actionItem = { clientIDs, action, diff --git a/cvat-core/src/annotations.js b/cvat-core/src/annotations.js index 9d7eadbf..63a316ac 100644 --- a/cvat-core/src/annotations.js +++ b/cvat-core/src/annotations.js @@ -327,6 +327,19 @@ ); } + function freezeHistory(session, frozen) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); + + if (cache.has(session)) { + return cache.get(session).history.freeze(frozen); + } + + throw new DataError( + 'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before', + ); + } + function clearActions(session) { const sessionType = session instanceof Task ? 'task' : 'job'; const cache = getCache(sessionType); @@ -372,6 +385,7 @@ exportDataset, undoActions, redoActions, + freezeHistory, clearActions, getActions, closeSession, diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 6a994e0a..45187896 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -170,6 +170,11 @@ .apiWrapper.call(this, prototype.actions.redo, count); return result; }, + async freeze(frozen) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.actions.freeze, frozen); + return result; + }, async clear() { const result = await PluginRegistry .apiWrapper.call(this, prototype.actions.clear); @@ -545,6 +550,14 @@ * @instance * @async */ + /** + * Freeze history (do not save new actions) + * @method freeze + * @memberof Session.actions + * @throws {module:API.cvat.exceptions.PluginError} + * @instance + * @async + */ /** * Remove all actions from history * @method clear @@ -745,6 +758,7 @@ this.actions = { undo: Object.getPrototypeOf(this).actions.undo.bind(this), redo: Object.getPrototypeOf(this).actions.redo.bind(this), + freeze: Object.getPrototypeOf(this).actions.freeze.bind(this), clear: Object.getPrototypeOf(this).actions.clear.bind(this), get: Object.getPrototypeOf(this).actions.get.bind(this), }; @@ -1299,6 +1313,7 @@ this.actions = { undo: Object.getPrototypeOf(this).actions.undo.bind(this), redo: Object.getPrototypeOf(this).actions.redo.bind(this), + freeze: Object.getPrototypeOf(this).actions.freeze.bind(this), clear: Object.getPrototypeOf(this).actions.clear.bind(this), get: Object.getPrototypeOf(this).actions.get.bind(this), }; @@ -1390,6 +1405,7 @@ exportDataset, undoActions, redoActions, + freezeHistory, clearActions, getActions, closeSession, @@ -1582,6 +1598,11 @@ return result; }; + Job.prototype.actions.freeze.implementation = function (frozen) { + const result = freezeHistory(this, frozen); + return result; + }; + Job.prototype.actions.clear.implementation = function () { const result = clearActions(this); return result; @@ -1846,6 +1867,11 @@ return result; }; + Task.prototype.actions.freeze.implementation = function (frozen) { + const result = freezeHistory(this, frozen); + return result; + }; + Task.prototype.actions.clear.implementation = function () { const result = clearActions(this); return result; diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 3bea5b6c..efeaa81e 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -20,6 +20,7 @@ import { Rotation, ContextMenuType, Workspace, + Model, } from 'reducers/interfaces'; import getCore from 'cvat-core-wrapper'; @@ -187,6 +188,7 @@ export enum AnnotationActionTypes { CHANGE_WORKSPACE = 'CHANGE_WORKSPACE', SAVE_LOGS_SUCCESS = 'SAVE_LOGS_SUCCESS', SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED', + INTERACT_WITH_CANVAS = 'INTERACT_WITH_CANVAS', } export function saveLogsAsync(): ThunkAction { @@ -1385,6 +1387,16 @@ export function pasteShapeAsync(): ThunkAction { }; } +export function interactWithCanvas(activeInteractor: Model, activeLabelID: number): AnyAction { + return { + type: AnnotationActionTypes.INTERACT_WITH_CANVAS, + payload: { + activeInteractor, + activeLabelID, + }, + }; +} + export function repeatDrawShapeAsync(): ThunkAction { return async (dispatch: ActionCreator): Promise => { const { @@ -1401,6 +1413,7 @@ export function repeatDrawShapeAsync(): ThunkAction { }, }, drawing: { + activeInteractor, activeObjectType, activeLabelID, activeShapeType, @@ -1410,6 +1423,16 @@ export function repeatDrawShapeAsync(): ThunkAction { } = getStore().getState().annotation; let activeControl = ActiveControl.CURSOR; + if (activeInteractor) { + canvasInstance.interact({ + enabled: true, + shapeType: 'points', + minPosVertices: 4, // TODO: Add parameter to interactor + }); + dispatch(interactWithCanvas(activeInteractor, activeLabelID)); + return; + } + if (activeShapeType === ShapeType.RECTANGLE) { activeControl = ActiveControl.DRAW_RECTANGLE; } else if (activeShapeType === ShapeType.POINTS) { @@ -1443,7 +1466,7 @@ export function repeatDrawShapeAsync(): ThunkAction { rectDrawingMethod: activeRectDrawingMethod, numberOfPoints: activeNumOfPoints, shapeType: activeShapeType, - crosshair: activeShapeType === ShapeType.RECTANGLE, + crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID].includes(activeShapeType), }); } }; @@ -1490,7 +1513,7 @@ export function redrawShapeAsync(): ThunkAction { enabled: true, redraw: activatedStateID, shapeType: state.shapeType, - crosshair: state.shapeType === ShapeType.RECTANGLE, + crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID].includes(state.shapeType), }); } } diff --git a/cvat-ui/src/actions/models-actions.ts b/cvat-ui/src/actions/models-actions.ts index 1f2afe14..2627e5ff 100644 --- a/cvat-ui/src/actions/models-actions.ts +++ b/cvat-ui/src/actions/models-actions.ts @@ -10,11 +10,6 @@ export enum ModelsActionTypes { GET_MODELS = 'GET_MODELS', GET_MODELS_SUCCESS = 'GET_MODELS_SUCCESS', GET_MODELS_FAILED = 'GET_MODELS_FAILED', - DELETE_MODEL = 'DELETE_MODEL', - CREATE_MODEL = 'CREATE_MODEL', - CREATE_MODEL_SUCCESS = 'CREATE_MODEL_SUCCESS', - CREATE_MODEL_FAILED = 'CREATE_MODEL_FAILED', - CREATE_MODEL_STATUS_UPDATED = 'CREATE_MODEL_STATUS_UPDATED', START_INFERENCE_FAILED = 'START_INFERENCE_FAILED', GET_INFERENCE_STATUS_SUCCESS = 'GET_INFERENCE_STATUS_SUCCESS', GET_INFERENCE_STATUS_FAILED = 'GET_INFERENCE_STATUS_FAILED', @@ -84,8 +79,7 @@ export function getModelsAsync(): ThunkAction { dispatch(modelsActions.getModels()); try { - const models = (await core.lambda.list()) - .filter((model: Model) => ['detector', 'reid'].includes(model.type)); + const models = await core.lambda.list(); dispatch(modelsActions.getModelsSuccess(models)); } catch (error) { dispatch(modelsActions.getModelsFailed(error)); @@ -162,7 +156,6 @@ export function startInferenceAsync( return async (dispatch): Promise => { try { const requestID: string = await core.lambda.run(taskInstance, model, body); - const dispatchCallback = (action: ModelsActions): void => { dispatch(action); }; diff --git a/cvat-ui/src/assets/ai-tools-icon.svg b/cvat-ui/src/assets/ai-tools-icon.svg new file mode 100644 index 00000000..c8b4f304 --- /dev/null +++ b/cvat-ui/src/assets/ai-tools-icon.svg @@ -0,0 +1,18 @@ + + + + + diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx index f6ffeb2a..a202d5c9 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx @@ -14,6 +14,7 @@ import CursorControl from './cursor-control'; import MoveControl from './move-control'; import FitControl from './fit-control'; import ResizeControl from './resize-control'; +import ToolsControl from './tools-control'; import DrawRectangleControl from './draw-rectangle-control'; import DrawPolygonControl from './draw-polygon-control'; import DrawPolylineControl from './draw-polyline-control'; @@ -84,7 +85,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { preventDefault(event); const drawing = [ActiveControl.DRAW_POINTS, ActiveControl.DRAW_POLYGON, ActiveControl.DRAW_POLYLINE, ActiveControl.DRAW_RECTANGLE, - ActiveControl.DRAW_CUBOID].includes(activeControl); + ActiveControl.DRAW_CUBOID, ActiveControl.INTERACTION].includes(activeControl); if (!drawing) { canvasInstance.cancel(); @@ -97,6 +98,12 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { repeatDrawShape(); } } else { + if (activeControl === ActiveControl.INTERACTION) { + // separated API method + canvasInstance.interact({ enabled: false }); + return; + } + canvasInstance.draw({ enabled: false }); } }, @@ -178,7 +185,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
- + - { - setActivated(event.target.checked); - if (event.target.checked) { - activate(canvasInstance); - } else { - deactivate(canvasInstance); - } - }} - > - Make AI polygon - - - ) : null - ); -} - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(DEXTRPlugin); - -// TODO: Add dialog window with cancel button diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx index 5edfc758..d8ce4d33 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx @@ -14,7 +14,6 @@ import Text from 'antd/lib/typography/Text'; import { RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper'; import { ShapeType } from 'reducers/interfaces'; import { clamp } from 'utils/math'; -import DEXTRPlugin from './dextr-plugin'; interface Props { shapeType: ShapeType; @@ -91,7 +90,6 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
- { shapeType === ShapeType.POLYGON && } { shapeType === ShapeType.RECTANGLE && ( <> diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx new file mode 100644 index 00000000..5bdae3d4 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx @@ -0,0 +1,472 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { connect } from 'react-redux'; +import Icon from 'antd/lib/icon'; +import Popover from 'antd/lib/popover'; +import Select, { OptionProps } from 'antd/lib/select'; +import Button from 'antd/lib/button'; +import Modal from 'antd/lib/modal'; +import Text from 'antd/lib/typography/Text'; +import { Row, Col } from 'antd/lib/grid'; +import notification from 'antd/lib/notification'; + +import { AIToolsIcon } from 'icons'; +import { Canvas } from 'cvat-canvas-wrapper'; +import getCore from 'cvat-core-wrapper'; +import { + CombinedState, + ActiveControl, + Model, + ObjectType, + ShapeType, +} from 'reducers/interfaces'; +import { interactWithCanvas, fetchAnnotationsAsync, updateAnnotationsAsync } from 'actions/annotation-actions'; +import { InteractionResult } from 'cvat-canvas/src/typescript/canvas'; + +interface StateToProps { + canvasInstance: Canvas; + labels: any[]; + states: any[]; + activeLabelID: number; + jobInstance: any; + isInteraction: boolean; + frame: number; + interactors: Model[]; +} + +interface DispatchToProps { + onInteractionStart(activeInteractor: Model, activeLabelID: number): void; + updateAnnotations(statesToUpdate: any[]): void; + fetchAnnotations(): void; +} + +const core = getCore(); + +function mapStateToProps(state: CombinedState): StateToProps { + const { annotation } = state; + const { number: frame } = annotation.player.frame; + const { instance: jobInstance } = annotation.job; + const { instance: canvasInstance, activeControl } = annotation.canvas; + const { models } = state; + const { interactors } = models; + + return { + interactors, + isInteraction: activeControl === ActiveControl.INTERACTION, + activeLabelID: annotation.drawing.activeLabelID, + labels: annotation.job.labels, + states: annotation.annotations.states, + canvasInstance, + jobInstance, + frame, + }; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + onInteractionStart(activeInteractor: Model, activeLabelID: number): void { + dispatch(interactWithCanvas(activeInteractor, activeLabelID)); + }, + updateAnnotations(statesToUpdate: any[]): void { + dispatch(updateAnnotationsAsync(statesToUpdate)); + }, + fetchAnnotations(): void { + dispatch(fetchAnnotationsAsync()); + }, + }; +} + +function convertShapesForInteractor(shapes: InteractionResult[]): number[][] { + const reducer = (acc: number[][], _: number, index: number, array: number[]): number[][] => { + if (!(index % 2)) { // 0, 2, 4 + acc.push([ + array[index], + array[index + 1], + ]); + } + return acc; + }; + + return shapes.filter((shape: InteractionResult): boolean => shape.shapeType === 'points' && shape.button === 0) + .map((shape: InteractionResult): number[] => shape.points) + .flat().reduce(reducer, []); +} + +type Props = StateToProps & DispatchToProps; +interface State { + activeInteractor: Model | null; + activeLabelID: number; + interactiveStateID: number | null; + fetching: boolean; +} + +class ToolsControlComponent extends React.PureComponent { + private interactionIsAborted: boolean; + private interactionIsDone: boolean; + + public constructor(props: Props) { + super(props); + this.state = { + activeInteractor: props.interactors.length ? props.interactors[0] : null, + activeLabelID: props.labels[0].id, + interactiveStateID: null, + fetching: false, + }; + + this.interactionIsAborted = false; + this.interactionIsDone = false; + } + + public componentDidMount(): void { + const { canvasInstance } = this.props; + canvasInstance.html().addEventListener('canvas.interacted', this.interactionListener); + canvasInstance.html().addEventListener('canvas.canceled', this.cancelListener); + } + + public componentDidUpdate(prevProps: Props): void { + const { isInteraction } = this.props; + if (prevProps.isInteraction && !isInteraction) { + window.removeEventListener('contextmenu', this.contextmenuDisabler); + } else if (!prevProps.isInteraction && isInteraction) { + this.interactionIsDone = false; + this.interactionIsAborted = false; + window.addEventListener('contextmenu', this.contextmenuDisabler); + } + } + + public componentWillUnmount(): void { + const { canvasInstance } = this.props; + canvasInstance.html().removeEventListener('canvas.interacted', this.interactionListener); + canvasInstance.html().removeEventListener('canvas.canceled', this.cancelListener); + } + + private getInteractiveState(): any | null { + const { states } = this.props; + const { interactiveStateID } = this.state; + return states + .filter((_state: any): boolean => _state.clientID === interactiveStateID)[0] || null; + } + + private contextmenuDisabler = (e: MouseEvent): void => { + if (e.target && (e.target as Element).classList + && (e.target as Element).classList.toString().includes('ant-modal')) { + e.preventDefault(); + } + }; + + private cancelListener = async (): Promise => { + const { + isInteraction, + jobInstance, + frame, + fetchAnnotations, + } = this.props; + const { interactiveStateID, fetching } = this.state; + + if (isInteraction) { + if (fetching && !this.interactionIsDone) { + // user pressed ESC + this.setState({ fetching: false }); + this.interactionIsAborted = true; + } + + if (interactiveStateID !== null) { + const state = this.getInteractiveState(); + this.setState({ interactiveStateID: null }); + await state.delete(frame); + fetchAnnotations(); + } + + await jobInstance.actions.freeze(false); + } + }; + + private interactionListener = async (e: Event): Promise => { + const { + frame, + labels, + jobInstance, + isInteraction, + activeLabelID, + fetchAnnotations, + updateAnnotations, + } = this.props; + const { activeInteractor, interactiveStateID, fetching } = this.state; + + try { + if (!isInteraction) { + throw Error('Canvas raises event "canvas.interacted" when interaction is off'); + } + + if (fetching) { + this.interactionIsDone = (e as CustomEvent).detail.isDone; + return; + } + + const interactor = activeInteractor as Model; + + let result = []; + if ((e as CustomEvent).detail.shapesUpdated) { + this.setState({ fetching: true }); + try { + result = await core.lambda.call(jobInstance.task, interactor, { + task: jobInstance.task, + frame, + points: convertShapesForInteractor((e as CustomEvent).detail.shapes), + }); + + if (this.interactionIsAborted) { + // while the server request + // user has cancelled interaction (for example pressed ESC) + return; + } + } finally { + this.setState({ fetching: false }); + } + } + + if (this.interactionIsDone) { + // while the server request, user has done interaction (for example pressed N) + const object = new core.classes.ObjectState({ + frame, + objectType: ObjectType.SHAPE, + label: labels + .filter((label: any) => label.id === activeLabelID)[0], + shapeType: ShapeType.POLYGON, + points: result.flat(), + occluded: false, + zOrder: (e as CustomEvent).detail.zOrder, + }); + + await jobInstance.annotations.put([object]); + fetchAnnotations(); + } else { + // no shape yet, then create it and save to collection + if (interactiveStateID === null) { + // freeze history for interaction time + // (points updating shouldn't cause adding new actions to history) + await jobInstance.actions.freeze(true); + const object = new core.classes.ObjectState({ + frame, + objectType: ObjectType.SHAPE, + label: labels + .filter((label: any) => label.id === activeLabelID)[0], + shapeType: ShapeType.POLYGON, + points: result.flat(), + occluded: false, + zOrder: (e as CustomEvent).detail.zOrder, + }); + // need a clientID of a created object to interact with it further + // so, we do not use createAnnotationAction + const [clientID] = await jobInstance.annotations.put([object]); + + // update annotations on a canvas + fetchAnnotations(); + this.setState({ interactiveStateID: clientID }); + return; + } + + const state = this.getInteractiveState(); + if ((e as CustomEvent).detail.isDone) { + const finalObject = new core.classes.ObjectState({ + frame: state.frame, + objectType: state.objectType, + label: state.label, + shapeType: state.shapeType, + points: result.length ? result.flat() : state.points, + occluded: state.occluded, + zOrder: state.zOrder, + }); + this.setState({ interactiveStateID: null }); + await state.delete(frame); + await jobInstance.actions.freeze(false); + await jobInstance.annotations.put([finalObject]); + fetchAnnotations(); + } else { + state.points = result.flat(); + updateAnnotations([state]); + fetchAnnotations(); + } + } + } catch (err) { + notification.error({ + description: err.toString(), + message: 'Interaction error occured', + }); + } + }; + + private setActiveInteractor = (key: string): void => { + const { interactors } = this.props; + this.setState({ + activeInteractor: interactors.filter( + (interactor: Model) => interactor.id === key, + )[0], + }); + }; + + private renderLabelBlock(): JSX.Element { + const { labels } = this.props; + const { activeLabelID } = this.state; + return ( + <> + + + Label + + + + + + + + + ); + } + + private renderInteractorBlock(): JSX.Element { + const { interactors, canvasInstance, onInteractionStart } = this.props; + const { activeInteractor, activeLabelID, fetching } = this.state; + + return ( + <> + + + Interactor + + + + + + + + + + + + + + ); + } + + private renderPopoverContent(): JSX.Element { + return ( +
+ + + AI Tools + + + { this.renderLabelBlock() } + { this.renderInteractorBlock() } +
+ ); + } + + public render(): JSX.Element | null { + const { interactors, isInteraction, canvasInstance } = this.props; + const { fetching } = this.state; + + if (!interactors.length) return null; + + const dynamcPopoverPros = isInteraction ? { + overlayStyle: { + display: 'none', + }, + } : {}; + + const dynamicIconProps = isInteraction ? { + className: 'cvat-active-canvas-control cvat-tools-control', + onClick: (): void => { + canvasInstance.interact({ enabled: false }); + }, + } : { + className: 'cvat-tools-control', + }; + + return ( + <> + + Waiting for a server response.. + + + + + + + ); + } +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(ToolsControlComponent); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss index a9514add..71eb7e49 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss @@ -83,17 +83,31 @@ padding: 0; } -.cvat-draw-shape-popover > -.ant-popover-content > -.ant-popover-inner > div > -.ant-popover-inner-content { - padding: 0; +.cvat-draw-shape-popover, +.cvat-tools-control-popover { + > .ant-popover-content > + .ant-popover-inner > div > + .ant-popover-inner-content { + padding: 0; + } +} + +.cvat-tools-interact-button { + width: 100%; + margin-top: 10px; } .cvat-draw-shape-popover-points-selector { width: 100%; } +.cvat-tools-control-popover-content { + padding: 10px; + border-radius: 5px; + background: $background-color-2; + width: 270px; +} + .cvat-draw-shape-popover-content { padding: 10px; border-radius: 5px; diff --git a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx index 234c7fd0..d8fd959e 100644 --- a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx +++ b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx @@ -21,7 +21,8 @@ import { } from 'reducers/interfaces'; interface Props { - models: Model[]; + reid: Model[]; + detectors: Model[]; activeProcesses: StringObject; visible: boolean; taskInstance: any; @@ -88,14 +89,14 @@ export default class ModelRunnerModalComponent extends React.PureComponent @@ -166,10 +168,7 @@ export default class ModelRunnerModalComponent extends React.PureComponent @@ -203,11 +202,7 @@ export default class ModelRunnerModalComponent extends React.PureComponent @@ -346,16 +338,10 @@ export default class ModelRunnerModalComponent extends React.PureComponent _model.name === selectedModel)[0]; @@ -414,13 +400,15 @@ export default class ModelRunnerModalComponent extends React.PureComponent model.name === selectedModel, )[0]; diff --git a/cvat-ui/src/components/models-page/built-model-item.tsx b/cvat-ui/src/components/models-page/built-model-item.tsx deleted file mode 100644 index f100dfe5..00000000 --- a/cvat-ui/src/components/models-page/built-model-item.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (C) 2020 Intel Corporation -// -// SPDX-License-Identifier: MIT - -import React from 'react'; -import { Row, Col } from 'antd/lib/grid'; -import Tag from 'antd/lib/tag'; -import Select from 'antd/lib/select'; -import Text from 'antd/lib/typography/Text'; - -import { Model } from 'reducers/interfaces'; - -interface Props { - model: Model; -} - -export default function BuiltModelItemComponent(props: Props): JSX.Element { - const { model } = props; - - return ( - - - {model.framework} - - - - {model.name} - - - - - - - - ); -} diff --git a/cvat-ui/src/components/models-page/deployed-models-list.tsx b/cvat-ui/src/components/models-page/deployed-models-list.tsx index 93e301be..45d62aa8 100644 --- a/cvat-ui/src/components/models-page/deployed-models-list.tsx +++ b/cvat-ui/src/components/models-page/deployed-models-list.tsx @@ -9,7 +9,6 @@ import Text from 'antd/lib/typography/Text'; import { Model } from 'reducers/interfaces'; import DeployedModelItem from './deployed-model-item'; - interface Props { models: Model[]; } diff --git a/cvat-ui/src/components/models-page/models-page.tsx b/cvat-ui/src/components/models-page/models-page.tsx index a5606a2a..3b4fe521 100644 --- a/cvat-ui/src/components/models-page/models-page.tsx +++ b/cvat-ui/src/components/models-page/models-page.tsx @@ -12,11 +12,21 @@ import FeedbackComponent from '../feedback/feedback'; import { Model } from '../../reducers/interfaces'; interface Props { - deployedModels: Model[]; + interactors: Model[]; + detectors: Model[]; + trackers: Model[]; + reid: Model[]; } export default function ModelsPageComponent(props: Props): JSX.Element { - const { deployedModels } = props; + const { + interactors, + detectors, + trackers, + reid, + } = props; + + const deployedModels = [...detectors, ...interactors, ...trackers, ...reid]; return (
diff --git a/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx b/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx index b956876c..3f63bd7f 100644 --- a/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx +++ b/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx @@ -9,7 +9,8 @@ import { Model, CombinedState } from 'reducers/interfaces'; import { startInferenceAsync, modelsActions } from 'actions/models-actions'; interface StateToProps { - models: Model[]; + reid: Model[]; + detectors: Model[]; activeProcesses: { [index: string]: string; }; @@ -30,7 +31,8 @@ function mapStateToProps(state: CombinedState): StateToProps { const { models } = state; return { - models: models.models, + reid: models.reid, + detectors: models.detectors, activeProcesses: {}, taskInstance: models.activeRunTask, visible: models.visibleRunWindows, diff --git a/cvat-ui/src/containers/models-page/models-page.tsx b/cvat-ui/src/containers/models-page/models-page.tsx index 8282d41d..734ad0dc 100644 --- a/cvat-ui/src/containers/models-page/models-page.tsx +++ b/cvat-ui/src/containers/models-page/models-page.tsx @@ -11,14 +11,26 @@ import { } from 'reducers/interfaces'; interface StateToProps { - deployedModels: Model[]; + interactors: Model[]; + detectors: Model[]; + trackers: Model[]; + reid: Model[]; } function mapStateToProps(state: CombinedState): StateToProps { const { models } = state; + const { + interactors, + detectors, + trackers, + reid, + } = models; return { - deployedModels: models.models, + interactors, + detectors, + trackers, + reid, }; } diff --git a/cvat-ui/src/cvat-canvas-wrapper.ts b/cvat-ui/src/cvat-canvas-wrapper.ts index 631a52a9..2dc70b8d 100644 --- a/cvat-ui/src/cvat-canvas-wrapper.ts +++ b/cvat-ui/src/cvat-canvas-wrapper.ts @@ -8,8 +8,13 @@ import { CanvasVersion, RectDrawingMethod, CuboidDrawingMethod, + InteractionData as InteractionDataType, + InteractionResult as InteractionResultType, } from 'cvat-canvas/src/typescript/canvas'; +export type InteractionData = InteractionDataType; +export type InteractionResult = InteractionResultType; + export { Canvas, CanvasMode, diff --git a/cvat-ui/src/icons.tsx b/cvat-ui/src/icons.tsx index fbf48607..a3e92e92 100644 --- a/cvat-ui/src/icons.tsx +++ b/cvat-ui/src/icons.tsx @@ -42,6 +42,8 @@ import SVGForegroundIcon from './assets/foreground-icon.svg'; import SVGCubeIcon from './assets/cube-icon.svg'; import SVGResetPerspectiveIcon from './assets/reset-perspective.svg'; import SVGColorizeIcon from './assets/colorize-icon.svg'; +import SVGAITools from './assets/ai-tools-icon.svg'; + export const CVATLogo = React.memo( (): JSX.Element => , @@ -154,6 +156,9 @@ export const CubeIcon = React.memo( export const ResetPerspectiveIcon = React.memo( (): JSX.Element => , ); +export const AIToolsIcon = React.memo( + (): JSX.Element => , +); export const ColorizeIcon = React.memo( (): JSX.Element => , ); diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 4976977d..b37e3cb6 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -428,6 +428,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { activeControl, }, drawing: { + activeInteractor: undefined, activeLabelID: labelID, activeNumOfPoints: points, activeObjectType: objectType, @@ -1039,8 +1040,30 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }, }; } + case AnnotationActionTypes.INTERACT_WITH_CANVAS: { + return { + ...state, + annotations: { + ...state.annotations, + activatedStateID: null, + }, + drawing: { + ...state.drawing, + activeInteractor: action.payload.activeInteractor, + activeLabelID: action.payload.activeLabelID, + }, + canvas: { + ...state.canvas, + activeControl: ActiveControl.INTERACTION, + }, + }; + } case AnnotationActionTypes.CHANGE_WORKSPACE: { const { workspace } = action.payload; + if (state.canvas.activeControl !== ActiveControl.CURSOR) { + return state; + } + return { ...state, workspace, diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index fbc4b5ed..9a48b52e 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -76,7 +76,6 @@ export interface FormatsState { // eslint-disable-next-line import/prefer-default-export export enum SupportedPlugins { GIT_INTEGRATION = 'GIT_INTEGRATION', - DEXTR_SEGMENTATION = 'DEXTR_SEGMENTATION', ANALYTICS = 'ANALYTICS', } @@ -161,7 +160,10 @@ export interface ModelsState { initialized: boolean; fetching: boolean; creatingStatus: string; - models: Model[]; + interactors: Model[]; + detectors: Model[]; + trackers: Model[]; + reid: Model[]; inferences: { [index: number]: ActiveInference; }; @@ -206,9 +208,7 @@ export interface NotificationsState { fetching: null | ErrorState; }; models: { - creating: null | ErrorState; starting: null | ErrorState; - deleting: null | ErrorState; fetching: null | ErrorState; canceling: null | ErrorState; metaFetching: null | ErrorState; @@ -270,6 +270,7 @@ export enum ActiveControl { GROUP = 'group', SPLIT = 'split', EDIT = 'edit', + INTERACTION = 'interaction', } export enum ShapeType { @@ -342,6 +343,7 @@ export interface AnnotationState { frameAngles: number[]; }; drawing: { + activeInteractor?: Model; activeShapeType: ShapeType; activeRectDrawingMethod?: RectDrawingMethod; activeNumOfPoints?: number; diff --git a/cvat-ui/src/reducers/models-reducer.ts b/cvat-ui/src/reducers/models-reducer.ts index 3e369584..18371aea 100644 --- a/cvat-ui/src/reducers/models-reducer.ts +++ b/cvat-ui/src/reducers/models-reducer.ts @@ -5,13 +5,16 @@ import { boundariesActions, BoundariesActionTypes } from 'actions/boundaries-actions'; import { ModelsActionTypes, ModelsActions } from 'actions/models-actions'; import { AuthActionTypes, AuthActions } from 'actions/auth-actions'; -import { ModelsState } from './interfaces'; +import { ModelsState, Model } from './interfaces'; const defaultState: ModelsState = { initialized: false, fetching: false, creatingStatus: '', - models: [], + interactors: [], + detectors: [], + trackers: [], + reid: [], visibleRunWindows: false, activeRunTask: null, inferences: {}, @@ -32,7 +35,10 @@ export default function ( case ModelsActionTypes.GET_MODELS_SUCCESS: { return { ...state, - models: action.payload.models, + interactors: action.payload.models.filter((model: Model) => ['interactor'].includes(model.type)), + detectors: action.payload.models.filter((model: Model) => ['detector'].includes(model.type)), + trackers: action.payload.models.filter((model: Model) => ['tracker'].includes(model.type)), + reid: action.payload.models.filter((model: Model) => ['reid'].includes(model.type)), initialized: true, fetching: false, }; diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index dfbfdbf5..e87d22b8 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -51,9 +51,7 @@ const defaultState: NotificationsState = { fetching: null, }, models: { - creating: null, starting: null, - deleting: null, fetching: null, canceling: null, metaFetching: null, @@ -414,21 +412,6 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } - case ModelsActionTypes.CREATE_MODEL_FAILED: { - return { - ...state, - errors: { - ...state.errors, - models: { - ...state.errors.models, - creating: { - message: 'Could not create the model', - reason: action.payload.error.toString(), - }, - }, - }, - }; - } case ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS: { if (action.payload.activeInference.status === 'finished') { const { taskID } = action.payload; diff --git a/cvat-ui/src/reducers/plugins-reducer.ts b/cvat-ui/src/reducers/plugins-reducer.ts index 71cfc3ab..b18db987 100644 --- a/cvat-ui/src/reducers/plugins-reducer.ts +++ b/cvat-ui/src/reducers/plugins-reducer.ts @@ -4,7 +4,6 @@ import { PluginsActionTypes, PluginActions } from 'actions/plugins-actions'; import { registerGitPlugin } from 'utils/git-utils'; -import { registerDEXTRPlugin } from 'utils/dextr-utils'; import { PluginsState } from './interfaces'; const defaultState: PluginsState = { @@ -12,7 +11,6 @@ const defaultState: PluginsState = { initialized: false, list: { GIT_INTEGRATION: false, - DEXTR_SEGMENTATION: false, ANALYTICS: false, }, }; @@ -36,10 +34,6 @@ export default function ( registerGitPlugin(); } - if (!state.list.DEXTR_SEGMENTATION && list.DEXTR_SEGMENTATION) { - registerDEXTRPlugin(); - } - return { ...state, initialized: true, diff --git a/cvat-ui/src/utils/dextr-utils.ts b/cvat-ui/src/utils/dextr-utils.ts deleted file mode 100644 index a9d92abf..00000000 --- a/cvat-ui/src/utils/dextr-utils.ts +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright (C) 2020 Intel Corporation -// -// SPDX-License-Identifier: MIT - -import getCore from 'cvat-core-wrapper'; -import { Canvas } from 'cvat-canvas-wrapper'; -import { ShapeType, CombinedState } from 'reducers/interfaces'; -import { getCVATStore } from 'cvat-store'; - -const core = getCore(); - -interface DEXTRPlugin { - name: string; - description: string; - cvat: { - classes: { - Job: { - prototype: { - annotations: { - put: { - enter(self: any, objects: any[]): Promise; - }; - }; - }; - }; - }; - }; - data: { - canceled: boolean; - enabled: boolean; - }; -} - -interface Point { - x: number; - y: number; -} - -const antModalRoot = document.createElement('div'); -const antModalMask = document.createElement('div'); -antModalMask.classList.add('ant-modal-mask'); -const antModalWrap = document.createElement('div'); -antModalWrap.classList.add('ant-modal-wrap'); -antModalWrap.setAttribute('role', 'dialog'); -const antModal = document.createElement('div'); -antModal.classList.add('ant-modal'); -antModal.style.width = '300px'; -antModal.style.top = '40%'; -antModal.setAttribute('role', 'document'); -const antModalContent = document.createElement('div'); -antModalContent.classList.add('ant-modal-content'); -const antModalBody = document.createElement('div'); -antModalBody.classList.add('ant-modal-body'); -antModalBody.style.textAlign = 'center'; -const antModalSpan = document.createElement('span'); -antModalSpan.innerText = 'Segmentation request is being processed'; -antModalSpan.style.display = 'block'; -const antModalButton = document.createElement('button'); -antModalButton.disabled = true; -antModalButton.classList.add('ant-btn', 'ant-btn-primary'); -antModalButton.style.width = '100px'; -antModalButton.style.margin = '10px auto'; -const antModalButtonSpan = document.createElement('span'); -antModalButtonSpan.innerText = 'Cancel'; - -antModalBody.append(antModalSpan, antModalButton); -antModalButton.append(antModalButtonSpan); -antModalContent.append(antModalBody); -antModal.append(antModalContent); -antModalWrap.append(antModal); -antModalRoot.append(antModalMask, antModalWrap); - -async function serverRequest( - taskInstance: any, - frame: number, - points: number[], -): Promise { - const reducer = (acc: number[][], - _: number, index: number, - array: number[]): number[][] => { - if (!(index % 2)) { // 0, 2, 4 - acc.push([ - array[index], - array[index + 1], - ]); - } - return acc; - }; - - const reducedPoints = points.reduce(reducer, []); - const models = await core.lambda.list(); - const model = models.filter((func: any): boolean => func.id === 'openvino.dextr')[0]; - const result = await core.lambda.call(taskInstance, model, { - task: taskInstance, - frame, - points: reducedPoints, - }); - - return result.flat(); -} - -async function enter(this: any, self: DEXTRPlugin, objects: any[]): Promise { - try { - if (self.data.enabled && objects.length === 1) { - const state = (getCVATStore().getState() as CombinedState); - const isPolygon = state.annotation - .drawing.activeShapeType === ShapeType.POLYGON; - if (!isPolygon) return; - - document.body.append(antModalRoot); - const promises: Record> = {}; - for (let i = 0; i < objects.length; i++) { - if (objects[i].points.length >= 8) { - promises[i] = serverRequest( - this.task, - objects[i].frame, - objects[i].points, - ); - } else { - promises[i] = new Promise((resolve) => { - resolve(objects[i].points); - }); - } - } - - const transformed = await Promise - .all(Object.values(promises)); - for (let i = 0; i < objects.length; i++) { - // eslint-disable-next-line no-param-reassign - objects[i] = new core.classes.ObjectState({ - frame: objects[i].frame, - objectType: objects[i].objectType, - label: objects[i].label, - shapeType: ShapeType.POLYGON, - points: transformed[i], - occluded: objects[i].occluded, - zOrder: objects[i].zOrder, - }); - } - } - - return; - } catch (error) { - throw new core.exceptions.PluginError(error.toString()); - } finally { - // eslint-disable-next-line no-param-reassign - self.data.canceled = false; - antModalButton.disabled = true; - if (antModalRoot.parentElement === document.body) { - document.body.removeChild(antModalRoot); - } - } -} - -const plugin: DEXTRPlugin = { - name: 'Deep extreme cut', - description: 'Plugin allows to get a polygon from extreme points using AI', - cvat: { - classes: { - Job: { - prototype: { - annotations: { - put: { - enter, - }, - }, - }, - }, - }, - }, - data: { - canceled: false, - enabled: false, - }, -}; - - -antModalButton.onclick = () => { - plugin.data.canceled = true; -}; - -export function activate(canvasInstance: Canvas): void { - if (!plugin.data.enabled) { - // eslint-disable-next-line no-param-reassign - canvasInstance.draw = (drawData: any): void => { - if (drawData.enabled && drawData.shapeType === ShapeType.POLYGON - && (typeof (drawData.numberOfPoints) === 'undefined' || drawData.numberOfPoints >= 4) - && (typeof (drawData.initialState) === 'undefined') - ) { - const patchedData = { ...drawData }; - patchedData.shapeType = ShapeType.POINTS; - patchedData.crosshair = true; - Object.getPrototypeOf(canvasInstance) - .draw.call(canvasInstance, patchedData); - } else { - Object.getPrototypeOf(canvasInstance) - .draw.call(canvasInstance, drawData); - } - }; - plugin.data.enabled = true; - } -} - -export function deactivate(canvasInstance: Canvas): void { - if (plugin.data.enabled) { - // eslint-disable-next-line no-param-reassign - canvasInstance.draw = Object.getPrototypeOf(canvasInstance).draw; - plugin.data.enabled = false; - } -} - -export function registerDEXTRPlugin(): void { - core.plugins.register(plugin); -} diff --git a/cvat-ui/src/utils/plugin-checker.ts b/cvat-ui/src/utils/plugin-checker.ts index 38c29575..e4150610 100644 --- a/cvat-ui/src/utils/plugin-checker.ts +++ b/cvat-ui/src/utils/plugin-checker.ts @@ -17,14 +17,6 @@ class PluginChecker { case SupportedPlugins.GIT_INTEGRATION: { return isReachable(`${serverHost}/git/repository/meta/get`, 'OPTIONS'); } - case SupportedPlugins.DEXTR_SEGMENTATION: { - try { - const list = await core.lambda.list(); - return list.map((func: any): boolean => func.id).includes('openvino.dextr'); - } catch (_) { - return false; - } - } case SupportedPlugins.ANALYTICS: { return isReachable(`${serverHost}/analytics/app/kibana`, 'GET'); } From dcc1a6a4778cd8cc1b5bbb0317e747185f602938 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 22:21:27 +0300 Subject: [PATCH 022/467] Bump django-extensions from 3.0.5 to 3.0.6 in /cvat/requirements (#2109) Bumps [django-extensions](https://github.com/django-extensions/django-extensions) from 3.0.5 to 3.0.6. - [Release notes](https://github.com/django-extensions/django-extensions/releases) - [Changelog](https://github.com/django-extensions/django-extensions/blob/master/CHANGELOG.md) - [Commits](https://github.com/django-extensions/django-extensions/compare/3.0.5...3.0.6) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- cvat/requirements/development.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/requirements/development.txt b/cvat/requirements/development.txt index 7efae677..990bf336 100644 --- a/cvat/requirements/development.txt +++ b/cvat/requirements/development.txt @@ -8,6 +8,6 @@ pylint-django==2.3.0 pylint-plugin-utils==0.6 rope==0.17.0 wrapt==1.12.1 -django-extensions==3.0.5 +django-extensions==3.0.6 Werkzeug==1.0.1 snakeviz==2.1.0 From 82ea21960241274a8c813420ed43131e1307fd70 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 22:22:03 +0300 Subject: [PATCH 023/467] Bump django-cors-headers from 3.4.0 to 3.5.0 in /cvat/requirements (#2110) Bumps [django-cors-headers](https://github.com/adamchainz/django-cors-headers) from 3.4.0 to 3.5.0. - [Release notes](https://github.com/adamchainz/django-cors-headers/releases) - [Changelog](https://github.com/adamchainz/django-cors-headers/blob/master/HISTORY.rst) - [Commits](https://github.com/adamchainz/django-cors-headers/compare/3.4.0...3.5.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- cvat/requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index 57ea9031..05bd8815 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -44,7 +44,7 @@ keras==2.4.2 opencv-python==4.4.0.42 h5py==2.10.0 imgaug==0.4.0 -django-cors-headers==3.4.0 +django-cors-headers==3.5.0 furl==2.1.0 av==6.2.0 # The package is used by pyunpack as a command line tool to support multiple From 8207eaeabac209a2923f96c34b48b5f5317f8574 Mon Sep 17 00:00:00 2001 From: Dmitry Kruchinin <33020454+dvkruchinin@users.noreply.github.com> Date: Tue, 1 Sep 2020 10:59:27 +0300 Subject: [PATCH 024/467] Cypress tests via Firefox browser. (#2092) * Cypress tests via Firefox browser. Added browser verification functionality. Added user and tasks removing functionality. * Applying comments. Co-authored-by: Dmitry Kruchinin --- .travis.yml | 4 +- tests/cypress.json | 3 +- .../cypress/integration/remove_users_tasks.js | 69 +++++++++++++++++++ tests/cypress/support/index.js | 13 ++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 tests/cypress/integration/remove_users_tasks.js diff --git a/.travis.yml b/.travis.yml index 68533c0d..9acef6b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ cache: - ~/.cache addons: + firefox: "latest" apt: packages: - libgconf-2-4 @@ -43,7 +44,8 @@ script: - docker exec -it cvat bash -ic "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell" # Install Cypress and run tests - cd ./tests && npm install - - $(npm bin)/cypress run --headless --browser chrome && cd .. + - $(npm bin)/cypress run --headless --browser chrome + - $(npm bin)/cypress run --headless --browser firefox && cd .. after_success: # https://coveralls-python.readthedocs.io/en/latest/usage/multilang.html diff --git a/tests/cypress.json b/tests/cypress.json index 59316936..73741d19 100644 --- a/tests/cypress.json +++ b/tests/cypress.json @@ -10,6 +10,7 @@ }, "testFiles": [ "auth_page.js", - "issue_*.js" + "issue_*.js", + "remove_users_tasks.js" ] } diff --git a/tests/cypress/integration/remove_users_tasks.js b/tests/cypress/integration/remove_users_tasks.js new file mode 100644 index 00000000..711b4e09 --- /dev/null +++ b/tests/cypress/integration/remove_users_tasks.js @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2020 Intel Corporation + * + * SPDX-License-Identifier: MIT + */ + +/// + +let authKey = '' + +describe('Delete users and tasks created during the test run.', () => { + it('Get token', () => { + cy.request({ + method: 'POST', + url: '/api/v1/auth/login', + body: { + username: Cypress.env('user'), + password: Cypress.env('password') + } + }) + .then(async (responce) => { + authKey = await responce['body']['key'] + }) + }) + it('Get a list of users and delete all except id:1', () => { + cy.request({ + url: '/api/v1/users', + headers: { + Authorization: `Token ${authKey}` + } + }) + .then(async (responce) => { + const responceResult = await responce['body']['results'] + for (let user of responceResult) { + let userId = user['id'] + if (userId !== 1) { + cy.request({ + method: 'DELETE', + url: `/api/v1/users/${userId}`, + headers: { + Authorization: `Token ${authKey}` + } + }) + } + } + }) + }) + it('Get a list of tasks and delete them all', ()=> { + cy.request({ + url: '/api/v1/tasks?page_size=1000', + headers: { + Authorization: `Token ${authKey}` + } + }) + .then(async (responce) => { + const responceResult = await responce['body']['results'] + for (let tasks of responceResult) { + let taskId = tasks['id'] + cy.request({ + method: 'DELETE', + url: `/api/v1/tasks/${taskId}`, + headers: { + Authorization: `Token ${authKey}` + } + }) + } + }) + }) +}) diff --git a/tests/cypress/support/index.js b/tests/cypress/support/index.js index bf9295aa..b23790a4 100644 --- a/tests/cypress/support/index.js +++ b/tests/cypress/support/index.js @@ -5,3 +5,16 @@ */ import './commands' + +before(() => { + if (Cypress.browser.name === 'firefox') { + cy.visit('/') + cy.get('.ant-modal-body').within(() => { + cy.get('.ant-modal-confirm-title') + .should('contain', 'Unsupported platform detected') + cy.get('.ant-modal-confirm-btns') + .contains('OK') + .click() + }) + } +}) From 7acd8dddc66f536503ea2462f2e6d5977d79c612 Mon Sep 17 00:00:00 2001 From: Dmitry Kruchinin <33020454+dvkruchinin@users.noreply.github.com> Date: Tue, 1 Sep 2020 12:05:44 +0300 Subject: [PATCH 025/467] Cypress test for issue 1498 (#2083) * Cypress test for issue 1498 * Applying comments. Co-authored-by: Dmitry Kruchinin --- .../issue_1498_message_ui_raw_labels_wrong.js | 169 ++++++++++++++++++ tests/cypress/plugins/index.js | 6 + tests/cypress/support/commands.js | 6 +- 3 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 tests/cypress/integration/issue_1498_message_ui_raw_labels_wrong.js diff --git a/tests/cypress/integration/issue_1498_message_ui_raw_labels_wrong.js b/tests/cypress/integration/issue_1498_message_ui_raw_labels_wrong.js new file mode 100644 index 00000000..ca6a50bb --- /dev/null +++ b/tests/cypress/integration/issue_1498_message_ui_raw_labels_wrong.js @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2020 Intel Corporation + * + * SPDX-License-Identifier: MIT + */ + +/// + +context('Message in UI when raw labels are wrong.', () => { + + const issueId = '1498' + const labelName = `Issue ${issueId}` + const attrName = `Attr for ${labelName}` + const textDefaultValue = 'Some default value for type Text' + let taskRaw = [ + { + name: labelName, + id: 1, + color: '#c4a71f', + attributes: [ + { + id: 1, + name: attrName, + input_type: 'text', + mutable: false, + values: [ + textDefaultValue + ] + } + ] + } + ] + + before(() => { + cy.visit('auth/login') + cy.login() + cy.get('#cvat-create-task-button').click() + cy.url().should('include', '/tasks/create') + cy.get('[role="tab"]').contains('Raw').click() + }) + + beforeEach('Clear "Raw" field', () =>{ + cy.get('#labels').clear() + cy.task('log', '\n') + }) + + describe(`Testing issue "${issueId}"`, () => { + it('"Raw" field is empty.', () =>{ + cy.get('.ant-form-explain') + .should('exist').invoke('text').then(($explainText) => { + cy.task('log', `- "${$explainText}"`) + }) + }) + it('Label "name" as a number.', () => { + let taskRawNameNumber = JSON.parse(JSON.stringify(taskRaw)) + taskRawNameNumber[0].name = 1 + let jsonNameNumber = JSON.stringify(taskRawNameNumber) + cy.get('#labels').type(jsonNameNumber, { parseSpecialCharSequences: false }) + cy.get('.ant-form-explain') + .should('exist').invoke('text').then(($explainText) => { + cy.task('log', `- "${$explainText}"`) + }) + }) + it('Label "id" as a string.', () => { + let taskRawLabelString = JSON.parse(JSON.stringify(taskRaw)) + taskRawLabelString[0].id = "1" + let jsonLabelString = JSON.stringify(taskRawLabelString) + cy.get('#labels').type(jsonLabelString, { parseSpecialCharSequences: false }) + cy.get('.ant-form-explain') + .should('exist').invoke('text').then(($explainText) => { + cy.task('log', `- "${$explainText}"`) + }) + }) + it('Label "attributes" as a number.', () => { + let taskRawAttrNumber = JSON.parse(JSON.stringify(taskRaw)) + taskRawAttrNumber[0].attributes = 1 + let jsonAttrNumber = JSON.stringify(taskRawAttrNumber) + cy.get('#labels').type(jsonAttrNumber, { parseSpecialCharSequences: false }) + cy.get('.ant-form-explain') + .should('exist').invoke('text').then(($explainText) => { + cy.task('log', `- "${$explainText}"`) + }) + }) + it('Label "color" as a number.', () => { + let taskRawColorNumber = JSON.parse(JSON.stringify(taskRaw)) + taskRawColorNumber[0].color = 1 + let jsonColorNumber = JSON.stringify(taskRawColorNumber) + cy.get('#labels').type(jsonColorNumber, { parseSpecialCharSequences: false }) + cy.get('.ant-form-explain') + .should('exist').invoke('text').then(($explainText) => { + cy.task('log', `- "${$explainText}"`) + }) + }) + it('Label "attributes id" as a string.', () => { + let taskRawAttrIdString = JSON.parse(JSON.stringify(taskRaw)) + taskRawAttrIdString[0].attributes[0].id = "1" + let jsonAttrIdString = JSON.stringify(taskRawAttrIdString) + cy.get('#labels').type(jsonAttrIdString, { parseSpecialCharSequences: false }) + cy.get('.ant-form-explain') + .should('exist').invoke('text').then(($explainText) => { + cy.task('log', `- "${$explainText}"`) + }) + }) + it('Label "attributes input_type" is incorrect.', () => { + const inputTypes = ['select radio', 'textt', 'nnumber'] + let taskRawAttrTypeNumber = JSON.parse(JSON.stringify(taskRaw)) + for (let type of inputTypes) { + taskRawAttrTypeNumber[0].attributes[0].input_type = type + let jsonAttrTypeNumber = JSON.stringify(taskRawAttrTypeNumber) + cy.get('#labels').type(jsonAttrTypeNumber, { parseSpecialCharSequences: false }) + cy.get('.ant-form-explain') + .should('exist').invoke('text').then(($explainText) => { + cy.task('log', `- "${$explainText}"`) + }) + cy.get('#labels').clear() + } + }) + it('Label "attributes mutable" as a number.', () => { + let taskRawAttrMutableNumber = JSON.parse(JSON.stringify(taskRaw)) + taskRawAttrMutableNumber[0].attributes[0].mutable = 1 + let jsonAttrMutableNumber = JSON.stringify(taskRawAttrMutableNumber) + cy.get('#labels').type(jsonAttrMutableNumber, { parseSpecialCharSequences: false }) + cy.get('.ant-form-explain') + .should('exist').invoke('text').then(($explainText) => { + cy.task('log', `- "${$explainText}"`) + }) + }) + it('Label "attributes values" as a number.', () => { + let taskRawAttrValuesNumber = JSON.parse(JSON.stringify(taskRaw)) + taskRawAttrValuesNumber[0].attributes[0].values = 1 + let jsonAttrValueNumber = JSON.stringify(taskRawAttrValuesNumber) + cy.get('#labels').type(jsonAttrValueNumber, { parseSpecialCharSequences: false }) + cy.get('.ant-form-explain') + .should('exist').invoke('text').then(($explainText) => { + cy.task('log', `- "${$explainText}"`) + }) + }) + it('Label "attributes values" as a array with number.', () => { + let taskRawAttrValuesNumberArr = JSON.parse(JSON.stringify(taskRaw)) + taskRawAttrValuesNumberArr[0].attributes[0].values = [1] + let jsonAttrValuesNumberArr = JSON.stringify(taskRawAttrValuesNumberArr) + cy.get('#labels').type(jsonAttrValuesNumberArr, { parseSpecialCharSequences: false }) + cy.get('.ant-form-explain') + .should('exist').invoke('text').then(($explainText) => { + cy.task('log', `- "${$explainText}"`) + }) + }) + it('Label "attributes name" as a number.', () => { + let taskRawAttrNameNumber = JSON.parse(JSON.stringify(taskRaw)) + taskRawAttrNameNumber[0].attributes[0].name = 1 + let jsonAttrNameNumber = JSON.stringify(taskRawAttrNameNumber) + cy.get('#labels').type(jsonAttrNameNumber, { parseSpecialCharSequences: false }) + cy.get('.ant-form-explain') + .should('exist').invoke('text').then(($explainText) => { + cy.task('log', `- "${$explainText}"`) + }) + }) + it('Label "attributes values" as a empty array.', () => { + let taskRawAttrValuesEmptyArr = JSON.parse(JSON.stringify(taskRaw)) + taskRawAttrValuesEmptyArr[0].attributes[0].values = [] + let jsonAttrValuesEmptyArr = JSON.stringify(taskRawAttrValuesEmptyArr) + cy.get('#labels').type(jsonAttrValuesEmptyArr, { parseSpecialCharSequences: false }) + cy.get('.ant-form-explain') + .should('exist').invoke('text').then(($explainText) => { + cy.task('log', `- "${$explainText}"`) + }) + }) + }) +}) diff --git a/tests/cypress/plugins/index.js b/tests/cypress/plugins/index.js index 8e09a2cf..bcb84bca 100644 --- a/tests/cypress/plugins/index.js +++ b/tests/cypress/plugins/index.js @@ -12,4 +12,10 @@ const {createZipArchive} = require('../plugins/createZipArchive/addPlugin') module.exports = (on) => { on('task', {imageGenerator}) on('task', {createZipArchive}) + on('task', { + log(message) { + console.log(message) + return null + } + }) } diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 981b0de4..519a4433 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -32,12 +32,12 @@ Cypress.Commands.add('createAnnotationTask', (taksName='New annotation task', image='image.png', multiJobs=false, segmentSize=1) => { - cy.contains('button', 'Create new task').click() + cy.get('#cvat-create-task-button').click() cy.url().should('include', '/tasks/create') cy.get('[id="name"]').type(taksName) - cy.contains('button', 'Add label').click() + cy.get('.cvat-constructor-viewer-new-item').click() cy.get('[placeholder="Label name"]').type(labelName) - cy.contains('button', 'Add an attribute').click() + cy.get('.cvat-new-attribute-button').click() cy.get('[placeholder="Name"]').type(attrName) cy.get('div[title="Select"]').click() cy.get('li').contains('Text').click() From a6884427d4d29e9de4fa8f783c5cc77a5262bc6d Mon Sep 17 00:00:00 2001 From: Dmitry Kruchinin <33020454+dvkruchinin@users.noreply.github.com> Date: Tue, 1 Sep 2020 12:06:03 +0300 Subject: [PATCH 026/467] Cypress test for issue 1540. (#2096) Co-authored-by: Dmitry Kruchinin --- .../integration/issue_1540_add_remove_tag.js | 52 +++++++++++++++++++ tests/cypress/support/commands.js | 10 ++++ 2 files changed, 62 insertions(+) create mode 100644 tests/cypress/integration/issue_1540_add_remove_tag.js diff --git a/tests/cypress/integration/issue_1540_add_remove_tag.js b/tests/cypress/integration/issue_1540_add_remove_tag.js new file mode 100644 index 00000000..683985b6 --- /dev/null +++ b/tests/cypress/integration/issue_1540_add_remove_tag.js @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 Intel Corporation + * + * SPDX-License-Identifier: MIT + */ + +/// + +context('Check if the UI not to crash after remove a tag', () => { + + const issueId = '1540' + const labelName = `Issue ${issueId}` + const taskName = `New annotation task for ${labelName}` + const attrName = `Attr for ${labelName}` + const textDefaultValue = 'Some default value for type Text' + const image = `image_${issueId}.png` + const width = 800 + const height = 800 + const posX = 10 + const posY = 10 + const color = 'gray' + + before(() => { + cy.visit('auth/login') + cy.login() + cy.imageGenerator('cypress/fixtures', image, width, height, color, posX, posY, labelName) + cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, image) + cy.openTaskJob(taskName) + }) + + describe(`Testing issue "${issueId}"`, () => { + it('Add a tag', () => { + cy.changeAnnotationMode('Tag annotation') + cy.get('.cvat-tag-annotation-sidebar-buttons').within(() => { + cy.get('button') + .contains('Add tag') + .click({force: true}) + }) + cy.changeAnnotationMode('Standard') + }) + it('Remove the tag', () => { + cy.get('#cvat-objects-sidebar-state-item-1') + .should('contain', '1').and('contain', 'TAG') + .trigger('mouseover') + .trigger('keydown', {key: 'Delete'}) + }) + it('Page with the error is missing', () => { + cy.contains('Oops, something went wrong') + .should('not.exist') + }) + }) +}) diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 519a4433..b0274a4a 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -170,3 +170,13 @@ Cypress.Commands.add('closeSettings', () => { cy.contains('button', 'Close').click() }) }) + +Cypress.Commands.add('changeAnnotationMode', (mode) => { + cy.get('.cvat-workspace-selector') + .click() + cy.get('.ant-select-dropdown-menu-item') + .contains(mode) + .click() + cy.get('.cvat-workspace-selector') + .should('contain.text', mode) +}) From 510191f64ba420b4dc5addc6da363001655607bf Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Tue, 1 Sep 2020 12:41:49 +0300 Subject: [PATCH 027/467] Added password reset functionality (#2058) * added reset password functionality * updated changelog and versions of cvat-core, cvat-ui * fixed comments * Update cvat-ui/src/components/reset-password-confirm-page/reset-password-confirm-form.tsx * Fix CHANGELOG * fixed comments Co-authored-by: Nikita Manovich --- CHANGELOG.md | 2 +- cvat-core/src/api-implementation.js | 8 + cvat-core/src/api.js | 35 ++++ cvat-core/src/server-proxy.js | 37 +++++ cvat-ui/package-lock.json | 2 +- cvat-ui/package.json | 2 +- cvat-ui/src/actions/auth-actions.ts | 62 ++++++- cvat-ui/src/components/cvat-app.tsx | 5 +- .../src/components/login-page/login-page.tsx | 12 ++ .../reset-password-confirm-form.tsx | 156 ++++++++++++++++++ .../reset-password-confirm-page.tsx | 83 ++++++++++ .../reset-password-form.tsx | 81 +++++++++ .../reset-password-page.tsx | 79 +++++++++ .../src/containers/login-page/login-page.tsx | 2 + cvat-ui/src/index.tsx | 2 + cvat-ui/src/reducers/auth-reducer.ts | 34 +++- cvat-ui/src/reducers/interfaces.ts | 5 + cvat-ui/src/reducers/notifications-reducer.ts | 59 +++++++ cvat/apps/authentication/serializers.py | 33 +++- .../authentication/password_reset_email.html | 14 ++ cvat/settings/base.py | 6 +- 21 files changed, 699 insertions(+), 20 deletions(-) create mode 100644 cvat-ui/src/components/reset-password-confirm-page/reset-password-confirm-form.tsx create mode 100644 cvat-ui/src/components/reset-password-confirm-page/reset-password-confirm-page.tsx create mode 100644 cvat-ui/src/components/reset-password-page/reset-password-form.tsx create mode 100644 cvat-ui/src/components/reset-password-page/reset-password-page.tsx create mode 100644 cvat/apps/authentication/templates/authentication/password_reset_email.html diff --git a/CHANGELOG.md b/CHANGELOG.md index d1534bca..562ef21f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.2.0] - Unreleased ### Added -- +- Added password reset functionality () ### Changed - UI models (like DEXTR) were redesigned to be more interactive () diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index 5fc7958c..39c0d911 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -99,6 +99,14 @@ await serverProxy.server.changePassword(oldPassword, newPassword1, newPassword2); }; + cvat.server.requestPasswordReset.implementation = async (email) => { + await serverProxy.server.requestPasswordReset(email); + }; + + cvat.server.resetPassword.implementation = async(newPassword1, newPassword2, uid, token) => { + await serverProxy.server.resetPassword(newPassword1, newPassword2, uid, token); + }; + cvat.server.authorized.implementation = async () => { const result = await serverProxy.server.authorized(); return result; diff --git a/cvat-core/src/api.js b/cvat-core/src/api.js index 9d040683..1b709118 100644 --- a/cvat-core/src/api.js +++ b/cvat-core/src/api.js @@ -199,6 +199,9 @@ function build() { * @method changePassword * @async * @memberof module:API.cvat.server + * @param {string} oldPassword Current password for the account + * @param {string} newPassword1 New password for the account + * @param {string} newPassword2 Confirmation password for the account * @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.ServerError} */ @@ -207,6 +210,38 @@ function build() { .apiWrapper(cvat.server.changePassword, oldPassword, newPassword1, newPassword2); return result; }, + /** + * Method allows to reset user password + * @method requestPasswordReset + * @async + * @memberof module:API.cvat.server + * @param {string} email A email address for the account + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} + */ + async requestPasswordReset(email) { + const result = await PluginRegistry + .apiWrapper(cvat.server.requestPasswordReset, email); + return result; + }, + /** + * Method allows to confirm reset user password + * @method resetPassword + * @async + * @memberof module:API.cvat.server + * @param {string} newPassword1 New password for the account + * @param {string} newPassword2 Confirmation password for the account + * @param {string} uid User id + * @param {string} token Request authentication token + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} + */ + async resetPassword(newPassword1, newPassword2, uid, token) { + const result = await PluginRegistry + .apiWrapper(cvat.server.resetPassword, newPassword1, newPassword2, + uid, token); + return result; + }, /** * Method allows to know whether you are authorized on the server * @method authorized diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index 4a0c9ebd..58724d35 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -264,6 +264,41 @@ } } + async function requestPasswordReset(email) { + try { + const data = JSON.stringify({ + email, + }); + await Axios.post(`${config.backendAPI}/auth/password/reset`, data, { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); + } + } + + async function resetPassword(newPassword1, newPassword2, uid, token) { + try { + const data = JSON.stringify({ + new_password1: newPassword1, + new_password2: newPassword2, + uid, + token, + }); + await Axios.post(`${config.backendAPI}/auth/password/reset/confirm`, data, { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); + } + } + async function authorized() { try { await module.exports.users.getSelf(); @@ -787,6 +822,8 @@ login, logout, changePassword, + requestPasswordReset, + resetPassword, authorized, register, request: serverRequest, diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index fbbd3cea..4cfa9ddf 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.8.4", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 1065c3f8..e587666f 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.8.4", + "version": "1.9.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/auth-actions.ts b/cvat-ui/src/actions/auth-actions.ts index f12049fa..ad09019c 100644 --- a/cvat-ui/src/actions/auth-actions.ts +++ b/cvat-ui/src/actions/auth-actions.ts @@ -25,6 +25,12 @@ export enum AuthActionTypes { CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS', CHANGE_PASSWORD_FAILED = 'CHANGE_PASSWORD_FAILED', SWITCH_CHANGE_PASSWORD_DIALOG = 'SWITCH_CHANGE_PASSWORD_DIALOG', + REQUEST_PASSWORD_RESET = 'REQUEST_PASSWORD_RESET', + REQUEST_PASSWORD_RESET_SUCCESS = 'REQUEST_PASSWORD_RESET_SUCCESS', + REQUEST_PASSWORD_RESET_FAILED = 'REQUEST_PASSWORD_RESET_FAILED', + RESET_PASSWORD = 'RESET_PASSWORD_CONFIRM', + RESET_PASSWORD_SUCCESS = 'RESET_PASSWORD_CONFIRM_SUCCESS', + RESET_PASSWORD_FAILED = 'RESET_PASSWORD_CONFIRM_FAILED', LOAD_AUTH_ACTIONS = 'LOAD_AUTH_ACTIONS', LOAD_AUTH_ACTIONS_SUCCESS = 'LOAD_AUTH_ACTIONS_SUCCESS', LOAD_AUTH_ACTIONS_FAILED = 'LOAD_AUTH_ACTIONS_FAILED', @@ -50,9 +56,22 @@ export const authActions = { switchChangePasswordDialog: (showChangePasswordDialog: boolean) => ( createAction(AuthActionTypes.SWITCH_CHANGE_PASSWORD_DIALOG, { showChangePasswordDialog }) ), + requestPasswordReset: () => createAction(AuthActionTypes.REQUEST_PASSWORD_RESET), + requestPasswordResetSuccess: () => createAction(AuthActionTypes.REQUEST_PASSWORD_RESET_SUCCESS), + requestPasswordResetFailed: (error: any) => ( + createAction(AuthActionTypes.REQUEST_PASSWORD_RESET_FAILED, { error }) + ), + resetPassword: () => createAction(AuthActionTypes.RESET_PASSWORD), + resetPasswordSuccess: () => createAction(AuthActionTypes.RESET_PASSWORD_SUCCESS), + resetPasswordFailed: (error: any) => ( + createAction(AuthActionTypes.RESET_PASSWORD_FAILED, { error }) + ), loadServerAuthActions: () => createAction(AuthActionTypes.LOAD_AUTH_ACTIONS), - loadServerAuthActionsSuccess: (allowChangePassword: boolean) => ( - createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_SUCCESS, { allowChangePassword }) + loadServerAuthActionsSuccess: (allowChangePassword: boolean, allowResetPassword: boolean) => ( + createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_SUCCESS, { + allowChangePassword, + allowResetPassword, + }) ), loadServerAuthActionsFailed: (error: any) => ( createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED, { error }) @@ -135,16 +154,49 @@ export const changePasswordAsync = (oldPassword: string, } }; +export const requestPasswordResetAsync = (email: string): ThunkAction => async (dispatch) => { + dispatch(authActions.requestPasswordReset()); + + try { + await cvat.server.requestPasswordReset(email); + dispatch(authActions.requestPasswordResetSuccess()); + } catch (error) { + dispatch(authActions.requestPasswordResetFailed(error)); + } +}; + +export const resetPasswordAsync = ( + newPassword1: string, + newPassword2: string, + uid: string, + token: string, +): ThunkAction => async (dispatch) => { + dispatch(authActions.resetPassword()); + + try { + await cvat.server.resetPassword(newPassword1, newPassword2, uid, token); + dispatch(authActions.resetPasswordSuccess()); + } catch (error) { + dispatch(authActions.resetPasswordFailed(error)); + } +}; + export const loadAuthActionsAsync = (): ThunkAction => async (dispatch) => { dispatch(authActions.loadServerAuthActions()); try { const promises: Promise[] = [ isReachable(`${cvat.config.backendAPI}/auth/password/change`, 'OPTIONS'), + isReachable(`${cvat.config.backendAPI}/auth/password/reset`, 'OPTIONS'), ]; - const [allowChangePassword] = await Promise.all(promises); - - dispatch(authActions.loadServerAuthActionsSuccess(allowChangePassword)); + const [ + allowChangePassword, + allowResetPassword] = await Promise.all(promises); + + dispatch(authActions.loadServerAuthActionsSuccess( + allowChangePassword, + allowResetPassword, + )); } catch (error) { dispatch(authActions.loadServerAuthActionsFailed(error)); } diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 5c1af2cf..c32717c7 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -23,6 +23,8 @@ import ModelsPageContainer from 'containers/models-page/models-page'; import AnnotationPageContainer from 'containers/annotation-page/annotation-page'; import LoginPageContainer from 'containers/login-page/login-page'; import RegisterPageContainer from 'containers/register-page/register-page'; +import ResetPasswordPageComponent from 'components/reset-password-page/reset-password-page'; +import ResetPasswordPageConfirmComponent from 'components/reset-password-confirm-page/reset-password-confirm-page'; import Header from 'components/header/header'; import { customWaViewHit } from 'utils/enviroment'; import showPlatformNotification, { stopNotifications, platformInfo } from 'utils/platform-checker'; @@ -61,7 +63,6 @@ interface CVATAppProps { userAgreementsInitialized: boolean; authActionsFetching: boolean; authActionsInitialized: boolean; - allowChangePassword: boolean; notifications: NotificationsState; user: any; } @@ -332,6 +333,8 @@ class CVATApplication extends React.PureComponent + + diff --git a/cvat-ui/src/components/login-page/login-page.tsx b/cvat-ui/src/components/login-page/login-page.tsx index 87cebb51..23dff9e1 100644 --- a/cvat-ui/src/components/login-page/login-page.tsx +++ b/cvat-ui/src/components/login-page/login-page.tsx @@ -14,6 +14,7 @@ import CookieDrawer from './cookie-policy-drawer'; interface LoginPageComponentProps { fetching: boolean; + renderResetPassword: boolean; onLogin: (username: string, password: string) => void; } @@ -29,6 +30,7 @@ function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps const { fetching, onLogin, + renderResetPassword, } = props; return ( @@ -50,6 +52,16 @@ function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps + { renderResetPassword + && ( + + + + Forgot your password? + + + + )} diff --git a/cvat-ui/src/components/reset-password-confirm-page/reset-password-confirm-form.tsx b/cvat-ui/src/components/reset-password-confirm-page/reset-password-confirm-form.tsx new file mode 100644 index 00000000..38d4fdb0 --- /dev/null +++ b/cvat-ui/src/components/reset-password-confirm-page/reset-password-confirm-form.tsx @@ -0,0 +1,156 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; +import Form, { FormComponentProps } from 'antd/lib/form/Form'; +import Button from 'antd/lib/button'; +import Icon from 'antd/lib/icon'; +import Input from 'antd/lib/input'; + +import patterns from 'utils/validation-patterns'; + +export interface ResetPasswordConfirmData { + newPassword1: string; + newPassword2: string; + uid: string; + token: string; +} + +type ResetPasswordConfirmFormProps = { + fetching: boolean; + onSubmit(resetPasswordConfirmData: ResetPasswordConfirmData): void; +} & FormComponentProps & RouteComponentProps; + +class ResetPasswordConfirmFormComponent extends React.PureComponent { + private validateConfirmation = (_: any, value: string, callback: Function): void => { + const { form } = this.props; + if (value && value !== form.getFieldValue('newPassword1')) { + callback('Passwords do not match!'); + } else { + callback(); + } + }; + + private validatePassword = (_: any, value: string, callback: Function): void => { + const { form } = this.props; + if (!patterns.validatePasswordLength.pattern.test(value)) { + callback(patterns.validatePasswordLength.message); + } + + if (!patterns.passwordContainsNumericCharacters.pattern.test(value)) { + callback(patterns.passwordContainsNumericCharacters.message); + } + + if (!patterns.passwordContainsUpperCaseCharacter.pattern.test(value)) { + callback(patterns.passwordContainsUpperCaseCharacter.message); + } + + if (!patterns.passwordContainsLowerCaseCharacter.pattern.test(value)) { + callback(patterns.passwordContainsLowerCaseCharacter.message); + } + + if (value) { + form.validateFields(['newPassword2'], { force: true }); + } + callback(); + }; + + private handleSubmit = (e: React.FormEvent): void => { + e.preventDefault(); + const { + form, + onSubmit, + location, + } = this.props; + + const params = new URLSearchParams(location.search); + const uid = params.get('uid'); + const token = params.get('token'); + + form.validateFields((error, values): void => { + if (!error) { + const validatedFields = { + ...values, + uid, + token, + }; + + onSubmit(validatedFields); + } + }); + }; + + private renderNewPasswordField(): JSX.Element { + const { form } = this.props; + + return ( + + {form.getFieldDecorator('newPassword1', { + rules: [{ + required: true, + message: 'Please input new password!', + }, { + validator: this.validatePassword, + }], + })(} + placeholder='New password' + />)} + + ); + } + + private renderNewPasswordConfirmationField(): JSX.Element { + const { form } = this.props; + + return ( + + {form.getFieldDecorator('newPassword2', { + rules: [{ + required: true, + message: 'Please confirm your new password!', + }, { + validator: this.validateConfirmation, + }], + })(} + placeholder='Confirm new password' + />)} + + ); + } + + public render(): JSX.Element { + const { fetching } = this.props; + + return ( +
+ {this.renderNewPasswordField()} + {this.renderNewPasswordConfirmationField()} + + + + +
+ ); + } +} + +export default withRouter( + Form.create()(ResetPasswordConfirmFormComponent), +); diff --git a/cvat-ui/src/components/reset-password-confirm-page/reset-password-confirm-page.tsx b/cvat-ui/src/components/reset-password-confirm-page/reset-password-confirm-page.tsx new file mode 100644 index 00000000..50a4af5d --- /dev/null +++ b/cvat-ui/src/components/reset-password-confirm-page/reset-password-confirm-page.tsx @@ -0,0 +1,83 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { Link } from 'react-router-dom'; +import { connect } from 'react-redux'; +import Title from 'antd/lib/typography/Title'; +import Text from 'antd/lib/typography/Text'; +import { Row, Col } from 'antd/lib/grid'; + +import { CombinedState } from 'reducers/interfaces'; +import { resetPasswordAsync } from 'actions/auth-actions'; + +import ResetPasswordConfirmForm, { ResetPasswordConfirmData } from './reset-password-confirm-form'; + +interface StateToProps { + fetching: boolean; +} + +interface DispatchToProps { + onResetPasswordConfirm: typeof resetPasswordAsync; +} + +interface ResetPasswordConfirmPageComponentProps { + fetching: boolean; + onResetPasswordConfirm: ( + newPassword1: string, + newPassword2: string, + uid: string, + token: string) => void; +} + +function mapStateToProps(state: CombinedState): StateToProps { + return { + fetching: state.auth.fetching, + }; +} + +const mapDispatchToProps: DispatchToProps = { + onResetPasswordConfirm: resetPasswordAsync, +}; + +function ResetPasswordPagePageComponent( + props: ResetPasswordConfirmPageComponentProps, +): JSX.Element { + const sizes = { + xs: { span: 14 }, + sm: { span: 14 }, + md: { span: 10 }, + lg: { span: 4 }, + xl: { span: 4 }, + }; + + const { + fetching, + onResetPasswordConfirm, + } = props; + + return ( + + + Change password + { + onResetPasswordConfirm( + resetPasswordConfirmData.newPassword1, + resetPasswordConfirmData.newPassword2, + resetPasswordConfirmData.uid, + resetPasswordConfirmData.token, + ); + }} + /> + + + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(ResetPasswordPagePageComponent); diff --git a/cvat-ui/src/components/reset-password-page/reset-password-form.tsx b/cvat-ui/src/components/reset-password-page/reset-password-form.tsx new file mode 100644 index 00000000..9b0b1b3b --- /dev/null +++ b/cvat-ui/src/components/reset-password-page/reset-password-form.tsx @@ -0,0 +1,81 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import Form, { FormComponentProps } from 'antd/lib/form/Form'; +import Button from 'antd/lib/button'; +import Icon from 'antd/lib/icon'; +import Input from 'antd/lib/input'; + +export interface ResetPasswordData { + email: string; +} + +type ResetPasswordFormProps = { + fetching: boolean; + onSubmit(resetPasswordData: ResetPasswordData): void; +} & FormComponentProps; + +class ResetPasswordFormComponent extends React.PureComponent { + private handleSubmit = (e: React.FormEvent): void => { + e.preventDefault(); + const { + form, + onSubmit, + } = this.props; + + form.validateFields((error, values): void => { + if (!error) { + onSubmit(values); + } + }); + }; + + private renderEmailField(): JSX.Element { + const { form } = this.props; + + return ( + + {form.getFieldDecorator('email', { + rules: [{ + type: 'email', + message: 'The input is not valid E-mail!', + }, { + required: true, + message: 'Please specify an email address', + }], + })( + } + placeholder='Email address' + />, + )} + + ); + } + + public render(): JSX.Element { + const { fetching } = this.props; + return ( +
+ {this.renderEmailField()} + + + + +
+ ); + } +} + +export default Form.create()(ResetPasswordFormComponent); diff --git a/cvat-ui/src/components/reset-password-page/reset-password-page.tsx b/cvat-ui/src/components/reset-password-page/reset-password-page.tsx new file mode 100644 index 00000000..0b7ebcca --- /dev/null +++ b/cvat-ui/src/components/reset-password-page/reset-password-page.tsx @@ -0,0 +1,79 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { Link } from 'react-router-dom'; +import { connect } from 'react-redux'; +import Title from 'antd/lib/typography/Title'; +import Text from 'antd/lib/typography/Text'; +import { Row, Col } from 'antd/lib/grid'; + +import { requestPasswordResetAsync } from 'actions/auth-actions'; +import { CombinedState } from 'reducers/interfaces'; +import ResetPasswordForm, { ResetPasswordData } from './reset-password-form'; + +interface StateToProps { + fetching: boolean; +} + +interface DispatchToProps { + onResetPassword: typeof requestPasswordResetAsync; +} + +interface ResetPasswordPageComponentProps { + fetching: boolean; + onResetPassword: (email: string) => void; +} + +function mapStateToProps(state: CombinedState): StateToProps { + return { + fetching: state.auth.fetching, + }; +} + +const mapDispatchToProps: DispatchToProps = { + onResetPassword: requestPasswordResetAsync, +}; + +function ResetPasswordPagePageComponent(props: ResetPasswordPageComponentProps): JSX.Element { + const sizes = { + xs: { span: 14 }, + sm: { span: 14 }, + md: { span: 10 }, + lg: { span: 4 }, + xl: { span: 4 }, + }; + + const { + fetching, + onResetPassword, + } = props; + + return ( + + + Reset password + { + onResetPassword(resetPasswordData.email); + }} + /> + + + + Go to + login page + + + + + + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(ResetPasswordPagePageComponent); diff --git a/cvat-ui/src/containers/login-page/login-page.tsx b/cvat-ui/src/containers/login-page/login-page.tsx index fdf40ac7..0605d44d 100644 --- a/cvat-ui/src/containers/login-page/login-page.tsx +++ b/cvat-ui/src/containers/login-page/login-page.tsx @@ -9,6 +9,7 @@ import { loginAsync } from 'actions/auth-actions'; interface StateToProps { fetching: boolean; + renderResetPassword: boolean; } interface DispatchToProps { @@ -18,6 +19,7 @@ interface DispatchToProps { function mapStateToProps(state: CombinedState): StateToProps { return { fetching: state.auth.fetching, + renderResetPassword: state.auth.allowResetPassword, }; } diff --git a/cvat-ui/src/index.tsx b/cvat-ui/src/index.tsx index f1f7f7f1..ce3355c4 100644 --- a/cvat-ui/src/index.tsx +++ b/cvat-ui/src/index.tsx @@ -57,6 +57,7 @@ interface StateToProps { authActionsFetching: boolean; authActionsInitialized: boolean; allowChangePassword: boolean; + allowResetPassword: boolean; notifications: NotificationsState; user: any; keyMap: Record; @@ -105,6 +106,7 @@ function mapStateToProps(state: CombinedState): StateToProps { authActionsFetching: auth.authActionsFetching, authActionsInitialized: auth.authActionsInitialized, allowChangePassword: auth.allowChangePassword, + allowResetPassword: auth.allowResetPassword, notifications: state.notifications, user: auth.user, keyMap: shortcuts.keyMap, diff --git a/cvat-ui/src/reducers/auth-reducer.ts b/cvat-ui/src/reducers/auth-reducer.ts index d433e7d5..da424cca 100644 --- a/cvat-ui/src/reducers/auth-reducer.ts +++ b/cvat-ui/src/reducers/auth-reducer.ts @@ -14,6 +14,7 @@ const defaultState: AuthState = { authActionsInitialized: false, allowChangePassword: false, showChangePasswordDialog: false, + allowResetPassword: false, }; export default function (state = defaultState, action: AuthActions | boundariesActions): AuthState { @@ -83,7 +84,6 @@ export default function (state = defaultState, action: AuthActions | boundariesA ...state, fetching: false, showChangePasswordDialog: false, - }; case AuthActionTypes.CHANGE_PASSWORD_FAILED: return { @@ -97,6 +97,36 @@ export default function (state = defaultState, action: AuthActions | boundariesA ? !state.showChangePasswordDialog : action.payload.showChangePasswordDialog, }; + case AuthActionTypes.REQUEST_PASSWORD_RESET: + return { + ...state, + fetching: true, + }; + case AuthActionTypes.REQUEST_PASSWORD_RESET_SUCCESS: + return { + ...state, + fetching: false, + }; + case AuthActionTypes.REQUEST_PASSWORD_RESET_FAILED: + return { + ...state, + fetching: false, + }; + case AuthActionTypes.RESET_PASSWORD: + return { + ...state, + fetching: true, + }; + case AuthActionTypes.RESET_PASSWORD_SUCCESS: + return { + ...state, + fetching: false, + }; + case AuthActionTypes.RESET_PASSWORD_FAILED: + return { + ...state, + fetching: false, + }; case AuthActionTypes.LOAD_AUTH_ACTIONS: return { ...state, @@ -108,6 +138,7 @@ export default function (state = defaultState, action: AuthActions | boundariesA authActionsFetching: false, authActionsInitialized: true, allowChangePassword: action.payload.allowChangePassword, + allowResetPassword: action.payload.allowResetPassword, }; case AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED: return { @@ -115,6 +146,7 @@ export default function (state = defaultState, action: AuthActions | boundariesA authActionsFetching: false, authActionsInitialized: true, allowChangePassword: false, + allowResetPassword: false, }; case BoundariesActionTypes.RESET_AFTER_ERROR: { return { ...defaultState }; diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 9a48b52e..79f5531a 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -17,6 +17,7 @@ export interface AuthState { authActionsInitialized: boolean; showChangePasswordDialog: boolean; allowChangePassword: boolean; + allowResetPassword: boolean; } export interface TasksQuery { @@ -184,6 +185,8 @@ export interface NotificationsState { logout: null | ErrorState; register: null | ErrorState; changePassword: null | ErrorState; + requestPasswordReset: null | ErrorState; + resetPassword: null | ErrorState; loadAuthActions: null | ErrorState; }; tasks: { @@ -253,6 +256,8 @@ export interface NotificationsState { auth: { changePasswordDone: string; registerDone: string; + requestPasswordResetDone: string; + resetPasswordDone: string; }; }; } diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index e87d22b8..96863e6b 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -27,6 +27,8 @@ const defaultState: NotificationsState = { logout: null, register: null, changePassword: null, + requestPasswordReset: null, + resetPassword: null, loadAuthActions: null, }, tasks: { @@ -96,6 +98,8 @@ const defaultState: NotificationsState = { auth: { changePasswordDone: '', registerDone: '', + requestPasswordResetDone: '', + resetPasswordDone: '', }, }, }; @@ -208,6 +212,61 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } + case AuthActionTypes.REQUEST_PASSWORD_RESET_SUCCESS: { + return { + ...state, + messages: { + ...state.messages, + auth: { + ...state.messages.auth, + requestPasswordResetDone: `Check your email for a link to reset your password. + If it doesn’t appear within a few minutes, check your spam folder.`, + }, + }, + }; + } + case AuthActionTypes.REQUEST_PASSWORD_RESET_FAILED: { + return { + ...state, + errors: { + ...state.errors, + auth: { + ...state.errors.auth, + requestPasswordReset: { + message: 'Could not reset password on the server.', + reason: action.payload.error.toString(), + }, + }, + }, + }; + } + case AuthActionTypes.RESET_PASSWORD_SUCCESS: { + return { + ...state, + messages: { + ...state.messages, + auth: { + ...state.messages.auth, + resetPasswordDone: 'Password has been reset with the new password.', + }, + }, + }; + } + case AuthActionTypes.RESET_PASSWORD_FAILED: { + return { + ...state, + errors: { + ...state.errors, + auth: { + ...state.errors.auth, + resetPassword: { + message: 'Could not set new password on the server.', + reason: action.payload.error.toString(), + }, + }, + }, + }; + } case AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED: { return { ...state, diff --git a/cvat/apps/authentication/serializers.py b/cvat/apps/authentication/serializers.py index 1269b8fc..e04ca18b 100644 --- a/cvat/apps/authentication/serializers.py +++ b/cvat/apps/authentication/serializers.py @@ -1,16 +1,31 @@ from rest_auth.registration.serializers import RegisterSerializer +from rest_auth.serializers import PasswordResetSerializer from rest_framework import serializers +from django.conf import settings + class RegisterSerializerEx(RegisterSerializer): - first_name = serializers.CharField(required=False) - last_name = serializers.CharField(required=False) + first_name = serializers.CharField(required=False) + last_name = serializers.CharField(required=False) + + def get_cleaned_data(self): + data = super().get_cleaned_data() + data.update({ + 'first_name': self.validated_data.get('first_name', ''), + 'last_name': self.validated_data.get('last_name', ''), + }) - def get_cleaned_data(self): - data = super().get_cleaned_data() - data.update({ - 'first_name': self.validated_data.get('first_name', ''), - 'last_name': self.validated_data.get('last_name', ''), - }) + return data - return data +class PasswordResetSerializerEx(PasswordResetSerializer): + def get_email_options(self): + domain = None + if hasattr(settings, 'UI_HOST') and settings.UI_HOST: + domain = settings.UI_HOST + if hasattr(settings, 'UI_PORT') and settings.UI_PORT: + domain += ':{}'.format(settings.UI_PORT) + return { + 'email_template_name': 'authentication/password_reset_email.html', + 'domain_override': domain + } diff --git a/cvat/apps/authentication/templates/authentication/password_reset_email.html b/cvat/apps/authentication/templates/authentication/password_reset_email.html new file mode 100644 index 00000000..542e266a --- /dev/null +++ b/cvat/apps/authentication/templates/authentication/password_reset_email.html @@ -0,0 +1,14 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} + +{% trans "Please go to the following page and choose a new password:" %} +{% block reset_link %} +{{ protocol }}://{{ domain }}/auth/password/reset/confirm?uid={{ uid }}&token={{ token }} +{% endblock %} +{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} + +{% trans "Thanks for using our site!" %} + +{% blocktrans %}The {{ site_name }} team{% endblocktrans %} + +{% endautoescape %} diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 6a9a7a6a..37d26546 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -154,7 +154,11 @@ REST_FRAMEWORK = { } REST_AUTH_REGISTER_SERIALIZERS = { - 'REGISTER_SERIALIZER': 'cvat.apps.restrictions.serializers.RestrictedRegisterSerializer' + 'REGISTER_SERIALIZER': 'cvat.apps.restrictions.serializers.RestrictedRegisterSerializer', +} + +REST_AUTH_SERIALIZERS = { + 'PASSWORD_RESET_SERIALIZER': 'cvat.apps.authentication.serializers.PasswordResetSerializerEx', } if os.getenv('DJANGO_LOG_VIEWER_HOST'): From f969e502dd5c8fb1b15f2a2424d8496965b44869 Mon Sep 17 00:00:00 2001 From: Nikita Manovich Date: Tue, 1 Sep 2020 12:47:42 +0300 Subject: [PATCH 028/467] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0029d86c..bdb6c11b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.2.0] - Unreleased ### Added - Added password reset functionality () +- Ability to work with data on the fly (https://github.com/opencv/cvat/pull/2007) ### Changed - UI models (like DEXTR) were redesigned to be more interactive () @@ -33,7 +34,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ability to configure email verification for new users () - Link to django admin page from UI () - Notification message when users use wrong browser () -- Ability to work with data on the fly (https://github.com/opencv/cvat/pull/2007) ### Changed - Shape coordinates are rounded to 2 digits in dumped annotations () From 0e37d70b1a0525dd0ded3e3bb760e6c5ca8d1f30 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Tue, 1 Sep 2020 13:53:38 +0300 Subject: [PATCH 029/467] CVAT UI: batch of fixes (#2084) * fixed object item border color * Fixed default collapsed prop in object item * Added color picker for shape outline * Added CHANGELOG, increased npm version * Fixed object details collapsing * Fixed default collapsed --- CHANGELOG.md | 3 ++ cvat-ui/package-lock.json | 2 +- cvat-ui/package.json | 2 +- cvat-ui/src/actions/auth-actions.ts | 4 +- cvat-ui/src/actions/settings-actions.ts | 9 ++-- cvat-ui/src/base.scss | 2 +- .../annotation-page/appearance-block.tsx | 43 +++++++++++++------ .../canvas-context-menu.tsx | 6 ++- .../standard-workspace/canvas-wrapper.tsx | 14 +++--- .../objects-side-bar/objects-list.tsx | 12 ++++-- .../standard-workspace/canvas-wrapper.tsx | 9 ++-- .../objects-side-bar/object-item.tsx | 3 +- .../objects-side-bar/objects-list.tsx | 12 +++--- cvat-ui/src/reducers/annotation-reducer.ts | 4 ++ cvat-ui/src/reducers/interfaces.ts | 4 +- cvat-ui/src/reducers/settings-reducer.ts | 8 ++-- 16 files changed, 92 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 562ef21f..fe430429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ability to configure email verification for new users () - Link to django admin page from UI () - Notification message when users use wrong browser () +- Annotation in process outline color wheel () ### Changed - Shape coordinates are rounded to 2 digits in dumped annotations () @@ -44,6 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a case in which exported masks could have wrong color order () - Fixed error with creating task with labels with the same name () - Django RQ dashboard view () +- Object's details menu settings () +- ## [1.1.0-beta] - 2020-08-03 ### Added diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 4cfa9ddf..7370e0fa 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.9.0", + "version": "1.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index e587666f..66ba9e86 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.9.0", + "version": "1.9.1", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/auth-actions.ts b/cvat-ui/src/actions/auth-actions.ts index ad09019c..c1698747 100644 --- a/cvat-ui/src/actions/auth-actions.ts +++ b/cvat-ui/src/actions/auth-actions.ts @@ -94,8 +94,8 @@ export const registerAsync = ( dispatch(authActions.register()); try { - const user = await cvat.server.register(username, firstName, lastName, email, password1, password2, - confirmations); + const user = await cvat.server.register(username, firstName, lastName, email, password1, + password2, confirmations); dispatch(authActions.registerSuccess(user)); } catch (error) { diff --git a/cvat-ui/src/actions/settings-actions.ts b/cvat-ui/src/actions/settings-actions.ts index 9108b02a..737baa26 100644 --- a/cvat-ui/src/actions/settings-actions.ts +++ b/cvat-ui/src/actions/settings-actions.ts @@ -17,7 +17,7 @@ export enum SettingsActionTypes { CHANGE_SHAPES_OPACITY = 'CHANGE_SHAPES_OPACITY', CHANGE_SELECTED_SHAPES_OPACITY = 'CHANGE_SELECTED_SHAPES_OPACITY', CHANGE_SHAPES_COLOR_BY = 'CHANGE_SHAPES_COLOR_BY', - CHANGE_SHAPES_BLACK_BORDERS = 'CHANGE_SHAPES_BLACK_BORDERS', + CHANGE_SHAPES_OUTLINED_BORDERS = 'CHANGE_SHAPES_OUTLINED_BORDERS', CHANGE_SHAPES_SHOW_PROJECTIONS = 'CHANGE_SHAPES_SHOW_PROJECTIONS', CHANGE_SHOW_UNLABELED_REGIONS = 'CHANGE_SHOW_UNLABELED_REGIONS', CHANGE_FRAME_STEP = 'CHANGE_FRAME_STEP', @@ -63,11 +63,12 @@ export function changeShapesColorBy(colorBy: ColorBy): AnyAction { }; } -export function changeShapesBlackBorders(blackBorders: boolean): AnyAction { +export function changeShapesOutlinedBorders(outlined: boolean, color: string): AnyAction { return { - type: SettingsActionTypes.CHANGE_SHAPES_BLACK_BORDERS, + type: SettingsActionTypes.CHANGE_SHAPES_OUTLINED_BORDERS, payload: { - blackBorders, + outlined, + color, }, }; } diff --git a/cvat-ui/src/base.scss b/cvat-ui/src/base.scss index 75b561ca..86bae731 100644 --- a/cvat-ui/src/base.scss +++ b/cvat-ui/src/base.scss @@ -22,7 +22,7 @@ $info-icon-color: #0074d9; $objects-bar-tabs-color: #bebebe; $objects-bar-icons-color: #242424; // #6e6e6e $active-label-background-color: #d8ecff; -$object-item-border-color: #000; +$object-item-border-color: rgba(0, 0, 0, 0.7); $slider-color: #1890ff; $monospaced-fonts-stack: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace; diff --git a/cvat-ui/src/components/annotation-page/appearance-block.tsx b/cvat-ui/src/components/annotation-page/appearance-block.tsx index 06a060fa..42e8fc20 100644 --- a/cvat-ui/src/components/annotation-page/appearance-block.tsx +++ b/cvat-ui/src/components/annotation-page/appearance-block.tsx @@ -11,6 +11,8 @@ import Slider, { SliderValue } from 'antd/lib/slider'; import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox'; import Collapse from 'antd/lib/collapse'; +import ColorPicker from 'components/annotation-page/standard-workspace/objects-side-bar/color-picker'; +import { ColorizeIcon } from 'icons'; import { ColorBy, CombinedState } from 'reducers/interfaces'; import { collapseAppearance as collapseAppearanceAction, @@ -20,17 +22,19 @@ import { changeShapesColorBy as changeShapesColorByAction, changeShapesOpacity as changeShapesOpacityAction, changeSelectedShapesOpacity as changeSelectedShapesOpacityAction, - changeShapesBlackBorders as changeShapesBlackBordersAction, + changeShapesOutlinedBorders as changeShapesOutlinedBordersAction, changeShowBitmap as changeShowBitmapAction, changeShowProjections as changeShowProjectionsAction, } from 'actions/settings-actions'; +import Button from 'antd/lib/button'; interface StateToProps { appearanceCollapsed: boolean; colorBy: ColorBy; opacity: number; selectedOpacity: number; - blackBorders: boolean; + outlined: boolean; + outlineColor: string; showBitmap: boolean; showProjections: boolean; } @@ -40,7 +44,7 @@ interface DispatchToProps { changeShapesColorBy(event: RadioChangeEvent): void; changeShapesOpacity(event: SliderValue): void; changeSelectedShapesOpacity(event: SliderValue): void; - changeShapesBlackBorders(event: CheckboxChangeEvent): void; + changeShapesOutlinedBorders(outlined: boolean, color: string): void; changeShowBitmap(event: CheckboxChangeEvent): void; changeShowProjections(event: CheckboxChangeEvent): void; } @@ -72,7 +76,8 @@ function mapStateToProps(state: CombinedState): StateToProps { colorBy, opacity, selectedOpacity, - blackBorders, + outlined, + outlineColor, showBitmap, showProjections, }, @@ -84,7 +89,8 @@ function mapStateToProps(state: CombinedState): StateToProps { colorBy, opacity, selectedOpacity, - blackBorders, + outlined, + outlineColor, showBitmap, showProjections, }; @@ -119,8 +125,8 @@ function mapDispatchToProps(dispatch: Dispatch): DispatchToProps { changeSelectedShapesOpacity(value: SliderValue): void { dispatch(changeSelectedShapesOpacityAction(value as number)); }, - changeShapesBlackBorders(event: CheckboxChangeEvent): void { - dispatch(changeShapesBlackBordersAction(event.target.checked)); + changeShapesOutlinedBorders(outlined: boolean, color: string): void { + dispatch(changeShapesOutlinedBordersAction(outlined, color)); }, changeShowBitmap(event: CheckboxChangeEvent): void { dispatch(changeShowBitmapAction(event.target.checked)); @@ -139,14 +145,15 @@ function AppearanceBlock(props: Props): JSX.Element { colorBy, opacity, selectedOpacity, - blackBorders, + outlined, + outlineColor, showBitmap, showProjections, collapseAppearance, changeShapesColorBy, changeShapesOpacity, changeSelectedShapesOpacity, - changeShapesBlackBorders, + changeShapesOutlinedBorders, changeShowBitmap, changeShowProjections, } = props; @@ -185,10 +192,22 @@ function AppearanceBlock(props: Props): JSX.Element { max={100} /> { + changeShapesOutlinedBorders(event.target.checked, outlineColor); + }} + checked={outlined} > - Black borders + Outlined borders + changeShapesOutlinedBorders(outlined, color)} + value={outlineColor} + placement='top' + resetVisible={false} + > + + - +
, window.document.body, ); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index 14a13014..35de27e1 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -42,7 +42,8 @@ interface Props { opacity: number; colorBy: ColorBy; selectedOpacity: number; - blackBorders: boolean; + outlined: boolean; + outlineColor: string; showBitmap: boolean; showProjections: boolean; grid: boolean; @@ -125,7 +126,8 @@ export default class CanvasWrapperComponent extends React.PureComponent { opacity, colorBy, selectedOpacity, - blackBorders, + outlined, + outlineColor, showBitmap, frameData, frameAngle, @@ -230,7 +232,8 @@ export default class CanvasWrapperComponent extends React.PureComponent { }, { once: true }); } - if (prevProps.opacity !== opacity || prevProps.blackBorders !== blackBorders + if (prevProps.opacity !== opacity || prevProps.outlined !== outlined + || prevProps.outlineColor !== outlineColor || prevProps.selectedOpacity !== selectedOpacity || prevProps.colorBy !== colorBy ) { this.updateShapesView(); @@ -602,7 +605,8 @@ export default class CanvasWrapperComponent extends React.PureComponent { annotations, opacity, colorBy, - blackBorders, + outlined, + outlineColor, } = this.props; for (const state of annotations) { @@ -625,7 +629,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { } (shapeView as any).instance.fill({ color: shapeColor, opacity: opacity / 100 }); - (shapeView as any).instance.stroke({ color: blackBorders ? 'black' : shapeColor }); + (shapeView as any).instance.stroke({ color: outlined ? outlineColor : shapeColor }); } } } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx index f91989ad..45682626 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx @@ -12,7 +12,7 @@ interface Props { listHeight: number; statesHidden: boolean; statesLocked: boolean; - statesCollapsed: boolean; + statesCollapsedAll: boolean; statesOrdering: StatesOrdering; sortedStatesID: number[]; switchLockAllShortcut: string; @@ -31,7 +31,7 @@ function ObjectListComponent(props: Props): JSX.Element { listHeight, statesHidden, statesLocked, - statesCollapsed, + statesCollapsedAll, statesOrdering, sortedStatesID, switchLockAllShortcut, @@ -50,7 +50,7 @@ function ObjectListComponent(props: Props): JSX.Element {
{ sortedStatesID.map((id: number): JSX.Element => ( - + ))}
diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx index 4591ad5a..513b289a 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -64,7 +64,8 @@ interface StateToProps { opacity: number; colorBy: ColorBy; selectedOpacity: number; - blackBorders: boolean; + outlined: boolean; + outlineColor: string; showBitmap: boolean; showProjections: boolean; grid: boolean; @@ -179,7 +180,8 @@ function mapStateToProps(state: CombinedState): StateToProps { opacity, colorBy, selectedOpacity, - blackBorders, + outlined, + outlineColor, showBitmap, showProjections, }, @@ -204,7 +206,8 @@ function mapStateToProps(state: CombinedState): StateToProps { opacity, colorBy, selectedOpacity, - blackBorders, + outlined, + outlineColor, showBitmap, showProjections, grid, diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index e0b29e36..09701627 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -30,6 +30,7 @@ import { shift } from 'utils/math'; interface OwnProps { clientID: number; + initialCollapsed: boolean; } interface StateToProps { @@ -101,7 +102,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { .indexOf(own.clientID); const collapsedState = typeof (statesCollapsed[own.clientID]) === 'undefined' - ? true : statesCollapsed[own.clientID]; + ? own.initialCollapsed : statesCollapsed[own.clientID]; return { objectState: states[index], diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx index 14e445de..58204bc2 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx @@ -30,7 +30,8 @@ interface StateToProps { listHeight: number; statesHidden: boolean; statesLocked: boolean; - statesCollapsed: boolean; + statesCollapsedAll: boolean; + collapsedStates: Record; objectStates: any[]; annotationsFilters: string[]; colors: string[]; @@ -62,6 +63,7 @@ function mapStateToProps(state: CombinedState): StateToProps { filters: annotationsFilters, filtersHistory: annotationsFiltersHistory, collapsed, + collapsedAll, activatedStateID, zLayer: { min: minZLayer, @@ -95,25 +97,23 @@ function mapStateToProps(state: CombinedState): StateToProps { let statesHidden = true; let statesLocked = true; - let statesCollapsed = true; objectStates.forEach((objectState: any) => { - const { clientID, lock } = objectState; + const { lock } = objectState; if (!lock) { if (objectState.objectType !== ObjectType.TAG) { statesHidden = statesHidden && objectState.hidden; } statesLocked = statesLocked && objectState.lock; } - const stateCollapsed = clientID in collapsed ? collapsed[clientID] : true; - statesCollapsed = statesCollapsed && stateCollapsed; }); return { listHeight, statesHidden, statesLocked, - statesCollapsed, + statesCollapsedAll: collapsedAll, + collapsedStates: collapsed, objectStates, frameNumber, jobInstance, diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index b37e3cb6..ca9567e1 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -67,6 +67,7 @@ const defaultState: AnnotationState = { statuses: [], }, collapsed: {}, + collapsedAll: true, states: [], filters: [], filtersHistory: JSON.parse( @@ -352,6 +353,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { } = action.payload; const updatedCollapsedStates = { ...state.annotations.collapsed }; + const totalStatesCount = state.annotations.states.length; for (const objectState of states) { updatedCollapsedStates[objectState.clientID] = collapsed; } @@ -361,6 +363,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { annotations: { ...state.annotations, collapsed: updatedCollapsedStates, + collapsedAll: states.length === totalStatesCount + ? collapsed : state.annotations.collapsedAll, }, }; } diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 79f5531a..8f9e7af4 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -361,6 +361,7 @@ export interface AnnotationState { activatedStateID: number | null; activatedAttributeID: number | null; collapsed: Record; + collapsedAll: boolean; states: any[]; filters: string[]; filtersHistory: string[]; @@ -452,7 +453,8 @@ export interface ShapesSettingsState { colorBy: ColorBy; opacity: number; selectedOpacity: number; - blackBorders: boolean; + outlined: boolean; + outlineColor: string; showBitmap: boolean; showProjections: boolean; } diff --git a/cvat-ui/src/reducers/settings-reducer.ts b/cvat-ui/src/reducers/settings-reducer.ts index eb4fd755..bb71a700 100644 --- a/cvat-ui/src/reducers/settings-reducer.ts +++ b/cvat-ui/src/reducers/settings-reducer.ts @@ -21,7 +21,8 @@ const defaultState: SettingsState = { colorBy: ColorBy.LABEL, opacity: 3, selectedOpacity: 30, - blackBorders: false, + outlined: false, + outlineColor: '#000000', showBitmap: false, showProjections: false, }, @@ -124,12 +125,13 @@ export default (state = defaultState, action: AnyAction): SettingsState => { }, }; } - case SettingsActionTypes.CHANGE_SHAPES_BLACK_BORDERS: { + case SettingsActionTypes.CHANGE_SHAPES_OUTLINED_BORDERS: { return { ...state, shapes: { ...state.shapes, - blackBorders: action.payload.blackBorders, + outlined: action.payload.outlined, + outlineColor: action.payload.color, }, }; } From cae85760b8acf4cda49011b5e54c602b50064c91 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 1 Sep 2020 14:02:23 +0300 Subject: [PATCH 030/467] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe430429..b3de0fad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.2.0] - Unreleased ### Added - Added password reset functionality () +- Annotation in process outline color wheel () ### Changed - UI models (like DEXTR) were redesigned to be more interactive () @@ -33,7 +34,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ability to configure email verification for new users () - Link to django admin page from UI () - Notification message when users use wrong browser () -- Annotation in process outline color wheel () ### Changed - Shape coordinates are rounded to 2 digits in dumped annotations () From 51ff63069d1e7149234943eed1513afd5d54fbe6 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Wed, 2 Sep 2020 21:51:07 +0300 Subject: [PATCH 031/467] Ubuntu 20.04 as a base image for CVAT Dockerfile (#2101) * used ubuntu 20.04 as base image * updated CI image * Fixed indent * updated contributing guide and changelog --- CHANGELOG.md | 1 + CONTRIBUTING.md | 15 ++------------- Dockerfile | 25 +++++++++---------------- Dockerfile.ci | 15 +++++++++------ cvat/requirements/base.txt | 10 ++-------- datumaro/requirements.txt | 2 +- 6 files changed, 24 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dbb0a0d..968e5f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - UI models (like DEXTR) were redesigned to be more interactive () +- Used Ubuntu:20.04 as a base image for CVAT Dockerfile () ### Deprecated - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08c3cfd0..afa51684 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,25 +14,14 @@ Next steps should work on clear Ubuntu 18.04. - Install necessary dependencies: ```sh - sudo apt-get update && sudo apt-get --no-install-recommends install -y ffmpeg build-essential curl redis-server python3-dev python3-pip python3-venv python3-tk libldap2-dev libsasl2-dev + sudo apt-get update && sudo apt-get --no-install-recommends install -y build-essential curl redis-server python3-dev python3-pip python3-venv python3-tk libldap2-dev libsasl2-dev pkg-config libavformat-dev libavcodec-dev libavdevice-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev ``` - Also please make sure that you have installed ffmpeg with all necessary libav* libraries and pkg-config package. + Please make sure you have installed FFmpeg libraries (libav*) version 4.0 or higher. ```sh # Node and npm (you can use default versions of these packages from apt (8.*, 3.*), but we would recommend to use newer versions) curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - sudo apt-get install -y nodejs - # General dependencies - sudo apt-get install -y pkg-config - - # Library components - sudo apt-get install -y \ - libavformat-dev libavcodec-dev libavdevice-dev \ - libavutil-dev libswscale-dev libswresample-dev libavfilter-dev - ``` - See [PyAV Dependencies installation guide](http://docs.mikeboers.com/pyav/develop/overview/installation.html#dependencies) - for details. - - Install [Visual Studio Code](https://code.visualstudio.com/docs/setup/linux#_debian-and-ubuntu-based-distributions) for development diff --git a/Dockerfile b/Dockerfile index 07060f19..18fb7f75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:16.04 +FROM ubuntu:20.04 ARG http_proxy ARG https_proxy @@ -23,8 +23,6 @@ ENV DJANGO_CONFIGURATION=${DJANGO_CONFIGURATION} RUN apt-get update && \ apt-get --no-install-recommends install -yq \ software-properties-common && \ - add-apt-repository ppa:mc3man/xerus-media -y && \ - add-apt-repository ppa:mc3man/gstffmpeg-keep -y && \ apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \ apache2 \ @@ -33,15 +31,13 @@ RUN apt-get update && \ build-essential \ libapache2-mod-xsendfile \ supervisor \ - ffmpeg \ - gstreamer0.10-ffmpeg \ - libavcodec-dev \ - libavdevice-dev \ - libavfilter-dev \ - libavformat-dev \ - libavutil-dev \ - libswresample-dev \ - libswscale-dev \ + libavcodec-dev=7:4.2.4-1ubuntu0.1 \ + libavdevice-dev=7:4.2.4-1ubuntu0.1 \ + libavfilter-dev=7:4.2.4-1ubuntu0.1 \ + libavformat-dev=7:4.2.4-1ubuntu0.1 \ + libavutil-dev=7:4.2.4-1ubuntu0.1 \ + libswresample-dev=7:4.2.4-1ubuntu0.1 \ + libswscale-dev=7:4.2.4-1ubuntu0.1 \ libldap2-dev \ libsasl2-dev \ pkg-config \ @@ -50,16 +46,13 @@ RUN apt-get update && \ tzdata \ p7zip-full \ git \ + git-lfs \ ssh \ poppler-utils \ curl && \ - curl https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \ - apt-get --no-install-recommends install -y git-lfs && git lfs install && \ python3 -m pip install --no-cache-dir -U pip==20.0.1 setuptools==49.6.0 wheel==0.35.1 && \ ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \ dpkg-reconfigure -f noninteractive tzdata && \ - add-apt-repository --remove ppa:mc3man/gstffmpeg-keep -y && \ - add-apt-repository --remove ppa:mc3man/xerus-media -y && \ rm -rf /var/lib/apt/lists/* && \ echo 'application/wasm wasm' >> /etc/mime.types diff --git a/Dockerfile.ci b/Dockerfile.ci index 422259de..f65cf36c 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -3,17 +3,20 @@ FROM cvat/server ENV DJANGO_CONFIGURATION=testing USER root -RUN curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \ - echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list && \ - curl https://deb.nodesource.com/setup_12.x | bash - && \ - apt-get update && \ +RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \ + gpg-agent \ apt-utils \ build-essential \ + python3-dev \ + ruby \ + && \ + curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \ + echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list && \ + curl https://deb.nodesource.com/setup_12.x | bash - && \ + DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \ google-chrome-stable \ nodejs \ - python3-dev \ - ruby \ && \ rm -rf /var/lib/apt/lists/*; diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index f035fbee..3d4cd001 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -18,7 +18,6 @@ rjsmin==1.1.0 requests==2.24.0 rq==1.5.1 rq-scheduler==0.10.0 -scipy==1.4.1 sqlparse==0.3.1 django-sendfile==0.3.11 dj-pagination==2.5.0 @@ -34,19 +33,14 @@ Pygments==2.6.1 drf-yasg==1.17.1 Shapely==1.7.1 pdf2image==1.14.0 -pascal_voc_writer==0.1.4 django-rest-auth[with_social]==0.9.5 cython==0.29.21 -matplotlib==3.0.3 -scikit-image==0.15.0 -tensorflow==2.2.0 -keras==2.4.2 opencv-python==4.4.0.42 h5py==2.10.0 -imgaug==0.4.0 django-cors-headers==3.5.0 furl==2.1.0 -av==6.2.0 +av==8.0.2 --no-binary=av +tensorflow==2.2.0 # Optional requirement of Datumaro # The package is used by pyunpack as a command line tool to support multiple # archives. Don't use as a python module because it has GPL license. patool==1.12 diff --git a/datumaro/requirements.txt b/datumaro/requirements.txt index ce583783..b5142853 100644 --- a/datumaro/requirements.txt +++ b/datumaro/requirements.txt @@ -3,7 +3,7 @@ Cython>=0.27.3 # include before pycocotools defusedxml>=0.6.0 GitPython>=3.0.8 lxml>=4.4.1 -matplotlib<3.1 # 3.1+ requires python3.6, but we have 3.5 in cvat +matplotlib>=3.3.1 opencv-python-headless>=4.1.0.25 Pillow>=6.1.0 pycocotools>=2.0.0 From e52ff96adf89f2206f1d4bd7a70921000d0959ed Mon Sep 17 00:00:00 2001 From: Savan Visalpara Date: Wed, 2 Sep 2020 14:05:25 -0500 Subject: [PATCH 032/467] fix: discard polygons with length of 4 or less (#2100) * discard polygons with length of 4 or less * updated minimum length of polygons * added line in CHANGELOG Co-authored-by: Nikita Manovich --- CHANGELOG.md | 3 +-- .../tensorflow/matterport/mask_rcnn/nuclio/model_loader.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 968e5f04..a033a92e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - ### Fixed -- +- Fixed multiple errors which arises when polygon is of length 5 or less () ### Security - @@ -48,7 +48,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed error with creating task with labels with the same name () - Django RQ dashboard view () - Object's details menu settings () -- ## [1.1.0-beta] - 2020-08-03 ### Added diff --git a/serverless/tensorflow/matterport/mask_rcnn/nuclio/model_loader.py b/serverless/tensorflow/matterport/mask_rcnn/nuclio/model_loader.py index b210338a..1e56b6e6 100644 --- a/serverless/tensorflow/matterport/mask_rcnn/nuclio/model_loader.py +++ b/serverless/tensorflow/matterport/mask_rcnn/nuclio/model_loader.py @@ -63,7 +63,7 @@ class ModelLoader: contour = np.flip(contour, axis=1) # Approximate the contour and reduce the number of points contour = approximate_polygon(contour, tolerance=2.5) - if len(contour) < 3: + if len(contour) < 6: continue label = self.labels[class_id] From ae6ec401354ac525a49cd399c8b42172988fa7ec Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Wed, 2 Sep 2020 22:07:08 +0300 Subject: [PATCH 033/467] Image copying in CVAT format (#2091) --- cvat/apps/dataset_manager/formats/cvat.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cvat/apps/dataset_manager/formats/cvat.py b/cvat/apps/dataset_manager/formats/cvat.py index ae1257c2..962138c7 100644 --- a/cvat/apps/dataset_manager/formats/cvat.py +++ b/cvat/apps/dataset_manager/formats/cvat.py @@ -530,15 +530,16 @@ def _export(dst_file, task_data, anno_callback, save_images=False): frame_provider = FrameProvider(task_data.db_task.data) frames = frame_provider.get_frames( frame_provider.Quality.ORIGINAL, - frame_provider.Type.NUMPY_ARRAY) + frame_provider.Type.BUFFER) for frame_id, (frame_data, _) in enumerate(frames): frame_name = task_data.frame_info[frame_id]['path'] - if '.' in frame_name: - save_image(osp.join(img_dir, frame_name), - frame_data, jpeg_quality=100, create_dir=True) - else: - save_image(osp.join(img_dir, frame_name + '.png'), - frame_data, create_dir=True) + ext = '' + if not '.' in osp.basename(frame_name): + ext = '.png' + img_path = osp.join(img_dir, frame_name + ext) + os.makedirs(osp.dirname(img_path), exist_ok=True) + with open(img_path, 'wb') as f: + f.write(frame_data.getvalue()) make_zip_archive(temp_dir, dst_file) From 98c06a342ab674dab877bc4482fe6c6fa1a06d9e Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Wed, 2 Sep 2020 22:22:02 +0300 Subject: [PATCH 034/467] [Datumaro] Diff with exact annotation matching (#1989) * Add exact diff command * Update changelog * fix * fix merge * Add image matching, add test * Add point matching test * linter * Update CHANGELOG.md Co-authored-by: Nikita Manovich --- CHANGELOG.md | 1 + .../datumaro/cli/contexts/project/__init__.py | 103 +++++- .../datumaro/cli/contexts/project/diff.py | 2 +- datumaro/datumaro/components/comparator.py | 113 ------ datumaro/datumaro/components/extractor.py | 6 +- datumaro/datumaro/components/operations.py | 346 +++++++++++++++++- datumaro/datumaro/util/__init__.py | 3 + datumaro/datumaro/util/test_utils.py | 3 +- datumaro/tests/test_diff.py | 235 ++++++++---- 9 files changed, 611 insertions(+), 201 deletions(-) delete mode 100644 datumaro/datumaro/components/comparator.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a033a92e..1b1b7ed2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added password reset functionality () - Ability to work with data on the fly (https://github.com/opencv/cvat/pull/2007) - Annotation in process outline color wheel () +- [Datumaro] CLI command for dataset equality comparison () ### Changed - UI models (like DEXTR) were redesigned to be more interactive () diff --git a/datumaro/datumaro/cli/contexts/project/__init__.py b/datumaro/datumaro/cli/contexts/project/__init__.py index 63c84076..8915086b 100644 --- a/datumaro/datumaro/cli/contexts/project/__init__.py +++ b/datumaro/datumaro/cli/contexts/project/__init__.py @@ -4,25 +4,26 @@ # SPDX-License-Identifier: MIT import argparse -from enum import Enum import json import logging as log import os import os.path as osp import shutil +from enum import Enum -from datumaro.components.project import Project, Environment, \ - PROJECT_DEFAULT_CONFIG as DEFAULT_CONFIG -from datumaro.components.comparator import Comparator +from datumaro.components.cli_plugin import CliPlugin from datumaro.components.dataset_filter import DatasetItemEncoder from datumaro.components.extractor import AnnotationType -from datumaro.components.cli_plugin import CliPlugin -from datumaro.components.operations import \ - compute_image_statistics, compute_ann_statistics +from datumaro.components.operations import (DistanceComparator, + ExactComparator, compute_ann_statistics, compute_image_statistics, mean_std) +from datumaro.components.project import \ + PROJECT_DEFAULT_CONFIG as DEFAULT_CONFIG +from datumaro.components.project import Environment, Project + +from ...util import (CliException, MultilineFormatter, add_subparser, + make_file_name) +from ...util.project import generate_next_file_name, load_project from .diff import DiffVisualizer -from ...util import add_subparser, CliException, MultilineFormatter, \ - make_file_name -from ...util.project import load_project, generate_next_file_name def build_create_parser(parser_ctor=argparse.ArgumentParser): @@ -503,12 +504,12 @@ def merge_command(args): def build_diff_parser(parser_ctor=argparse.ArgumentParser): parser = parser_ctor(help="Compare projects", description=""" - Compares two projects.|n + Compares two projects, match annotations by distance.|n |n Examples:|n - - Compare two projects, consider bboxes matching if their IoU > 0.7,|n + - Compare two projects, match boxes if IoU > 0.7,|n |s|s|s|sprint results to Tensorboard: - |s|sdiff path/to/other/project -o diff/ -f tensorboard --iou-thresh 0.7 + |s|sdiff path/to/other/project -o diff/ -v tensorboard --iou-thresh 0.7 """, formatter_class=MultilineFormatter) @@ -516,7 +517,7 @@ def build_diff_parser(parser_ctor=argparse.ArgumentParser): help="Directory of the second project to be compared") parser.add_argument('-o', '--output-dir', dest='dst_dir', default=None, help="Directory to save comparison results (default: do not save)") - parser.add_argument('-f', '--format', + parser.add_argument('-v', '--visualizer', default=DiffVisualizer.DEFAULT_FORMAT, choices=[f.name for f in DiffVisualizer.Format], help="Output format (default: %(default)s)") @@ -536,9 +537,7 @@ def diff_command(args): first_project = load_project(args.project_dir) second_project = load_project(args.other_project_dir) - comparator = Comparator( - iou_threshold=args.iou_thresh, - conf_threshold=args.conf_thresh) + comparator = DistanceComparator(iou_threshold=args.iou_thresh) dst_dir = args.dst_dir if dst_dir: @@ -556,7 +555,7 @@ def diff_command(args): dst_dir_existed = osp.exists(dst_dir) try: visualizer = DiffVisualizer(save_dir=dst_dir, comparator=comparator, - output_format=args.format) + output_format=args.visualizer) visualizer.save_dataset_diff( first_project.make_dataset(), second_project.make_dataset()) @@ -567,6 +566,73 @@ def diff_command(args): return 0 +def build_ediff_parser(parser_ctor=argparse.ArgumentParser): + parser = parser_ctor(help="Compare projects for equality", + description=""" + Compares two projects for equality.|n + |n + Examples:|n + - Compare two projects, exclude annotation group |n + |s|s|sand the 'is_crowd' attribute from comparison:|n + |s|sediff other/project/ -if group -ia is_crowd + """, + formatter_class=MultilineFormatter) + + parser.add_argument('other_project_dir', + help="Directory of the second project to be compared") + parser.add_argument('-iia', '--ignore-item-attr', action='append', + help="Ignore item attribute (repeatable)") + parser.add_argument('-ia', '--ignore-attr', action='append', + help="Ignore annotation attribute (repeatable)") + parser.add_argument('-if', '--ignore-field', + action='append', default=['id', 'group'], + help="Ignore annotation field (repeatable, default: %(default)s)") + parser.add_argument('--match-images', action='store_true', + help='Match dataset items by images instead of ids') + parser.add_argument('--all', action='store_true', + help="Include matches in the output") + parser.add_argument('-p', '--project', dest='project_dir', default='.', + help="Directory of the first project to be compared (default: current dir)") + parser.set_defaults(command=ediff_command) + + return parser + +def ediff_command(args): + first_project = load_project(args.project_dir) + second_project = load_project(args.other_project_dir) + + comparator = ExactComparator( + match_images=args.match_images, + ignored_fields=args.ignore_field, + ignored_attrs=args.ignore_attr, + ignored_item_attrs=args.ignore_item_attr) + matches, mismatches, a_extra, b_extra, errors = \ + comparator.compare_datasets( + first_project.make_dataset(), second_project.make_dataset()) + output = { + "mismatches": mismatches, + "a_extra_items": sorted(a_extra), + "b_extra_items": sorted(b_extra), + "errors": errors, + } + if args.all: + output["matches"] = matches + + output_file = generate_next_file_name('diff', ext='.json') + with open(output_file, 'w') as f: + json.dump(output, f, indent=4, sort_keys=True) + + print("Found:") + print("The first project has %s unmatched items" % len(a_extra)) + print("The second project has %s unmatched items" % len(b_extra)) + print("%s item conflicts" % len(errors)) + print("%s matching annotations" % len(matches)) + print("%s mismatching annotations" % len(mismatches)) + + log.info("Output has been saved to '%s'" % output_file) + + return 0 + def build_transform_parser(parser_ctor=argparse.ArgumentParser): builtins = sorted(Environment().transforms.items) @@ -753,6 +819,7 @@ def build_parser(parser_ctor=argparse.ArgumentParser): add_subparser(subparsers, 'extract', build_extract_parser) add_subparser(subparsers, 'merge', build_merge_parser) add_subparser(subparsers, 'diff', build_diff_parser) + add_subparser(subparsers, 'ediff', build_ediff_parser) add_subparser(subparsers, 'transform', build_transform_parser) add_subparser(subparsers, 'info', build_info_parser) add_subparser(subparsers, 'stats', build_stats_parser) diff --git a/datumaro/datumaro/cli/contexts/project/diff.py b/datumaro/datumaro/cli/contexts/project/diff.py index 785c6c8e..571908f6 100644 --- a/datumaro/datumaro/cli/contexts/project/diff.py +++ b/datumaro/datumaro/cli/contexts/project/diff.py @@ -217,7 +217,7 @@ class DiffVisualizer: _, mispred, a_unmatched, b_unmatched = diff if 0 < len(a_unmatched) + len(b_unmatched) + len(mispred): - img_a = item_a.image.copy() + img_a = item_a.image.data.copy() img_b = img_a.copy() for a_bbox, b_bbox in mispred: self.draw_bbox(img_a, a_bbox, (0, 255, 0)) diff --git a/datumaro/datumaro/components/comparator.py b/datumaro/datumaro/components/comparator.py deleted file mode 100644 index 842a3963..00000000 --- a/datumaro/datumaro/components/comparator.py +++ /dev/null @@ -1,113 +0,0 @@ - -# Copyright (C) 2019 Intel Corporation -# -# SPDX-License-Identifier: MIT - -from itertools import zip_longest -import numpy as np - -from datumaro.components.extractor import AnnotationType, LabelCategories - - -class Comparator: - def __init__(self, - iou_threshold=0.5, conf_threshold=0.9): - self.iou_threshold = iou_threshold - self.conf_threshold = conf_threshold - - @staticmethod - def iou(box_a, box_b): - return box_a.iou(box_b) - - # pylint: disable=no-self-use - def compare_dataset_labels(self, extractor_a, extractor_b): - a_label_cat = extractor_a.categories().get(AnnotationType.label) - b_label_cat = extractor_b.categories().get(AnnotationType.label) - if not a_label_cat and not b_label_cat: - return None - if not a_label_cat: - a_label_cat = LabelCategories() - if not b_label_cat: - b_label_cat = LabelCategories() - - mismatches = [] - for a_label, b_label in zip_longest(a_label_cat.items, b_label_cat.items): - if a_label != b_label: - mismatches.append((a_label, b_label)) - return mismatches - # pylint: enable=no-self-use - - def compare_item_labels(self, item_a, item_b): - conf_threshold = self.conf_threshold - - a_labels = set([ann.label for ann in item_a.annotations \ - if ann.type is AnnotationType.label and \ - conf_threshold < ann.attributes.get('score', 1)]) - b_labels = set([ann.label for ann in item_b.annotations \ - if ann.type is AnnotationType.label and \ - conf_threshold < ann.attributes.get('score', 1)]) - - a_unmatched = a_labels - b_labels - b_unmatched = b_labels - a_labels - matches = a_labels & b_labels - - return matches, a_unmatched, b_unmatched - - def compare_item_bboxes(self, item_a, item_b): - iou_threshold = self.iou_threshold - conf_threshold = self.conf_threshold - - a_boxes = [ann for ann in item_a.annotations \ - if ann.type is AnnotationType.bbox and \ - conf_threshold < ann.attributes.get('score', 1)] - b_boxes = [ann for ann in item_b.annotations \ - if ann.type is AnnotationType.bbox and \ - conf_threshold < ann.attributes.get('score', 1)] - a_boxes.sort(key=lambda ann: 1 - ann.attributes.get('score', 1)) - b_boxes.sort(key=lambda ann: 1 - ann.attributes.get('score', 1)) - - # a_matches: indices of b_boxes matched to a bboxes - # b_matches: indices of a_boxes matched to b bboxes - a_matches = -np.ones(len(a_boxes), dtype=int) - b_matches = -np.ones(len(b_boxes), dtype=int) - - iou_matrix = np.array([ - [self.iou(a, b) for b in b_boxes] for a in a_boxes - ]) - - # matches: boxes we succeeded to match completely - # mispred: boxes we succeeded to match, having label mismatch - matches = [] - mispred = [] - - for a_idx, a_bbox in enumerate(a_boxes): - if len(b_boxes) == 0: - break - matched_b = a_matches[a_idx] - iou_max = max(iou_matrix[a_idx, matched_b], iou_threshold) - for b_idx, b_bbox in enumerate(b_boxes): - if 0 <= b_matches[b_idx]: # assign a_bbox with max conf - continue - iou = iou_matrix[a_idx, b_idx] - if iou < iou_max: - continue - iou_max = iou - matched_b = b_idx - - if matched_b < 0: - continue - a_matches[a_idx] = matched_b - b_matches[matched_b] = a_idx - - b_bbox = b_boxes[matched_b] - - if a_bbox.label == b_bbox.label: - matches.append( (a_bbox, b_bbox) ) - else: - mispred.append( (a_bbox, b_bbox) ) - - # *_umatched: boxes of (*) we failed to match - a_unmatched = [a_boxes[i] for i, m in enumerate(a_matches) if m < 0] - b_unmatched = [b_boxes[i] for i, m in enumerate(b_matches) if m < 0] - - return matches, mispred, a_unmatched, b_unmatched diff --git a/datumaro/datumaro/components/extractor.py b/datumaro/datumaro/components/extractor.py index d7991cd1..0473a250 100644 --- a/datumaro/datumaro/components/extractor.py +++ b/datumaro/datumaro/components/extractor.py @@ -46,7 +46,7 @@ class Annotation: @attrs class Categories: attributes = attrib(factory=set, validator=default_if_none(set), - kw_only=True) + kw_only=True, eq=False) @attrs class LabelCategories(Categories): @@ -137,6 +137,8 @@ class MaskCategories(Categories): def __eq__(self, other): if not super().__eq__(other): return False + if not isinstance(other, __class__): + return False for label_id, my_color in self.colormap.items(): other_color = other.colormap.get(label_id) if not np.array_equal(my_color, other_color): @@ -179,6 +181,8 @@ class Mask(Annotation): def __eq__(self, other): if not super().__eq__(other): return False + if not isinstance(other, __class__): + return False return \ (self.label == other.label) and \ (self.z_order == other.z_order) and \ diff --git a/datumaro/datumaro/components/operations.py b/datumaro/datumaro/components/operations.py index 9e63d3a7..2e3a6813 100644 --- a/datumaro/datumaro/components/operations.py +++ b/datumaro/datumaro/components/operations.py @@ -5,18 +5,20 @@ from collections import OrderedDict from copy import deepcopy +import hashlib import logging as log import attr import cv2 import numpy as np from attr import attrib, attrs +from unittest import TestCase from datumaro.components.cli_plugin import CliPlugin from datumaro.components.extractor import AnnotationType, Bbox, Label from datumaro.components.project import Dataset -from datumaro.util import find -from datumaro.util.attrs_util import ensure_cls +from datumaro.util import find, filter_dict +from datumaro.util.attrs_util import ensure_cls, default_if_none from datumaro.util.annotation_util import (segment_iou, bbox_iou, mean_bbox, OKS, find_instances, max_bbox, smooth_line) @@ -585,7 +587,7 @@ class MaskMatcher(_ShapeMatcher): @attrs(kw_only=True) class PointsMatcher(_ShapeMatcher): - sigma = attrib(converter=list, default=None) + sigma = attrib(type=list, default=None) instance_map = attrib(converter=dict) def distance(self, a, b): @@ -1003,3 +1005,341 @@ def compute_ann_statistics(dataset): } for c, (bin_min, bin_max) in zip(hist, zip(bins[:-1], bins[1:]))] return stats + +@attrs +class DistanceComparator: + iou_threshold = attrib(converter=float, default=0.5) + + @staticmethod + def match_datasets(a, b): + a_items = set((item.id, item.subset) for item in a) + b_items = set((item.id, item.subset) for item in b) + + matches = a_items & b_items + a_unmatched = a_items - b_items + b_unmatched = b_items - a_items + return matches, a_unmatched, b_unmatched + + @staticmethod + def match_classes(a, b): + a_label_cat = a.categories().get(AnnotationType.label, LabelCategories()) + b_label_cat = b.categories().get(AnnotationType.label, LabelCategories()) + + a_labels = set(c.name for c in a_label_cat) + b_labels = set(c.name for c in b_label_cat) + + matches = a_labels & b_labels + a_unmatched = a_labels - b_labels + b_unmatched = b_labels - a_labels + return matches, a_unmatched, b_unmatched + + def match_annotations(self, item_a, item_b): + return { t: self._match_ann_type(t, item_a, item_b) } + + def _match_ann_type(self, t, *args): + # pylint: disable=no-value-for-parameter + if t == AnnotationType.label: + return self.match_labels(*args) + elif t == AnnotationType.bbox: + return self.match_boxes(*args) + elif t == AnnotationType.polygon: + return self.match_polygons(*args) + elif t == AnnotationType.mask: + return self.match_masks(*args) + elif t == AnnotationType.points: + return self.match_points(*args) + elif t == AnnotationType.polyline: + return self.match_lines(*args) + # pylint: enable=no-value-for-parameter + else: + raise NotImplementedError("Unexpected annotation type %s" % t) + + @staticmethod + def _get_ann_type(t, item): + return get_ann_type(item.annotations, t) + + def match_labels(self, item_a, item_b): + a_labels = set(a.label for a in + self._get_ann_type(AnnotationType.label, item_a)) + b_labels = set(a.label for a in + self._get_ann_type(AnnotationType.label, item_b)) + + matches = a_labels & b_labels + a_unmatched = a_labels - b_labels + b_unmatched = b_labels - a_labels + return matches, a_unmatched, b_unmatched + + def _match_segments(self, t, item_a, item_b): + a_boxes = self._get_ann_type(t, item_a) + b_boxes = self._get_ann_type(t, item_b) + return match_segments(a_boxes, b_boxes, dist_thresh=self.iou_threshold) + + def match_polygons(self, item_a, item_b): + return self._match_segments(AnnotationType.polygon, item_a, item_b) + + def match_masks(self, item_a, item_b): + return self._match_segments(AnnotationType.mask, item_a, item_b) + + def match_boxes(self, item_a, item_b): + return self._match_segments(AnnotationType.bbox, item_a, item_b) + + def match_points(self, item_a, item_b): + a_points = self._get_ann_type(AnnotationType.points, item_a) + b_points = self._get_ann_type(AnnotationType.points, item_b) + + instance_map = {} + for s in [item_a.annotations, item_b.annotations]: + s_instances = find_instances(s) + for inst in s_instances: + inst_bbox = max_bbox(inst) + for ann in inst: + instance_map[id(ann)] = [inst, inst_bbox] + matcher = PointsMatcher(instance_map=instance_map) + + return match_segments(a_points, b_points, + dist_thresh=self.iou_threshold, distance=matcher.distance) + + def match_lines(self, item_a, item_b): + a_lines = self._get_ann_type(AnnotationType.polyline, item_a) + b_lines = self._get_ann_type(AnnotationType.polyline, item_b) + + matcher = LineMatcher() + + return match_segments(a_lines, b_lines, + dist_thresh=self.iou_threshold, distance=matcher.distance) + +def match_items_by_id(a, b): + a_items = set((item.id, item.subset) for item in a) + b_items = set((item.id, item.subset) for item in b) + + matches = a_items & b_items + matches = [([m], [m]) for m in matches] + a_unmatched = a_items - b_items + b_unmatched = b_items - a_items + return matches, a_unmatched, b_unmatched + +def match_items_by_image_hash(a, b): + def _hash(item): + if not item.image.has_data: + log.warning("Image (%s, %s) has no image " + "data, counted as unmatched", item.id, item.subset) + return None + return hashlib.md5(item.image.data.tobytes()).hexdigest() + + def _build_hashmap(source): + d = {} + for item in source: + h = _hash(item) + if h is None: + h = str(id(item)) # anything unique + d.setdefault(h, []).append((item.id, item.subset)) + return d + + a_hash = _build_hashmap(a) + b_hash = _build_hashmap(b) + + a_items = set(a_hash) + b_items = set(b_hash) + + matches = a_items & b_items + a_unmatched = a_items - b_items + b_unmatched = b_items - a_items + + matches = [(a_hash[h], b_hash[h]) for h in matches] + a_unmatched = set(i for h in a_unmatched for i in a_hash[h]) + b_unmatched = set(i for h in b_unmatched for i in b_hash[h]) + + return matches, a_unmatched, b_unmatched + +@attrs +class ExactComparator: + match_images = attrib(kw_only=True, type=bool, default=False) + ignored_fields = attrib(kw_only=True, + factory=set, validator=default_if_none(set)) + ignored_attrs = attrib(kw_only=True, + factory=set, validator=default_if_none(set)) + ignored_item_attrs = attrib(kw_only=True, + factory=set, validator=default_if_none(set)) + + _test = attrib(init=False, type=TestCase) + errors = attrib(init=False, type=list) + + def __attrs_post_init__(self): + self._test = TestCase() + self._test.maxDiff = None + + + def _match_items(self, a, b): + if self.match_images: + return match_items_by_image_hash(a, b) + else: + return match_items_by_id(a, b) + + def _compare_categories(self, a, b): + test = self._test + errors = self.errors + + try: + test.assertEqual( + sorted(a, key=lambda t: t.value), + sorted(b, key=lambda t: t.value) + ) + except AssertionError as e: + errors.append({'type': 'categories', 'message': str(e)}) + + if AnnotationType.label in a: + try: + test.assertEqual( + a[AnnotationType.label].items, + b[AnnotationType.label].items, + ) + except AssertionError as e: + errors.append({'type': 'labels', 'message': str(e)}) + if AnnotationType.mask in a: + try: + test.assertEqual( + a[AnnotationType.mask].colormap, + b[AnnotationType.mask].colormap, + ) + except AssertionError as e: + errors.append({'type': 'colormap', 'message': str(e)}) + if AnnotationType.points in a: + try: + test.assertEqual( + a[AnnotationType.points].items, + b[AnnotationType.points].items, + ) + except AssertionError as e: + errors.append({'type': 'points', 'message': str(e)}) + + def _compare_annotations(self, a, b): + ignored_fields = self.ignored_fields + ignored_attrs = self.ignored_attrs + + a_fields = { k: None for k in vars(a) if k in ignored_fields } + b_fields = { k: None for k in vars(b) if k in ignored_fields } + if 'attributes' not in ignored_fields: + a_fields['attributes'] = filter_dict(a.attributes, ignored_attrs) + b_fields['attributes'] = filter_dict(b.attributes, ignored_attrs) + + result = a.wrap(**a_fields) == b.wrap(**b_fields) + + return result + + def _compare_items(self, item_a, item_b): + test = self._test + + a_id = (item_a.id, item_a.subset) + b_id = (item_b.id, item_b.subset) + + matched = [] + unmatched = [] + errors = [] + + try: + test.assertEqual( + filter_dict(item_a.attributes, self.ignored_item_attrs), + filter_dict(item_b.attributes, self.ignored_item_attrs) + ) + except AssertionError as e: + errors.append({'type': 'item_attr', + 'a_item': a_id, 'b_item': b_id, 'message': str(e)}) + + b_annotations = item_b.annotations[:] + for ann_a in item_a.annotations: + ann_b_candidates = [x for x in item_b.annotations + if x.type == ann_a.type] + + ann_b = find(enumerate(self._compare_annotations(ann_a, x) + for x in ann_b_candidates), lambda x: x[1]) + if ann_b is None: + unmatched.append({ + 'item': a_id, 'source': 'a', 'ann': str(ann_a), + }) + continue + else: + ann_b = ann_b_candidates[ann_b[0]] + + b_annotations.remove(ann_b) # avoid repeats + matched.append({'a_item': a_id, 'b_item': b_id, + 'a': str(ann_a), 'b': str(ann_b)}) + + for ann_b in b_annotations: + unmatched.append({'item': b_id, 'source': 'b', 'ann': str(ann_b)}) + + return matched, unmatched, errors + + def compare_datasets(self, a, b): + self.errors = [] + errors = self.errors + + self._compare_categories(a.categories(), b.categories()) + + matched = [] + unmatched = [] + + matches, a_unmatched, b_unmatched = self._match_items(a, b) + + if a.categories().get(AnnotationType.label) != \ + b.categories().get(AnnotationType.label): + return matched, unmatched, a_unmatched, b_unmatched, errors + + _dist = lambda s: len(s[1]) + len(s[2]) + for a_ids, b_ids in matches: + # build distance matrix + match_status = {} # (a_id, b_id): [matched, unmatched, errors] + a_matches = { a_id: None for a_id in a_ids } + b_matches = { b_id: None for b_id in b_ids } + + for a_id in a_ids: + item_a = a.get(*a_id) + candidates = {} + + for b_id in b_ids: + item_b = b.get(*b_id) + + i_m, i_um, i_err = self._compare_items(item_a, item_b) + candidates[b_id] = [i_m, i_um, i_err] + + if len(i_um) == 0: + a_matches[a_id] = b_id + b_matches[b_id] = a_id + matched.extend(i_m) + errors.extend(i_err) + break + + match_status[a_id] = candidates + + # assign + for a_id in a_ids: + if len(b_ids) == 0: + break + + # find the closest, ignore already assigned + matched_b = a_matches[a_id] + if matched_b is not None: + continue + min_dist = -1 + for b_id in b_ids: + if b_matches[b_id] is not None: + continue + d = _dist(match_status[a_id][b_id]) + if d < min_dist and 0 <= min_dist: + continue + min_dist = d + matched_b = b_id + + if matched_b is None: + continue + a_matches[a_id] = matched_b + b_matches[matched_b] = a_id + + m = match_status[a_id][matched_b] + matched.extend(m[0]) + unmatched.extend(m[1]) + errors.extend(m[2]) + + a_unmatched |= set(a_id for a_id, m in a_matches.items() if not m) + b_unmatched |= set(b_id for b_id, m in b_matches.items() if not m) + + return matched, unmatched, a_unmatched, b_unmatched, errors \ No newline at end of file diff --git a/datumaro/datumaro/util/__init__.py b/datumaro/datumaro/util/__init__.py index 293bb5f6..010057d5 100644 --- a/datumaro/datumaro/util/__init__.py +++ b/datumaro/datumaro/util/__init__.py @@ -88,3 +88,6 @@ def str_to_bool(s): return False else: raise ValueError("Can't convert value '%s' to bool" % s) + +def filter_dict(d, exclude_keys): + return { k: v for k, v in d.items() if k not in exclude_keys } \ No newline at end of file diff --git a/datumaro/datumaro/util/test_utils.py b/datumaro/datumaro/util/test_utils.py index f93a74ce..62973ca5 100644 --- a/datumaro/datumaro/util/test_utils.py +++ b/datumaro/datumaro/util/test_utils.py @@ -100,8 +100,7 @@ def compare_datasets(test, expected, actual, ignored_attrs=None): ann_b = find(ann_b_matches, lambda x: _compare_annotations(x, ann_a, ignored_attrs=ignored_attrs)) if ann_b is None: - test.assertEqual(ann_a, ann_b, - 'ann %s, candidates %s' % (ann_a, ann_b_matches)) + test.fail('ann %s, candidates %s' % (ann_a, ann_b_matches)) item_b.annotations.remove(ann_b) # avoid repeats def compare_datasets_strict(test, expected, actual): diff --git a/datumaro/tests/test_diff.py b/datumaro/tests/test_diff.py index 9ad9c1de..33dd79da 100644 --- a/datumaro/tests/test_diff.py +++ b/datumaro/tests/test_diff.py @@ -1,123 +1,96 @@ -from unittest import TestCase +import numpy as np + +from datumaro.components.extractor import (DatasetItem, Label, Bbox, + Caption, Mask, Points) +from datumaro.components.project import Dataset +from datumaro.components.operations import DistanceComparator, ExactComparator -from datumaro.components.extractor import DatasetItem, Label, Bbox -from datumaro.components.comparator import Comparator +from unittest import TestCase -class DiffTest(TestCase): +class DistanceComparatorTest(TestCase): def test_no_bbox_diff_with_same_item(self): detections = 3 anns = [ - Bbox(i * 10, 10, 10, 10, label=i, - attributes={'score': (1.0 + i) / detections}) \ - for i in range(detections) + Bbox(i * 10, 10, 10, 10, label=i) + for i in range(detections) ] item = DatasetItem(id=0, annotations=anns) iou_thresh = 0.5 - conf_thresh = 0.5 - comp = Comparator( - iou_threshold=iou_thresh, conf_threshold=conf_thresh) + comp = DistanceComparator(iou_threshold=iou_thresh) - result = comp.compare_item_bboxes(item, item) + result = comp.match_boxes(item, item) matches, mispred, a_greater, b_greater = result self.assertEqual(0, len(mispred)) self.assertEqual(0, len(a_greater)) self.assertEqual(0, len(b_greater)) - self.assertEqual(len([it for it in item.annotations \ - if conf_thresh < it.attributes['score']]), - len(matches)) + self.assertEqual(len(item.annotations), len(matches)) for a_bbox, b_bbox in matches: self.assertLess(iou_thresh, a_bbox.iou(b_bbox)) self.assertEqual(a_bbox.label, b_bbox.label) - self.assertLess(conf_thresh, a_bbox.attributes['score']) - self.assertLess(conf_thresh, b_bbox.attributes['score']) def test_can_find_bbox_with_wrong_label(self): detections = 3 class_count = 2 item1 = DatasetItem(id=1, annotations=[ - Bbox(i * 10, 10, 10, 10, label=i, - attributes={'score': (1.0 + i) / detections}) \ - for i in range(detections) + Bbox(i * 10, 10, 10, 10, label=i) + for i in range(detections) ]) item2 = DatasetItem(id=2, annotations=[ - Bbox(i * 10, 10, 10, 10, label=(i + 1) % class_count, - attributes={'score': (1.0 + i) / detections}) \ - for i in range(detections) + Bbox(i * 10, 10, 10, 10, label=(i + 1) % class_count) + for i in range(detections) ]) iou_thresh = 0.5 - conf_thresh = 0.5 - comp = Comparator( - iou_threshold=iou_thresh, conf_threshold=conf_thresh) + comp = DistanceComparator(iou_threshold=iou_thresh) - result = comp.compare_item_bboxes(item1, item2) + result = comp.match_boxes(item1, item2) matches, mispred, a_greater, b_greater = result - self.assertEqual(len([it for it in item1.annotations \ - if conf_thresh < it.attributes['score']]), - len(mispred)) + self.assertEqual(len(item1.annotations), len(mispred)) self.assertEqual(0, len(a_greater)) self.assertEqual(0, len(b_greater)) self.assertEqual(0, len(matches)) for a_bbox, b_bbox in mispred: self.assertLess(iou_thresh, a_bbox.iou(b_bbox)) self.assertEqual((a_bbox.label + 1) % class_count, b_bbox.label) - self.assertLess(conf_thresh, a_bbox.attributes['score']) - self.assertLess(conf_thresh, b_bbox.attributes['score']) def test_can_find_missing_boxes(self): detections = 3 class_count = 2 item1 = DatasetItem(id=1, annotations=[ - Bbox(i * 10, 10, 10, 10, label=i, - attributes={'score': (1.0 + i) / detections}) \ - for i in range(detections) if i % 2 == 0 + Bbox(i * 10, 10, 10, 10, label=i) + for i in range(detections) if i % 2 == 0 ]) item2 = DatasetItem(id=2, annotations=[ - Bbox(i * 10, 10, 10, 10, label=(i + 1) % class_count, - attributes={'score': (1.0 + i) / detections}) \ - for i in range(detections) if i % 2 == 1 + Bbox(i * 10, 10, 10, 10, label=(i + 1) % class_count) + for i in range(detections) if i % 2 == 1 ]) iou_thresh = 0.5 - conf_thresh = 0.5 - comp = Comparator( - iou_threshold=iou_thresh, conf_threshold=conf_thresh) + comp = DistanceComparator(iou_threshold=iou_thresh) - result = comp.compare_item_bboxes(item1, item2) + result = comp.match_boxes(item1, item2) matches, mispred, a_greater, b_greater = result self.assertEqual(0, len(mispred)) - self.assertEqual(len([it for it in item1.annotations \ - if conf_thresh < it.attributes['score']]), - len(a_greater)) - self.assertEqual(len([it for it in item2.annotations \ - if conf_thresh < it.attributes['score']]), - len(b_greater)) + self.assertEqual(len(item1.annotations), len(a_greater)) + self.assertEqual(len(item2.annotations), len(b_greater)) self.assertEqual(0, len(matches)) def test_no_label_diff_with_same_item(self): detections = 3 - anns = [ - Label(i, attributes={'score': (1.0 + i) / detections}) \ - for i in range(detections) - ] + anns = [ Label(i) for i in range(detections) ] item = DatasetItem(id=1, annotations=anns) - conf_thresh = 0.5 - comp = Comparator(conf_threshold=conf_thresh) - - result = comp.compare_item_labels(item, item) + result = DistanceComparator().match_labels(item, item) matches, a_greater, b_greater = result self.assertEqual(0, len(a_greater)) self.assertEqual(0, len(b_greater)) - self.assertEqual(len([it for it in item.annotations \ - if conf_thresh < it.attributes['score']]), - len(matches)) + self.assertEqual(len(item.annotations), len(matches)) def test_can_find_wrong_label(self): item1 = DatasetItem(id=1, annotations=[ @@ -131,12 +104,148 @@ class DiffTest(TestCase): Label(4), ]) - conf_thresh = 0.5 - comp = Comparator(conf_threshold=conf_thresh) - - result = comp.compare_item_labels(item1, item2) + result = DistanceComparator().match_labels(item1, item2) matches, a_greater, b_greater = result self.assertEqual(2, len(a_greater)) self.assertEqual(2, len(b_greater)) - self.assertEqual(1, len(matches)) \ No newline at end of file + self.assertEqual(1, len(matches)) + + def test_can_match_points(self): + item1 = DatasetItem(id=1, annotations=[ + Points([1, 2, 2, 0, 1, 1], label=0), + + Points([3, 5, 5, 7, 5, 3], label=0), + ]) + item2 = DatasetItem(id=2, annotations=[ + Points([1.5, 2, 2, 0.5, 1, 1.5], label=0), + + Points([5, 7, 7, 7, 7, 5], label=0), + ]) + + result = DistanceComparator().match_points(item1, item2) + + matches, mismatches, a_greater, b_greater = result + self.assertEqual(1, len(a_greater)) + self.assertEqual(1, len(b_greater)) + self.assertEqual(1, len(matches)) + self.assertEqual(0, len(mismatches)) + +class ExactComparatorTest(TestCase): + def test_class_comparison(self): + a = Dataset.from_iterable([], categories=['a', 'b', 'c']) + b = Dataset.from_iterable([], categories=['b', 'c']) + + comp = ExactComparator() + _, _, _, _, errors = comp.compare_datasets(a, b) + + self.assertEqual(1, len(errors), errors) + + def test_item_comparison(self): + a = Dataset.from_iterable([ + DatasetItem(id=1, subset='train'), + DatasetItem(id=2, subset='test', attributes={'x': 1}), + ], categories=['a', 'b', 'c']) + + b = Dataset.from_iterable([ + DatasetItem(id=2, subset='test'), + DatasetItem(id=3), + ], categories=['a', 'b', 'c']) + + comp = ExactComparator() + _, _, a_extra_items, b_extra_items, errors = comp.compare_datasets(a, b) + + self.assertEqual({('1', 'train')}, a_extra_items) + self.assertEqual({('3', '')}, b_extra_items) + self.assertEqual(1, len(errors), errors) + + def test_annotation_comparison(self): + a = Dataset.from_iterable([ + DatasetItem(id=1, annotations=[ + Caption('hello'), # unmatched + Caption('world', group=5), + Label(2, attributes={ 'x': 1, 'y': '2', }), + Bbox(1, 2, 3, 4, label=4, z_order=1, attributes={ + 'score': 1.0, + }), + Bbox(5, 6, 7, 8, group=5), + Points([1, 2, 2, 0, 1, 1], label=0, z_order=4), + Mask(label=3, z_order=2, image=np.ones((2, 3))), + ]), + ], categories=['a', 'b', 'c', 'd']) + + b = Dataset.from_iterable([ + DatasetItem(id=1, annotations=[ + Caption('world', group=5), + Label(2, attributes={ 'x': 1, 'y': '2', }), + Bbox(1, 2, 3, 4, label=4, z_order=1, attributes={ + 'score': 1.0, + }), + Bbox(5, 6, 7, 8, group=5), + Bbox(5, 6, 7, 8, group=5), # unmatched + Points([1, 2, 2, 0, 1, 1], label=0, z_order=4), + Mask(label=3, z_order=2, image=np.ones((2, 3))), + ]), + ], categories=['a', 'b', 'c', 'd']) + + comp = ExactComparator() + matched, unmatched, _, _, errors = comp.compare_datasets(a, b) + + self.assertEqual(6, len(matched), matched) + self.assertEqual(2, len(unmatched), unmatched) + self.assertEqual(0, len(errors), errors) + + def test_image_comparison(self): + a = Dataset.from_iterable([ + DatasetItem(id=11, image=np.ones((5, 4, 3)), annotations=[ + Bbox(5, 6, 7, 8), + ]), + DatasetItem(id=12, image=np.ones((5, 4, 3)), annotations=[ + Bbox(1, 2, 3, 4), + Bbox(5, 6, 7, 8), + ]), + DatasetItem(id=13, image=np.ones((5, 4, 3)), annotations=[ + Bbox(9, 10, 11, 12), # mismatch + ]), + + DatasetItem(id=14, image=np.zeros((5, 4, 3)), annotations=[ + Bbox(1, 2, 3, 4), + Bbox(5, 6, 7, 8), + ], attributes={ 'a': 1 }), + + DatasetItem(id=15, image=np.zeros((5, 5, 3)), annotations=[ + Bbox(1, 2, 3, 4), + Bbox(5, 6, 7, 8), + ]), + ], categories=['a', 'b', 'c', 'd']) + + b = Dataset.from_iterable([ + DatasetItem(id=21, image=np.ones((5, 4, 3)), annotations=[ + Bbox(5, 6, 7, 8), + ]), + DatasetItem(id=22, image=np.ones((5, 4, 3)), annotations=[ + Bbox(1, 2, 3, 4), + Bbox(5, 6, 7, 8), + ]), + DatasetItem(id=23, image=np.ones((5, 4, 3)), annotations=[ + Bbox(10, 10, 11, 12), # mismatch + ]), + + DatasetItem(id=24, image=np.zeros((5, 4, 3)), annotations=[ + Bbox(6, 6, 7, 8), # 1 ann missing, mismatch + ], attributes={ 'a': 2 }), + + DatasetItem(id=25, image=np.zeros((4, 4, 3)), annotations=[ + Bbox(6, 6, 7, 8), + ]), + ], categories=['a', 'b', 'c', 'd']) + + comp = ExactComparator(match_images=True) + matched_ann, unmatched_ann, a_unmatched, b_unmatched, errors = \ + comp.compare_datasets(a, b) + + self.assertEqual(3, len(matched_ann), matched_ann) + self.assertEqual(5, len(unmatched_ann), unmatched_ann) + self.assertEqual(1, len(a_unmatched), a_unmatched) + self.assertEqual(1, len(b_unmatched), b_unmatched) + self.assertEqual(1, len(errors), errors) \ No newline at end of file From a30921b27190a7202debf5bfa8b94c3d3a929506 Mon Sep 17 00:00:00 2001 From: Dmitry Kruchinin <33020454+dvkruchinin@users.noreply.github.com> Date: Thu, 3 Sep 2020 09:38:24 +0300 Subject: [PATCH 035/467] Cypress test for issue 1433. (#2116) Co-authored-by: Dmitry Kruchinin --- .../issue_1433_hide_functionality.js | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/cypress/integration/issue_1433_hide_functionality.js diff --git a/tests/cypress/integration/issue_1433_hide_functionality.js b/tests/cypress/integration/issue_1433_hide_functionality.js new file mode 100644 index 00000000..410c8425 --- /dev/null +++ b/tests/cypress/integration/issue_1433_hide_functionality.js @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 Intel Corporation + * + * SPDX-License-Identifier: MIT + */ + +/// + +context('Check hide functionality (H)', () => { + + const issueId = '1433' + const labelName = `Issue ${issueId}` + const taskName = `New annotation task for ${labelName}` + const attrName = `Attr for ${labelName}` + const textDefaultValue = 'Some default value for type Text' + const image = `image_${issueId}.png` + const width = 800 + const height = 800 + const posX = 10 + const posY = 10 + const color = 'gray' + + before(() => { + cy.visit('auth/login') + cy.login() + cy.imageGenerator('cypress/fixtures', image, width, height, color, posX, posY, labelName) + cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, image) + cy.openTaskJob(taskName) + cy.createShape(309, 431, 616, 671) + }) + + describe(`Testing issue "${issueId}"`, () => { + it('Object is hidden', () => { + cy.get('#cvat_canvas_shape_1') + .trigger('mousemove') + .trigger('mouseover') + .trigger('keydown', {key: 'h'}) + .should('be.hidden') + }) + }) +}) From 0982ea3f57dc8dd1f7226055cf6a47de61ef1f33 Mon Sep 17 00:00:00 2001 From: Dmitry Kruchinin <33020454+dvkruchinin@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:59:59 +0300 Subject: [PATCH 036/467] Cypress test for issue 1568. (#2106) Co-authored-by: Dmitry Kruchinin --- .../issue_1568_cuboid_dump_annotation.js | 60 +++++++++++++++++++ tests/cypress/support/commands.js | 15 +++++ 2 files changed, 75 insertions(+) create mode 100644 tests/cypress/integration/issue_1568_cuboid_dump_annotation.js diff --git a/tests/cypress/integration/issue_1568_cuboid_dump_annotation.js b/tests/cypress/integration/issue_1568_cuboid_dump_annotation.js new file mode 100644 index 00000000..1a21471f --- /dev/null +++ b/tests/cypress/integration/issue_1568_cuboid_dump_annotation.js @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2020 Intel Corporation + * + * SPDX-License-Identifier: MIT + */ + +/// + +context('Dump annotation if cuboid created', () => { + + const issueId = '1568' + const labelName = `Issue ${issueId}` + const taskName = `New annotation task for ${labelName}` + const attrName = `Attr for ${labelName}` + const textDefaultValue = 'Some default value for type Text' + const image = `image_${issueId}.png` + const width = 800 + const height = 800 + const posX = 10 + const posY = 10 + const color = 'gray' + + before(() => { + cy.visit('auth/login') + cy.login() + cy.imageGenerator('cypress/fixtures', image, width, height, color, posX, posY, labelName) + cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, image) + cy.openTaskJob(taskName) + }) + + describe(`Testing issue "${issueId}"`, () => { + it('Create a cuboid', () => { + cy.createCuboid('Shape', 309, 431, 616, 671) + cy.get('#cvat-objects-sidebar-state-item-1') + .should('contain', '1').and('contain', 'CUBOID SHAPE') + }) + it('Dump an annotation', () => { + cy.get('.cvat-annotation-header-left-group').within(() => { + cy.get('[title="Save current changes [Ctrl+S]"]') + cy.get('button').contains('Save') + .click({force: true}) + cy.get('button').contains('Menu') + .trigger('mouseover',{force: true}) + }) + cy.get('.cvat-annotation-menu').within(() => { + cy.get('[title="Dump annotations"]') + .trigger('mouseover') + }) + cy.get('.cvat-menu-dump-submenu-item').within(() => { + cy.contains('Datumaro') + .click() + }) + }) + it('Error notification is ot exists', () => { + cy.wait(5000) + cy.get('.ant-notification-notice') + .should('not.exist') + }) + }) +}) diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index b0274a4a..20767e48 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -180,3 +180,18 @@ Cypress.Commands.add('changeAnnotationMode', (mode) => { cy.get('.cvat-workspace-selector') .should('contain.text', mode) }) + +Cypress.Commands.add('createCuboid', (mode, firstX, firstY, lastX, lastY) => { + cy.get('.cvat-draw-cuboid-control').click() + cy.contains('Draw new cuboid') + .parents('.cvat-draw-shape-popover-content') + .within(() => { + cy.get('button') + .contains(mode) + .click({force: true}) + }) + cy.get('.cvat-canvas-container') + .click(firstX, firstY) + cy.get('.cvat-canvas-container') + .click(lastX, lastY) +}) From 8cbe3956a3d63efa6d022413c9704fb339c5e139 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Fri, 4 Sep 2020 11:03:35 +0300 Subject: [PATCH 037/467] Fix cvat format (#2071) --- cvat/apps/dataset_manager/formats/cvat.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cvat/apps/dataset_manager/formats/cvat.py b/cvat/apps/dataset_manager/formats/cvat.py index 962138c7..ac5823b6 100644 --- a/cvat/apps/dataset_manager/formats/cvat.py +++ b/cvat/apps/dataset_manager/formats/cvat.py @@ -398,8 +398,9 @@ def dump_as_cvat_interpolation(file_object, annotations): z_order=shape.z_order, frame=shape.frame, attributes=shape.attributes, - ), - annotations.TrackedShape( + )] + + ( # add a finishing frame if it does not hop over the last frame + [annotations.TrackedShape( type=shape.type, points=shape.points, occluded=shape.occluded, @@ -408,8 +409,10 @@ def dump_as_cvat_interpolation(file_object, annotations): z_order=shape.z_order, frame=shape.frame + annotations.frame_step, attributes=shape.attributes, + )] if shape.frame + annotations.frame_step < \ + int(annotations.meta['task']['stop_frame']) \ + else [] ), - ], )) counter += 1 From 87d76c9a1a972b3b1ebecd6ba653987f13577de0 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Fri, 4 Sep 2020 11:53:42 +0300 Subject: [PATCH 038/467] Update Django 2->3 (#2121) * used ubuntu 20.04 as base image * updated CI image * Fixed indent * updated contributing guide and changelog * update django * Fixed migration: this migration contains several "duplicates" (for example, it changes the id field and then changes the tarck_id field of the related table). Since version 3.x, Django cannot apply such migrations and throws an exception. Also checked the auto-generated migration: it doesn't contain those double changes. In both cases, the schemas of these tables are the same. --- .../migrations/0009_auto_20180917_1424.py | 65 ------------------- cvat/requirements/base.txt | 2 +- 2 files changed, 1 insertion(+), 66 deletions(-) diff --git a/cvat/apps/engine/migrations/0009_auto_20180917_1424.py b/cvat/apps/engine/migrations/0009_auto_20180917_1424.py index a20ec2cf..6c8d534e 100644 --- a/cvat/apps/engine/migrations/0009_auto_20180917_1424.py +++ b/cvat/apps/engine/migrations/0009_auto_20180917_1424.py @@ -102,69 +102,4 @@ class Migration(migrations.Migration): name='id', field=models.BigAutoField(primary_key=True, serialize=False), ), - migrations.AlterField( - model_name='objectpathattributeval', - name='track_id', - field=models.BigIntegerField(), - ), - migrations.AlterField( - model_name='objectpathattributeval', - name='track_id', - field=models.BigIntegerField(), - ), - migrations.AlterField( - model_name='trackedpoints', - name='track_id', - field=models.BigIntegerField(), - ), - migrations.AlterField( - model_name='trackedpolygon', - name='track_id', - field=models.BigIntegerField(), - ), - migrations.AlterField( - model_name='trackedpolyline', - name='track_id', - field=models.BigIntegerField(), - ), - migrations.AlterField( - model_name='trackedboxattributeval', - name='box_id', - field=models.BigIntegerField(), - ), - migrations.AlterField( - model_name='trackedpointsattributeval', - name='points_id', - field=models.BigIntegerField(), - ), - migrations.AlterField( - model_name='trackedpolygonattributeval', - name='polygon_id', - field=models.BigIntegerField(), - ), - migrations.AlterField( - model_name='trackedpolylineattributeval', - name='polyline_id', - field=models.BigIntegerField(), - ), - migrations.AlterField( - model_name='labeledboxattributeval', - name='box_id', - field=models.BigIntegerField(), - ), - migrations.AlterField( - model_name='labeledpointsattributeval', - name='points_id', - field=models.BigIntegerField(), - ), - migrations.AlterField( - model_name='labeledpolygonattributeval', - name='polygon_id', - field=models.BigIntegerField(), - ), - migrations.AlterField( - model_name='labeledpolylineattributeval', - name='polyline_id', - field=models.BigIntegerField(), - ), ] diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index 3d4cd001..1cd368a6 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -1,5 +1,5 @@ click==7.1.2 -Django==2.2.13 +Django==3.1.1 django-appconf==1.0.4 django-auth-ldap==2.2.0 django-cacheops==5.0.1 From 4dbfa3bfdf67372c237a5993a9e00a7a956614e2 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Fri, 4 Sep 2020 12:39:08 +0300 Subject: [PATCH 039/467] [Datumaro] Update docs (#2125) * Update docs, add type hints, rename extract * Add developer guide * Update license headers, add license text * Update developer_guide.md Co-authored-by: Nikita Manovich --- datumaro/CONTRIBUTING.md | 122 +---------- datumaro/LICENSE | 22 ++ datumaro/datumaro/__init__.py | 2 +- datumaro/datumaro/__main__.py | 2 +- datumaro/datumaro/cli/__init__.py | 2 +- datumaro/datumaro/cli/__main__.py | 2 +- datumaro/datumaro/cli/commands/__init__.py | 2 +- datumaro/datumaro/cli/commands/add.py | 2 +- datumaro/datumaro/cli/commands/convert.py | 2 +- datumaro/datumaro/cli/commands/create.py | 2 +- datumaro/datumaro/cli/commands/explain.py | 2 +- datumaro/datumaro/cli/commands/export.py | 2 +- datumaro/datumaro/cli/commands/remove.py | 2 +- datumaro/datumaro/cli/contexts/__init__.py | 2 +- .../datumaro/cli/contexts/item/__init__.py | 2 +- .../datumaro/cli/contexts/model/__init__.py | 2 +- .../datumaro/cli/contexts/project/__init__.py | 17 +- .../datumaro/cli/contexts/project/diff.py | 2 +- .../datumaro/cli/contexts/source/__init__.py | 2 +- datumaro/datumaro/cli/util/__init__.py | 2 +- datumaro/datumaro/cli/util/project.py | 2 +- datumaro/datumaro/components/__init__.py | 2 +- .../components/algorithms/__init__.py | 2 +- .../datumaro/components/algorithms/rise.py | 2 +- datumaro/datumaro/components/config.py | 2 +- datumaro/datumaro/components/config_model.py | 2 +- datumaro/datumaro/components/converter.py | 2 +- .../datumaro/components/dataset_filter.py | 2 +- datumaro/datumaro/components/extractor.py | 29 ++- datumaro/datumaro/components/launcher.py | 2 +- datumaro/datumaro/components/project.py | 105 ++++----- .../accuracy_checker_plugin/__init__.py | 4 + .../datumaro/plugins/coco_format/extractor.py | 2 +- .../datumaro/plugins/coco_format/format.py | 2 +- .../datumaro/plugins/coco_format/importer.py | 2 +- .../datumaro/plugins/cvat_format/converter.py | 2 +- .../datumaro/plugins/cvat_format/extractor.py | 2 +- .../datumaro/plugins/cvat_format/format.py | 2 +- .../datumaro/plugins/cvat_format/importer.py | 2 +- .../plugins/datumaro_format/converter.py | 2 +- .../plugins/datumaro_format/extractor.py | 2 +- .../plugins/datumaro_format/format.py | 2 +- .../plugins/datumaro_format/importer.py | 2 +- datumaro/datumaro/plugins/image_dir.py | 2 +- datumaro/datumaro/plugins/labelme_format.py | 1 - datumaro/datumaro/plugins/mot_format.py | 3 +- .../datumaro/plugins/openvino_launcher.py | 2 +- .../tf_detection_api_format/converter.py | 2 +- .../tf_detection_api_format/extractor.py | 2 +- .../plugins/tf_detection_api_format/format.py | 2 +- .../tf_detection_api_format/importer.py | 2 +- datumaro/datumaro/plugins/transforms.py | 1 - .../datumaro/plugins/voc_format/extractor.py | 2 +- .../datumaro/plugins/voc_format/format.py | 2 +- .../datumaro/plugins/voc_format/importer.py | 2 +- .../datumaro/plugins/yolo_format/converter.py | 2 +- .../datumaro/plugins/yolo_format/extractor.py | 2 +- .../datumaro/plugins/yolo_format/format.py | 2 +- .../datumaro/plugins/yolo_format/importer.py | 2 +- datumaro/datumaro/util/__init__.py | 2 +- datumaro/datumaro/util/annotation_util.py | 1 - datumaro/datumaro/util/attrs_util.py | 1 - datumaro/datumaro/util/command_targets.py | 2 +- datumaro/datumaro/util/image.py | 2 +- datumaro/datumaro/util/image_cache.py | 4 + datumaro/datumaro/util/mask_tools.py | 2 +- datumaro/datumaro/util/test_utils.py | 2 +- datumaro/datumaro/util/tf_util.py | 2 +- datumaro/docs/developer_guide.md | 200 ++++++++++++++++++ datumaro/docs/user_manual.md | 64 ++++-- datumaro/setup.py | 4 +- .../assets/pytorch_launcher/samplenet.py | 2 +- datumaro/tests/test_project.py | 2 +- 73 files changed, 401 insertions(+), 293 deletions(-) create mode 100644 datumaro/LICENSE create mode 100644 datumaro/docs/developer_guide.md diff --git a/datumaro/CONTRIBUTING.md b/datumaro/CONTRIBUTING.md index 97373b28..f9a1afc1 100644 --- a/datumaro/CONTRIBUTING.md +++ b/datumaro/CONTRIBUTING.md @@ -72,124 +72,4 @@ python manage.py test datumaro/ ## Design and code structure - [Design document](docs/design.md) - -### Command-line - -Use [Docker](https://www.docker.com/) as an example. Basically, -the interface is divided on contexts and single commands. -Contexts are semantically grouped commands, -related to a single topic or target. Single commands are handy shorter -alternatives for the most used commands and also special commands, -which are hard to be put into any specific context. - -![cli-design-image](docs/images/cli_design.png) - -- The diagram above was created with [FreeMind](http://freemind.sourceforge.net/wiki/index.php/Main_Page) - -Model-View-ViewModel (MVVM) UI pattern is used. - -![mvvm-image](docs/images/mvvm.png) - -### Datumaro project and environment structure - - -``` -├── [datumaro module] -└── [project folder] - ├── .datumaro/ - | ├── config.yml - │   ├── .git/ - │   ├── models/ - │   └── plugins/ - │   ├── plugin1/ - │   | ├── file1.py - │   | └── file2.py - │   ├── plugin2.py - │   ├── custom_extractor1.py - │   └── ... - ├── dataset/ - └── sources/ - ├── source1 - └── ... -``` - - -### Plugins - -Plugins are optional components, which extend the project. In Datumaro there are -several types of plugins, which include: -- `extractor` - produces dataset items from data source -- `importer` - recognizes dataset type and creates project -- `converter` - exports dataset to a specific format -- `transformation` - modifies dataset items or other properties -- `launcher` - executes models - -Plugins reside in plugin directories: -- `datumaro/plugins` for builtin components -- `/.datumaro/plugins` for project-specific components - -A plugin is a python file or package with any name, which exports some symbols. -To export a symbol, put it to `exports` list of the module like this: - -``` python -class MyComponent1: ... -class MyComponent2: ... -exports = [MyComponent1, MyComponent2] -``` - -or inherit it from one of special classes: -``` python -from datumaro.components.extractor import Importer, SourceExtractor, Transform -from datumaro.components.launcher import Launcher -from datumaro.components.converter import Converter -``` - -There is an additional class to modify plugin appearance at command line: - -``` python -from datumaro.components.cli_plugin import CliPlugin -``` - -Plugin example: - - - -``` -datumaro/plugins/ -- my_plugin1/file1.py -- my_plugin1/file2.py -- my_plugin2.py -``` - - - -`my_plugin1/file2.py` contents: - -``` python -from datumaro.components.extractor import Transform, CliPlugin -from .file1 import something, useful - -class MyTransform(Transform, CliPlugin): - NAME = "custom_name" - """ - Some description. - """ - @classmethod - def build_cmdline_parser(cls, **kwargs): - parser = super().build_cmdline_parser(**kwargs) - parser.add_argument('-q', help="Some help") - return parser - ... -``` - -`my_plugin2.py` contents: - -``` python -from datumaro.components.extractor import SourceExtractor - -class MyFormat: ... -class MyFormatExtractor(SourceExtractor): ... - -exports = [MyFormat] # explicit exports declaration -# MyFormatExtractor won't be exported -``` +- [Developer guide](docs/developer_guide.md) \ No newline at end of file diff --git a/datumaro/LICENSE b/datumaro/LICENSE new file mode 100644 index 00000000..ae9cf710 --- /dev/null +++ b/datumaro/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (C) 2019-2020 Intel Corporation +  +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: +  +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. +  +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. +  diff --git a/datumaro/datumaro/__init__.py b/datumaro/datumaro/__init__.py index cd825f56..eb864e52 100644 --- a/datumaro/datumaro/__init__.py +++ b/datumaro/datumaro/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/__main__.py b/datumaro/datumaro/__main__.py index 27148356..be1cb092 100644 --- a/datumaro/datumaro/__main__.py +++ b/datumaro/datumaro/__main__.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/__init__.py b/datumaro/datumaro/cli/__init__.py index cd825f56..eb864e52 100644 --- a/datumaro/datumaro/cli/__init__.py +++ b/datumaro/datumaro/cli/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/__main__.py b/datumaro/datumaro/cli/__main__.py index fabe43f8..80a8805f 100644 --- a/datumaro/datumaro/cli/__main__.py +++ b/datumaro/datumaro/cli/__main__.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/commands/__init__.py b/datumaro/datumaro/cli/commands/__init__.py index 7249842e..fe74bc2b 100644 --- a/datumaro/datumaro/cli/commands/__init__.py +++ b/datumaro/datumaro/cli/commands/__init__.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/commands/add.py b/datumaro/datumaro/cli/commands/add.py index b2864039..288d7c04 100644 --- a/datumaro/datumaro/cli/commands/add.py +++ b/datumaro/datumaro/cli/commands/add.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/commands/convert.py b/datumaro/datumaro/cli/commands/convert.py index d867614d..6398bac7 100644 --- a/datumaro/datumaro/cli/commands/convert.py +++ b/datumaro/datumaro/cli/commands/convert.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/commands/create.py b/datumaro/datumaro/cli/commands/create.py index 16f6737c..97e3c9b4 100644 --- a/datumaro/datumaro/cli/commands/create.py +++ b/datumaro/datumaro/cli/commands/create.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/commands/explain.py b/datumaro/datumaro/cli/commands/explain.py index a0a5f1cc..4d5d16b2 100644 --- a/datumaro/datumaro/cli/commands/explain.py +++ b/datumaro/datumaro/cli/commands/explain.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/commands/export.py b/datumaro/datumaro/cli/commands/export.py index afeb73cd..be47245d 100644 --- a/datumaro/datumaro/cli/commands/export.py +++ b/datumaro/datumaro/cli/commands/export.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/commands/remove.py b/datumaro/datumaro/cli/commands/remove.py index 0e0d076f..7b9c0d3a 100644 --- a/datumaro/datumaro/cli/commands/remove.py +++ b/datumaro/datumaro/cli/commands/remove.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/contexts/__init__.py b/datumaro/datumaro/cli/contexts/__init__.py index 95019b7b..433efe9b 100644 --- a/datumaro/datumaro/cli/contexts/__init__.py +++ b/datumaro/datumaro/cli/contexts/__init__.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/contexts/item/__init__.py b/datumaro/datumaro/cli/contexts/item/__init__.py index 1df66809..8f74826d 100644 --- a/datumaro/datumaro/cli/contexts/item/__init__.py +++ b/datumaro/datumaro/cli/contexts/item/__init__.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/contexts/model/__init__.py b/datumaro/datumaro/cli/contexts/model/__init__.py index 0c4f2018..69b7da1e 100644 --- a/datumaro/datumaro/cli/contexts/model/__init__.py +++ b/datumaro/datumaro/cli/contexts/model/__init__.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/contexts/project/__init__.py b/datumaro/datumaro/cli/contexts/project/__init__.py index 8915086b..bab5da6f 100644 --- a/datumaro/datumaro/cli/contexts/project/__init__.py +++ b/datumaro/datumaro/cli/contexts/project/__init__.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT @@ -278,7 +278,7 @@ def build_export_parser(parser_ctor=argparse.ArgumentParser): parser = parser_ctor(help="Export project", description=""" Exports the project dataset in some format. Optionally, a filter - can be passed, check 'extract' command description for more info. + can be passed, check 'filter' command description for more info. Each dataset format has its own options, which are passed after '--' separator (see examples), pass '-- -h' for more info. If not stated otherwise, by default @@ -362,7 +362,7 @@ def export_command(args): return 0 -def build_extract_parser(parser_ctor=argparse.ArgumentParser): +def build_filter_parser(parser_ctor=argparse.ArgumentParser): parser = parser_ctor(help="Extract subproject", description=""" Extracts a subproject that contains only items matching filter. @@ -414,11 +414,11 @@ def build_extract_parser(parser_ctor=argparse.ArgumentParser): help="Overwrite existing files in the save directory") parser.add_argument('-p', '--project', dest='project_dir', default='.', help="Directory of the project to operate on (default: current dir)") - parser.set_defaults(command=extract_command) + parser.set_defaults(command=filter_command) return parser -def extract_command(args): +def filter_command(args): project = load_project(args.project_dir) if not args.dry_run: @@ -437,7 +437,7 @@ def extract_command(args): filter_args = FilterModes.make_filter_args(args.mode) if args.dry_run: - dataset = dataset.extract(filter_expr=args.filter, **filter_args) + dataset = dataset.filter(expr=args.filter, **filter_args) for item in dataset: encoded_item = DatasetItemEncoder.encode(item, dataset.categories()) xml_item = DatasetItemEncoder.to_string(encoded_item) @@ -447,8 +447,7 @@ def extract_command(args): if not args.filter: raise CliException("Expected a filter expression ('-e' argument)") - dataset.extract_project(save_dir=dst_dir, filter_expr=args.filter, - **filter_args) + dataset.filter_project(save_dir=dst_dir, expr=args.filter, **filter_args) log.info("Subproject has been extracted to '%s'" % dst_dir) @@ -816,7 +815,7 @@ def build_parser(parser_ctor=argparse.ArgumentParser): add_subparser(subparsers, 'create', build_create_parser) add_subparser(subparsers, 'import', build_import_parser) add_subparser(subparsers, 'export', build_export_parser) - add_subparser(subparsers, 'extract', build_extract_parser) + add_subparser(subparsers, 'filter', build_filter_parser) add_subparser(subparsers, 'merge', build_merge_parser) add_subparser(subparsers, 'diff', build_diff_parser) add_subparser(subparsers, 'ediff', build_ediff_parser) diff --git a/datumaro/datumaro/cli/contexts/project/diff.py b/datumaro/datumaro/cli/contexts/project/diff.py index 571908f6..358f3860 100644 --- a/datumaro/datumaro/cli/contexts/project/diff.py +++ b/datumaro/datumaro/cli/contexts/project/diff.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/contexts/source/__init__.py b/datumaro/datumaro/cli/contexts/source/__init__.py index ef9edafb..45dbdb1b 100644 --- a/datumaro/datumaro/cli/contexts/source/__init__.py +++ b/datumaro/datumaro/cli/contexts/source/__init__.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/util/__init__.py b/datumaro/datumaro/cli/util/__init__.py index 3884b156..4ee0b72b 100644 --- a/datumaro/datumaro/cli/util/__init__.py +++ b/datumaro/datumaro/cli/util/__init__.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/cli/util/project.py b/datumaro/datumaro/cli/util/project.py index 75013053..56590a4d 100644 --- a/datumaro/datumaro/cli/util/project.py +++ b/datumaro/datumaro/cli/util/project.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/components/__init__.py b/datumaro/datumaro/components/__init__.py index a9773073..5a1ec10f 100644 --- a/datumaro/datumaro/components/__init__.py +++ b/datumaro/datumaro/components/__init__.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/components/algorithms/__init__.py b/datumaro/datumaro/components/algorithms/__init__.py index a9773073..5a1ec10f 100644 --- a/datumaro/datumaro/components/algorithms/__init__.py +++ b/datumaro/datumaro/components/algorithms/__init__.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/components/algorithms/rise.py b/datumaro/datumaro/components/algorithms/rise.py index 2f65c8cf..3fb9a895 100644 --- a/datumaro/datumaro/components/algorithms/rise.py +++ b/datumaro/datumaro/components/algorithms/rise.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/components/config.py b/datumaro/datumaro/components/config.py index ca66eff8..a79cda15 100644 --- a/datumaro/datumaro/components/config.py +++ b/datumaro/datumaro/components/config.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/components/config_model.py b/datumaro/datumaro/components/config_model.py index f46682d2..c6f65179 100644 --- a/datumaro/datumaro/components/config_model.py +++ b/datumaro/datumaro/components/config_model.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/components/converter.py b/datumaro/datumaro/components/converter.py index a7c6e101..05dedb48 100644 --- a/datumaro/datumaro/components/converter.py +++ b/datumaro/datumaro/components/converter.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/components/dataset_filter.py b/datumaro/datumaro/components/dataset_filter.py index e9fc5e35..2fe1443d 100644 --- a/datumaro/datumaro/components/dataset_filter.py +++ b/datumaro/datumaro/components/dataset_filter.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/components/extractor.py b/datumaro/datumaro/components/extractor.py index 0473a250..b213b623 100644 --- a/datumaro/datumaro/components/extractor.py +++ b/datumaro/datumaro/components/extractor.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT @@ -27,26 +27,25 @@ AnnotationType = Enum('AnnotationType', _COORDINATE_ROUNDING_DIGITS = 2 -@attrs +@attrs(kw_only=True) class Annotation: - id = attrib(default=0, validator=default_if_none(int), kw_only=True) - attributes = attrib(factory=dict, validator=default_if_none(dict), kw_only=True) - group = attrib(default=0, validator=default_if_none(int), kw_only=True) + id = attrib(default=0, validator=default_if_none(int)) + attributes = attrib(factory=dict, validator=default_if_none(dict)) + group = attrib(default=0, validator=default_if_none(int)) def __attrs_post_init__(self): assert isinstance(self.type, AnnotationType) @property - def type(self): + def type(self) -> AnnotationType: return self._type # must be set in subclasses - def wrap(item, **kwargs): - return attr.evolve(item, **kwargs) + def wrap(self, **kwargs): + return attr.evolve(self, **kwargs) -@attrs +@attrs(kw_only=True) class Categories: - attributes = attrib(factory=set, validator=default_if_none(set), - kw_only=True, eq=False) + attributes = attrib(factory=set, validator=default_if_none(set), eq=False) @attrs class LabelCategories(Categories): @@ -92,7 +91,7 @@ class LabelCategories(Categories): indices[item.name] = index self._indices = indices - def add(self, name, parent=None, attributes=None): + def add(self, name: str, parent: str = None, attributes: dict = None): assert name not in self._indices, name if attributes is None: attributes = set() @@ -109,7 +108,7 @@ class LabelCategories(Categories): self._indices[name] = index return index - def find(self, name): + def find(self, name: str): index = self._indices.get(name) if index is not None: return index, self.items[index] @@ -601,7 +600,7 @@ class SourceExtractor(Extractor): def get_subset(self, name): if name != self._subset: - return None + raise Exception("Unknown subset '%s' requested" % name) return self class Importer: @@ -629,5 +628,5 @@ class Transform(Extractor): def categories(self): return self._extractor.categories() - def transform_item(self, item): + def transform_item(self, item: DatasetItem) -> DatasetItem: raise NotImplementedError() diff --git a/datumaro/datumaro/components/launcher.py b/datumaro/datumaro/components/launcher.py index b66bf237..adc31fb5 100644 --- a/datumaro/datumaro/components/launcher.py +++ b/datumaro/datumaro/components/launcher.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/components/project.py b/datumaro/datumaro/components/project.py index 8ac3ceb0..07f8f019 100644 --- a/datumaro/datumaro/components/project.py +++ b/datumaro/datumaro/components/project.py @@ -1,12 +1,12 @@ - -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT from collections import OrderedDict, defaultdict from functools import reduce -import git from glob import glob +from typing import Iterable, Union, Dict, List +import git import importlib import inspect import logging as log @@ -19,7 +19,7 @@ from datumaro.components.config import Config, DEFAULT_FORMAT from datumaro.components.config_model import (Model, Source, PROJECT_DEFAULT_CONFIG, PROJECT_SCHEMA) from datumaro.components.extractor import Extractor, LabelCategories,\ - AnnotationType + AnnotationType, DatasetItem from datumaro.components.launcher import ModelTransform from datumaro.components.dataset_filter import \ XPathDatasetFilter, XPathAnnotationsFilter @@ -304,50 +304,40 @@ class Environment: self.models.unregister(name) -class Subset(Extractor): - def __init__(self, parent): - self._parent = parent - self.items = OrderedDict() +class Dataset(Extractor): + class Subset(Extractor): + def __init__(self, parent): + self.parent = parent + self.items = OrderedDict() - def __iter__(self): - for item in self.items.values(): - yield item + def __iter__(self): + yield from self.items.values() - def __len__(self): - return len(self.items) + def __len__(self): + return len(self.items) - def categories(self): - return self._parent.categories() + def categories(self): + return self.parent.categories() -class Dataset(Extractor): @classmethod - def from_iterable(cls, iterable, categories=None): - """Generation of Dataset from iterable object - - Args: - iterable: Iterable object contains DatasetItems - categories (dict, optional): You can pass dict of categories or - you can pass list of names. It'll interpreted as list of names of - LabelCategories. Defaults to {}. - - Returns: - Dataset: Dataset object - """ - + def from_iterable(cls, iterable: Iterable[DatasetItem], + categories: Union[Dict, List[str]] = None): if isinstance(categories, list): - categories = {AnnotationType.label : LabelCategories.from_iterable(categories)} + categories = { AnnotationType.label: + LabelCategories.from_iterable(categories) + } if not categories: categories = {} - class tmpExtractor(Extractor): + class _extractor(Extractor): def __iter__(self): return iter(iterable) def categories(self): return categories - return cls.from_extractors(tmpExtractor()) + return cls.from_extractors(_extractor()) @classmethod def from_extractors(cls, *sources): @@ -355,7 +345,7 @@ class Dataset(Extractor): dataset = Dataset(categories=categories) # merge items - subsets = defaultdict(lambda: Subset(dataset)) + subsets = defaultdict(lambda: cls.Subset(dataset)) for source in sources: for item in source: existing_item = subsets[item.subset].items.get(item.id) @@ -416,20 +406,19 @@ class Dataset(Extractor): if subset is None: subset = item.subset - item = item.wrap(path=None, annotations=item.annotations) - if item.subset not in self._subsets: - self._subsets[item.subset] = Subset(self) + item = item.wrap(id=item_id, subset=subset, path=None) + if subset not in self._subsets: + self._subsets[subset] = self.Subset(self) self._subsets[subset].items[item_id] = item self._length = None return item - def extract(self, filter_expr, filter_annotations=False, remove_empty=False): + def filter(self, expr, filter_annotations=False, remove_empty=False): if filter_annotations: - return self.transform(XPathAnnotationsFilter, filter_expr, - remove_empty) + return self.transform(XPathAnnotationsFilter, expr, remove_empty) else: - return self.transform(XPathDatasetFilter, filter_expr) + return self.transform(XPathDatasetFilter, expr) def update(self, items): for item in items: @@ -500,17 +489,14 @@ class ProjectDataset(Dataset): sources = {} for s_name, source in config.sources.items(): - s_format = source.format - if not s_format: - s_format = env.PROJECT_EXTRACTOR_NAME + s_format = source.format or env.PROJECT_EXTRACTOR_NAME options = {} options.update(source.options) url = source.url if not source.url: url = osp.join(config.project_dir, config.sources_dir, s_name) - sources[s_name] = env.make_extractor(s_format, - url, **options) + sources[s_name] = env.make_extractor(s_format, url, **options) self._sources = sources own_source = None @@ -531,7 +517,7 @@ class ProjectDataset(Dataset): self._categories = categories # merge items - subsets = defaultdict(lambda: Subset(self)) + subsets = defaultdict(lambda: self.Subset(self)) for source_name, source in self._sources.items(): log.debug("Loading '%s' source contents..." % source_name) for item in source: @@ -548,11 +534,8 @@ class ProjectDataset(Dataset): # NOTE: consider imported sources as our own dataset path = None else: - path = item.path - if path is None: - path = [] - path = [source_name] + path - item = item.wrap(path=path, annotations=item.annotations) + path = [source_name] + (item.path or []) + item = item.wrap(path=path) subsets[item.subset].items[item.id] = item @@ -563,8 +546,7 @@ class ProjectDataset(Dataset): existing_item = subsets[item.subset].items.get(item.id) if existing_item is not None: item = item.wrap(path=None, - image=self._merge_images(existing_item, item), - annotations=item.annotations) + image=self._merge_images(existing_item, item)) subsets[item.subset].items[item.id] = item @@ -590,6 +572,7 @@ class ProjectDataset(Dataset): def put(self, item, item_id=None, subset=None, path=None): if path is None: path = item.path + if path: source = path[0] rest_path = path[1:] @@ -602,9 +585,9 @@ class ProjectDataset(Dataset): if subset is None: subset = item.subset - item = item.wrap(path=path, annotations=item.annotations) - if item.subset not in self._subsets: - self._subsets[item.subset] = Subset(self) + item = item.wrap(path=path) + if subset not in self._subsets: + self._subsets[subset] = self.Subset(self) self._subsets[subset].items[item_id] = item self._length = None @@ -713,7 +696,7 @@ class ProjectDataset(Dataset): # NOTE: probably this function should be in the ViewModel layer dataset = self if filter_expr: - dataset = dataset.extract(filter_expr, + dataset = dataset.filter(filter_expr, filter_annotations=filter_annotations, remove_empty=remove_empty) @@ -727,15 +710,15 @@ class ProjectDataset(Dataset): shutil.rmtree(save_dir) raise - def extract_project(self, filter_expr, filter_annotations=False, + def filter_project(self, filter_expr, filter_annotations=False, save_dir=None, remove_empty=False): # NOTE: probably this function should be in the ViewModel layer - filtered = self + dataset = self if filter_expr: - filtered = self.extract(filter_expr, + dataset = dataset.filter(filter_expr, filter_annotations=filter_annotations, remove_empty=remove_empty) - self._save_branch_project(filtered, save_dir=save_dir) + self._save_branch_project(dataset, save_dir=save_dir) class Project: @classmethod diff --git a/datumaro/datumaro/plugins/accuracy_checker_plugin/__init__.py b/datumaro/datumaro/plugins/accuracy_checker_plugin/__init__.py index e69de29b..fdd6d291 100644 --- a/datumaro/datumaro/plugins/accuracy_checker_plugin/__init__.py +++ b/datumaro/datumaro/plugins/accuracy_checker_plugin/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2020 Intel Corporation +# +# SPDX-License-Identifier: MIT + diff --git a/datumaro/datumaro/plugins/coco_format/extractor.py b/datumaro/datumaro/plugins/coco_format/extractor.py index 8bb6e464..73e78820 100644 --- a/datumaro/datumaro/plugins/coco_format/extractor.py +++ b/datumaro/datumaro/plugins/coco_format/extractor.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/coco_format/format.py b/datumaro/datumaro/plugins/coco_format/format.py index 6db04f0c..5129d49d 100644 --- a/datumaro/datumaro/plugins/coco_format/format.py +++ b/datumaro/datumaro/plugins/coco_format/format.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/coco_format/importer.py b/datumaro/datumaro/plugins/coco_format/importer.py index 4c32064b..3896b725 100644 --- a/datumaro/datumaro/plugins/coco_format/importer.py +++ b/datumaro/datumaro/plugins/coco_format/importer.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/cvat_format/converter.py b/datumaro/datumaro/plugins/cvat_format/converter.py index 37751703..4849619b 100644 --- a/datumaro/datumaro/plugins/cvat_format/converter.py +++ b/datumaro/datumaro/plugins/cvat_format/converter.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/cvat_format/extractor.py b/datumaro/datumaro/plugins/cvat_format/extractor.py index 75a3e5d8..7e37c2dd 100644 --- a/datumaro/datumaro/plugins/cvat_format/extractor.py +++ b/datumaro/datumaro/plugins/cvat_format/extractor.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/cvat_format/format.py b/datumaro/datumaro/plugins/cvat_format/format.py index c73fd467..e5572a89 100644 --- a/datumaro/datumaro/plugins/cvat_format/format.py +++ b/datumaro/datumaro/plugins/cvat_format/format.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/cvat_format/importer.py b/datumaro/datumaro/plugins/cvat_format/importer.py index 31f8dbd4..a3a83757 100644 --- a/datumaro/datumaro/plugins/cvat_format/importer.py +++ b/datumaro/datumaro/plugins/cvat_format/importer.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/datumaro_format/converter.py b/datumaro/datumaro/plugins/datumaro_format/converter.py index 81c2cd55..2d862094 100644 --- a/datumaro/datumaro/plugins/datumaro_format/converter.py +++ b/datumaro/datumaro/plugins/datumaro_format/converter.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/datumaro_format/extractor.py b/datumaro/datumaro/plugins/datumaro_format/extractor.py index 71eb6856..c1ae40d4 100644 --- a/datumaro/datumaro/plugins/datumaro_format/extractor.py +++ b/datumaro/datumaro/plugins/datumaro_format/extractor.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/datumaro_format/format.py b/datumaro/datumaro/plugins/datumaro_format/format.py index ef587b9b..501c100b 100644 --- a/datumaro/datumaro/plugins/datumaro_format/format.py +++ b/datumaro/datumaro/plugins/datumaro_format/format.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/datumaro_format/importer.py b/datumaro/datumaro/plugins/datumaro_format/importer.py index ed2f7527..dbb90f86 100644 --- a/datumaro/datumaro/plugins/datumaro_format/importer.py +++ b/datumaro/datumaro/plugins/datumaro_format/importer.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/image_dir.py b/datumaro/datumaro/plugins/image_dir.py index 410a91f8..062387e1 100644 --- a/datumaro/datumaro/plugins/image_dir.py +++ b/datumaro/datumaro/plugins/image_dir.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/labelme_format.py b/datumaro/datumaro/plugins/labelme_format.py index 5218e36f..e037afba 100644 --- a/datumaro/datumaro/plugins/labelme_format.py +++ b/datumaro/datumaro/plugins/labelme_format.py @@ -1,4 +1,3 @@ - # Copyright (C) 2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/mot_format.py b/datumaro/datumaro/plugins/mot_format.py index f3776078..12d3d07c 100644 --- a/datumaro/datumaro/plugins/mot_format.py +++ b/datumaro/datumaro/plugins/mot_format.py @@ -1,4 +1,3 @@ - # Copyright (C) 2020 Intel Corporation # # SPDX-License-Identifier: MIT @@ -20,7 +19,7 @@ from datumaro.components.extractor import (SourceExtractor, from datumaro.components.extractor import Importer from datumaro.components.converter import Converter from datumaro.util import cast -from datumaro.util.image import Image, save_image +from datumaro.util.image import Image MotLabel = Enum('MotLabel', [ diff --git a/datumaro/datumaro/plugins/openvino_launcher.py b/datumaro/datumaro/plugins/openvino_launcher.py index 4e150b03..abdaa0fc 100644 --- a/datumaro/datumaro/plugins/openvino_launcher.py +++ b/datumaro/datumaro/plugins/openvino_launcher.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/tf_detection_api_format/converter.py b/datumaro/datumaro/plugins/tf_detection_api_format/converter.py index 7ff3569d..a178bdba 100644 --- a/datumaro/datumaro/plugins/tf_detection_api_format/converter.py +++ b/datumaro/datumaro/plugins/tf_detection_api_format/converter.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/tf_detection_api_format/extractor.py b/datumaro/datumaro/plugins/tf_detection_api_format/extractor.py index f91c8b72..6962d3c0 100644 --- a/datumaro/datumaro/plugins/tf_detection_api_format/extractor.py +++ b/datumaro/datumaro/plugins/tf_detection_api_format/extractor.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/tf_detection_api_format/format.py b/datumaro/datumaro/plugins/tf_detection_api_format/format.py index 829a89e4..f4a879a6 100644 --- a/datumaro/datumaro/plugins/tf_detection_api_format/format.py +++ b/datumaro/datumaro/plugins/tf_detection_api_format/format.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/tf_detection_api_format/importer.py b/datumaro/datumaro/plugins/tf_detection_api_format/importer.py index 169618ba..b3d8a47d 100644 --- a/datumaro/datumaro/plugins/tf_detection_api_format/importer.py +++ b/datumaro/datumaro/plugins/tf_detection_api_format/importer.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/transforms.py b/datumaro/datumaro/plugins/transforms.py index 82493610..7e7cea8b 100644 --- a/datumaro/datumaro/plugins/transforms.py +++ b/datumaro/datumaro/plugins/transforms.py @@ -1,4 +1,3 @@ - # Copyright (C) 2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/voc_format/extractor.py b/datumaro/datumaro/plugins/voc_format/extractor.py index 669d7810..0fe667d3 100644 --- a/datumaro/datumaro/plugins/voc_format/extractor.py +++ b/datumaro/datumaro/plugins/voc_format/extractor.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/voc_format/format.py b/datumaro/datumaro/plugins/voc_format/format.py index 471866be..a03446d5 100644 --- a/datumaro/datumaro/plugins/voc_format/format.py +++ b/datumaro/datumaro/plugins/voc_format/format.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/voc_format/importer.py b/datumaro/datumaro/plugins/voc_format/importer.py index 78dc6cc9..e9354e6c 100644 --- a/datumaro/datumaro/plugins/voc_format/importer.py +++ b/datumaro/datumaro/plugins/voc_format/importer.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/yolo_format/converter.py b/datumaro/datumaro/plugins/yolo_format/converter.py index a8ed3524..9217c774 100644 --- a/datumaro/datumaro/plugins/yolo_format/converter.py +++ b/datumaro/datumaro/plugins/yolo_format/converter.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/yolo_format/extractor.py b/datumaro/datumaro/plugins/yolo_format/extractor.py index 9e34508c..c8c39c42 100644 --- a/datumaro/datumaro/plugins/yolo_format/extractor.py +++ b/datumaro/datumaro/plugins/yolo_format/extractor.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/yolo_format/format.py b/datumaro/datumaro/plugins/yolo_format/format.py index c88c99d4..02a07669 100644 --- a/datumaro/datumaro/plugins/yolo_format/format.py +++ b/datumaro/datumaro/plugins/yolo_format/format.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/plugins/yolo_format/importer.py b/datumaro/datumaro/plugins/yolo_format/importer.py index 344475c6..a040ea4e 100644 --- a/datumaro/datumaro/plugins/yolo_format/importer.py +++ b/datumaro/datumaro/plugins/yolo_format/importer.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/util/__init__.py b/datumaro/datumaro/util/__init__.py index 010057d5..0a75756b 100644 --- a/datumaro/datumaro/util/__init__.py +++ b/datumaro/datumaro/util/__init__.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/util/annotation_util.py b/datumaro/datumaro/util/annotation_util.py index 38a2c814..63950a14 100644 --- a/datumaro/datumaro/util/annotation_util.py +++ b/datumaro/datumaro/util/annotation_util.py @@ -1,4 +1,3 @@ - # Copyright (C) 2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/util/attrs_util.py b/datumaro/datumaro/util/attrs_util.py index 15f0c318..e631f35a 100644 --- a/datumaro/datumaro/util/attrs_util.py +++ b/datumaro/datumaro/util/attrs_util.py @@ -1,4 +1,3 @@ - # Copyright (C) 2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/util/command_targets.py b/datumaro/datumaro/util/command_targets.py index d8035a23..50c854f2 100644 --- a/datumaro/datumaro/util/command_targets.py +++ b/datumaro/datumaro/util/command_targets.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/util/image.py b/datumaro/datumaro/util/image.py index fc6a113c..626d8499 100644 --- a/datumaro/datumaro/util/image.py +++ b/datumaro/datumaro/util/image.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/util/image_cache.py b/datumaro/datumaro/util/image_cache.py index fd1ad0d7..08f02582 100644 --- a/datumaro/datumaro/util/image_cache.py +++ b/datumaro/datumaro/util/image_cache.py @@ -1,3 +1,7 @@ +# Copyright (C) 2019-2020 Intel Corporation +# +# SPDX-License-Identifier: MIT + from collections import OrderedDict diff --git a/datumaro/datumaro/util/mask_tools.py b/datumaro/datumaro/util/mask_tools.py index 680093d9..95c8633a 100644 --- a/datumaro/datumaro/util/mask_tools.py +++ b/datumaro/datumaro/util/mask_tools.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/util/test_utils.py b/datumaro/datumaro/util/test_utils.py index 62973ca5..db2767db 100644 --- a/datumaro/datumaro/util/test_utils.py +++ b/datumaro/datumaro/util/test_utils.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/datumaro/util/tf_util.py b/datumaro/datumaro/util/tf_util.py index f5d70090..9eda97ba 100644 --- a/datumaro/datumaro/util/tf_util.py +++ b/datumaro/datumaro/util/tf_util.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT diff --git a/datumaro/docs/developer_guide.md b/datumaro/docs/developer_guide.md new file mode 100644 index 00000000..e2fd101d --- /dev/null +++ b/datumaro/docs/developer_guide.md @@ -0,0 +1,200 @@ +## Basics + +The center part of the library is the `Dataset` class, which allows to iterate +over its elements. `DatasetItem`, an element of a dataset, represents a single +dataset entry with annotations - an image, video sequence, audio track etc. +It can contain only annotated data or meta information, only annotations, or +all of this. + +Basic library usage and data flow: + +```lang-none +Extractors -> Dataset -> Converter + | + Filtration + Transformations + Statistics + Merging + Inference + Quality Checking + Comparison + ... +``` + +1. Data is read (or produced) by one or many `Extractor`s and merged + into a `Dataset` +1. A dataset is processed in some way +1. A dataset is saved with a `Converter` + +Datumaro has a number of dataset and annotation features: +- iteration over dataset elements +- filtering of datasets and annotations by a custom criteria +- working with subsets (e.g. `train`, `val`, `test`) +- computing of dataset statistics +- comparison and merging of datasets +- various annotation operations + +```python +from datumaro.components.project import Environment + +# Import and save a dataset +env = Environment() +dataset = env.make_importer('voc')('src/dir').make_dataset() +env.converters.get('coco').convert(dataset, save_dir='dst/dir') +``` + +## Library contents + +### Dataset Formats + +Dataset reading is supported by `Extractor`s and `Importer`s: +- An `Extractor` produces a list of `DatasetItem`s corresponding +to the dataset. +- An `Importer` creates a project from the data source location. + +It is possible to add custom Extractors and Importers. To do this, you need +to put an `Extractor` and `Importer` implementations to a plugin directory. + +Dataset writing is supported by `Converter`s. +A Converter produces a dataset of a specific format from dataset items. +It is possible to add custom `Converter`s. To do this, you need to put a +Converter implementation script to a plugin directory. + +### Dataset Conversions ("Transforms") + +A `Transform` is a function for altering a dataset and producing a new one. +It can update dataset items, annotations, classes, and other properties. +A list of available transforms for dataset conversions can be extended by +adding a `Transform` implementation script into a plugin directory. + +### Model launchers + +A list of available launchers for model execution can be extended by +adding a `Launcher` implementation script into a plugin directory. + +## Plugins + +Datumaro comes with a number of built-in formats and other tools, +but it also can be extended by plugins. Plugins are optional components, +which dependencies are not installed by default. +In Datumaro there are several types of plugins, which include: +- `extractor` - produces dataset items from data source +- `importer` - recognizes dataset type and creates project +- `converter` - exports dataset to a specific format +- `transformation` - modifies dataset items or other properties +- `launcher` - executes models + +A plugin is a regular Python module. It must be present in a plugin directory: +- `/.datumaro/plugins` for project-specific plugins +- `/plugins` for global plugins + +A plugin can be used either via the `Environment` class instance, +or by regular module importing: + +```python +from datumaro.components.project import Environment, Project +from datumaro.plugins.yolo_format.converter import YoloConverter + +# Import a dataset +dataset = Environment().make_importer('voc')(src_dir).make_dataset() + +# Load an existing project, save the dataset in some project-specific format +project = Project.load('project/dir') +project.env.converters.get('custom_format').convert(dataset, save_dir=dst_dir) + +# Save the dataset in some built-in format +Environment().converters.get('yolo').convert(dataset, save_dir=dst_dir) +YoloConverter.convert(dataset, save_dir=dst_dir) +``` + +### Writing a plugin + +A plugin is a Python module with any name, which exports some symbols. +To export a symbol, inherit it from one of special classes: + +```python +from datumaro.components.extractor import Importer, SourceExtractor, Transform +from datumaro.components.launcher import Launcher +from datumaro.components.converter import Converter +``` + +The `exports` list of the module can be used to override default behaviour: +```python +class MyComponent1: ... +class MyComponent2: ... +exports = [MyComponent2] # exports only MyComponent2 +``` + +There is also an additional class to modify plugin appearance in command line: + +```python +from datumaro.components.cli_plugin import CliPlugin +``` + +#### Plugin example + + + +``` +datumaro/plugins/ +- my_plugin1/file1.py +- my_plugin1/file2.py +- my_plugin2.py +``` + + + +`my_plugin1/file2.py` contents: + +```python +from datumaro.components.extractor import Transform, CliPlugin +from .file1 import something, useful + +class MyTransform(Transform, CliPlugin): + NAME = "custom_name" # could be generated automatically + + """ + Some description. The text will be displayed in the command line output. + """ + + @classmethod + def build_cmdline_parser(cls, **kwargs): + parser = super().build_cmdline_parser(**kwargs) + parser.add_argument('-q', help="Very useful parameter") + return parser + + def __init__(self, extractor, q): + super().__init__(extractor) + self.q = q + + def transform_item(self, item): + return item +``` + +`my_plugin2.py` contents: + +```python +from datumaro.components.extractor import SourceExtractor + +class MyFormat: ... +class MyFormatExtractor(SourceExtractor): ... + +exports = [MyFormat] # explicit exports declaration +# MyFormatExtractor won't be exported +``` + +## Command-line + +Basically, the interface is divided on contexts and single commands. +Contexts are semantically grouped commands, related to a single topic or target. +Single commands are handy shorter alternatives for the most used commands +and also special commands, which are hard to be put into any specific context. +[Docker](https://www.docker.com/) is an example of similar approach. + +![cli-design-image](images/cli_design.png) + +- The diagram above was created with [FreeMind](http://freemind.sourceforge.net/wiki/index.php/Main_Page) + +Model-View-ViewModel (MVVM) UI pattern is used. + +![mvvm-image](images/mvvm.png) diff --git a/datumaro/docs/user_manual.md b/datumaro/docs/user_manual.md index e2e798e2..9e68f8f9 100644 --- a/datumaro/docs/user_manual.md +++ b/datumaro/docs/user_manual.md @@ -6,22 +6,23 @@ - [Interfaces](#interfaces) - [Supported dataset formats and annotations](#supported-formats) - [Command line workflow](#command-line-workflow) + - [Project structure](#project-structure) - [Command reference](#command-reference) - [Convert datasets](#convert-datasets) - - [Create a project](#create-project) + - [Create project](#create-project) - [Add and remove data](#add-and-remove-data) - - [Import a project](#import-project) - - [Extract a subproject](#extract-subproject) + - [Import project](#import-project) + - [Filter project](#filter-project) - [Update project (merge)](#update-project) - [Merge projects](#merge-projects) - - [Export a project](#export-project) + - [Export project](#export-project) - [Compare projects](#compare-projects) - [Obtaining project info](#get-project-info) - [Obtaining project statistics](#get-project-statistics) - - [Register a model](#register-model) + - [Register model](#register-model) - [Run inference](#run-inference) - [Run inference explanation](#explain-inference) - - [Transform a project](#transform-project) + - [Transform project](#transform-project) - [Extending](#extending) - [Links](#links) @@ -111,15 +112,39 @@ List of supported annotation types: ## Command line workflow -The key object is a project, so most CLI commands operate on projects. However, there -are few commands operating on datasets directly. A project is a combination of -a project's own dataset, a number of external data sources and an environment. +The key object is a project, so most CLI commands operate on projects. +However, there are few commands operating on datasets directly. +A project is a combination of a project's own dataset, a number of +external data sources and an environment. An empty Project can be created by `project create` command, an existing dataset can be imported with `project import` command. A typical way to obtain projects is to export tasks in CVAT UI. If you want to interact with models, you need to add them to project first. +### Project structure + + +``` +└── project/ + ├── .datumaro/ + | ├── config.yml + │   ├── .git/ + │   ├── models/ + │   └── plugins/ + │   ├── plugin1/ + │   | ├── file1.py + │   | └── file2.py + │   ├── plugin2.py + │   ├── custom_extractor1.py + │   └── ... + ├── dataset/ + └── sources/ + ├── source1 + └── ... +``` + + ## Command reference > **Note**: command invocation syntax is subject to change, @@ -270,11 +295,11 @@ datum source add path -f image_dir datum project export -f tf_detection_api ``` -### Extract subproject +### Filter project This command allows to create a sub-Project from a Project. The new project includes only items satisfying some condition. [XPath](https://devhints.io/xpath) -is used as query format. +is used as a query format. There are several filtering modes available (`-m/--mode` parameter). Supported modes: @@ -290,38 +315,34 @@ returns `annotation` elements (see examples). Usage: ``` bash -datum project extract --help +datum project filter --help -datum project extract \ +datum project filter \ -p \ - -o \ -e '' ``` Example: extract a dataset with only images which `width` < `height` ``` bash -datum project extract \ +datum project filter \ -p test_project \ - -o test_project-extract \ -e '/item[image/width < image/height]' ``` Example: extract a dataset with only large annotations of class `cat` and any non-`persons` ``` bash -datum project extract \ +datum project filter \ -p test_project \ - -o test_project-extract \ --mode annotations -e '/item/annotation[(label="cat" and area > 99.5) or label!="person"]' ``` Example: extract a dataset with only occluded annotations, remove empty images ``` bash -datum project extract \ +datum project filter \ -p test_project \ - -o test_project-extract \ -m i+a -e '/item/annotation[occluded="True"]' ``` @@ -362,7 +383,8 @@ Item representations are available with `--dry-run` parameter: ### Update project -This command updates items in a project from another one (check [Merge Projects](#merge-projects) for complex merging). +This command updates items in a project from another one +(check [Merge Projects](#merge-projects) for complex merging). Usage: diff --git a/datumaro/setup.py b/datumaro/setup.py index 4ebf1119..cf6d0433 100644 --- a/datumaro/setup.py +++ b/datumaro/setup.py @@ -1,5 +1,5 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2020 Intel Corporation # # SPDX-License-Identifier: MIT @@ -36,7 +36,7 @@ setuptools.setup( version=find_version(), author="Intel", author_email="maxim.zhiltsov@intel.com", - description="Dataset Framework", + description="Dataset Management Framework (Datumaro)", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/opencv/cvat/datumaro", diff --git a/datumaro/tests/assets/pytorch_launcher/samplenet.py b/datumaro/tests/assets/pytorch_launcher/samplenet.py index a742a650..7282e43a 100644 --- a/datumaro/tests/assets/pytorch_launcher/samplenet.py +++ b/datumaro/tests/assets/pytorch_launcher/samplenet.py @@ -1,5 +1,5 @@ """ -Copyright (c) 2019 Intel Corporation +Copyright (C) 2019-2020 Intel Corporation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/datumaro/tests/test_project.py b/datumaro/tests/test_project.py index ed4ad976..50d21d38 100644 --- a/datumaro/tests/test_project.py +++ b/datumaro/tests/test_project.py @@ -250,7 +250,7 @@ class ProjectTest(TestCase): project.env.extractors.register(e_type, TestExtractor) project.add_source('source', { 'format': e_type }) - dataset = project.make_dataset().extract('/item[id < 5]') + dataset = project.make_dataset().filter('/item[id < 5]') self.assertEqual(5, len(dataset)) From ffb71fb7a2643b2dbe1c7ab4316a409415f06ce1 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Fri, 4 Sep 2020 12:58:18 +0300 Subject: [PATCH 040/467] [Datumaro] Merge with different categories (#2098) * Add category merging * Update error message * Add category merging test * update changelog * Fix field access * remove import * Update CHANGELOG.md Co-authored-by: Nikita Manovich --- CHANGELOG.md | 1 + datumaro/datumaro/components/extractor.py | 33 ++-- datumaro/datumaro/components/operations.py | 215 ++++++++++++++++++--- datumaro/datumaro/util/annotation_util.py | 2 +- datumaro/requirements.txt | 2 +- datumaro/tests/test_ops.py | 94 ++++++++- 6 files changed, 290 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b1b7ed2..84dbdf61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ability to work with data on the fly (https://github.com/opencv/cvat/pull/2007) - Annotation in process outline color wheel () - [Datumaro] CLI command for dataset equality comparison () +- [Datumaro] Merging of datasets with different labels () ### Changed - UI models (like DEXTR) were redesigned to be more interactive () diff --git a/datumaro/datumaro/components/extractor.py b/datumaro/datumaro/components/extractor.py index b213b623..dcb7b036 100644 --- a/datumaro/datumaro/components/extractor.py +++ b/datumaro/datumaro/components/extractor.py @@ -49,7 +49,11 @@ class Categories: @attrs class LabelCategories(Categories): - Category = namedtuple('Category', ['name', 'parent', 'attributes']) + @attrs(repr_ns='LabelCategories') + class Category: + name = attrib(converter=str, validator=not_empty) + parent = attrib(default='', validator=default_if_none(str)) + attributes = attrib(factory=set, validator=default_if_none(set)) items = attrib(factory=list, validator=default_if_none(list)) _indices = attrib(factory=dict, init=False, eq=False) @@ -93,15 +97,6 @@ class LabelCategories(Categories): def add(self, name: str, parent: str = None, attributes: dict = None): assert name not in self._indices, name - if attributes is None: - attributes = set() - else: - if not isinstance(attributes, set): - attributes = set(attributes) - for attr in attributes: - assert isinstance(attr, str) - if parent is None: - parent = '' index = len(self.items) self.items.append(self.Category(name, parent, attributes)) @@ -386,7 +381,10 @@ setattr(Bbox, '__init__', Bbox.__actual_init__) @attrs class PointsCategories(Categories): - Category = namedtuple('Category', ['labels', 'joints']) + @attrs(repr_ns="PointsCategories") + class Category: + labels = attrib(factory=list, validator=default_if_none(list)) + joints = attrib(factory=set, validator=default_if_none(set)) items = attrib(factory=dict, validator=default_if_none(dict)) @@ -396,28 +394,19 @@ class PointsCategories(Categories): Args: iterable ([type]): This iterable object can be: - 1)simple int - will generate one Category with int as label - 2)list of int - will interpreted as list of Category labels - 3)list of positional argumetns - will generate Categories - with this arguments + 1) list of positional argumetns - will generate Categories + with these arguments Returns: PointsCategories: PointsCategories object """ temp_categories = cls() - if isinstance(iterable, int): - iterable = [[iterable]] - for category in iterable: - if isinstance(category, int): - category = [category] temp_categories.add(*category) return temp_categories def add(self, label_id, labels=None, joints=None): - if labels is None: - labels = [] if joints is None: joints = [] joints = set(map(tuple, joints)) diff --git a/datumaro/datumaro/components/operations.py b/datumaro/datumaro/components/operations.py index 2e3a6813..d887add9 100644 --- a/datumaro/datumaro/components/operations.py +++ b/datumaro/datumaro/components/operations.py @@ -15,7 +15,8 @@ from attr import attrib, attrs from unittest import TestCase from datumaro.components.cli_plugin import CliPlugin -from datumaro.components.extractor import AnnotationType, Bbox, Label +from datumaro.components.extractor import (AnnotationType, Bbox, Label, + LabelCategories, PointsCategories, MaskCategories) from datumaro.components.project import Dataset from datumaro.util import find, filter_dict from datumaro.util.attrs_util import ensure_cls, default_if_none @@ -53,7 +54,8 @@ def merge_categories(sources): for cat_type, source_cat in source.items(): if not categories[cat_type] == source_cat: raise NotImplementedError( - "Merging different categories is not implemented yet") + "Merging of datasets with different categories is " + "only allowed in 'merge' command.") return categories class MergingStrategy(CliPlugin): @@ -180,7 +182,8 @@ class IntersectMerge(MergingStrategy): _categories = attrib(init=False) # merged categories def __call__(self, datasets): - self._categories = merge_categories(d.categories() for d in datasets) + self._categories = self._merge_categories( + [d.categories() for d in datasets]) merged = Dataset(categories=self._categories) self._check_groups_definition() @@ -283,6 +286,126 @@ class IntersectMerge(MergingStrategy): return matches, item_map + def _merge_label_categories(self, sources): + same = True + common = None + for src_categories in sources: + src_cat = src_categories.get(AnnotationType.label) + if common is None: + common = src_cat + elif common != src_cat: + same = False + break + + if same: + return common + + dst_cat = LabelCategories() + for src_id, src_categories in enumerate(sources): + src_cat = src_categories.get(AnnotationType.label) + if src_cat is None: + continue + + for src_label in src_cat.items: + dst_label = dst_cat.find(src_label.name)[1] + if dst_label is not None: + if dst_label != src_label: + if src_label.parent and dst_label.parent and \ + src_label.parent != dst_label.parent: + raise ValueError("Can't merge label category " + "%s (from #%s): " + "parent label conflict: %s vs. %s" % \ + (src_label.name, src_id, + src_label.parent, dst_label.parent) + ) + dst_label.parent = dst_label.parent or src_label.parent + dst_label.attributes |= src_label.attributes + else: + pass + else: + dst_cat.add(src_label.name, + src_label.parent, src_label.attributes) + + return dst_cat + + def _merge_point_categories(self, sources, label_cat): + dst_point_cat = PointsCategories() + + for src_id, src_categories in enumerate(sources): + src_label_cat = src_categories.get(AnnotationType.label) + src_point_cat = src_categories.get(AnnotationType.points) + if src_label_cat is None or src_point_cat is None: + continue + + for src_label_id, src_cat in src_point_cat.items.items(): + src_label = src_label_cat.items[src_label_id].name + dst_label_id = label_cat.find(src_label)[0] + dst_cat = dst_point_cat.items.get(dst_label_id) + if dst_cat is not None: + if dst_cat != src_cat: + raise ValueError("Can't merge point category for label " + "%s (from #%s): %s vs. %s" % \ + (src_label, src_id, src_cat, dst_cat) + ) + else: + pass + else: + dst_point_cat.add(dst_label_id, + src_cat.labels, src_cat.joints) + + if len(dst_point_cat.items) == 0: + return None + + return dst_point_cat + + def _merge_mask_categories(self, sources, label_cat): + dst_mask_cat = MaskCategories() + + for src_id, src_categories in enumerate(sources): + src_label_cat = src_categories.get(AnnotationType.label) + src_mask_cat = src_categories.get(AnnotationType.mask) + if src_label_cat is None or src_mask_cat is None: + continue + + for src_label_id, src_cat in src_mask_cat.colormap.items(): + src_label = src_label_cat.items[src_label_id].name + dst_label_id = label_cat.find(src_label)[0] + dst_cat = dst_mask_cat.colormap.get(dst_label_id) + if dst_cat is not None: + if dst_cat != src_cat: + raise ValueError("Can't merge mask category for label " + "%s (from #%s): %s vs. %s" % \ + (src_label, src_id, src_cat, dst_cat) + ) + else: + pass + else: + dst_mask_cat.colormap[dst_label_id] = src_cat + + if len(dst_mask_cat.colormap) == 0: + return None + + return dst_mask_cat + + def _merge_categories(self, sources): + dst_categories = {} + + label_cat = self._merge_label_categories(sources) + if label_cat is None: + return dst_categories + + dst_categories[AnnotationType.label] = label_cat + + points_cat = self._merge_point_categories(sources, label_cat) + if points_cat is not None: + dst_categories[AnnotationType.points] = points_cat + + mask_cat = self._merge_mask_categories(sources, label_cat) + if mask_cat is not None: + dst_categories[AnnotationType.mask] = mask_cat + + return dst_categories + def _match_annotations(self, sources): all_by_type = {} for s in sources: @@ -473,8 +596,29 @@ class IntersectMerge(MergingStrategy): _check_group(group_labels, group) def _get_label_name(self, label_id): + if label_id is None: + return None return self._categories[AnnotationType.label].items[label_id].name + def _get_label_id(self, label): + return self._categories[AnnotationType.label].find(label)[0] + + def _get_src_label_name(self, ann, label_id): + if label_id is None: + return None + item_id = self._ann_map[id(ann)][1] + dataset_id = self._item_map[item_id][1] + return self._dataset_map[dataset_id][0] \ + .categories()[AnnotationType.label].items[label_id].name + + def _get_any_label_name(self, ann, label_id): + if label_id is None: + return None + try: + return self._get_src_label_name(ann, label_id) + except KeyError: + return self._get_label_name(label_id) + def _check_groups_definition(self): for group in self.conf.groups: for label, _ in group: @@ -486,16 +630,19 @@ class IntersectMerge(MergingStrategy): self._categories[AnnotationType.label].items]) ) -@attrs +@attrs(kw_only=True) class AnnotationMatcher: + _context = attrib(type=IntersectMerge, default=None) + def match_annotations(self, sources): raise NotImplementedError() @attrs class LabelMatcher(AnnotationMatcher): - @staticmethod - def distance(a, b): - return a.label == b.label + def distance(self, a, b): + a_label = self._context._get_any_label_name(a, a.label) + b_label = self._context._get_any_label_name(b, b.label) + return a_label == b_label def match_annotations(self, sources): return [sum(sources, [])] @@ -507,6 +654,7 @@ class _ShapeMatcher(AnnotationMatcher): def match_annotations(self, sources): distance = self.distance + label_matcher = self.label_matcher pairwise_dist = self.pairwise_dist cluster_dist = self.cluster_dist @@ -537,9 +685,10 @@ class _ShapeMatcher(AnnotationMatcher): for a_idx, src_a in enumerate(sources): for src_b in sources[a_idx+1 :]: matches, _, _, _ = match_segments(src_a, src_b, - dist_thresh=pairwise_dist, distance=distance) - for m in matches: - adjacent[id(m[0])].append(id(m[1])) + dist_thresh=pairwise_dist, + distance=distance, label_matcher=label_matcher) + for a, b in matches: + adjacent[id(a)].append(id(b)) # join all segments into matching clusters clusters = [] @@ -573,6 +722,11 @@ class _ShapeMatcher(AnnotationMatcher): def distance(a, b): return segment_iou(a, b) + def label_matcher(self, a, b): + a_label = self._context._get_any_label_name(a, a.label) + b_label = self._context._get_any_label_name(b, b.label) + return a_label == b_label + @attrs class BboxMatcher(_ShapeMatcher): pass @@ -626,8 +780,6 @@ class CaptionsMatcher(AnnotationMatcher): @attrs(kw_only=True) class AnnotationMerger: - _context = attrib(type=IntersectMerge, default=None) - def merge_clusters(self, clusters): raise NotImplementedError() @@ -641,20 +793,22 @@ class LabelMerger(AnnotationMerger, LabelMatcher): return [] votes = {} # label -> score - for label_ann in clusters[0]: - votes[label_ann.label] = 1 + votes.get(label_ann.label, 0) + for ann in clusters[0]: + label = self._context._get_src_label_name(ann, ann.label) + votes[label] = 1 + votes.get(label, 0) merged = [] for label, count in votes.items(): if count < self.quorum: sources = set(self.get_ann_source(id(a)) for a in clusters[0] - if label not in [l.label for l in a]) + if label not in [self._context._get_src_label_name(l, l.label) + for l in a]) sources = [self._context._dataset_map[s][1] for s in sources] self._context.add_item_error(FailedLabelVotingError, sources, votes) continue - merged.append(Label(label, attributes={ + merged.append(Label(self._context._get_label_id(label), attributes={ 'score': count / len(self._context._dataset_map) })) @@ -682,14 +836,17 @@ class _ShapeMerger(AnnotationMerger, _ShapeMatcher): def find_cluster_label(self, cluster): votes = {} for s in cluster: - state = votes.setdefault(s.label, [0, 0]) + label = self._context._get_src_label_name(s, s.label) + state = votes.setdefault(label, [0, 0]) state[0] += s.attributes.get('score', 1.0) state[1] += 1 label, (score, count) = max(votes.items(), key=lambda e: e[1][0]) if count < self.quorum: self._context.add_item_error(FailedLabelVotingError, votes) - score = score / count if count else None + label = None + score = score / len(self._context._dataset_map) + label = self._context._get_label_id(label) return label, score @staticmethod @@ -729,11 +886,10 @@ class LineMerger(_ShapeMerger, LineMatcher): class CaptionsMerger(AnnotationMerger, CaptionsMatcher): pass -def match_segments(a_segms, b_segms, distance='iou', dist_thresh=1.0): - if distance == 'iou': - distance = segment_iou - else: - assert callable(distance) +def match_segments(a_segms, b_segms, distance=segment_iou, dist_thresh=1.0, + label_matcher=lambda a, b: a.label == b.label): + assert callable(distance), distance + assert callable(label_matcher), label_matcher a_segms.sort(key=lambda ann: 1 - ann.attributes.get('score', 1)) b_segms.sort(key=lambda ann: 1 - ann.attributes.get('score', 1)) @@ -753,13 +909,16 @@ def match_segments(a_segms, b_segms, distance='iou', dist_thresh=1.0): for a_idx, a_segm in enumerate(a_segms): if len(b_segms) == 0: break - matched_b = a_matches[a_idx] - max_dist = max(distances[a_idx, matched_b], dist_thresh) - for b_idx, b_segm in enumerate(b_segms): + matched_b = -1 + max_dist = -1 + b_indices = np.argsort([not label_matcher(a_segm, b_segm) + for b_segm in b_segms], + kind='stable') # prioritize those with same label, keep score order + for b_idx in b_indices: if 0 <= b_matches[b_idx]: # assign a_segm with max conf continue d = distances[a_idx, b_idx] - if d < max_dist: + if d < dist_thresh or d <= max_dist: continue max_dist = d matched_b = b_idx @@ -771,7 +930,7 @@ def match_segments(a_segms, b_segms, distance='iou', dist_thresh=1.0): b_segm = b_segms[matched_b] - if a_segm.label == b_segm.label: + if label_matcher(a_segm, b_segm): matches.append( (a_segm, b_segm) ) else: mispred.append( (a_segm, b_segm) ) diff --git a/datumaro/datumaro/util/annotation_util.py b/datumaro/datumaro/util/annotation_util.py index 63950a14..3daa313f 100644 --- a/datumaro/datumaro/util/annotation_util.py +++ b/datumaro/datumaro/util/annotation_util.py @@ -118,7 +118,7 @@ def segment_iou(a, b): if ann.type == AnnotationType.polygon: return mask_utils.frPyObjects([ann.points], h, w) elif isinstance(ann, RleMask): - return [ann._rle] + return [ann.rle] elif ann.type == AnnotationType.mask: return mask_utils.frPyObjects([mask_to_rle(ann.image)], h, w) else: diff --git a/datumaro/requirements.txt b/datumaro/requirements.txt index b5142853..6bc3c7ee 100644 --- a/datumaro/requirements.txt +++ b/datumaro/requirements.txt @@ -7,6 +7,6 @@ matplotlib>=3.3.1 opencv-python-headless>=4.1.0.25 Pillow>=6.1.0 pycocotools>=2.0.0 -PyYAML>=5.1.1 +PyYAML>=5.3.1 scikit-image>=0.15.0 tensorboardX>=1.8 diff --git a/datumaro/tests/test_ops.py b/datumaro/tests/test_ops.py index dd4520b5..5b7355bf 100644 --- a/datumaro/tests/test_ops.py +++ b/datumaro/tests/test_ops.py @@ -3,7 +3,8 @@ from unittest import TestCase import numpy as np from datumaro.components.extractor import (Bbox, Caption, DatasetItem, - Extractor, Label, Mask, Points, Polygon, PolyLine) + Extractor, Label, Mask, Points, Polygon, PolyLine, + LabelCategories, PointsCategories, MaskCategories, AnnotationType) from datumaro.components.operations import (FailedAttrVotingError, IntersectMerge, NoMatchingAnnError, NoMatchingItemError, WrongGroupError, compute_ann_statistics, mean_std) @@ -198,7 +199,7 @@ class TestMultimerge(TestCase): Bbox(1, 2, 3, 4, label=1), # common - Mask(label=3, z_order=2, image=np.array([ + Mask(label=2, z_order=2, image=np.array([ [0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 0], @@ -218,7 +219,7 @@ class TestMultimerge(TestCase): source1 = Dataset.from_iterable([ DatasetItem(1, annotations=[ # common - Mask(label=3, image=np.array([ + Mask(label=2, image=np.array([ [0, 0, 0, 0], [0, 1, 1, 1], [0, 1, 1, 1], @@ -238,7 +239,7 @@ class TestMultimerge(TestCase): source2 = Dataset.from_iterable([ DatasetItem(1, annotations=[ # common - Mask(label=3, z_order=3, image=np.array([ + Mask(label=2, z_order=3, image=np.array([ [0, 0, 1, 1], [0, 1, 1, 1], [1, 1, 1, 1], @@ -261,7 +262,7 @@ class TestMultimerge(TestCase): # common # nearest to mean bbox - Mask(label=3, z_order=3, image=np.array([ + Mask(label=2, z_order=3, image=np.array([ [0, 0, 0, 0], [0, 1, 1, 1], [0, 1, 1, 1], @@ -365,3 +366,86 @@ class TestMultimerge(TestCase): self.assertEqual(3, len([e for e in merger.errors if isinstance(e, WrongGroupError)]), merger.errors ) + + def test_can_merge_classes(self): + source0 = Dataset.from_iterable([ + DatasetItem(1, annotations=[ + Label(0), + Label(1), + Bbox(0, 0, 1, 1, label=1), + ]), + ], categories=['a', 'b']) + + source1 = Dataset.from_iterable([ + DatasetItem(1, annotations=[ + Label(0), + Label(1), + Bbox(0, 0, 1, 1, label=0), + Bbox(0, 0, 1, 1, label=1), + ]), + ], categories=['b', 'c']) + + expected = Dataset.from_iterable([ + DatasetItem(1, annotations=[ + Label(0), + Label(1), + Label(2), + Bbox(0, 0, 1, 1, label=1), + Bbox(0, 0, 1, 1, label=2), + ]), + ], categories=['a', 'b', 'c']) + + merger = IntersectMerge() + merged = merger([source0, source1]) + + compare_datasets(self, expected, merged, ignored_attrs={'score'}) + + def test_can_merge_categories(self): + source0 = Dataset.from_iterable([ + DatasetItem(1, annotations=[ Label(0), ]), + ], categories={ + AnnotationType.label: LabelCategories.from_iterable(['a', 'b']), + AnnotationType.points: PointsCategories.from_iterable([ + (0, ['l0', 'l1']), + (1, ['l2', 'l3']), + ]), + AnnotationType.mask: MaskCategories({ + 0: (0, 1, 2), + 1: (1, 2, 3), + }), + }) + + source1 = Dataset.from_iterable([ + DatasetItem(1, annotations=[ Label(0), ]), + ], categories={ + AnnotationType.label: LabelCategories.from_iterable(['c', 'b']), + AnnotationType.points: PointsCategories.from_iterable([ + (0, []), + (1, ['l2', 'l3']), + ]), + AnnotationType.mask: MaskCategories({ + 0: (0, 2, 4), + 1: (1, 2, 3), + }), + }) + + expected = Dataset.from_iterable([ + DatasetItem(1, annotations=[ Label(0), Label(2), ]), + ], categories={ + AnnotationType.label: LabelCategories.from_iterable(['a', 'b', 'c']), + AnnotationType.points: PointsCategories.from_iterable([ + (0, ['l0', 'l1']), + (1, ['l2', 'l3']), + (2, []), + ]), + AnnotationType.mask: MaskCategories({ + 0: (0, 1, 2), + 1: (1, 2, 3), + 2: (0, 2, 4), + }), + }) + + merger = IntersectMerge() + merged = merger([source0, source1]) + + compare_datasets(self, expected, merged, ignored_attrs={'score'}) \ No newline at end of file From bd143853a52c60aea23472b06ada1f28a6337fd5 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 4 Sep 2020 15:28:56 +0300 Subject: [PATCH 041/467] Running detectors on the fly (#2102) * Draft version * Removed extra file * Removed extra code * Updated icon: magic wand * Ctrl modifier, fixed some cases when interaction event isn't raised * Added tooltip description of an interactor * Locking UI while server fetching * Removing old code & refactoring * Fixed couple of bugs * Updated CHANGELOG.md, updated versions * Update crosshair.ts * Minor fixes * Fixed eslint issues * Prevent default action * Added minNegVertices=0 by default, ignored negative points for dextr, fixed context menu in some cases * On the fly annotations draft * Initial version of FBRS interactive segmentation * Fix fbrs model_handler * Fixed couple of minor bugs * Added ability to interrupt interaction * Do not show reid on annotation view * Prettified UI * Updated changelog, increased version * Removed extra files * Removed extra code * Fixed changelog Co-authored-by: Nikita Manovich --- CHANGELOG.md | 1 + cvat-ui/package-lock.json | 2 +- cvat-ui/package.json | 2 +- .../controls-side-bar/tools-control.tsx | 105 +++- .../standard-workspace/styles.scss | 2 +- .../model-runner-modal/detector-runner.tsx | 319 +++++++++++++ .../model-runner-dialog.tsx | 88 ++++ .../model-runner-modal/model-runner-modal.tsx | 450 ------------------ .../components/model-runner-modal/styles.scss | 4 +- .../src/components/task-page/task-page.tsx | 4 +- .../src/components/tasks-page/task-list.tsx | 4 +- .../model-runner-dialog.tsx | 60 --- cvat-ui/src/styles.scss | 4 + 13 files changed, 506 insertions(+), 539 deletions(-) create mode 100644 cvat-ui/src/components/model-runner-modal/detector-runner.tsx create mode 100644 cvat-ui/src/components/model-runner-modal/model-runner-dialog.tsx delete mode 100644 cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx delete mode 100644 cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 84dbdf61..844cc02f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added password reset functionality () - Ability to work with data on the fly (https://github.com/opencv/cvat/pull/2007) - Annotation in process outline color wheel () +- On the fly annotation using DL detectors () - [Datumaro] CLI command for dataset equality comparison () - [Datumaro] Merging of datasets with different labels () diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 7370e0fa..5f7a0b86 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.9.1", + "version": "1.9.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 66ba9e86..f91eabd3 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.9.1", + "version": "1.9.2", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx index 5bdae3d4..195466b2 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx @@ -10,6 +10,7 @@ import Select, { OptionProps } from 'antd/lib/select'; import Button from 'antd/lib/button'; import Modal from 'antd/lib/modal'; import Text from 'antd/lib/typography/Text'; +import Tabs from 'antd/lib/tabs'; import { Row, Col } from 'antd/lib/grid'; import notification from 'antd/lib/notification'; @@ -23,8 +24,14 @@ import { ObjectType, ShapeType, } from 'reducers/interfaces'; -import { interactWithCanvas, fetchAnnotationsAsync, updateAnnotationsAsync } from 'actions/annotation-actions'; +import { + interactWithCanvas, + fetchAnnotationsAsync, + updateAnnotationsAsync, + createAnnotationsAsync, +} from 'actions/annotation-actions'; import { InteractionResult } from 'cvat-canvas/src/typescript/canvas'; +import DetectorRunner from 'components/model-runner-modal/detector-runner'; interface StateToProps { canvasInstance: Canvas; @@ -35,11 +42,13 @@ interface StateToProps { isInteraction: boolean; frame: number; interactors: Model[]; + detectors: Model[]; } interface DispatchToProps { onInteractionStart(activeInteractor: Model, activeLabelID: number): void; updateAnnotations(statesToUpdate: any[]): void; + createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void; fetchAnnotations(): void; } @@ -51,10 +60,11 @@ function mapStateToProps(state: CombinedState): StateToProps { const { instance: jobInstance } = annotation.job; const { instance: canvasInstance, activeControl } = annotation.canvas; const { models } = state; - const { interactors } = models; + const { interactors, detectors } = models; return { interactors, + detectors, isInteraction: activeControl === ActiveControl.INTERACTION, activeLabelID: annotation.drawing.activeLabelID, labels: annotation.job.labels, @@ -65,19 +75,12 @@ function mapStateToProps(state: CombinedState): StateToProps { }; } -function mapDispatchToProps(dispatch: any): DispatchToProps { - return { - onInteractionStart(activeInteractor: Model, activeLabelID: number): void { - dispatch(interactWithCanvas(activeInteractor, activeLabelID)); - }, - updateAnnotations(statesToUpdate: any[]): void { - dispatch(updateAnnotationsAsync(statesToUpdate)); - }, - fetchAnnotations(): void { - dispatch(fetchAnnotationsAsync()); - }, - }; -} +const mapDispatchToProps = { + onInteractionStart: interactWithCanvas, + updateAnnotations: updateAnnotationsAsync, + fetchAnnotations: fetchAnnotationsAsync, + createAnnotations: createAnnotationsAsync, +}; function convertShapesForInteractor(shapes: InteractionResult[]): number[][] { const reducer = (acc: number[][], _: number, index: number, array: number[]): number[][] => { @@ -378,9 +381,10 @@ class ToolsControlComponent extends React.PureComponent { - - + + + + + + ); +} + + +export default React.memo(DetectorRunner, (prevProps: Props, nextProps: Props): boolean => ( + prevProps.task === nextProps.task + && prevProps.runInference === nextProps.runInference + && prevProps.models.length === nextProps.models.length + && nextProps.models.reduce((acc: boolean, model: Model, index: number): boolean => ( + acc && model.id === prevProps.models[index].id + ), true) +)); diff --git a/cvat-ui/src/components/model-runner-modal/model-runner-dialog.tsx b/cvat-ui/src/components/model-runner-modal/model-runner-dialog.tsx new file mode 100644 index 00000000..cff9914b --- /dev/null +++ b/cvat-ui/src/components/model-runner-modal/model-runner-dialog.tsx @@ -0,0 +1,88 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React from 'react'; +import { connect } from 'react-redux'; +import Modal from 'antd/lib/modal'; + +import { ThunkDispatch } from 'utils/redux'; +import { modelsActions, startInferenceAsync } from 'actions/models-actions'; +import { Model, CombinedState } from 'reducers/interfaces'; +import DetectorRunner from './detector-runner'; + + +interface StateToProps { + visible: boolean; + task: any; + detectors: Model[]; + reid: Model[]; +} + +interface DispatchToProps { + runInference(task: any, model: Model, body: object): void; + closeDialog(): void; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { models } = state; + const { detectors, reid } = models; + + return { + visible: models.visibleRunWindows, + task: models.activeRunTask, + reid, + detectors, + }; +} + +function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps { + return { + runInference(task: any, model: Model, body: object) { + dispatch(startInferenceAsync(task, model, body)); + }, + closeDialog() { + dispatch(modelsActions.closeRunModelDialog()); + }, + }; +} + +function ModelRunnerDialog(props: StateToProps & DispatchToProps): JSX.Element { + const { + reid, + detectors, + task, + visible, + runInference, + closeDialog, + } = props; + + const models = [...reid, ...detectors]; + + return ( + closeDialog()} + maskClosable + title='Automatic annotation' + > + { + closeDialog(); + runInference(...args); + }} + /> + + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(ModelRunnerDialog); diff --git a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx deleted file mode 100644 index d8fd959e..00000000 --- a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx +++ /dev/null @@ -1,450 +0,0 @@ -// Copyright (C) 2020 Intel Corporation -// -// SPDX-License-Identifier: MIT - -import './styles.scss'; -import React from 'react'; -import { Row, Col } from 'antd/lib/grid'; -import Icon from 'antd/lib/icon'; -import Select from 'antd/lib/select'; -import Checkbox from 'antd/lib/checkbox'; -import Tooltip from 'antd/lib/tooltip'; -import Modal from 'antd/lib/modal'; -import Tag from 'antd/lib/tag'; -import notification from 'antd/lib/notification'; -import Text from 'antd/lib/typography/Text'; -import InputNumber from 'antd/lib/input-number'; - -import { - Model, - StringObject, -} from 'reducers/interfaces'; - -interface Props { - reid: Model[]; - detectors: Model[]; - activeProcesses: StringObject; - visible: boolean; - taskInstance: any; - closeDialog(): void; - runInference( - taskInstance: any, - model: Model, - body: object, - ): void; -} - -interface State { - selectedModel: string | null; - cleanup: boolean; - mapping: StringObject; - colors: StringObject; - matching: { - model: string; - task: string; - }; - - threshold: number; - maxDistance: number; -} - -function colorGenerator(): () => string { - const values = [ - 'magenta', 'green', 'geekblue', - 'orange', 'red', 'cyan', - 'blue', 'volcano', 'purple', - ]; - - let index = 0; - - return (): string => { - const color = values[index++]; - if (index >= values.length) { - index = 0; - } - - return color; - }; -} - -const nextColor = colorGenerator(); - -export default class ModelRunnerModalComponent extends React.PureComponent { - public constructor(props: Props) { - super(props); - this.state = { - selectedModel: null, - mapping: {}, - colors: {}, - cleanup: false, - matching: { - model: '', - task: '', - }, - - threshold: 0.5, - maxDistance: 50, - }; - } - - public componentDidUpdate(prevProps: Props, prevState: State): void { - const { - reid, - detectors, - taskInstance, - visible, - } = this.props; - - const { selectedModel } = this.state; - const models = [...reid, ...detectors]; - - if (!prevProps.visible && visible) { - this.setState({ - selectedModel: null, - mapping: {}, - matching: { - model: '', - task: '', - }, - cleanup: false, - }); - } - - if (selectedModel && prevState.selectedModel !== selectedModel) { - const selectedModelInstance = models - .filter((model) => model.name === selectedModel)[0]; - - if (selectedModelInstance.type !== 'reid' && !selectedModelInstance.labels.length) { - notification.warning({ - message: 'The selected model does not include any lables', - }); - } - - let taskLabels: string[] = taskInstance.labels - .map((label: any): string => label.name); - const [defaultMapping, defaultColors]: StringObject[] = selectedModelInstance.labels - .reduce((acc: StringObject[], label): StringObject[] => { - if (taskLabels.includes(label)) { - acc[0][label] = label; - acc[1][label] = nextColor(); - taskLabels = taskLabels.filter((_label): boolean => _label !== label); - } - - return acc; - }, [{}, {}]); - - this.setState({ - mapping: defaultMapping, - colors: defaultColors, - }); - } - } - - private renderModelSelector(): JSX.Element { - const { reid, detectors } = this.props; - const models = [...reid, ...detectors]; - - return ( - - Model: - - - - - ); - } - - private renderMappingTag(modelLabel: string, taskLabel: string): JSX.Element { - const { colors, mapping } = this.state; - - return ( - - - {modelLabel} - - - {taskLabel} - - - - { - const newMapping = { ...mapping }; - delete newMapping[modelLabel]; - this.setState({ - mapping: newMapping, - }); - }} - /> - - - - ); - } - - private renderMappingInputSelector( - value: string, - current: string, - options: string[], - ): JSX.Element { - const { matching, mapping, colors } = this.state; - - return ( - - ); - } - - private renderMappingInput( - availableModelLabels: string[], - availableTaskLabels: string[], - ): JSX.Element { - const { matching } = this.state; - return ( - - - {this.renderMappingInputSelector( - matching.model, - 'Model', - availableModelLabels, - )} - - - {this.renderMappingInputSelector( - matching.task, - 'Task', - availableTaskLabels, - )} - - - - - - - - ); - } - - private renderReidContent(): JSX.Element { - const { threshold, maxDistance } = this.state; - - return ( -
- - - Threshold - - - - { - if (typeof (value) === 'number') { - this.setState({ - threshold: value, - }); - } - }} - /> - - - - - - Maximum distance - - - - { - if (typeof (value) === 'number') { - this.setState({ - maxDistance: value, - }); - } - }} - /> - - - -
- ); - } - - private renderContent(): JSX.Element { - const { selectedModel, cleanup, mapping } = this.state; - const { reid, detectors, taskInstance } = this.props; - - const models = [...reid, ...detectors]; - const model = selectedModel && models - .filter((_model): boolean => _model.name === selectedModel)[0]; - - const excludedModelLabels: string[] = Object.keys(mapping); - const isDetector = model && model.type === 'detector'; - const isReId = model && model.type === 'reid'; - const tags = isDetector ? excludedModelLabels - .map((modelLabel: string) => this.renderMappingTag( - modelLabel, - mapping[modelLabel], - )) : []; - - const availableModelLabels = model ? model.labels - .filter( - (label: string) => !excludedModelLabels.includes(label), - ) : []; - const taskLabels = taskInstance.labels.map( - (label: any) => label.name, - ); - - const mappingISAvailable = !!availableModelLabels.length - && !!taskLabels.length; - - return ( -
- { this.renderModelSelector() } - { isDetector && tags} - { isDetector - && mappingISAvailable - && this.renderMappingInput(availableModelLabels, taskLabels)} - { isDetector - && ( -
- this.setState({ - cleanup: e.target.checked, - })} - > - Clean old annotations - -
- )} - { isReId && this.renderReidContent() } -
- ); - } - - public render(): JSX.Element | false { - const { - selectedModel, - mapping, - cleanup, - threshold, - maxDistance, - } = this.state; - - const { - reid, - detectors, - visible, - taskInstance, - runInference, - closeDialog, - } = this.props; - - const models = [...reid, ...detectors]; - const activeModel = models.filter( - (model): boolean => model.name === selectedModel, - )[0]; - - const enabledSubmit = !!activeModel && (activeModel.type === 'reid' - || !!Object.keys(mapping).length); - - return ( - visible && ( - { - runInference( - taskInstance, - models - .filter((model): boolean => model.name === selectedModel)[0], - activeModel.type === 'detector' ? { - mapping, - cleanup, - } : { - threshold, - max_distance: maxDistance, - }, - ); - closeDialog(); - }} - onCancel={(): void => closeDialog()} - okButtonProps={{ disabled: !enabledSubmit }} - title='Automatic annotation' - visible - > - { this.renderContent() } - - ) - ); - } -} diff --git a/cvat-ui/src/components/model-runner-modal/styles.scss b/cvat-ui/src/components/model-runner-modal/styles.scss index 9c9bc0c0..80bef525 100644 --- a/cvat-ui/src/components/model-runner-modal/styles.scss +++ b/cvat-ui/src/components/model-runner-modal/styles.scss @@ -4,10 +4,10 @@ @import '../../base.scss'; -.cvat-run-model-dialog > div:not(first-child) { +.cvat-run-model-content > div:not(first-child) { margin-top: 10px; } -.cvat-run-model-dialog-remove-mapping-icon { +.cvat-run-model-content-remove-mapping-icon { color: $danger-icon-color; } diff --git a/cvat-ui/src/components/task-page/task-page.tsx b/cvat-ui/src/components/task-page/task-page.tsx index c66db671..ea6cdf80 100644 --- a/cvat-ui/src/components/task-page/task-page.tsx +++ b/cvat-ui/src/components/task-page/task-page.tsx @@ -12,7 +12,7 @@ import Result from 'antd/lib/result'; import DetailsContainer from 'containers/task-page/details'; import JobListContainer from 'containers/task-page/job-list'; -import ModelRunnerModalContainer from 'containers/model-runner-dialog/model-runner-dialog'; +import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog'; import { Task } from 'reducers/interfaces'; import TopBarComponent from './top-bar'; @@ -75,7 +75,7 @@ class TaskPageComponent extends React.PureComponent {
- + ); } diff --git a/cvat-ui/src/components/tasks-page/task-list.tsx b/cvat-ui/src/components/tasks-page/task-list.tsx index 68515a62..01bfcb6a 100644 --- a/cvat-ui/src/components/tasks-page/task-list.tsx +++ b/cvat-ui/src/components/tasks-page/task-list.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { Row, Col } from 'antd/lib/grid'; import Pagination from 'antd/lib/pagination'; -import ModelRunnerModalContainer from 'containers/model-runner-dialog/model-runner-dialog'; +import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog'; import TaskItem from 'containers/tasks-page/task-item'; export interface ContentListProps { @@ -46,7 +46,7 @@ export default function TaskListComponent(props: ContentListProps): JSX.Element /> - + ); } diff --git a/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx b/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx deleted file mode 100644 index 3f63bd7f..00000000 --- a/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (C) 2020 Intel Corporation -// -// SPDX-License-Identifier: MIT - -import { connect } from 'react-redux'; - -import ModelRunnerModalComponent from 'components/model-runner-modal/model-runner-modal'; -import { Model, CombinedState } from 'reducers/interfaces'; -import { startInferenceAsync, modelsActions } from 'actions/models-actions'; - -interface StateToProps { - reid: Model[]; - detectors: Model[]; - activeProcesses: { - [index: string]: string; - }; - taskInstance: any; - visible: boolean; -} - -interface DispatchToProps { - runInference( - taskInstance: any, - model: Model, - body: object, - ): void; - closeDialog(): void; -} - -function mapStateToProps(state: CombinedState): StateToProps { - const { models } = state; - - return { - reid: models.reid, - detectors: models.detectors, - activeProcesses: {}, - taskInstance: models.activeRunTask, - visible: models.visibleRunWindows, - }; -} - -function mapDispatchToProps(dispatch: any): DispatchToProps { - return ({ - runInference( - taskInstance: any, - model: Model, - body: object, - ): void { - dispatch(startInferenceAsync(taskInstance, model, body)); - }, - closeDialog(): void { - dispatch(modelsActions.closeRunModelDialog()); - }, - }); -} - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(ModelRunnerModalComponent); diff --git a/cvat-ui/src/styles.scss b/cvat-ui/src/styles.scss index cbd6a4c2..284b2772 100644 --- a/cvat-ui/src/styles.scss +++ b/cvat-ui/src/styles.scss @@ -43,6 +43,10 @@ hr { color: $info-icon-color; } +.cvat-danger-circle-icon { + color: $danger-icon-color; +} + #root { width: 100%; height: 100%; From bd6cefeada8d4ce3c73e5903c99dc853d49baeb1 Mon Sep 17 00:00:00 2001 From: Dmitry Kruchinin <33020454+dvkruchinin@users.noreply.github.com> Date: Mon, 7 Sep 2020 09:27:41 +0300 Subject: [PATCH 042/467] Cypress test for issue 1425. (#2122) Co-authored-by: Dmitry Kruchinin --- ...d_attribute_correspond_chosen_attribute.js | 66 +++++++++++++++++++ tests/cypress/support/commands.js | 21 +++++- 2 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 tests/cypress/integration/issue_1425_highlighted_attribute_correspond_chosen_attribute.js diff --git a/tests/cypress/integration/issue_1425_highlighted_attribute_correspond_chosen_attribute.js b/tests/cypress/integration/issue_1425_highlighted_attribute_correspond_chosen_attribute.js new file mode 100644 index 00000000..756a4702 --- /dev/null +++ b/tests/cypress/integration/issue_1425_highlighted_attribute_correspond_chosen_attribute.js @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2020 Intel Corporation + * + * SPDX-License-Identifier: MIT + */ + +/// + +context('The highlighted attribute in AAM should correspond to the chosen attribute', () => { + + const issueId = '1425' + const labelName = `Issue ${issueId}` + const taskName = `New annotation task for ${labelName}` + const attrName = `Attr for ${labelName}` + const textDefaultValue = 'Some default value for type Text' + const image = `image_${issueId}.png` + const width = 800 + const height = 800 + const posX = 10 + const posY = 10 + const color = 'gray' + const additionalAttrName = `Attr 2` + const additionalValue = `Attr value 2` + const typeAttribute = 'Text' + let textValue = '' + + before(() => { + cy.visit('auth/login') + cy.login() + cy.imageGenerator('cypress/fixtures', image, width, height, color, posX, posY, labelName) + }) + + describe(`Testing issue "${issueId}"`, () => { + it('Create a task with multiple attributes, create a object', () => { + cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, image, false, 1, true, additionalAttrName, typeAttribute, additionalValue) + cy.openTaskJob(taskName) + cy.createShape(309, 431, 616, 671) + }) + it('Go to AAM', () => { + cy.changeAnnotationMode('Attribute annotation') + }) + it('Check if highlighted attribute correspond to the chosen attribute in right panel', () => { + cy.get('.cvat_canvas_text').within(() => { + cy.get('[style="fill: red;"]').then($textValue => { + textValue = $textValue.text().split(': ')[1] + }) + }) + cy.get('.attribute-annotation-sidebar-attr-editor').within(() => { + cy.get('[type="text"]').should('have.value', textValue) + }) + }) + it('Go to next attribute and check again', () => { + cy.get('.attribute-annotation-sidebar-attribute-switcher') + .find('.anticon-right') + .click({force: true}) + cy.get('.cvat_canvas_text').within(() => { + cy.get('[style="fill: red;"]').then($textValue => { + textValue = $textValue.text().split(': ')[1] + }) + }) + cy.get('.attribute-annotation-sidebar-attr-editor').within(() => { + cy.get('[type="text"]').should('have.value', textValue) + }) + }) + }) +}) diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 20767e48..a9cef747 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -31,7 +31,11 @@ Cypress.Commands.add('createAnnotationTask', (taksName='New annotation task', textDefaultValue='Some default value for type Text', image='image.png', multiJobs=false, - segmentSize=1) => { + segmentSize=1, + multiAttr=false, + additionalAttrName, + typeAttribute, + additionalValue) => { cy.get('#cvat-create-task-button').click() cy.url().should('include', '/tasks/create') cy.get('[id="name"]').type(taksName) @@ -42,15 +46,18 @@ Cypress.Commands.add('createAnnotationTask', (taksName='New annotation task', cy.get('div[title="Select"]').click() cy.get('li').contains('Text').click() cy.get('[placeholder="Default value"]').type(textDefaultValue) + if (multiAttr) { + cy.updateAttributes(additionalAttrName, typeAttribute, additionalValue) + } cy.contains('button', 'Done').click() - cy.get('input[type="file"]').attachFile(image, { subjectType: 'drag-n-drop' }); + cy.get('input[type="file"]').attachFile(image, { subjectType: 'drag-n-drop' }) if (multiJobs) { cy.contains('Advanced configuration').click() cy.get('#segmentSize') .type(segmentSize) } cy.contains('button', 'Submit').click() - cy.contains('The task has been created', {timeout: '8000'}) + cy.contains('The task has been created') cy.get('[value="tasks"]').click() cy.url().should('include', '/tasks?page=') }) @@ -195,3 +202,11 @@ Cypress.Commands.add('createCuboid', (mode, firstX, firstY, lastX, lastY) => { cy.get('.cvat-canvas-container') .click(lastX, lastY) }) + +Cypress.Commands.add('updateAttributes', (additionalAttrName, typeAttribute, additionalValue) => { + cy.contains('button', 'Add an attribute').click() + cy.get('[placeholder="Name"]').first().type(additionalAttrName) + cy.get('div[title="Select"]').first().click() + cy.get('.ant-select-dropdown').last().contains(typeAttribute).click() + cy.get('[placeholder="Default value"]').first().type(additionalValue) +}) From ab3fac5d93ac151a588e3e5966432f7ce175525d Mon Sep 17 00:00:00 2001 From: Dmitry Kruchinin <33020454+dvkruchinin@users.noreply.github.com> Date: Mon, 7 Sep 2020 09:28:53 +0300 Subject: [PATCH 043/467] Cypress test for issue 1438. (#2113) Co-authored-by: Dmitry Kruchinin --- ..._1438_cancel_multiple_paste_ui_not_lock.js | 49 ++ tests/package-lock.json | 462 +++++++++++------- tests/package.json | 4 +- 3 files changed, 333 insertions(+), 182 deletions(-) create mode 100644 tests/cypress/integration/issue_1438_cancel_multiple_paste_ui_not_lock.js diff --git a/tests/cypress/integration/issue_1438_cancel_multiple_paste_ui_not_lock.js b/tests/cypress/integration/issue_1438_cancel_multiple_paste_ui_not_lock.js new file mode 100644 index 00000000..e8cd2609 --- /dev/null +++ b/tests/cypress/integration/issue_1438_cancel_multiple_paste_ui_not_lock.js @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 Intel Corporation + * + * SPDX-License-Identifier: MIT + */ + +/// + +context('Cancel "multiple paste". UI is not locked.', () => { + + const issueId = '1438' + const labelName = `Issue ${issueId}` + const taskName = `New annotation task for ${labelName}` + const attrName = `Attr for ${labelName}` + const textDefaultValue = 'Some default value for type Text' + const image = `image_${issueId}.png` + const width = 800 + const height = 800 + const posX = 10 + const posY = 10 + const color = 'gray' + + before(() => { + cy.visit('auth/login') + cy.login() + cy.imageGenerator('cypress/fixtures', image, width, height, color, posX, posY, labelName) + cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, image) + cy.openTaskJob(taskName) + cy.createShape(309, 431, 616, 671) + }) + + describe(`Testing issue "${issueId}"`, () => { + it('Copy, paste opject. Cancel pasting.', () => { + cy.get('#cvat_canvas_shape_1') + .trigger('mousemove') + .trigger('mouseover') + cy.get('body') + .type('{ctrl}c') + .type('{ctrl}v') + .click({ctrlKey: true}) + .type('{esc}') + }) + it('UI is not locked.', () => { + cy.get('.cvat-draw-rectangle-control').click() + cy.get('.cvat-draw-shape-popover-content') + .should('be.visible') + }) + }) +}) diff --git a/tests/package-lock.json b/tests/package-lock.json index 3e0194dd..659a9cc7 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -414,14 +414,20 @@ } }, "@samverschueren/stream-to-observable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", - "integrity": "sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", + "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==", "dev": true, "requires": { "any-observable": "^0.3.0" } }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, "@types/sinonjs__fake-timers": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz", @@ -435,9 +441,9 @@ "dev": true }, "ajv": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", - "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -550,6 +556,12 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -557,9 +569,9 @@ "dev": true }, "aws4": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", - "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", + "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", "dev": true }, "balanced-match": { @@ -603,6 +615,12 @@ } } }, + "blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -661,32 +679,23 @@ "dev": true }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "dependencies": { "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "dev": true, "requires": { - "color-convert": "^1.9.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" } } } @@ -713,14 +722,14 @@ } }, "cli-table3": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", - "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz", + "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==", "dev": true, "requires": { "colors": "^1.1.2", "object-assign": "^4.1.0", - "string-width": "^2.1.1" + "string-width": "^4.2.0" } }, "cli-truncate": { @@ -762,18 +771,18 @@ "dev": true }, "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "color-name": "1.1.3" + "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "colors": { @@ -879,22 +888,20 @@ } }, "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, "cypress": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-4.12.1.tgz", - "integrity": "sha512-9SGIPEmqU8vuRA6xst2CMTYd9sCFCxKSzrHt0wr+w2iAQMCIIsXsQ5Gplns1sT6LDbZcmLv6uehabAOl3fhc9Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-5.0.0.tgz", + "integrity": "sha512-jhPd0PMO1dPSBNpx6pHVLkmnnaTfMy3wCoacHAKJ9LJG06y16zqUNSFri64N4BjuGe8y6mNMt8TdgKnmy9muCg==", "dev": true, "requires": { "@cypress/listr-verbose-renderer": "^0.4.1", @@ -903,26 +910,27 @@ "@types/sinonjs__fake-timers": "^6.0.1", "@types/sizzle": "^2.3.2", "arch": "^2.1.2", + "blob-util": "2.0.2", "bluebird": "^3.7.2", "cachedir": "^2.3.0", - "chalk": "^2.4.2", + "chalk": "^4.1.0", "check-more-types": "^2.24.0", - "cli-table3": "~0.5.1", + "cli-table3": "~0.6.0", "commander": "^4.1.1", "common-tags": "^1.8.0", "debug": "^4.1.1", "eventemitter2": "^6.4.2", - "execa": "^1.0.0", + "execa": "^4.0.2", "executable": "^4.1.1", "extract-zip": "^1.7.0", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "getos": "^3.2.1", "is-ci": "^2.0.0", "is-installed-globally": "^0.3.2", "lazy-ass": "^1.6.0", "listr": "^0.14.3", "lodash": "^4.17.19", - "log-symbols": "^3.0.0", + "log-symbols": "^4.0.0", "minimist": "^1.2.5", "moment": "^2.27.0", "ospath": "^1.2.2", @@ -930,16 +938,16 @@ "ramda": "~0.26.1", "request-progress": "^3.0.0", "supports-color": "^7.1.0", - "tmp": "~0.1.0", + "tmp": "~0.2.1", "untildify": "^4.0.0", "url": "^0.11.0", "yauzl": "^2.10.0" } }, "cypress-file-upload": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/cypress-file-upload/-/cypress-file-upload-4.0.7.tgz", - "integrity": "sha512-rFFmnoZ2bWyWFpSV09AhkSUgYEiVy70pcQ6nf/mGTMTrVHvKCCCIfRu3TbgVYHbgBq+0hqjfjQrtz4IbgH7qZA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/cypress-file-upload/-/cypress-file-upload-4.1.1.tgz", + "integrity": "sha512-tX6UhuJ63rNgjdzxglpX+ZYf/bM6PDhFMtt1qCBljLtAgdearqyfD1AHqyh59rOHCjfM+bf6FA3o9b/mdaX6pw==", "dev": true, "requires": { "mime": "^2.4.4" @@ -1004,6 +1012,12 @@ "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", "dev": true }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -1025,18 +1039,31 @@ "dev": true }, "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", + "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "dependencies": { + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + } } }, "executable": { @@ -1159,14 +1186,15 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", "dev": true, "requires": { + "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" } }, "fs.realpath": { @@ -1175,9 +1203,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "requires": { "pump": "^3.0.0" @@ -1272,9 +1300,9 @@ } }, "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, "http-signature": { @@ -1288,6 +1316,12 @@ "sshpk": "^1.7.0" } }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", @@ -1334,9 +1368,9 @@ } }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "is-function": { @@ -1376,9 +1410,9 @@ "dev": true }, "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", "dev": true }, "is-typedarray": { @@ -1446,12 +1480,13 @@ "dev": true }, "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", "dev": true, "requires": { - "graceful-fs": "^4.1.6" + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" } }, "jsprim": { @@ -1495,6 +1530,14 @@ "listr-verbose-renderer": "^0.5.0", "p-map": "^2.0.0", "rxjs": "^6.3.3" + }, + "dependencies": { + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + } } }, "listr-silent-renderer": { @@ -1561,6 +1604,26 @@ "figures": "^2.0.0" }, "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -1570,6 +1633,21 @@ "restore-cursor": "^2.0.0" } }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -1579,6 +1657,18 @@ "escape-string-regexp": "^1.0.5" } }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, "onetime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", @@ -1597,6 +1687,15 @@ "onetime": "^2.0.0", "signal-exit": "^3.0.2" } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } } } }, @@ -1616,9 +1715,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "lodash.defaults": { @@ -1653,12 +1752,12 @@ "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" }, "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", "dev": true, "requires": { - "chalk": "^2.4.2" + "chalk": "^4.0.0" } }, "log-update": { @@ -1681,6 +1780,12 @@ "restore-cursor": "^2.0.0" } }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, "onetime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", @@ -1702,6 +1807,12 @@ } } }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -1723,9 +1834,9 @@ } }, "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, "min-document": { @@ -1769,24 +1880,18 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "requires": { - "path-key": "^2.0.0" + "path-key": "^3.0.0" } }, "number-is-nan": { @@ -1832,12 +1937,6 @@ "integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=", "dev": true }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, "p-map": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", @@ -1879,9 +1978,9 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, "pend": { @@ -1921,9 +2020,9 @@ "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==" }, "pretty-bytes": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz", - "integrity": "sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.4.1.tgz", + "integrity": "sha512-s1Iam6Gwz3JI5Hweaz4GoCD1WUNUIyzePFy5+Js2hjwGVt2Z79wNN+ZKOZ2vB6C+Xs6njyB84Z1IthQg8d9LxA==", "dev": true }, "process": { @@ -2030,9 +2129,9 @@ } }, "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -2064,25 +2163,19 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { - "shebang-regex": "^1.0.0" + "shebang-regex": "^3.0.0" } }, "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, "signal-exit": { @@ -2115,28 +2208,29 @@ } }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^5.0.0" } } } @@ -2165,27 +2259,19 @@ "ansi-regex": "^2.0.0" } }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - } } }, "symbol-observable": { @@ -2235,12 +2321,12 @@ "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" }, "tmp": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", - "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "dev": true, "requires": { - "rimraf": "^2.6.3" + "rimraf": "^3.0.0" } }, "tough-cookie": { @@ -2281,9 +2367,9 @@ "dev": true }, "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", "dev": true }, "untildify": { @@ -2293,9 +2379,9 @@ "dev": true }, "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", "dev": true, "requires": { "punycode": "^2.1.0" @@ -2350,9 +2436,9 @@ } }, "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { "isexe": "^2.0.0" @@ -2374,6 +2460,22 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", diff --git a/tests/package.json b/tests/package.json index 5861f8d3..feb9ff07 100644 --- a/tests/package.json +++ b/tests/package.json @@ -1,7 +1,7 @@ { "devDependencies": { - "cypress": "^4.12.1", - "cypress-file-upload": "^4.0.7" + "cypress": "^5.0.0", + "cypress-file-upload": "^4.1.1" }, "dependencies": { "archiver": "^5.0.0", From cb4d8e6ee571594f74ff96bc6cbc7c597c732e4a Mon Sep 17 00:00:00 2001 From: Dmitry Kruchinin <33020454+dvkruchinin@users.noreply.github.com> Date: Mon, 7 Sep 2020 09:30:04 +0300 Subject: [PATCH 044/467] Cypress test for issue 1825. (#2124) Co-authored-by: Dmitry Kruchinin --- .../issue_1825_tooltip_hidden_mouseout.js | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/cypress/integration/issue_1825_tooltip_hidden_mouseout.js diff --git a/tests/cypress/integration/issue_1825_tooltip_hidden_mouseout.js b/tests/cypress/integration/issue_1825_tooltip_hidden_mouseout.js new file mode 100644 index 00000000..b8f81ca0 --- /dev/null +++ b/tests/cypress/integration/issue_1825_tooltip_hidden_mouseout.js @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 Intel Corporation + * + * SPDX-License-Identifier: MIT + */ + +/// + +context('Tooltip does not interfere with interaction with elements.', () => { + + const issueId = '1825' + const labelName = `Issue ${issueId}` + const taskName = `New annotation task for ${labelName}` + const attrName = `Attr for ${labelName}` + const textDefaultValue = 'Some default value for type Text' + const image = `image_${issueId}.png` + const width = 800 + const height = 800 + const posX = 10 + const posY = 10 + const color = 'gray' + + before(() => { + cy.visit('auth/login') + cy.login() + cy.imageGenerator('cypress/fixtures', image, width, height, color, posX, posY, labelName) + cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, image) + cy.openTaskJob(taskName) + }) + + describe(`Testing issue "${issueId}"`, () => { + it('Mouseover to "Shape" button when draw new rectangle. The tooltip open.', () => { + cy.get('.cvat-draw-rectangle-control').click() + cy.get('.cvat-draw-shape-popover-content') + cy.contains('Shape') + .invoke('show') + .trigger('mouseover', 'top') + .should('have.class', 'ant-tooltip-open') + }) + it('The radio element was clicked successfully', () => { + /*Before the fix, cypress can't click on the radio element + due to its covered with the tooltip. After the fix, cypress + successfully clicks on the element, but the tooltip does not + disappear visually.*/ + cy.contains('By 4 Points') + .click() + }) + }) +}) From f43863aa79da44fd1115378281bba2e172e155df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2020 12:46:29 +0300 Subject: [PATCH 045/467] Bump http-proxy from 1.17.0 to 1.18.1 in /cvat-canvas (#2134) Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.17.0 to 1.18.1. - [Release notes](https://github.com/http-party/node-http-proxy/releases) - [Changelog](https://github.com/http-party/node-http-proxy/blob/master/CHANGELOG.md) - [Commits](https://github.com/http-party/node-http-proxy/compare/1.17.0...1.18.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- cvat-canvas/package-lock.json | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/cvat-canvas/package-lock.json b/cvat-canvas/package-lock.json index 5ee19968..87e10955 100644 --- a/cvat-canvas/package-lock.json +++ b/cvat-canvas/package-lock.json @@ -3668,12 +3668,6 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, - "eventemitter3": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", - "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", - "dev": true - }, "events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", @@ -5167,14 +5161,22 @@ "dev": true }, "http-proxy": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", - "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "requires": { - "eventemitter3": "^3.0.0", + "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" + }, + "dependencies": { + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + } } }, "http-proxy-middleware": { From 7552c906319ccd9e3464c270580b930d63aec8ba Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2020 22:03:13 +0300 Subject: [PATCH 046/467] Bump gitpython from 3.1.3 to 3.1.8 in /cvat/requirements (#2143) Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.3 to 3.1.8. - [Release notes](https://github.com/gitpython-developers/GitPython/releases) - [Changelog](https://github.com/gitpython-developers/GitPython/blob/master/CHANGES) - [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.3...3.1.8) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- cvat/requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index 1cd368a6..8ee81726 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -24,7 +24,7 @@ dj-pagination==2.5.0 python-logstash==0.4.6 django-revproxy==0.10.0 rules==2.2 -GitPython==3.1.3 +GitPython==3.1.8 coreapi==2.3.3 django-filter==2.3.0 Markdown==3.2.2 From 6f635436a9f66019348af97a36fbd0b422190463 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2020 22:03:50 +0300 Subject: [PATCH 047/467] Bump isort from 4.3.21 to 5.5.1 in /cvat/requirements (#2142) Bumps [isort](https://github.com/pycqa/isort) from 4.3.21 to 5.5.1. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/4.3.21...5.5.1) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- cvat/requirements/development.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/requirements/development.txt b/cvat/requirements/development.txt index 990bf336..4b43f3e7 100644 --- a/cvat/requirements/development.txt +++ b/cvat/requirements/development.txt @@ -1,6 +1,6 @@ -r base.txt astroid==2.4.2 -isort==4.3.21 +isort==5.5.1 lazy-object-proxy==1.5.1 mccabe==0.6.1 pylint==2.6.0 From 67de3f86dd7df6887e0bce2b5db20f99086d17d9 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2020 22:37:33 +0300 Subject: [PATCH 048/467] Bump diskcache from 4.1.0 to 5.0.2 in /cvat/requirements (#2145) Bumps [diskcache](https://github.com/grantjenks/python-diskcache) from 4.1.0 to 5.0.2. - [Release notes](https://github.com/grantjenks/python-diskcache/releases) - [Commits](https://github.com/grantjenks/python-diskcache/compare/v4.1.0...v5.0.2) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- cvat/requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index 8ee81726..d4457948 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -44,4 +44,4 @@ tensorflow==2.2.0 # Optional requirement of Datumaro # The package is used by pyunpack as a command line tool to support multiple # archives. Don't use as a python module because it has GPL license. patool==1.12 -diskcache==4.1.0 \ No newline at end of file +diskcache==5.0.2 \ No newline at end of file From 21e67c20474556c14fdd9d4d24089658fb816483 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2020 22:38:07 +0300 Subject: [PATCH 049/467] Bump django-extensions from 3.0.6 to 3.0.8 in /cvat/requirements (#2144) Bumps [django-extensions](https://github.com/django-extensions/django-extensions) from 3.0.6 to 3.0.8. - [Release notes](https://github.com/django-extensions/django-extensions/releases) - [Changelog](https://github.com/django-extensions/django-extensions/blob/master/CHANGELOG.md) - [Commits](https://github.com/django-extensions/django-extensions/compare/3.0.6...3.0.8) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- cvat/requirements/development.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/requirements/development.txt b/cvat/requirements/development.txt index 4b43f3e7..b6ac4f72 100644 --- a/cvat/requirements/development.txt +++ b/cvat/requirements/development.txt @@ -8,6 +8,6 @@ pylint-django==2.3.0 pylint-plugin-utils==0.6 rope==0.17.0 wrapt==1.12.1 -django-extensions==3.0.6 +django-extensions==3.0.8 Werkzeug==1.0.1 snakeviz==2.1.0 From 416df8980afa18b3a686e63405f5cc9a946c4e95 Mon Sep 17 00:00:00 2001 From: Nikita Manovich Date: Mon, 7 Sep 2020 22:38:38 +0300 Subject: [PATCH 050/467] Don't allow lambda manager to return objects with a label which doesn't exist in the task. (#2131) --- cvat/apps/lambda_manager/views.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cvat/apps/lambda_manager/views.py b/cvat/apps/lambda_manager/views.py index d307ca51..52c1de41 100644 --- a/cvat/apps/lambda_manager/views.py +++ b/cvat/apps/lambda_manager/views.py @@ -134,6 +134,16 @@ class LambdaFunction: }) quality = data.get("quality") mapping = data.get("mapping") + mapping_by_default = {db_label.name:db_label.name + for db_label in db_task.label_set.all()} + if not mapping: + # use mapping by default to avoid labels in mapping which + # don't exist in the task + mapping = mapping_by_default + else: + # filter labels in mapping which don't exist in the task + mapping = {k:v for k,v in mapping.items() if v in mapping_by_default} + if self.kind == LambdaType.DETECTOR: payload.update({ "image": self._get_image(db_task, data["frame"], quality) From 0efc11d2b03278e9a0e4cc1f5759a480be930df1 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2020 06:50:42 +0300 Subject: [PATCH 051/467] Bump psycopg2-binary from 2.8.5 to 2.8.6 in /cvat/requirements (#2146) Bumps [psycopg2-binary](https://github.com/psycopg/psycopg2) from 2.8.5 to 2.8.6. - [Release notes](https://github.com/psycopg/psycopg2/releases) - [Changelog](https://github.com/psycopg/psycopg2/blob/master/NEWS) - [Commits](https://github.com/psycopg/psycopg2/commits) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- cvat/requirements/production.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/requirements/production.txt b/cvat/requirements/production.txt index 36e64e29..cfc5a350 100644 --- a/cvat/requirements/production.txt +++ b/cvat/requirements/production.txt @@ -1,3 +1,3 @@ -r base.txt -psycopg2-binary==2.8.5 +psycopg2-binary==2.8.6 mod-wsgi==4.7.1 \ No newline at end of file From 0933ee236263013f04082dfa600d577fe649bbbf Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 8 Sep 2020 22:44:13 +0300 Subject: [PATCH 052/467] Fix CVAT format import for frame stepped tasks (#2151) * Fix cvat format import with frame step * update changelog --- CHANGELOG.md | 1 + cvat/apps/dataset_manager/formats/cvat.py | 4 +++- cvat/apps/engine/tests/_test_rest_api.py | 13 ++++++++++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 844cc02f..c7f5e7a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed multiple errors which arises when polygon is of length 5 or less () +- Fixed CVAT format import for frame stepped tasks () ### Security - diff --git a/cvat/apps/dataset_manager/formats/cvat.py b/cvat/apps/dataset_manager/formats/cvat.py index ac5823b6..3c349947 100644 --- a/cvat/apps/dataset_manager/formats/cvat.py +++ b/cvat/apps/dataset_manager/formats/cvat.py @@ -442,7 +442,9 @@ def load(file_object, annotations): ) elif el.tag == 'image': image_is_opened = True - frame_id = match_dm_item(DatasetItem(id=el.attrib['id'], image=el.attrib['name']), annotations) + frame_id = annotations.abs_frame_id(match_dm_item( + DatasetItem(id=el.attrib['id'], image=el.attrib['name']), + annotations)) elif el.tag in supported_shapes and (track is not None or image_is_opened): attributes = [] shape = { diff --git a/cvat/apps/engine/tests/_test_rest_api.py b/cvat/apps/engine/tests/_test_rest_api.py index 1e773b2f..374a4cd2 100644 --- a/cvat/apps/engine/tests/_test_rest_api.py +++ b/cvat/apps/engine/tests/_test_rest_api.py @@ -2082,7 +2082,14 @@ class JobAnnotationAPITestCase(APITestCase): "client_files[0]": generate_image_file("test_1.jpg")[1], "client_files[1]": generate_image_file("test_2.jpg")[1], "client_files[2]": generate_image_file("test_3.jpg")[1], + "client_files[4]": generate_image_file("test_4.jpg")[1], + "client_files[5]": generate_image_file("test_5.jpg")[1], + "client_files[6]": generate_image_file("test_6.jpg")[1], + "client_files[7]": generate_image_file("test_7.jpg")[1], + "client_files[8]": generate_image_file("test_8.jpg")[1], + "client_files[9]": generate_image_file("test_9.jpg")[1], "image_quality": 75, + "frame_filter": "step=3", } response = self.client.post("/api/v1/tasks/{}/data".format(tid), data=images) assert response.status_code == status.HTTP_202_ACCEPTED @@ -2202,7 +2209,7 @@ class JobAnnotationAPITestCase(APITestCase): "occluded": False }, { - "frame": 1, + "frame": 2, "label_id": task["labels"][1]["id"], "group": None, "source": "manual", @@ -2239,7 +2246,7 @@ class JobAnnotationAPITestCase(APITestCase): ] }, { - "frame": 1, + "frame": 2, "attributes": [], "points": [2.0, 2.1, 100, 300.222], "type": "rectangle", @@ -2256,7 +2263,7 @@ class JobAnnotationAPITestCase(APITestCase): "attributes": [], "shapes": [ { - "frame": 1, + "frame": 2, "attributes": [], "points": [1.0, 2.1, 100, 300.222], "type": "rectangle", From a5b63a4f53335fb69f36188ceed1243c879bed66 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 8 Sep 2020 20:59:00 +0100 Subject: [PATCH 053/467] Add python3-setuptools to install list (#2153) Installing python3-setuptools was required on a fresh ubuntu 18.04 VM image. Otherwise the next line fails with "no module setuptools" --- cvat/apps/documentation/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/documentation/installation.md b/cvat/apps/documentation/installation.md index ce2e9008..bc83b7c0 100644 --- a/cvat/apps/documentation/installation.md +++ b/cvat/apps/documentation/installation.md @@ -67,7 +67,7 @@ server. Proxy is an advanced topic and it is not covered by the guide. defining and running multi-container docker applications. ```bash - sudo apt-get --no-install-recommends install -y python3-pip + sudo apt-get --no-install-recommends install -y python3-pip python3-setuptools sudo python3 -m pip install setuptools docker-compose ``` From 4e219299e1ef3f209b43daa7a0896c7f1005d00d Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 8 Sep 2020 23:01:35 +0300 Subject: [PATCH 054/467] UI Tracking with serverless functions (#2136) * tmp * Refactored * Refactoring & added button to context menu * Updated changelog, updated versions * Improved styles * Removed outdated code * Updated icon --- CHANGELOG.md | 1 + .../src/typescript/interactionHandler.ts | 4 - cvat-core/package-lock.json | 2 +- cvat-core/package.json | 2 +- cvat-ui/package-lock.json | 2 +- cvat-ui/package.json | 2 +- cvat-ui/src/actions/annotation-actions.ts | 33 +- cvat-ui/src/actions/plugins-actions.ts | 5 +- .../controls-side-bar/controls-side-bar.tsx | 4 +- .../controls-side-bar/tools-control.tsx | 346 ++++++++++++++++-- .../objects-side-bar/object-item-basics.tsx | 3 + .../objects-side-bar/object-item-menu.tsx | 32 +- .../objects-side-bar/object-item.tsx | 3 + .../standard-workspace/standard-workspace.tsx | 1 + .../standard-workspace/styles.scss | 3 +- .../objects-side-bar/object-item.tsx | 14 +- cvat-ui/src/reducers/annotation-reducer.ts | 4 +- cvat-ui/src/reducers/interfaces.ts | 4 +- cvat-ui/src/utils/range.ts | 27 ++ 19 files changed, 431 insertions(+), 61 deletions(-) create mode 100644 cvat-ui/src/utils/range.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c7f5e7a2..7d22ae58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ability to work with data on the fly (https://github.com/opencv/cvat/pull/2007) - Annotation in process outline color wheel () - On the fly annotation using DL detectors () +- Automatic tracking of bounding boxes using serverless functions () - [Datumaro] CLI command for dataset equality comparison () - [Datumaro] Merging of datasets with different labels () diff --git a/cvat-canvas/src/typescript/interactionHandler.ts b/cvat-canvas/src/typescript/interactionHandler.ts index 76237cf0..7ede21e3 100644 --- a/cvat-canvas/src/typescript/interactionHandler.ts +++ b/cvat-canvas/src/typescript/interactionHandler.ts @@ -155,10 +155,6 @@ export class InteractionHandlerImpl implements InteractionHandler { this.shapesWereUpdated = true; this.canvas.off('mousedown.interaction', eventListener); - if (this.shouldRaiseEvent(false)) { - this.onInteraction(this.prepareResult(), true, false); - } - this.interact({ enabled: false }); }).addClass('cvat_canvas_shape_drawing').attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json index 91ac8a0f..55038b63 100644 --- a/cvat-core/package-lock.json +++ b/cvat-core/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "3.6.0", + "version": "3.6.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-core/package.json b/cvat-core/package.json index 4418f157..869464ac 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "3.6.0", + "version": "3.6.1", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "babel.config.js", "scripts": { diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 5f7a0b86..5c8e3541 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.9.2", + "version": "1.9.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index f91eabd3..f504a65b 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.9.2", + "version": "1.9.3", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index efeaa81e..e6b3ef2f 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -27,6 +27,7 @@ import getCore from 'cvat-core-wrapper'; import logger, { LogType } from 'cvat-logger'; import { RectDrawingMethod } from 'cvat-canvas-wrapper'; import { getCVATStore } from 'cvat-store'; +import { MutableRefObject } from 'react'; interface AnnotationsParameters { filters: string[]; @@ -189,6 +190,7 @@ export enum AnnotationActionTypes { SAVE_LOGS_SUCCESS = 'SAVE_LOGS_SUCCESS', SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED', INTERACT_WITH_CANVAS = 'INTERACT_WITH_CANVAS', + SET_AI_TOOLS_REF = 'SET_AI_TOOLS_REF', } export function saveLogsAsync(): ThunkAction { @@ -1397,6 +1399,16 @@ export function interactWithCanvas(activeInteractor: Model, activeLabelID: numbe }; } +export function setAIToolsRef(ref: MutableRefObject): AnyAction { + return { + type: AnnotationActionTypes.SET_AI_TOOLS_REF, + payload: { + aiToolsRef: ref, + }, + }; +} + + export function repeatDrawShapeAsync(): ThunkAction { return async (dispatch: ActionCreator): Promise => { const { @@ -1424,12 +1436,21 @@ export function repeatDrawShapeAsync(): ThunkAction { let activeControl = ActiveControl.CURSOR; if (activeInteractor) { - canvasInstance.interact({ - enabled: true, - shapeType: 'points', - minPosVertices: 4, // TODO: Add parameter to interactor - }); - dispatch(interactWithCanvas(activeInteractor, activeLabelID)); + if (activeInteractor.type === 'tracker') { + canvasInstance.interact({ + enabled: true, + shapeType: 'rectangle', + }); + dispatch(interactWithCanvas(activeInteractor, activeLabelID)); + } else { + canvasInstance.interact({ + enabled: true, + shapeType: 'points', + minPosVertices: 4, // TODO: Add parameter to interactor + }); + dispatch(interactWithCanvas(activeInteractor, activeLabelID)); + } + return; } diff --git a/cvat-ui/src/actions/plugins-actions.ts b/cvat-ui/src/actions/plugins-actions.ts index 6b5ca2c9..c1c739f6 100644 --- a/cvat-ui/src/actions/plugins-actions.ts +++ b/cvat-ui/src/actions/plugins-actions.ts @@ -30,19 +30,16 @@ export function checkPluginsAsync(): ThunkAction { const plugins: PluginObjects = { ANALYTICS: false, GIT_INTEGRATION: false, - DEXTR_SEGMENTATION: false, }; const promises: Promise[] = [ // check must return true/false with no exceptions PluginChecker.check(SupportedPlugins.ANALYTICS), PluginChecker.check(SupportedPlugins.GIT_INTEGRATION), - PluginChecker.check(SupportedPlugins.DEXTR_SEGMENTATION), ]; const values = await Promise.all(promises); - [plugins.ANALYTICS, plugins.GIT_INTEGRATION, - plugins.DEXTR_SEGMENTATION] = values; + [plugins.ANALYTICS, plugins.GIT_INTEGRATION] = values; dispatch(pluginActions.checkedAllPlugins(plugins)); }; } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx index a202d5c9..ef4b457e 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx @@ -85,7 +85,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { preventDefault(event); const drawing = [ActiveControl.DRAW_POINTS, ActiveControl.DRAW_POLYGON, ActiveControl.DRAW_POLYLINE, ActiveControl.DRAW_RECTANGLE, - ActiveControl.DRAW_CUBOID, ActiveControl.INTERACTION].includes(activeControl); + ActiveControl.DRAW_CUBOID, ActiveControl.AI_TOOLS].includes(activeControl); if (!drawing) { canvasInstance.cancel(); @@ -98,7 +98,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { repeatDrawShape(); } } else { - if (activeControl === ActiveControl.INTERACTION) { + if (activeControl === ActiveControl.AI_TOOLS) { // separated API method canvasInstance.interact({ enabled: false }); return; diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx index 195466b2..83db38e2 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -import React from 'react'; +import React, { MutableRefObject } from 'react'; import { connect } from 'react-redux'; import Icon from 'antd/lib/icon'; import Popover from 'antd/lib/popover'; @@ -13,9 +13,11 @@ import Text from 'antd/lib/typography/Text'; import Tabs from 'antd/lib/tabs'; import { Row, Col } from 'antd/lib/grid'; import notification from 'antd/lib/notification'; +import Progress from 'antd/lib/progress'; import { AIToolsIcon } from 'icons'; import { Canvas } from 'cvat-canvas-wrapper'; +import range from 'utils/range'; import getCore from 'cvat-core-wrapper'; import { CombinedState, @@ -32,6 +34,7 @@ import { } from 'actions/annotation-actions'; import { InteractionResult } from 'cvat-canvas/src/typescript/canvas'; import DetectorRunner from 'components/model-runner-modal/detector-runner'; +import InputNumber from 'antd/lib/input-number'; interface StateToProps { canvasInstance: Canvas; @@ -39,10 +42,13 @@ interface StateToProps { states: any[]; activeLabelID: number; jobInstance: any; - isInteraction: boolean; + isActivated: boolean; frame: number; interactors: Model[]; detectors: Model[]; + trackers: Model[]; + curZOrder: number; + aiToolsRef: MutableRefObject; } interface DispatchToProps { @@ -60,18 +66,21 @@ function mapStateToProps(state: CombinedState): StateToProps { const { instance: jobInstance } = annotation.job; const { instance: canvasInstance, activeControl } = annotation.canvas; const { models } = state; - const { interactors, detectors } = models; + const { interactors, detectors, trackers } = models; return { interactors, detectors, - isInteraction: activeControl === ActiveControl.INTERACTION, + trackers, + isActivated: activeControl === ActiveControl.AI_TOOLS, activeLabelID: annotation.drawing.activeLabelID, labels: annotation.job.labels, states: annotation.annotations.states, canvasInstance, jobInstance, frame, + curZOrder: annotation.annotations.zLayer.cur, + aiToolsRef: annotation.aiToolsRef, }; } @@ -103,10 +112,14 @@ interface State { activeInteractor: Model | null; activeLabelID: number; interactiveStateID: number | null; + activeTracker: Model | null; + trackingProgress: number | null; + trackingFrames: number; fetching: boolean; + mode: 'detection' | 'interaction' | 'tracking'; } -class ToolsControlComponent extends React.PureComponent { +export class ToolsControlComponent extends React.PureComponent { private interactionIsAborted: boolean; private interactionIsDone: boolean; @@ -114,9 +127,13 @@ class ToolsControlComponent extends React.PureComponent { super(props); this.state = { activeInteractor: props.interactors.length ? props.interactors[0] : null, + activeTracker: props.trackers.length ? props.trackers[0] : null, activeLabelID: props.labels[0].id, interactiveStateID: null, + trackingProgress: null, + trackingFrames: 10, fetching: false, + mode: 'interaction', }; this.interactionIsAborted = false; @@ -124,16 +141,18 @@ class ToolsControlComponent extends React.PureComponent { } public componentDidMount(): void { - const { canvasInstance } = this.props; + const { canvasInstance, aiToolsRef } = this.props; + aiToolsRef.current = this; canvasInstance.html().addEventListener('canvas.interacted', this.interactionListener); canvasInstance.html().addEventListener('canvas.canceled', this.cancelListener); } public componentDidUpdate(prevProps: Props): void { - const { isInteraction } = this.props; - if (prevProps.isInteraction && !isInteraction) { + const { isActivated } = this.props; + if (prevProps.isActivated && !isActivated) { window.removeEventListener('contextmenu', this.contextmenuDisabler); - } else if (!prevProps.isInteraction && isInteraction) { + } else if (!prevProps.isActivated && isActivated) { + // reset flags when start interaction/tracking this.interactionIsDone = false; this.interactionIsAborted = false; window.addEventListener('contextmenu', this.contextmenuDisabler); @@ -141,7 +160,8 @@ class ToolsControlComponent extends React.PureComponent { } public componentWillUnmount(): void { - const { canvasInstance } = this.props; + const { canvasInstance, aiToolsRef } = this.props; + aiToolsRef.current = undefined; canvasInstance.html().removeEventListener('canvas.interacted', this.interactionListener); canvasInstance.html().removeEventListener('canvas.canceled', this.cancelListener); } @@ -162,14 +182,14 @@ class ToolsControlComponent extends React.PureComponent { private cancelListener = async (): Promise => { const { - isInteraction, + isActivated, jobInstance, frame, fetchAnnotations, } = this.props; const { interactiveStateID, fetching } = this.state; - if (isInteraction) { + if (isActivated) { if (fetching && !this.interactionIsDone) { // user pressed ESC this.setState({ fetching: false }); @@ -187,12 +207,13 @@ class ToolsControlComponent extends React.PureComponent { } }; - private interactionListener = async (e: Event): Promise => { + private onInteraction = async (e: Event): Promise => { const { frame, labels, + curZOrder, jobInstance, - isInteraction, + isActivated, activeLabelID, fetchAnnotations, updateAnnotations, @@ -200,8 +221,8 @@ class ToolsControlComponent extends React.PureComponent { const { activeInteractor, interactiveStateID, fetching } = this.state; try { - if (!isInteraction) { - throw Error('Canvas raises event "canvas.interacted" when interaction is off'); + if (!isActivated) { + throw Error('Canvas raises event "canvas.interacted" when interaction with it is off'); } if (fetching) { @@ -216,7 +237,6 @@ class ToolsControlComponent extends React.PureComponent { this.setState({ fetching: true }); try { result = await core.lambda.call(jobInstance.task, interactor, { - task: jobInstance.task, frame, points: convertShapesForInteractor((e as CustomEvent).detail.shapes), }); @@ -241,7 +261,7 @@ class ToolsControlComponent extends React.PureComponent { shapeType: ShapeType.POLYGON, points: result.flat(), occluded: false, - zOrder: (e as CustomEvent).detail.zOrder, + zOrder: curZOrder, }); await jobInstance.annotations.put([object]); @@ -260,7 +280,7 @@ class ToolsControlComponent extends React.PureComponent { shapeType: ShapeType.POLYGON, points: result.flat(), occluded: false, - zOrder: (e as CustomEvent).detail.zOrder, + zOrder: curZOrder, }); // need a clientID of a created object to interact with it further // so, we do not use createAnnotationAction @@ -302,6 +322,71 @@ class ToolsControlComponent extends React.PureComponent { } }; + private onTracking = async (e: Event): Promise => { + const { + isActivated, + jobInstance, + frame, + curZOrder, + fetchAnnotations, + } = this.props; + const { activeLabelID } = this.state; + const [label] = jobInstance.task.labels.filter( + (_label: any): boolean => _label.id === activeLabelID, + ); + + if (!(e as CustomEvent).detail.isDone) { + return; + } + + this.interactionIsDone = true; + try { + if (!isActivated) { + throw Error('Canvas raises event "canvas.interacted" when interaction with it is off'); + } + + const { points } = (e as CustomEvent).detail.shapes[0]; + const state = new core.classes.ObjectState({ + shapeType: ShapeType.RECTANGLE, + objectType: ObjectType.TRACK, + zOrder: curZOrder, + label, + points, + frame, + occluded: false, + source: 'auto', + attributes: {}, + }); + + const [clientID] = await jobInstance.annotations.put([state]); + + // update annotations on a canvas + fetchAnnotations(); + + const states = await jobInstance.annotations.get(frame); + const [objectState] = states + .filter((_state: any): boolean => _state.clientID === clientID); + await this.trackState(objectState); + } catch (err) { + notification.error({ + description: err.toString(), + message: 'Tracking error occured', + }); + } + }; + + private interactionListener = async (e: Event): Promise => { + const { mode } = this.state; + + if (mode === 'interaction') { + await this.onInteraction(e); + } + + if (mode === 'tracking') { + await this.onTracking(e); + } + }; + private setActiveInteractor = (key: string): void => { const { interactors } = this.props; this.setState({ @@ -311,6 +396,72 @@ class ToolsControlComponent extends React.PureComponent { }); }; + private setActiveTracker = (key: string): void => { + const { trackers } = this.props; + this.setState({ + activeTracker: trackers.filter( + (tracker: Model) => tracker.id === key, + )[0], + }); + }; + + public async trackState(state: any): Promise { + const { jobInstance, frame } = this.props; + const { activeTracker, trackingFrames } = this.state; + const { clientID, points } = state; + + const tracker = activeTracker as Model; + try { + this.setState({ trackingProgress: 0, fetching: true }); + let response = await core.lambda.call(jobInstance.task, tracker, { + task: jobInstance.task, + frame, + shape: points, + }); + + for (const offset of range(1, trackingFrames + 1)) { + /* eslint-disable no-await-in-loop */ + const states = await jobInstance.annotations.get(frame + offset); + const [objectState] = states + .filter((_state: any): boolean => _state.clientID === clientID); + response = await core.lambda.call(jobInstance.task, tracker, { + task: jobInstance.task, + frame: frame + offset, + shape: response.points, + state: response.state, + }); + + const reduced = response.shape + .reduce((acc: number[], value: number, index: number): number[] => { + if (index % 2) { // y + acc[1] = Math.min(acc[1], value); + acc[3] = Math.max(acc[3], value); + } else { // x + acc[0] = Math.min(acc[0], value); + acc[2] = Math.max(acc[2], value); + } + return acc; + }, [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, + ]); + + objectState.points = reduced; + await objectState.save(); + + this.setState({ trackingProgress: offset / trackingFrames }); + } + } finally { + this.setState({ trackingProgress: null, fetching: false }); + } + } + + public trackingAvailable(): boolean { + const { activeTracker, trackingFrames } = this.state; + const { trackers } = this.props; + + return !!trackingFrames && !!trackers.length && activeTracker !== null; + } + private renderLabelBlock(): JSX.Element { const { labels } = this.props; const { activeLabelID } = this.state; @@ -355,10 +506,119 @@ class ToolsControlComponent extends React.PureComponent { ); } + private renderTrackerBlock(): JSX.Element { + const { + trackers, + canvasInstance, + jobInstance, + frame, + onInteractionStart, + } = this.props; + const { + activeTracker, + activeLabelID, + fetching, + trackingFrames, + } = this.state; + + if (!trackers.length) { + return ( + + + No available trackers found + + + ); + } + + return ( + <> + + + Tracker + + + + + + + + + + Tracking frames + + + { + if (typeof (value) !== 'undefined') { + this.setState({ + trackingFrames: value, + }); + } + }} + /> + + + + + + + + + ); + } + private renderInteractorBlock(): JSX.Element { const { interactors, canvasInstance, onInteractionStart } = this.props; const { activeInteractor, activeLabelID, fetching } = this.state; + if (!interactors.length) { + return ( + + + No available interactors found + + + ); + } + return ( <> @@ -389,6 +649,10 @@ class ToolsControlComponent extends React.PureComponent { className='cvat-tools-interact-button' disabled={!activeInteractor || fetching} onClick={() => { + this.setState({ + mode: 'interaction', + }); + if (activeInteractor) { canvasInstance.cancel(); canvasInstance.interact({ @@ -413,10 +677,21 @@ class ToolsControlComponent extends React.PureComponent { const { jobInstance, detectors, + curZOrder, frame, fetchAnnotations, } = this.props; + if (!detectors.length) { + return ( + + + No available detectors found + + + ); + } + return ( { task={jobInstance.task} runInference={async (task: any, model: Model, body: object) => { try { + this.setState({ + mode: 'detection', + }); + this.setState({ fetching: true }); const result = await core.lambda.call(task, model, { ...body, @@ -444,7 +723,7 @@ class ToolsControlComponent extends React.PureComponent { occluded: false, source: 'auto', attributes: {}, - zOrder: 0, // TODO: get current z order + zOrder: curZOrder, }) )); @@ -471,7 +750,7 @@ class ToolsControlComponent extends React.PureComponent { AI Tools - + { this.renderLabelBlock() } { this.renderInteractorBlock() } @@ -479,24 +758,34 @@ class ToolsControlComponent extends React.PureComponent { { this.renderDetectorBlock() } + + { this.renderLabelBlock() } + { this.renderTrackerBlock() } + ); } public render(): JSX.Element | null { - const { interactors, isInteraction, canvasInstance } = this.props; - const { fetching } = this.state; + const { + interactors, + detectors, + trackers, + isActivated, + canvasInstance, + } = this.props; + const { fetching, trackingProgress } = this.state; - if (!interactors.length) return null; + if (![...interactors, ...detectors, ...trackers].length) return null; - const dynamcPopoverPros = isInteraction ? { + const dynamcPopoverPros = isActivated ? { overlayStyle: { display: 'none', }, } : {}; - const dynamicIconProps = isInteraction ? { + const dynamicIconProps = isActivated ? { className: 'cvat-active-canvas-control cvat-tools-control', onClick: (): void => { canvasInstance.interact({ enabled: false }); @@ -517,12 +806,15 @@ class ToolsControlComponent extends React.PureComponent { > Waiting for a server response.. + { trackingProgress !== null && ( + + )} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-basics.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-basics.tsx index bbfc0e03..99765187 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-basics.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-basics.tsx @@ -41,6 +41,7 @@ interface Props { toBackground(): void; toForeground(): void; resetCuboidPerspective(): void; + activateTracking(): void; } function ItemTopComponent(props: Props): JSX.Element { @@ -72,6 +73,7 @@ function ItemTopComponent(props: Props): JSX.Element { toBackground, toForeground, resetCuboidPerspective, + activateTracking, } = props; const [menuVisible, setMenuVisible] = useState(false); @@ -150,6 +152,7 @@ function ItemTopComponent(props: Props): JSX.Element { toForeground, resetCuboidPerspective, changeColorPickerVisible, + activateTracking, })} > diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx index 99db3b84..5fe8af31 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx @@ -33,16 +33,17 @@ interface Props { toBackgroundShortcut: string; toForegroundShortcut: string; removeShortcut: string; - changeColor: (value: string) => void; - copy: (() => void); - remove: (() => void); - propagate: (() => void); - createURL: (() => void); - switchOrientation: (() => void); - toBackground: (() => void); - toForeground: (() => void); - resetCuboidPerspective: (() => void); - changeColorPickerVisible: (visible: boolean) => void; + changeColor(value: string): void; + copy(): void; + remove(): void; + propagate(): void; + createURL(): void; + switchOrientation(): void; + toBackground(): void; + toForeground(): void; + resetCuboidPerspective(): void; + changeColorPickerVisible(visible: boolean): void; + activateTracking(): void; } export default function ItemMenu(props: Props): JSX.Element { @@ -71,6 +72,7 @@ export default function ItemMenu(props: Props): JSX.Element { toForeground, resetCuboidPerspective, changeColorPickerVisible, + activateTracking, } = props; return ( @@ -94,6 +96,16 @@ export default function ItemMenu(props: Props): JSX.Element { + {objectType === ObjectType.TRACK && shapeType === ShapeType.RECTANGLE && ( + + + + + + )} { [ShapeType.POLYGON, ShapeType.POLYLINE, ShapeType.CUBOID].includes(shapeType) && ( - ); - }) - } + }, + )} ); } diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index d98d4284..ed87c19c 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -26,14 +26,7 @@ interface Props { } export default function AnnotationPageComponent(props: Props): JSX.Element { - const { - job, - fetching, - getJob, - closeJob, - saveLogs, - workspace, - } = props; + const { job, fetching, getJob, closeJob, saveLogs, workspace } = props; const history = useHistory(); useEffect(() => { @@ -63,7 +56,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { return ; } - if (typeof (job) === 'undefined') { + if (typeof job === 'undefined') { return ( - { workspace === Workspace.STANDARD && ( + {workspace === Workspace.STANDARD && ( )} - { workspace === Workspace.ATTRIBUTE_ANNOTATION && ( + {workspace === Workspace.ATTRIBUTE_ANNOTATION && ( )} - { workspace === Workspace.TAG_ANNOTATION && ( + {workspace === Workspace.TAG_ANNOTATION && ( diff --git a/cvat-ui/src/components/annotation-page/annotations-filters-input.tsx b/cvat-ui/src/components/annotation-page/annotations-filters-input.tsx index f11893db..064dd9c4 100644 --- a/cvat-ui/src/components/annotation-page/annotations-filters-input.tsx +++ b/cvat-ui/src/components/annotation-page/annotations-filters-input.tsx @@ -32,14 +32,9 @@ interface DispatchToProps { function mapStateToProps(state: CombinedState): StateToProps { const { annotation: { - annotations: { - filters: annotationsFilters, - filtersHistory: annotationsFiltersHistory, - }, - }, - shortcuts: { - normalizedKeyMap, + annotations: { filters: annotationsFilters, filtersHistory: annotationsFiltersHistory }, }, + shortcuts: { normalizedKeyMap }, } = state; return { @@ -53,13 +48,12 @@ function mapStateToProps(state: CombinedState): StateToProps { function mapDispatchToProps(dispatch: any): DispatchToProps { return { changeAnnotationsFilters(value: SelectValue) { - if (typeof (value) === 'string') { + if (typeof value === 'string') { dispatch(changeAnnotationsFiltersAction([value])); dispatch(fetchAnnotationsAsync()); - } else if (Array.isArray(value) - && value.every((element: string | number | LabeledValue): boolean => ( - typeof (element) === 'string' - )) + } else if ( + Array.isArray(value) && + value.every((element: string | number | LabeledValue): boolean => typeof element === 'string') ) { dispatch(changeAnnotationsFiltersAction(value as string[])); dispatch(fetchAnnotationsAsync()); @@ -68,40 +62,32 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { }; } -function filtersHelpModalContent( - searchForwardShortcut: string, - searchBackwardShortcut: string, -): JSX.Element { +function filtersHelpModalContent(searchForwardShortcut: string, searchBackwardShortcut: string): JSX.Element { return ( <> General - You can use filters to display only subset of objects on a frame - or to search objects that satisfy the filters using hotkeys - - {` ${searchForwardShortcut} `} - + You can use filters to display only subset of objects on a frame or to search objects that satisfy the + filters using hotkeys + {` ${searchForwardShortcut} `} and - - {` ${searchBackwardShortcut} `} - + {` ${searchBackwardShortcut} `} Supported properties: width, height, label, serverID, clientID, type, shape, occluded
Supported operators: - ==, !=, >, >=, <, <=, (), & and | + ==, !=, >, >=, <, <=, (), & and |
- If you have double quotes in your query string, - please escape them using back slash: \" (see the latest example) + If you have double quotes in your query string, please escape them using back slash: \" (see + the latest example)
- All properties and values are case-sensitive. - CVAT uses json queries to perform search. + All properties and values are case-sensitive. CVAT uses json queries to perform search.
Examples @@ -112,13 +98,12 @@ function filtersHelpModalContent(
  • attr["Attribute 1"] == attr["Attribute 2"]
  • clientID == 50
  • - (label=="car" & attr["parked"]==true) - | (label=="pedestrian" & width > 150) + (label=="car" & attr["parked"]==true) | (label=="pedestrian" + & width > 150)
  • - (( label==["car \"mazda\""]) - & (attr["sunglasses"]==true - | (width > 150 | height > 150 & (clientID == serverID))))) + (( label==["car \"mazda\""]) & (attr["sunglasses"]==true | + (width > 150 | height > 150 & (clientID == serverID)))))
  • @@ -155,10 +140,7 @@ function AnnotationsFiltersInput(props: StateToProps & DispatchToProps): JSX.Ele Modal.info({ width: 700, title: 'How to use filters?', - content: filtersHelpModalContent( - searchForwardShortcut, - searchBackwardShortcut, - ), + content: filtersHelpModalContent(searchForwardShortcut, searchBackwardShortcut), }); }} /> @@ -175,15 +157,15 @@ function AnnotationsFiltersInput(props: StateToProps & DispatchToProps): JSX.Ele onMouseEnter={() => setUnderCursor(true)} onMouseLeave={() => setUnderCursor(false)} > - {annotationsFiltersHistory.map((element: string): JSX.Element => ( - {element} - ))} + {annotationsFiltersHistory.map( + (element: string): JSX.Element => ( + + {element} + + ), + )} ); } - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(AnnotationsFiltersInput); +export default connect(mapStateToProps, mapDispatchToProps)(AnnotationsFiltersInput); diff --git a/cvat-ui/src/components/annotation-page/appearance-block.tsx b/cvat-ui/src/components/annotation-page/appearance-block.tsx index 42e8fc20..4ba3e749 100644 --- a/cvat-ui/src/components/annotation-page/appearance-block.tsx +++ b/cvat-ui/src/components/annotation-page/appearance-block.tsx @@ -52,9 +52,7 @@ interface DispatchToProps { export function computeHeight(): number { const [sidebar] = window.document.getElementsByClassName('cvat-objects-sidebar'); const [appearance] = window.document.getElementsByClassName('cvat-objects-appearance-collapse'); - const [tabs] = Array.from( - window.document.querySelectorAll('.cvat-objects-sidebar-tabs > .ant-tabs-card-bar'), - ); + const [tabs] = Array.from(window.document.querySelectorAll('.cvat-objects-sidebar-tabs > .ant-tabs-card-bar')); if (sidebar && appearance && tabs) { const maxHeight = sidebar ? sidebar.clientHeight : 0; @@ -68,19 +66,9 @@ export function computeHeight(): number { function mapStateToProps(state: CombinedState): StateToProps { const { - annotation: { - appearanceCollapsed, - }, + annotation: { appearanceCollapsed }, settings: { - shapes: { - colorBy, - opacity, - selectedOpacity, - outlined, - outlineColor, - showBitmap, - showProjections, - }, + shapes: { colorBy, opacity, selectedOpacity, outlined, outlineColor, showBitmap, showProjections }, }, } = state; @@ -96,13 +84,11 @@ function mapStateToProps(state: CombinedState): StateToProps { }; } - function mapDispatchToProps(dispatch: Dispatch): DispatchToProps { return { collapseAppearance(): void { dispatch(collapseAppearanceAction()); - const [collapser] = window.document - .getElementsByClassName('cvat-objects-appearance-collapse'); + const [collapser] = window.document.getElementsByClassName('cvat-objects-appearance-collapse'); if (collapser) { const listener = (event: Event): void => { @@ -164,12 +150,7 @@ function AppearanceBlock(props: Props): JSX.Element { activeKey={appearanceCollapsed ? [] : ['appearance']} className='cvat-objects-appearance-collapse' > - Appearance - } - key='appearance' - > + Appearance} key='appearance'>
    Color by @@ -178,19 +159,9 @@ function AppearanceBlock(props: Props): JSX.Element { {ColorBy.GROUP} Opacity - + Selected opacity - + { changeShapesOutlinedBorders(event.target.checked, outlineColor); @@ -209,16 +180,10 @@ function AppearanceBlock(props: Props): JSX.Element { - + Show bitmap - + Show projections
    @@ -227,7 +192,4 @@ function AppearanceBlock(props: Props): JSX.Element { ); } -export default connect( - mapStateToProps, - mapDispatchToProps, -)(React.memo(AppearanceBlock)); +export default connect(mapStateToProps, mapDispatchToProps)(React.memo(AppearanceBlock)); diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx index 71170227..b3179fb8 100644 --- a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx @@ -29,7 +29,6 @@ import AttributeSwitcher from './attribute-switcher'; import ObjectBasicsEditor from './object-basics-edtior'; import AttributeEditor from './attribute-editor'; - interface StateToProps { activatedStateID: number | null; activatedAttributeID: number | null; @@ -60,23 +59,12 @@ function mapStateToProps(state: CombinedState): StateToProps { activatedStateID, activatedAttributeID, states, - zLayer: { - cur, - }, - }, - job: { - instance: jobInstance, - labels, + zLayer: { cur }, }, - canvas: { - instance: canvasInstance, - ready: canvasIsReady, - }, - }, - shortcuts: { - keyMap, - normalizedKeyMap, + job: { instance: jobInstance, labels }, + canvas: { instance: canvasInstance, ready: canvasIsReady }, }, + shortcuts: { keyMap, normalizedKeyMap }, } = state; return { @@ -124,9 +112,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. curZLayer, } = props; - const filteredStates = states.filter((state) => !state.outside - && !state.hidden - && state.zOrder <= curZLayer); + const filteredStates = states.filter((state) => !state.outside && !state.hidden && state.zOrder <= curZLayer); const [labelAttrMap, setLabelAttrMap] = useState( labels.reduce((acc, label): LabelAttrMap => { acc[label.id] = label.attributes.length ? label.attributes[0] : null; @@ -137,13 +123,16 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const collapse = (): void => { - const [collapser] = window.document - .getElementsByClassName('attribute-annotation-sidebar'); + const [collapser] = window.document.getElementsByClassName('attribute-annotation-sidebar'); if (collapser) { - collapser.addEventListener('transitionend', () => { - canvasInstance.fitCanvas(); - }, { once: true }); + collapser.addEventListener( + 'transitionend', + () => { + canvasInstance.fitCanvas(); + }, + { once: true }, + ); } setSidebarCollapsed(!sidebarCollapsed); @@ -151,12 +140,10 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. const indexes = filteredStates.map((state) => state.clientID); const activatedIndex = indexes.indexOf(activatedStateID); - const activeObjectState = activatedStateID === null || activatedIndex === -1 - ? null : filteredStates[activatedIndex]; + const activeObjectState = + activatedStateID === null || activatedIndex === -1 ? null : filteredStates[activatedIndex]; - const activeAttribute = activeObjectState - ? labelAttrMap[activeObjectState.label.id] - : null; + const activeAttribute = activeObjectState ? labelAttrMap[activeObjectState.label.id] : null; if (canvasIsReady) { if (activeObjectState) { @@ -275,8 +262,8 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. NEXT_KEY_FRAME: (event: KeyboardEvent | undefined) => { preventDefault(event); if (activeObjectState && activeObjectState.objectType === ObjectType.TRACK) { - const frame = typeof (activeObjectState.keyframes.next) === 'number' - ? activeObjectState.keyframes.next : null; + const frame = + typeof activeObjectState.keyframes.next === 'number' ? activeObjectState.keyframes.next : null; if (frame !== null && canvasInstance.isAbleToChangeFrame()) { changeFrame(frame); } @@ -285,8 +272,8 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. PREV_KEY_FRAME: (event: KeyboardEvent | undefined) => { preventDefault(event); if (activeObjectState && activeObjectState.objectType === ObjectType.TRACK) { - const frame = typeof (activeObjectState.keyframes.prev) === 'number' - ? activeObjectState.keyframes.prev : null; + const frame = + typeof activeObjectState.keyframes.prev === 'number' ? activeObjectState.keyframes.prev : null; if (frame !== null && canvasInstance.isAbleToChangeFrame()) { changeFrame(frame); } @@ -304,8 +291,11 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. ant-layout-sider-zero-width-trigger-left`} onClick={collapse} > - {sidebarCollapsed ? - : } + {sidebarCollapsed ? ( + + ) : ( + + )} @@ -327,8 +317,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. labels={labels} changeLabel={(value: SelectValue): void => { const labelName = value as string; - const [newLabel] = labels - .filter((_label): boolean => _label.name === labelName); + const [newLabel] = labels.filter((_label): boolean => _label.name === labelName); activeObjectState.label = newLabel; updateAnnotations([activeObjectState]); }} @@ -339,46 +328,39 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. hiddenDisabled keyframeDisabled /> - { - activeAttribute - ? ( - <> - - { - const { attributes } = activeObjectState; - jobInstance.logger.log( - LogType.changeAttribute, { - id: activeAttribute.id, - object_id: activeObjectState.clientID, - value, - }, - ); - attributes[activeAttribute.id] = value; - activeObjectState.attributes = attributes; - updateAnnotations([activeObjectState]); - }} - /> - - - ) : ( -
    - No attributes found -
    - ) - } - - { !sidebarCollapsed && } + {activeAttribute ? ( + <> + + { + const { attributes } = activeObjectState; + jobInstance.logger.log(LogType.changeAttribute, { + id: activeAttribute.id, + object_id: activeObjectState.clientID, + value, + }); + attributes[activeAttribute.id] = value; + activeObjectState.attributes = attributes; + updateAnnotations([activeObjectState]); + }} + /> + + ) : ( +
    + No attributes found +
    + )} + + {!sidebarCollapsed && } ); } @@ -392,8 +374,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. ant-layout-sider-zero-width-trigger-left`} onClick={collapse} > - {sidebarCollapsed ? - : } + {sidebarCollapsed ? : } @@ -407,8 +388,4 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. ); } - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(AttributeAnnotationSidebar); +export default connect(mapStateToProps, mapDispatchToProps)(AttributeAnnotationSidebar); diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-editor.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-editor.tsx index 2f0b4c00..b6356e9e 100644 --- a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-editor.tsx +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-editor.tsx @@ -22,23 +22,14 @@ interface InputElementParameters { } function renderInputElement(parameters: InputElementParameters): JSX.Element { - const { - inputType, - attrID, - clientID, - values, - currentValue, - onChange, - } = parameters; + const { inputType, attrID, clientID, values, currentValue, onChange } = parameters; const renderCheckbox = (): JSX.Element => ( <> Checkbox:
    ( - onChange(event.target.checked ? 'true' : 'false') - )} + onChange={(event: CheckboxChangeEvent): void => onChange(event.target.checked ? 'true' : 'false')} checked={currentValue === 'true'} />
    @@ -52,16 +43,15 @@ function renderInputElement(parameters: InputElementParameters): JSX.Element { @@ -71,28 +61,21 @@ function renderInputElement(parameters: InputElementParameters): JSX.Element { <> Values:
    - ( - onChange(event.target.value) + onChange(event.target.value)}> + {values.map( + (value: string): JSX.Element => ( + + {value === consts.UNDEFINED_ATTRIBUTE_VALUE ? consts.NO_BREAK_SPACE : value} + + ), )} - > - {values.map((value: string): JSX.Element => ( - - {value === consts.UNDEFINED_ATTRIBUTE_VALUE - ? consts.NO_BREAK_SPACE : value} - - ))}
    ); const handleKeydown = (event: React.KeyboardEvent): void => { - if (['ArrowDown', 'ArrowUp', 'ArrowLeft', - 'ArrowRight', 'Tab', 'Shift', 'Control'] - .includes(event.key) - ) { + if (['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'Tab', 'Shift', 'Control'].includes(event.key)) { event.preventDefault(); const copyEvent = new KeyboardEvent('keydown', event); window.document.dispatchEvent(copyEvent); @@ -137,11 +120,7 @@ function renderInputElement(parameters: InputElementParameters): JSX.Element { element = renderText(); } - return ( -
    - {element} -
    - ); + return
    {element}
    ; } interface ListParameters { @@ -203,8 +182,7 @@ function renderList(parameters: ListParameters): JSX.Element | null { [key: string]: (keyEvent?: KeyboardEvent) => void; } = {}; - const filteredValues = values - .filter((value: string): boolean => value !== consts.UNDEFINED_ATTRIBUTE_VALUE); + const filteredValues = values.filter((value: string): boolean => value !== consts.UNDEFINED_ATTRIBUTE_VALUE); filteredValues.slice(0, 10).forEach((value: string, index: number): void => { const key = `SET_${index}_VALUE`; keyMap[key] = { @@ -226,12 +204,14 @@ function renderList(parameters: ListParameters): JSX.Element | null { return (
    - {filteredValues.map((value: string, index: number): JSX.Element => ( -
    - {`${index}:`} - {` ${value}`} -
    - ))} + {filteredValues.map( + (value: string, index: number): JSX.Element => ( +
    + {`${index}:`} + {` ${value}`} +
    + ), + )}
    ); } @@ -266,12 +246,7 @@ interface Props { } function AttributeEditor(props: Props): JSX.Element { - const { - attribute, - currentValue, - onChange, - clientID, - } = props; + const { attribute, currentValue, onChange, clientID } = props; const { inputType, values, id: attrID } = attribute; return ( diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-switcher.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-switcher.tsx index 66357b2f..0f812a77 100644 --- a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-switcher.tsx +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-switcher.tsx @@ -17,13 +17,7 @@ interface Props { } function AttributeSwitcher(props: Props): JSX.Element { - const { - currentAttribute, - currentIndex, - attributesCount, - nextAttribute, - normalizedKeyMap, - } = props; + const { currentAttribute, currentIndex, attributesCount, nextAttribute, normalizedKeyMap } = props; const title = `${currentAttribute} [${currentIndex + 1}/${attributesCount}]`; return ( diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/object-basics-edtior.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/object-basics-edtior.tsx index ea9db335..fafe22cd 100644 --- a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/object-basics-edtior.tsx +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/object-basics-edtior.tsx @@ -17,14 +17,13 @@ function ObjectBasicsEditor(props: Props): JSX.Element { return (
    ); diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/object-switcher.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/object-switcher.tsx index 5d69834d..515e7276 100644 --- a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/object-switcher.tsx +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/object-switcher.tsx @@ -19,15 +19,7 @@ interface Props { } function ObjectSwitcher(props: Props): JSX.Element { - const { - currentLabel, - clientID, - objectsCount, - currentIndex, - nextObject, - normalizedKeyMap, - } = props; - + const { currentLabel, clientID, objectsCount, currentIndex, nextObject, normalizedKeyMap } = props; const title = `${currentLabel} ${clientID} [${currentIndex + 1}/${objectsCount}]`; return ( diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-point-context-menu.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-point-context-menu.tsx index 82055ea0..d871ce30 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-point-context-menu.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-point-context-menu.tsx @@ -23,25 +23,18 @@ interface StateToProps { function mapStateToProps(state: CombinedState): StateToProps { const { annotation: { - annotations: { - states, - activatedStateID, - }, + annotations: { states, activatedStateID }, canvas: { - contextMenu: { - visible, - top, - left, - type, - pointID: selectedPoint, - }, + contextMenu: { visible, top, left, type, pointID: selectedPoint }, }, }, } = state; return { - activatedState: activatedStateID === null - ? null : states.filter((_state) => _state.clientID === activatedStateID)[0] || null, + activatedState: + activatedStateID === null + ? null + : states.filter((_state) => _state.clientID === activatedStateID)[0] || null, selectedPoint, visible, left, @@ -69,15 +62,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { type Props = StateToProps & DispatchToProps; function CanvasPointContextMenu(props: Props): React.ReactPortal | null { - const { - onCloseContextMenu, - onUpdateAnnotations, - activatedState, - visible, - type, - top, - left, - } = props; + const { onCloseContextMenu, onUpdateAnnotations, activatedState, visible, type, top, left } = props; const [contextMenuFor, setContextMenuFor] = useState(activatedState); @@ -91,7 +76,8 @@ function CanvasPointContextMenu(props: Props): React.ReactPortal | null { const onPointDelete = (): void => { const { selectedPoint } = props; if (contextMenuFor && selectedPoint !== null) { - contextMenuFor.points = contextMenuFor.points.slice(0, selectedPoint * 2) + contextMenuFor.points = contextMenuFor.points + .slice(0, selectedPoint * 2) .concat(contextMenuFor.points.slice(selectedPoint * 2 + 2)); onUpdateAnnotations([contextMenuFor]); onCloseContextMenu(); @@ -101,7 +87,8 @@ function CanvasPointContextMenu(props: Props): React.ReactPortal | null { const onSetStartPoint = (): void => { const { selectedPoint } = props; if (contextMenuFor && selectedPoint !== null && contextMenuFor.shapeType === 'polygon') { - contextMenuFor.points = contextMenuFor.points.slice(selectedPoint * 2) + contextMenuFor.points = contextMenuFor.points + .slice(selectedPoint * 2) .concat(contextMenuFor.points.slice(0, selectedPoint * 2)); onUpdateAnnotations([contextMenuFor]); onCloseContextMenu(); @@ -109,24 +96,22 @@ function CanvasPointContextMenu(props: Props): React.ReactPortal | null { }; return visible && contextMenuFor && type === ContextMenuType.CANVAS_SHAPE_POINT - ? (ReactDOM.createPortal( -
    - - - - {contextMenuFor && contextMenuFor.shapeType === 'polygon' && ( - - )} -
    , - window.document.body, - )) : null; + ? ReactDOM.createPortal( +
    + + + + {contextMenuFor && contextMenuFor.shapeType === 'polygon' && ( + + )} +
    , + window.document.body, + ) + : null; } -export default connect( - mapStateToProps, - mapDispatchToProps, -)(CanvasPointContextMenu); +export default connect(mapStateToProps, mapDispatchToProps)(CanvasPointContextMenu); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index 35de27e1..d760411a 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -10,14 +10,7 @@ import Icon from 'antd/lib/icon'; import Layout from 'antd/lib/layout/layout'; import Slider, { SliderValue } from 'antd/lib/slider'; -import { - ColorBy, - GridColor, - ObjectType, - ContextMenuType, - Workspace, - ShapeType, -} from 'reducers/interfaces'; +import { ColorBy, GridColor, ObjectType, ContextMenuType, Workspace, ShapeType } from 'reducers/interfaces'; import { LogType } from 'cvat-logger'; import { Canvas } from 'cvat-canvas-wrapper'; import getCore from 'cvat-core-wrapper'; @@ -83,8 +76,7 @@ interface Props { onSplitAnnotations(sessionInstance: any, frame: number, state: any): void; onActivateObject(activatedStateID: number | null): void; onSelectObjects(selectedStatesID: number[]): void; - onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, - pointID?: number): void; + onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void; onAddZLayer(): void; onSwitchZLayer(cur: number): void; onChangeBrightnessLevel(level: number): void; @@ -99,16 +91,11 @@ interface Props { export default class CanvasWrapperComponent extends React.PureComponent { public componentDidMount(): void { - const { - automaticBordering, - showObjectsTextAlways, - canvasInstance, - } = this.props; + const { automaticBordering, showObjectsTextAlways, canvasInstance } = this.props; // It's awful approach from the point of view React // But we do not have another way because cvat-canvas returns regular DOM element - const [wrapper] = window.document - .getElementsByClassName('cvat-canvas-container'); + const [wrapper] = window.document.getElementsByClassName('cvat-canvas-container'); wrapper.appendChild(canvasInstance.html()); canvasInstance.configure({ @@ -154,9 +141,10 @@ export default class CanvasWrapperComponent extends React.PureComponent { onFetchAnnotation, } = this.props; - if (prevProps.showObjectsTextAlways !== showObjectsTextAlways - || prevProps.automaticBordering !== automaticBordering - || prevProps.showProjections !== showProjections + if ( + prevProps.showObjectsTextAlways !== showObjectsTextAlways || + prevProps.automaticBordering !== automaticBordering || + prevProps.showProjections !== showProjections ) { canvasInstance.configure({ undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE, @@ -173,14 +161,17 @@ export default class CanvasWrapperComponent extends React.PureComponent { if (prevProps.sidebarCollapsed !== sidebarCollapsed) { const [sidebar] = window.document.getElementsByClassName('cvat-objects-sidebar'); if (sidebar) { - sidebar.addEventListener('transitionend', () => { - canvasInstance.fitCanvas(); - }, { once: true }); + sidebar.addEventListener( + 'transitionend', + () => { + canvasInstance.fitCanvas(); + }, + { once: true }, + ); } } - if (prevProps.activatedStateID !== null - && prevProps.activatedStateID !== activatedStateID) { + if (prevProps.activatedStateID !== null && prevProps.activatedStateID !== activatedStateID) { canvasInstance.activate(null); const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`); if (el) { @@ -192,9 +183,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.grid(gridSize, gridSize); } - if (gridOpacity !== prevProps.gridOpacity - || gridColor !== prevProps.gridColor - || grid !== prevProps.grid) { + if (gridOpacity !== prevProps.gridOpacity || gridColor !== prevProps.gridColor || grid !== prevProps.grid) { const gridElement = window.document.getElementById('cvat_canvas_grid'); const gridPattern = window.document.getElementById('cvat_canvas_grid_pattern'); if (gridElement) { @@ -206,35 +195,47 @@ export default class CanvasWrapperComponent extends React.PureComponent { } } - if (brightnessLevel !== prevProps.brightnessLevel - || contrastLevel !== prevProps.contrastLevel - || saturationLevel !== prevProps.saturationLevel) { + if ( + brightnessLevel !== prevProps.brightnessLevel || + contrastLevel !== prevProps.contrastLevel || + saturationLevel !== prevProps.saturationLevel + ) { const backgroundElement = window.document.getElementById('cvat_canvas_background'); if (backgroundElement) { - backgroundElement.style.filter = `brightness(${brightnessLevel / 100})` - + `contrast(${contrastLevel / 100})` - + `saturate(${saturationLevel / 100})`; + backgroundElement.style.filter = + `brightness(${brightnessLevel / 100})` + + `contrast(${contrastLevel / 100})` + + `saturate(${saturationLevel / 100})`; } } - if (prevProps.annotations !== annotations - || prevProps.frameData !== frameData - || prevProps.curZLayer !== curZLayer) { + if ( + prevProps.annotations !== annotations || + prevProps.frameData !== frameData || + prevProps.curZLayer !== curZLayer + ) { this.updateCanvas(); } - if (prevProps.frame !== frameData.number - && ((resetZoom && workspace !== Workspace.ATTRIBUTE_ANNOTATION) - || workspace === Workspace.TAG_ANNOTATION) + if ( + prevProps.frame !== frameData.number && + ((resetZoom && workspace !== Workspace.ATTRIBUTE_ANNOTATION) || workspace === Workspace.TAG_ANNOTATION) ) { - canvasInstance.html().addEventListener('canvas.setup', () => { - canvasInstance.fit(); - }, { once: true }); + canvasInstance.html().addEventListener( + 'canvas.setup', + () => { + canvasInstance.fit(); + }, + { once: true }, + ); } - if (prevProps.opacity !== opacity || prevProps.outlined !== outlined - || prevProps.outlineColor !== outlineColor - || prevProps.selectedOpacity !== selectedOpacity || prevProps.colorBy !== colorBy + if ( + prevProps.opacity !== opacity || + prevProps.outlined !== outlined || + prevProps.outlineColor !== outlineColor || + prevProps.selectedOpacity !== selectedOpacity || + prevProps.colorBy !== colorBy ) { this.updateShapesView(); } @@ -257,7 +258,9 @@ export default class CanvasWrapperComponent extends React.PureComponent { } if (prevProps.canvasBackgroundColor !== canvasBackgroundColor) { - const canvasWrapperElement = window.document.getElementsByClassName('cvat-canvas-container').item(0) as HTMLElement | null; + const canvasWrapperElement = window.document + .getElementsByClassName('cvat-canvas-container') + .item(0) as HTMLElement | null; if (canvasWrapperElement) { canvasWrapperElement.style.backgroundColor = canvasBackgroundColor; } @@ -301,14 +304,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { } private onCanvasShapeDrawn = (event: any): void => { - const { - jobInstance, - activeLabelID, - activeObjectType, - frame, - onShapeDrawn, - onCreateAnnotations, - } = this.props; + const { jobInstance, activeLabelID, activeObjectType, frame, onShapeDrawn, onCreateAnnotations } = this.props; if (!event.detail.continue) { onShapeDrawn(); @@ -323,8 +319,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { } state.objectType = state.objectType || activeObjectType; - state.label = state.label || jobInstance.task.labels - .filter((label: any) => label.id === activeLabelID)[0]; + state.label = state.label || jobInstance.task.labels.filter((label: any) => label.id === activeLabelID)[0]; state.occluded = state.occluded || false; state.frame = frame; const objectState = new cvat.classes.ObjectState(state); @@ -332,12 +327,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { }; private onCanvasObjectsMerged = (event: any): void => { - const { - jobInstance, - frame, - onMergeAnnotations, - onMergeObjects, - } = this.props; + const { jobInstance, frame, onMergeAnnotations, onMergeObjects } = this.props; onMergeObjects(false); @@ -350,12 +340,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { }; private onCanvasObjectsGroupped = (event: any): void => { - const { - jobInstance, - frame, - onGroupAnnotations, - onGroupObjects, - } = this.props; + const { jobInstance, frame, onGroupAnnotations, onGroupObjects } = this.props; onGroupObjects(false); @@ -364,12 +349,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { }; private onCanvasTrackSplitted = (event: any): void => { - const { - jobInstance, - frame, - onSplitAnnotations, - onSplitTrack, - } = this.props; + const { jobInstance, frame, onSplitAnnotations, onSplitTrack } = this.props; onSplitTrack(false); @@ -399,14 +379,10 @@ export default class CanvasWrapperComponent extends React.PureComponent { }; private onCanvasContextMenu = (e: MouseEvent): void => { - const { - activatedStateID, - onUpdateContextMenu, - } = this.props; + const { activatedStateID, onUpdateContextMenu } = this.props; if (e.target && !(e.target as HTMLElement).classList.contains('svg_select_points')) { - onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY, - ContextMenuType.CANVAS_SHAPE); + onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY, ContextMenuType.CANVAS_SHAPE); } }; @@ -434,8 +410,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { private onCanvasShapeClicked = (e: any): void => { const { clientID } = e.detail.state; - const sidebarItem = window.document - .getElementById(`cvat-objects-sidebar-state-item-${clientID}`); + const sidebarItem = window.document.getElementById(`cvat-objects-sidebar-state-item-${clientID}`); if (sidebarItem) { sidebarItem.scrollIntoView(); } @@ -454,22 +429,13 @@ export default class CanvasWrapperComponent extends React.PureComponent { }; private onCanvasCursorMoved = async (event: any): Promise => { - const { - jobInstance, - activatedStateID, - workspace, - onActivateObject, - } = this.props; + const { jobInstance, activatedStateID, workspace, onActivateObject } = this.props; if (workspace !== Workspace.STANDARD) { return; } - const result = await jobInstance.annotations.select( - event.detail.states, - event.detail.x, - event.detail.y, - ); + const result = await jobInstance.annotations.select(event.detail.states, event.detail.x, event.detail.y); if (result && result.state) { if (result.state.shapeType === 'polyline' || result.state.shapeType === 'points') { @@ -491,17 +457,11 @@ export default class CanvasWrapperComponent extends React.PureComponent { }; private onCanvasEditDone = (event: any): void => { - const { - onEditShape, - onUpdateAnnotations, - } = this.props; + const { onEditShape, onUpdateAnnotations } = this.props; onEditShape(false); - const { - state, - points, - } = event.detail; + const { state, points } = event.detail; state.points = points; onUpdateAnnotations([state]); }; @@ -541,8 +501,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { private onCanvasFindObject = async (e: any): Promise => { const { jobInstance, canvasInstance } = this.props; - const result = await jobInstance.annotations - .select(e.detail.states, e.detail.x, e.detail.y); + const result = await jobInstance.annotations.select(e.detail.states, e.detail.x, e.detail.y); if (result && result.state) { if (result.state.shapeType === 'polyline' || result.state.shapeType === 'points') { @@ -556,16 +515,17 @@ export default class CanvasWrapperComponent extends React.PureComponent { }; private onCanvasPointContextMenu = (e: any): void => { - const { - activatedStateID, - onUpdateContextMenu, - annotations, - } = this.props; + const { activatedStateID, onUpdateContextMenu, annotations } = this.props; - const [state] = annotations.filter((el: any) => (el.clientID === activatedStateID)); + const [state] = annotations.filter((el: any) => el.clientID === activatedStateID); if (![ShapeType.CUBOID, ShapeType.RECTANGLE].includes(state.shapeType)) { - onUpdateContextMenu(activatedStateID !== null, e.detail.mouseEvent.clientX, - e.detail.mouseEvent.clientY, ContextMenuType.CANVAS_SHAPE_POINT, e.detail.pointID); + onUpdateContextMenu( + activatedStateID !== null, + e.detail.mouseEvent.clientX, + e.detail.mouseEvent.clientY, + ContextMenuType.CANVAS_SHAPE_POINT, + e.detail.pointID, + ); } }; @@ -581,8 +541,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { } = this.props; if (activatedStateID !== null) { - const [activatedState] = annotations - .filter((state: any): boolean => state.clientID === activatedStateID); + const [activatedState] = annotations.filter((state: any): boolean => state.clientID === activatedStateID); if (workspace === Workspace.ATTRIBUTE_ANNOTATION) { if (activatedState.objectType !== ObjectType.TAG) { canvasInstance.focus(activatedStateID, aamZoomMargin); @@ -595,19 +554,13 @@ export default class CanvasWrapperComponent extends React.PureComponent { } const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`); if (el) { - (el as any as SVGElement).setAttribute('fill-opacity', `${selectedOpacity / 100}`); + ((el as any) as SVGElement).setAttribute('fill-opacity', `${selectedOpacity / 100}`); } } } private updateShapesView(): void { - const { - annotations, - opacity, - colorBy, - outlined, - outlineColor, - } = this.props; + const { annotations, opacity, colorBy, outlined, outlineColor } = this.props; for (const state of annotations) { let shapeColor = ''; @@ -635,16 +588,14 @@ export default class CanvasWrapperComponent extends React.PureComponent { } private updateCanvas(): void { - const { - curZLayer, - annotations, - frameData, - canvasInstance, - } = this.props; + const { curZLayer, annotations, frameData, canvasInstance } = this.props; if (frameData !== null) { - canvasInstance.setup(frameData, annotations - .filter((e) => e.objectType !== ObjectType.TAG), curZLayer); + canvasInstance.setup( + frameData, + annotations.filter((e) => e.objectType !== ObjectType.TAG), + curZLayer, + ); } } @@ -680,22 +631,29 @@ export default class CanvasWrapperComponent extends React.PureComponent { // Filters const backgroundElement = window.document.getElementById('cvat_canvas_background'); if (backgroundElement) { - backgroundElement.style.filter = `brightness(${brightnessLevel / 100})` - + `contrast(${contrastLevel / 100})` - + `saturate(${saturationLevel / 100})`; + backgroundElement.style.filter = + `brightness(${brightnessLevel / 100})` + + `contrast(${contrastLevel / 100})` + + `saturate(${saturationLevel / 100})`; } - const canvasWrapperElement = window.document.getElementsByClassName('cvat-canvas-container').item(0) as HTMLElement | null; + const canvasWrapperElement = window.document + .getElementsByClassName('cvat-canvas-container') + .item(0) as HTMLElement | null; if (canvasWrapperElement) { canvasWrapperElement.style.backgroundColor = canvasBackgroundColor; } // Events - canvasInstance.html().addEventListener('canvas.setup', () => { - const { activatedStateID, activatedAttributeID } = this.props; - canvasInstance.fit(); - canvasInstance.activate(activatedStateID, activatedAttributeID); - }, { once: true }); + canvasInstance.html().addEventListener( + 'canvas.setup', + () => { + const { activatedStateID, activatedAttributeID } = this.props; + canvasInstance.fit(); + canvasInstance.activate(activatedStateID, activatedAttributeID); + }, + { once: true }, + ); canvasInstance.html().addEventListener('mousedown', this.onCanvasMouseDown); canvasInstance.html().addEventListener('click', this.onCanvasClicked); @@ -770,7 +728,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { SWITCH_AUTOMATIC_BORDERING: keyMap.SWITCH_AUTOMATIC_BORDERING, }; - const step = 10; const handlers = { INCREASE_BRIGHTNESS: (event: KeyboardEvent | undefined) => { @@ -839,8 +796,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { }, CHANGE_GRID_COLOR: (event: KeyboardEvent | undefined) => { preventDefault(event); - const colors = [GridColor.Black, GridColor.Blue, - GridColor.Green, GridColor.Red, GridColor.White]; + const colors = [GridColor.Black, GridColor.Blue, GridColor.Green, GridColor.Red, GridColor.White]; const indexOf = colors.indexOf(gridColor) + 1; const color = colors[indexOf >= colors.length ? 0 : indexOf]; onChangeGridColor(color); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx index ef4b457e..ad894422 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx @@ -83,9 +83,14 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { }, SWITCH_DRAW_MODE: (event: KeyboardEvent | undefined) => { preventDefault(event); - const drawing = [ActiveControl.DRAW_POINTS, ActiveControl.DRAW_POLYGON, - ActiveControl.DRAW_POLYLINE, ActiveControl.DRAW_RECTANGLE, - ActiveControl.DRAW_CUBOID, ActiveControl.AI_TOOLS].includes(activeControl); + const drawing = [ + ActiveControl.DRAW_POINTS, + ActiveControl.DRAW_POLYGON, + ActiveControl.DRAW_POLYLINE, + ActiveControl.DRAW_RECTANGLE, + ActiveControl.DRAW_CUBOID, + ActiveControl.AI_TOOLS, + ].includes(activeControl); if (!drawing) { canvasInstance.cancel(); @@ -161,11 +166,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { }; return ( - + - +
    diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx index b3ba38ca..b1a719d1 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx @@ -17,23 +17,18 @@ interface Props { } function CursorControl(props: Props): JSX.Element { - const { - canvasInstance, - activeControl, - cursorShortkey, - } = props; + const { canvasInstance, activeControl, cursorShortkey } = props; return ( canvasInstance.cancel() - : undefined + className={ + activeControl === ActiveControl.CURSOR + ? 'cvat-active-canvas-control cvat-cursor-control' + : 'cvat-cursor-control' } + onClick={activeControl !== ActiveControl.CURSOR ? (): void => canvasInstance.cancel() : undefined} /> ); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-cuboid-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-cuboid-control.tsx index b813a3aa..64072440 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-cuboid-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-cuboid-control.tsx @@ -19,38 +19,33 @@ interface Props { } function DrawPolygonControl(props: Props): JSX.Element { - const { - canvasInstance, - isDrawing, - } = props; - - const dynamcPopoverPros = isDrawing ? { - overlayStyle: { - display: 'none', - }, - } : {}; - - const dynamicIconProps = isDrawing ? { - className: 'cvat-active-canvas-control', - onClick: (): void => { - canvasInstance.draw({ enabled: false }); - }, - } : {}; + const { canvasInstance, isDrawing } = props; + + const dynamcPopoverPros = isDrawing + ? { + overlayStyle: { + display: 'none', + }, + } + : {}; + + const dynamicIconProps = isDrawing + ? { + className: 'cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } + : {}; return ( - )} + content={} > - + ); } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-points-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-points-control.tsx index e034956a..9b8032f8 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-points-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-points-control.tsx @@ -20,33 +20,31 @@ interface Props { function DrawPointsControl(props: Props): JSX.Element { const { canvasInstance, isDrawing } = props; - const dynamcPopoverPros = isDrawing ? { - overlayStyle: { - display: 'none', - }, - } : {}; - - const dynamicIconProps = isDrawing ? { - className: 'cvat-active-canvas-control', - onClick: (): void => { - canvasInstance.draw({ enabled: false }); - }, - } : {}; + const dynamcPopoverPros = isDrawing + ? { + overlayStyle: { + display: 'none', + }, + } + : {}; + + const dynamicIconProps = isDrawing + ? { + className: 'cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } + : {}; return ( - )} + content={} > - + ); } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polygon-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polygon-control.tsx index 3b80a54c..95b60a33 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polygon-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polygon-control.tsx @@ -20,33 +20,31 @@ interface Props { function DrawPolygonControl(props: Props): JSX.Element { const { canvasInstance, isDrawing } = props; - const dynamcPopoverPros = isDrawing ? { - overlayStyle: { - display: 'none', - }, - } : {}; - - const dynamicIconProps = isDrawing ? { - className: 'cvat-active-canvas-control', - onClick: (): void => { - canvasInstance.draw({ enabled: false }); - }, - } : {}; + const dynamcPopoverPros = isDrawing + ? { + overlayStyle: { + display: 'none', + }, + } + : {}; + + const dynamicIconProps = isDrawing + ? { + className: 'cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } + : {}; return ( - )} + content={} > - + ); } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polyline-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polyline-control.tsx index f09d26e6..b6012fb2 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polyline-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polyline-control.tsx @@ -20,33 +20,31 @@ interface Props { function DrawPolylineControl(props: Props): JSX.Element { const { canvasInstance, isDrawing } = props; - const dynamcPopoverPros = isDrawing ? { - overlayStyle: { - display: 'none', - }, - } : {}; - - const dynamicIconProps = isDrawing ? { - className: 'cvat-active-canvas-control', - onClick: (): void => { - canvasInstance.draw({ enabled: false }); - }, - } : {}; + const dynamcPopoverPros = isDrawing + ? { + overlayStyle: { + display: 'none', + }, + } + : {}; + + const dynamicIconProps = isDrawing + ? { + className: 'cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } + : {}; return ( - )} + content={} > - + ); } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-rectangle-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-rectangle-control.tsx index 60e852b6..c0ee56d2 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-rectangle-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-rectangle-control.tsx @@ -20,35 +20,31 @@ interface Props { function DrawRectangleControl(props: Props): JSX.Element { const { canvasInstance, isDrawing } = props; - const dynamcPopoverPros = isDrawing ? { - overlayStyle: { - display: 'none', - }, - } : {}; - - const dynamicIconProps = isDrawing ? { - className: 'cvat-active-canvas-control', - onClick: (): void => { - canvasInstance.draw({ enabled: false }); - }, - } : {}; + const dynamcPopoverPros = isDrawing + ? { + overlayStyle: { + display: 'none', + }, + } + : {}; + + const dynamicIconProps = isDrawing + ? { + className: 'cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } + : {}; return ( - )} + content={} > - + ); } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx index d8ce4d33..6c72c5ec 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx @@ -68,7 +68,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element { showSearch filterOption={(input: string, option: React.ReactElement) => { const { children } = option.props; - if (typeof (children) === 'string') { + if (typeof children === 'string') { return children.toLowerCase().includes(input.toLowerCase()); } @@ -77,124 +77,95 @@ function DrawShapePopoverComponent(props: Props): JSX.Element { value={`${selectedLabeID}`} onChange={onChangeLabel} > - { - labels.map((label: any) => ( - - {label.name} - - )) - } + {labels.map((label: any) => ( + + {label.name} + + ))}
    - { - shapeType === ShapeType.RECTANGLE && ( - <> - - - Drawing method - - - - - - - By 2 Points - - - By 4 Points - - - - - - ) - } - { - shapeType === ShapeType.CUBOID && ( - <> - - - Drawing method - - - - - - - From rectangle - - - By 4 Points - - - - - - ) - } - { - shapeType !== ShapeType.RECTANGLE && shapeType !== ShapeType.CUBOID && ( - - - Number of points: + {shapeType === ShapeType.RECTANGLE && ( + <> + + + Drawing method - - { - if (typeof (value) === 'number') { - onChangePoints(Math.floor( - clamp(value, minimumPoints, Number.MAX_SAFE_INTEGER), - )); - } else if (!value) { - onChangePoints(undefined); - } - }} - className='cvat-draw-shape-popover-points-selector' - min={minimumPoints} - value={numberOfPoints} - step={1} - /> + + + + + + By 2 Points + + + By 4 Points + + + + + + )} + {shapeType === ShapeType.CUBOID && ( + <> + + + Drawing method + + + + + + + From rectangle + + + By 4 Points + + - ) - } + + )} + {shapeType !== ShapeType.RECTANGLE && shapeType !== ShapeType.CUBOID && ( + + + Number of points: + + + { + if (typeof value === 'number') { + onChangePoints(Math.floor(clamp(value, minimumPoints, Number.MAX_SAFE_INTEGER))); + } else if (!value) { + onChangePoints(undefined); + } + }} + className='cvat-draw-shape-popover-points-selector' + min={minimumPoints} + value={numberOfPoints} + step={1} + /> + + + )} - + - + diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/fit-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/fit-control.tsx index 84034525..05a73b4b 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/fit-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/fit-control.tsx @@ -18,11 +18,7 @@ function FitControl(props: Props): JSX.Element { return ( - canvasInstance.fit()} - /> + canvasInstance.fit()} /> ); } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx index 1432be7c..f549b780 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx @@ -19,32 +19,28 @@ interface Props { } function GroupControl(props: Props): JSX.Element { - const { - switchGroupShortcut, - resetGroupShortcut, - activeControl, - canvasInstance, - groupObjects, - } = props; + const { switchGroupShortcut, resetGroupShortcut, activeControl, canvasInstance, groupObjects } = props; - const dynamicIconProps = activeControl === ActiveControl.GROUP - ? { - className: 'cvat-group-control cvat-active-canvas-control', - onClick: (): void => { - canvasInstance.group({ enabled: false }); - groupObjects(false); - }, - } : { - className: 'cvat-group-control', - onClick: (): void => { - canvasInstance.cancel(); - canvasInstance.group({ enabled: true }); - groupObjects(true); - }, - }; + const dynamicIconProps = + activeControl === ActiveControl.GROUP + ? { + className: 'cvat-group-control cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.group({ enabled: false }); + groupObjects(false); + }, + } + : { + className: 'cvat-group-control', + onClick: (): void => { + canvasInstance.cancel(); + canvasInstance.group({ enabled: true }); + groupObjects(true); + }, + }; - const title = `Group shapes/tracks ${switchGroupShortcut}.` - + ` Select and press ${resetGroupShortcut} to reset a group`; + const title = + `Group shapes/tracks ${switchGroupShortcut}.` + ` Select and press ${resetGroupShortcut} to reset a group`; return ( diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx index 3b3fdd0a..d7537026 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx @@ -18,28 +18,25 @@ interface Props { } function MergeControl(props: Props): JSX.Element { - const { - switchMergeShortcut, - activeControl, - canvasInstance, - mergeObjects, - } = props; + const { switchMergeShortcut, activeControl, canvasInstance, mergeObjects } = props; - const dynamicIconProps = activeControl === ActiveControl.MERGE - ? { - className: 'cvat-merge-control cvat-active-canvas-control', - onClick: (): void => { - canvasInstance.merge({ enabled: false }); - mergeObjects(false); - }, - } : { - className: 'cvat-merge-control', - onClick: (): void => { - canvasInstance.cancel(); - canvasInstance.merge({ enabled: true }); - mergeObjects(true); - }, - }; + const dynamicIconProps = + activeControl === ActiveControl.MERGE + ? { + className: 'cvat-merge-control cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.merge({ enabled: false }); + mergeObjects(false); + }, + } + : { + className: 'cvat-merge-control', + onClick: (): void => { + canvasInstance.cancel(); + canvasInstance.merge({ enabled: true }); + mergeObjects(true); + }, + }; return ( diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx index fadb2142..798b868a 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx @@ -22,8 +22,11 @@ function MoveControl(props: Props): JSX.Element { { if (activeControl === ActiveControl.DRAG_CANVAS) { canvasInstance.dragCanvas(false); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx index 2e923307..c3b201d2 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx @@ -22,8 +22,11 @@ function ResizeControl(props: Props): JSX.Element { { if (activeControl === ActiveControl.ZOOM_CANVAS) { canvasInstance.zoomCanvas(false); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx index c5504430..22935369 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx @@ -23,16 +23,24 @@ function RotateControl(props: Props): JSX.Element { - + rotateFrame(Rotation.ANTICLOCKWISE90)} component={RotateIcon} /> - + rotateFrame(Rotation.CLOCKWISE90)} @@ -40,7 +48,7 @@ function RotateControl(props: Props): JSX.Element { /> - )} + } trigger='hover' > diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-control.tsx index a2b97084..7817e798 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-control.tsx @@ -19,20 +19,20 @@ interface Props { function SetupTagControl(props: Props): JSX.Element { const { isDrawing } = props; - const dynamcPopoverPros = isDrawing ? { - overlayStyle: { - display: 'none', - }, - } : {}; + const dynamcPopoverPros = isDrawing + ? { + overlayStyle: { + display: 'none', + }, + } + : {}; return ( - )} + content={} > diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover.tsx index 11c1c1f6..62e8cd6e 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover.tsx @@ -14,25 +14,19 @@ interface Props { selectedLabeID: number; repeatShapeShortcut: string; onChangeLabel(value: string): void; - onSetup( - labelID: number, - ): void; + onSetup(labelID: number): void; } function SetupTagPopover(props: Props): JSX.Element { - const { - labels, - selectedLabeID, - repeatShapeShortcut, - onChangeLabel, - onSetup, - } = props; + const { labels, selectedLabeID, repeatShapeShortcut, onChangeLabel, onSetup } = props; return (
    - Setup tag + + Setup tag + @@ -42,29 +36,19 @@ function SetupTagPopover(props: Props): JSX.Element { - + {labels.map((label: any) => ( + + {label.name} + + ))} - + diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx index 176563a7..3406a4a2 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx @@ -18,28 +18,25 @@ interface Props { } function SplitControl(props: Props): JSX.Element { - const { - switchSplitShortcut, - activeControl, - canvasInstance, - splitTrack, - } = props; + const { switchSplitShortcut, activeControl, canvasInstance, splitTrack } = props; - const dynamicIconProps = activeControl === ActiveControl.SPLIT - ? { - className: 'cvat-split-track-control cvat-active-canvas-control', - onClick: (): void => { - canvasInstance.split({ enabled: false }); - splitTrack(false); - }, - } : { - className: 'cvat-split-track-control', - onClick: (): void => { - canvasInstance.cancel(); - canvasInstance.split({ enabled: true }); - splitTrack(true); - }, - }; + const dynamicIconProps = + activeControl === ActiveControl.SPLIT + ? { + className: 'cvat-split-track-control cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.split({ enabled: false }); + splitTrack(false); + }, + } + : { + className: 'cvat-split-track-control', + onClick: (): void => { + canvasInstance.cancel(); + canvasInstance.split({ enabled: true }); + splitTrack(true); + }, + }; return ( diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx index 89d89912..01f4658a 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx @@ -19,13 +19,7 @@ import { AIToolsIcon } from 'icons'; import { Canvas } from 'cvat-canvas-wrapper'; import range from 'utils/range'; import getCore from 'cvat-core-wrapper'; -import { - CombinedState, - ActiveControl, - Model, - ObjectType, - ShapeType, -} from 'reducers/interfaces'; +import { CombinedState, ActiveControl, Model, ObjectType, ShapeType } from 'reducers/interfaces'; import { interactWithCanvas, fetchAnnotationsAsync, @@ -93,18 +87,18 @@ const mapDispatchToProps = { function convertShapesForInteractor(shapes: InteractionResult[]): number[][] { const reducer = (acc: number[][], _: number, index: number, array: number[]): number[][] => { - if (!(index % 2)) { // 0, 2, 4 - acc.push([ - array[index], - array[index + 1], - ]); + if (!(index % 2)) { + // 0, 2, 4 + acc.push([array[index], array[index + 1]]); } return acc; }; - return shapes.filter((shape: InteractionResult): boolean => shape.shapeType === 'points' && shape.button === 0) + return shapes + .filter((shape: InteractionResult): boolean => shape.shapeType === 'points' && shape.button === 0) .map((shape: InteractionResult): number[] => shape.points) - .flat().reduce(reducer, []); + .flat() + .reduce(reducer, []); } type Props = StateToProps & DispatchToProps; @@ -121,6 +115,7 @@ interface State { export class ToolsControlComponent extends React.PureComponent { private interactionIsAborted: boolean; + private interactionIsDone: boolean; public constructor(props: Props) { @@ -169,24 +164,21 @@ export class ToolsControlComponent extends React.PureComponent { private getInteractiveState(): any | null { const { states } = this.props; const { interactiveStateID } = this.state; - return states - .filter((_state: any): boolean => _state.clientID === interactiveStateID)[0] || null; + return states.filter((_state: any): boolean => _state.clientID === interactiveStateID)[0] || null; } private contextmenuDisabler = (e: MouseEvent): void => { - if (e.target && (e.target as Element).classList - && (e.target as Element).classList.toString().includes('ant-modal')) { + if ( + e.target && + (e.target as Element).classList && + (e.target as Element).classList.toString().includes('ant-modal') + ) { e.preventDefault(); } }; private cancelListener = async (): Promise => { - const { - isActivated, - jobInstance, - frame, - fetchAnnotations, - } = this.props; + const { isActivated, jobInstance, frame, fetchAnnotations } = this.props; const { interactiveStateID, fetching } = this.state; if (isActivated) { @@ -256,8 +248,7 @@ export class ToolsControlComponent extends React.PureComponent { const object = new core.classes.ObjectState({ frame, objectType: ObjectType.SHAPE, - label: labels - .filter((label: any) => label.id === activeLabelID)[0], + label: labels.filter((label: any) => label.id === activeLabelID)[0], shapeType: ShapeType.POLYGON, points: result.flat(), occluded: false, @@ -275,8 +266,7 @@ export class ToolsControlComponent extends React.PureComponent { const object = new core.classes.ObjectState({ frame, objectType: ObjectType.SHAPE, - label: labels - .filter((label: any) => label.id === activeLabelID)[0], + label: labels.filter((label: any) => label.id === activeLabelID)[0], shapeType: ShapeType.POLYGON, points: result.flat(), occluded: false, @@ -323,17 +313,9 @@ export class ToolsControlComponent extends React.PureComponent { }; private onTracking = async (e: Event): Promise => { - const { - isActivated, - jobInstance, - frame, - curZOrder, - fetchAnnotations, - } = this.props; + const { isActivated, jobInstance, frame, curZOrder, fetchAnnotations } = this.props; const { activeLabelID } = this.state; - const [label] = jobInstance.task.labels.filter( - (_label: any): boolean => _label.id === activeLabelID, - ); + const [label] = jobInstance.task.labels.filter((_label: any): boolean => _label.id === activeLabelID); if (!(e as CustomEvent).detail.isDone) { return; @@ -364,8 +346,7 @@ export class ToolsControlComponent extends React.PureComponent { fetchAnnotations(); const states = await jobInstance.annotations.get(frame); - const [objectState] = states - .filter((_state: any): boolean => _state.clientID === clientID); + const [objectState] = states.filter((_state: any): boolean => _state.clientID === clientID); await this.trackState(objectState); } catch (err) { notification.error({ @@ -390,18 +371,14 @@ export class ToolsControlComponent extends React.PureComponent { private setActiveInteractor = (key: string): void => { const { interactors } = this.props; this.setState({ - activeInteractor: interactors.filter( - (interactor: Model) => interactor.id === key, - )[0], + activeInteractor: interactors.filter((interactor: Model) => interactor.id === key)[0], }); }; private setActiveTracker = (key: string): void => { const { trackers } = this.props; this.setState({ - activeTracker: trackers.filter( - (tracker: Model) => tracker.id === key, - )[0], + activeTracker: trackers.filter((tracker: Model) => tracker.id === key)[0], }); }; @@ -422,8 +399,7 @@ export class ToolsControlComponent extends React.PureComponent { for (const offset of range(1, trackingFrames + 1)) { /* eslint-disable no-await-in-loop */ const states = await jobInstance.annotations.get(frame + offset); - const [objectState] = states - .filter((_state: any): boolean => _state.clientID === clientID); + const [objectState] = states.filter((_state: any): boolean => _state.clientID === clientID); response = await core.lambda.call(jobInstance.task, tracker, { task: jobInstance.task, frame: frame + offset, @@ -431,19 +407,26 @@ export class ToolsControlComponent extends React.PureComponent { state: response.state, }); - const reduced = response.shape - .reduce((acc: number[], value: number, index: number): number[] => { - if (index % 2) { // y + const reduced = response.shape.reduce( + (acc: number[], value: number, index: number): number[] => { + if (index % 2) { + // y acc[1] = Math.min(acc[1], value); acc[3] = Math.max(acc[3], value); - } else { // x + } else { + // x acc[0] = Math.min(acc[0], value); acc[2] = Math.max(acc[2], value); } return acc; - }, [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, - Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, - ]); + }, + [ + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, + ], + ); objectState.points = reduced; await objectState.save(); @@ -477,28 +460,24 @@ export class ToolsControlComponent extends React.PureComponent { @@ -507,25 +486,16 @@ export class ToolsControlComponent extends React.PureComponent { } private renderTrackerBlock(): JSX.Element { - const { - trackers, - canvasInstance, - jobInstance, - frame, - onInteractionStart, - } = this.props; - const { - activeTracker, - activeLabelID, - fetching, - trackingFrames, - } = this.state; + const { trackers, canvasInstance, jobInstance, frame, onInteractionStart } = this.props; + const { activeTracker, activeLabelID, fetching, trackingFrames } = this.state; if (!trackers.length) { return ( - No available trackers found + + No available trackers found + ); @@ -545,11 +515,13 @@ export class ToolsControlComponent extends React.PureComponent { defaultValue={trackers[0].name} onChange={this.setActiveTracker} > - {trackers.map((interactor: Model): JSX.Element => ( - - {interactor.name} - - ))} + {trackers.map( + (interactor: Model): JSX.Element => ( + + {interactor.name} + + ), + )} @@ -565,7 +537,7 @@ export class ToolsControlComponent extends React.PureComponent { precision={0} max={jobInstance.stopFrame - frame} onChange={(value: number | undefined): void => { - if (typeof (value) !== 'undefined') { + if (typeof value !== 'undefined') { this.setState({ trackingFrames: value, }); @@ -613,7 +585,9 @@ export class ToolsControlComponent extends React.PureComponent { return ( - No available interactors found + + No available interactors found + ); @@ -633,11 +607,13 @@ export class ToolsControlComponent extends React.PureComponent { defaultValue={interactors[0].name} onChange={this.setActiveInteractor} > - {interactors.map((interactor: Model): JSX.Element => ( - - {interactor.name} - - ))} + {interactors.map( + (interactor: Model): JSX.Element => ( + + {interactor.name} + + ), + )} @@ -674,19 +650,15 @@ export class ToolsControlComponent extends React.PureComponent { } private renderDetectorBlock(): JSX.Element { - const { - jobInstance, - detectors, - curZOrder, - frame, - fetchAnnotations, - } = this.props; + const { jobInstance, detectors, curZOrder, frame, fetchAnnotations } = this.props; if (!detectors.length) { return ( - No available detectors found + + No available detectors found + ); @@ -709,14 +681,11 @@ export class ToolsControlComponent extends React.PureComponent { frame, }); - const states = result - .map((data: any): any => ( + const states = result.map( + (data: any): any => new core.classes.ObjectState({ shapeType: data.type, - label: task.labels - .filter( - (label: any): boolean => label.name === data.label, - )[0], + label: task.labels.filter((label: any): boolean => label.name === data.label)[0], points: data.points, objectType: ObjectType.SHAPE, frame, @@ -724,8 +693,8 @@ export class ToolsControlComponent extends React.PureComponent { source: 'auto', attributes: {}, zOrder: curZOrder, - }) - )); + }), + ); await jobInstance.annotations.put(states); fetchAnnotations(); @@ -747,20 +716,22 @@ export class ToolsControlComponent extends React.PureComponent {
    - AI Tools + + AI Tools + - { this.renderLabelBlock() } - { this.renderInteractorBlock() } + {this.renderLabelBlock()} + {this.renderInteractorBlock()} - { this.renderDetectorBlock() } + {this.renderDetectorBlock()} - { this.renderLabelBlock() } - { this.renderTrackerBlock() } + {this.renderLabelBlock()} + {this.renderTrackerBlock()}
    @@ -768,31 +739,29 @@ export class ToolsControlComponent extends React.PureComponent { } public render(): JSX.Element | null { - const { - interactors, - detectors, - trackers, - isActivated, - canvasInstance, - } = this.props; + const { interactors, detectors, trackers, isActivated, canvasInstance } = this.props; const { fetching, trackingProgress } = this.state; if (![...interactors, ...detectors, ...trackers].length) return null; - const dynamcPopoverPros = isActivated ? { - overlayStyle: { - display: 'none', - }, - } : {}; - - const dynamicIconProps = isActivated ? { - className: 'cvat-active-canvas-control cvat-tools-control', - onClick: (): void => { - canvasInstance.interact({ enabled: false }); - }, - } : { - className: 'cvat-tools-control', - }; + const dynamcPopoverPros = isActivated + ? { + overlayStyle: { + display: 'none', + }, + } + : {}; + + const dynamicIconProps = isActivated + ? { + className: 'cvat-active-canvas-control cvat-tools-control', + onClick: (): void => { + canvasInstance.interact({ enabled: false }); + }, + } + : { + className: 'cvat-tools-control', + }; return ( <> @@ -802,11 +771,10 @@ export class ToolsControlComponent extends React.PureComponent { visible={fetching} closable={false} footer={[]} - > Waiting for a server response.. - { trackingProgress !== null && ( + {trackingProgress !== null && ( )} @@ -823,7 +791,4 @@ export class ToolsControlComponent extends React.PureComponent { } } -export default connect( - mapStateToProps, - mapDispatchToProps, -)(ToolsControlComponent); +export default connect(mapStateToProps, mapDispatchToProps)(ToolsControlComponent); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/color-picker.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/color-picker.tsx index 8247749c..ae3c1382 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/color-picker.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/color-picker.tsx @@ -21,19 +21,24 @@ interface Props { resetVisible?: boolean; onChange?: (value: string) => void; onVisibleChange?: (visible: boolean) => void; - placement?: 'left' | 'top' | 'right' | 'bottom' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' | 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom' | undefined; + placement?: + | 'left' + | 'top' + | 'right' + | 'bottom' + | 'topLeft' + | 'topRight' + | 'bottomLeft' + | 'bottomRight' + | 'leftTop' + | 'leftBottom' + | 'rightTop' + | 'rightBottom' + | undefined; } function ColorPicker(props: Props, ref: React.Ref): JSX.Element { - const { - children, - value, - visible, - resetVisible, - onChange, - onVisibleChange, - placement, - } = props; + const { children, value, visible, resetVisible, onChange, onVisibleChange, placement } = props; const [colorState, setColorState] = useState(value); const [pickerVisible, setPickerVisible] = useState(false); @@ -50,7 +55,7 @@ function ColorPicker(props: Props, ref: React.Ref): JSX.Element { return ( ): JSX.Element { - )} - title={( + } + title={ - - Select color - + Select color @@ -115,8 +118,7 @@ function ColorPicker(props: Props, ref: React.Ref): JSX.Element { - - )} + } placement={placement || 'left'} overlayClassName='cvat-label-color-picker' trigger='click' diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx index e5067658..73a0c4f3 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx @@ -8,7 +8,6 @@ import Icon from 'antd/lib/icon'; import Button from 'antd/lib/button'; import Text from 'antd/lib/typography/Text'; - interface Props { labelName: string; labelColor: string; @@ -46,17 +45,23 @@ function LabelItemComponent(props: Props): JSX.Element { diff --git a/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx b/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx index 8902860e..2903e448 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx @@ -75,17 +75,23 @@ function PlayerButtons(props: Props): JSX.Element { let prevButton = ; let prevButtonTooltipMessage = prevRegularText; if (prevButtonType === 'filtered') { - prevButton = ; + prevButton = ( + + ); prevButtonTooltipMessage = prevFilteredText; } else if (prevButtonType === 'empty') { - prevButton = ; + prevButton = ( + + ); prevButtonTooltipMessage = prevEmptyText; } let nextButton = ; let nextButtonTooltipMessage = nextRegularText; if (nextButtonType === 'filtered') { - nextButton = ; + nextButton = ( + + ); nextButtonTooltipMessage = nextFilteredText; } else if (nextButtonType === 'empty') { nextButton = ; @@ -103,7 +109,7 @@ function PlayerButtons(props: Props): JSX.Element { - )} + } > - + {prevButton} - {!playing - ? ( - - - - ) - : ( - - - - )} + {!playing ? ( + + + + ) : ( + + + + )} - )} + } > {nextButton} diff --git a/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx b/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx index f536b081..33edd2b5 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx @@ -80,10 +80,8 @@ function PlayerNavigation(props: Props): JSX.Element { type='number' value={frameInputValue} onChange={(value: number | undefined) => { - if (typeof (value) === 'number') { - setFrameInputValue(Math.floor( - clamp(value, startFrame, stopFrame), - )); + if (typeof value === 'number') { + setFrameInputValue(Math.floor(clamp(value, startFrame, stopFrame))); } }} onBlur={() => { diff --git a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx index 54c7723e..96f1364f 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx @@ -43,11 +43,7 @@ function RightGroup(props: Props): JSX.Element { Info
    - {Object.values(Workspace).map((ws) => ( {ws} diff --git a/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx b/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx index f38dabac..16f85d4d 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx @@ -52,9 +52,7 @@ export default function StatisticsModalComponent(props: Props): JSX.Element { if (collecting || !data) { return ( - + ); @@ -88,61 +86,79 @@ export default function StatisticsModalComponent(props: Props): JSX.Element { const makeShapesTracksTitle = (title: string): JSX.Element => ( - {title} + + {title} + ); - const columns = [{ - title: Label , - dataIndex: 'label', - key: 'label', - }, { - title: makeShapesTracksTitle('Rectangle'), - dataIndex: 'rectangle', - key: 'rectangle', - }, { - title: makeShapesTracksTitle('Polygon'), - dataIndex: 'polygon', - key: 'polygon', - }, { - title: makeShapesTracksTitle('Polyline'), - dataIndex: 'polyline', - key: 'polyline', - }, { - title: makeShapesTracksTitle('Points'), - dataIndex: 'points', - key: 'points', - }, { - title: Tags , - dataIndex: 'tags', - key: 'tags', - }, { - title: Manually , - dataIndex: 'manually', - key: 'manually', - }, { - title: Interpolated , - dataIndex: 'interpolated', - key: 'interpolated', - }, { - title: Total , - dataIndex: 'total', - key: 'total', - }]; + const columns = [ + { + title: Label , + dataIndex: 'label', + key: 'label', + }, + { + title: makeShapesTracksTitle('Rectangle'), + dataIndex: 'rectangle', + key: 'rectangle', + }, + { + title: makeShapesTracksTitle('Polygon'), + dataIndex: 'polygon', + key: 'polygon', + }, + { + title: makeShapesTracksTitle('Polyline'), + dataIndex: 'polyline', + key: 'polyline', + }, + { + title: makeShapesTracksTitle('Points'), + dataIndex: 'points', + key: 'points', + }, + { + title: Tags , + dataIndex: 'tags', + key: 'tags', + }, + { + title: Manually , + dataIndex: 'manually', + key: 'manually', + }, + { + title: Interpolated , + dataIndex: 'interpolated', + key: 'interpolated', + }, + { + title: Total , + dataIndex: 'total', + key: 'total', + }, + ]; return ( - +
    - Job status + + Job status + {savingJobStatus && } @@ -154,26 +170,36 @@ export default function StatisticsModalComponent(props: Props): JSX.Element { - Assignee + + Assignee + {assignee} - Start frame + + Start frame + {startFrame} - Stop frame + + Stop frame + {stopFrame} - Frames + + Frames + {stopFrame - startFrame + 1} - { !!bugTracker && ( + {!!bugTracker && ( - Bug tracker + + Bug tracker + {bugTracker} @@ -181,13 +207,7 @@ export default function StatisticsModalComponent(props: Props): JSX.Element { Annotations statistics - +
    diff --git a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx index c3e14c31..4bdab500 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx @@ -147,11 +147,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { /> - + ); } diff --git a/cvat-ui/src/components/change-password-modal/change-password-form.tsx b/cvat-ui/src/components/change-password-modal/change-password-form.tsx index f1548daa..6154e7ce 100644 --- a/cvat-ui/src/components/change-password-modal/change-password-form.tsx +++ b/cvat-ui/src/components/change-password-modal/change-password-form.tsx @@ -57,10 +57,7 @@ class ChangePasswordFormComponent extends React.PureComponent { e.preventDefault(); - const { - form, - onSubmit, - } = this.props; + const { form, onSubmit } = this.props; form.validateFields((error, values): void => { if (!error) { @@ -80,15 +77,19 @@ class ChangePasswordFormComponent extends React.PureComponent {form.getFieldDecorator('oldPassword', { - rules: [{ - required: true, - message: 'Please input your current password!', - }], - })(} - placeholder='Current password' - />)} + rules: [ + { + required: true, + message: 'Please input your current password!', + }, + ], + })( + } + placeholder='Current password' + />, + )} ); } @@ -99,17 +100,22 @@ class ChangePasswordFormComponent extends React.PureComponent {form.getFieldDecorator('newPassword1', { - rules: [{ - required: true, - message: 'Please input new password!', - }, { - validator: this.validatePassword, - }], - })(} - placeholder='New password' - />)} + rules: [ + { + required: true, + message: 'Please input new password!', + }, + { + validator: this.validatePassword, + }, + ], + })( + } + placeholder='New password' + />, + )} ); } @@ -120,17 +126,22 @@ class ChangePasswordFormComponent extends React.PureComponent {form.getFieldDecorator('newPassword2', { - rules: [{ - required: true, - message: 'Please confirm your new password!', - }, { - validator: this.validateConfirmation, - }], - })(} - placeholder='Confirm new password' - />)} + rules: [ + { + required: true, + message: 'Please confirm your new password!', + }, + { + validator: this.validateConfirmation, + }, + ], + })( + } + placeholder='Confirm new password' + />, + )} ); } @@ -139,10 +150,7 @@ class ChangePasswordFormComponent extends React.PureComponent + {this.renderOldPasswordField()} {this.renderNewPasswordField()} {this.renderNewPasswordConfirmationField()} diff --git a/cvat-ui/src/components/change-password-modal/change-password-modal.tsx b/cvat-ui/src/components/change-password-modal/change-password-modal.tsx index 2f4f538b..d3a3c0fe 100644 --- a/cvat-ui/src/components/change-password-modal/change-password-modal.tsx +++ b/cvat-ui/src/components/change-password-modal/change-password-modal.tsx @@ -11,17 +11,13 @@ import { changePasswordAsync } from 'actions/auth-actions'; import { CombinedState } from 'reducers/interfaces'; import ChangePasswordForm, { ChangePasswordData } from './change-password-form'; - interface StateToProps { fetching: boolean; visible: boolean; } interface DispatchToProps { - onChangePassword( - oldPassword: string, - newPassword1: string, - newPassword2: string): void; + onChangePassword(oldPassword: string, newPassword1: string, newPassword2: string): void; } interface ChangePasswordPageComponentProps { @@ -39,20 +35,15 @@ function mapStateToProps(state: CombinedState): StateToProps { } function mapDispatchToProps(dispatch: any): DispatchToProps { - return ({ + return { onChangePassword(oldPassword: string, newPassword1: string, newPassword2: string): void { dispatch(changePasswordAsync(oldPassword, newPassword1, newPassword2)); }, - }); + }; } function ChangePasswordComponent(props: ChangePasswordPageComponentProps): JSX.Element { - const { - fetching, - onChangePassword, - visible, - onClose, - } = props; + const { fetching, onChangePassword, visible, onClose } = props; return ( { public submit(): Promise { return new Promise((resolve, reject) => { - const { - form, - onSubmit, - } = this.props; + const { form, onSubmit } = this.props; form.validateFields((error, values): void => { if (!error) { @@ -47,14 +44,14 @@ class BasicConfigurationForm extends React.PureComponent { return ( e.preventDefault()}> Name}> - { getFieldDecorator('name', { - rules: [{ - required: true, - message: 'Please, specify a name', - }], - })( - , - ) } + {getFieldDecorator('name', { + rules: [ + { + required: true, + message: 'Please, specify a name', + }, + ], + })()} ); diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index 45a3e9c3..f30e7981 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -53,7 +53,9 @@ const defaultState = { class CreateTaskContent extends React.PureComponent { private basicConfigurationComponent: any; + private advancedConfigurationComponent: any; + private fileManagerContainer: any; public constructor(props: Props & RouteComponentProps) { @@ -65,13 +67,7 @@ class CreateTaskContent extends React.PureComponent history.push(`/tasks/${taskId}`)} - > - Open task - - ); + const btn = ; notification.info({ message: 'The task has been created', @@ -101,9 +97,7 @@ class CreateTaskContent extends React.PureComponent acc + files[key].length, 0, - ); + const totalLen = Object.keys(files).reduce((acc, key) => acc + files[key].length, 0); return !!totalLen; }; @@ -137,7 +131,8 @@ class CreateTaskContent extends React.PureComponent { if (this.advancedConfigurationComponent) { return this.advancedConfigurationComponent.submit(); @@ -146,10 +141,12 @@ class CreateTaskContent extends React.PureComponent { resolve(); }); - }).then((): void => { + }) + .then((): void => { const { onCreate } = this.props; onCreate(this.state); - }).catch((error: Error): void => { + }) + .catch((error: Error): void => { notification.error({ message: 'Could not create a task', description: error.toString(), @@ -161,9 +158,9 @@ class CreateTaskContent extends React.PureComponent { this.basicConfigurationComponent = component; } - } + wrappedComponentRef={(component: any): void => { + this.basicConfigurationComponent = component; + }} onSubmit={this.handleSubmitBasicConfiguration} /> @@ -179,13 +176,11 @@ class CreateTaskContent extends React.PureComponentLabels: { - this.setState({ - labels: newLabels, - }); - } - } + onSubmit={(newLabels): void => { + this.setState({ + labels: newLabels, + }); + }} /> ); @@ -197,9 +192,9 @@ class CreateTaskContent extends React.PureComponent* Select files: { this.fileManagerContainer = container; } - } + ref={(container: any): void => { + this.fileManagerContainer = container; + }} withRemote /> @@ -211,19 +206,12 @@ class CreateTaskContent extends React.PureComponent - Advanced configuration - } - > + Advanced configuration}> { - this.advancedConfigurationComponent = component; - } - } + wrappedComponentRef={(component: any): void => { + this.advancedConfigurationComponent = component; + }} onSubmit={this.handleSubmitAdvancedConfiguration} /> @@ -242,21 +230,14 @@ class CreateTaskContent extends React.PureComponentBasic configuration - { this.renderBasicBlock() } - { this.renderLabelsBlock() } - { this.renderFilesBlock() } - { this.renderAdvancedBlock() } + {this.renderBasicBlock()} + {this.renderLabelsBlock()} + {this.renderFilesBlock()} + {this.renderAdvancedBlock()} - - {loading ? : null} - + {loading ? : null} - diff --git a/cvat-ui/src/components/create-task-page/create-task-page.tsx b/cvat-ui/src/components/create-task-page/create-task-page.tsx index 39549ea6..ed18e541 100644 --- a/cvat-ui/src/components/create-task-page/create-task-page.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-page.tsx @@ -21,13 +21,7 @@ interface Props { } export default function CreateTaskPage(props: Props): JSX.Element { - const { - error, - status, - taskId, - onCreate, - installedGit, - } = props; + const { error, status, taskId, onCreate, installedGit } = props; useEffect(() => { if (error) { @@ -67,12 +61,7 @@ export default function CreateTaskPage(props: Props): JSX.Element { Create a new task - + ); diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index f134dd20..2ceea61a 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -32,7 +32,6 @@ import { customWaViewHit } from 'utils/enviroment'; import showPlatformNotification, { platformInfo, stopNotifications } from 'utils/platform-checker'; import '../styles.scss'; - interface CVATAppProps { loadFormats: () => void; loadUsers: () => void; @@ -176,10 +175,7 @@ class CVATApplication extends React.PureComponent - {`The browser you are using is ${info.name} ${info.version} based on ${info.engine} .` - + ' CVAT was tested in the latest versions of Chrome and Firefox.' - + ' We recommend to use Chrome (or another Chromium based browser)'} + {`The browser you are using is ${info.name} ${info.version} based on ${info.engine} .` + + ' CVAT was tested in the latest versions of Chrome and Firefox.' + + ' We recommend to use Chrome (or another Chromium based browser)'} - - {`The operating system is ${info.os}`} - + {`The operating system is ${info.os}`} @@ -304,7 +295,6 @@ class CVATApplication extends React.PureComponent - {isModelPluginActive && } + {isModelPluginActive && ( + + )} @@ -336,18 +328,24 @@ class CVATApplication extends React.PureComponent - + - + ); } - return ( - - ); + return ; } } diff --git a/cvat-ui/src/components/feedback/feedback.tsx b/cvat-ui/src/components/feedback/feedback.tsx index cc1869c7..736c334e 100644 --- a/cvat-ui/src/components/feedback/feedback.tsx +++ b/cvat-ui/src/components/feedback/feedback.tsx @@ -30,31 +30,38 @@ import { import consts from 'consts'; function renderContent(): JSX.Element { - const { - GITHUB_URL, - GITHUB_IMAGE_URL, - GITTER_PUBLIC_URL, - } = consts; + const { GITHUB_URL, GITHUB_IMAGE_URL, GITTER_PUBLIC_URL } = consts; return ( <> Star us on - GitHub + + {' '} + GitHub +
    Leave a - feedback + + {' '} + feedback +
    - + @@ -79,7 +86,10 @@ function renderContent(): JSX.Element {
    Do you need help? Contact us on - gitter + + {' '} + gitter + ); @@ -92,9 +102,7 @@ export default function Feedback(): JSX.Element { <> Help to make CVAT better - } + title={Help to make CVAT better} content={renderContent()} visible={visible} > @@ -106,8 +114,7 @@ export default function Feedback(): JSX.Element { setVisible(!visible); }} > - { visible ? - : } + {visible ? : } diff --git a/cvat-ui/src/components/file-manager/file-manager.tsx b/cvat-ui/src/components/file-manager/file-manager.tsx index 3c970cd5..d62d724b 100644 --- a/cvat-ui/src/components/file-manager/file-manager.tsx +++ b/cvat-ui/src/components/file-manager/file-manager.tsx @@ -51,10 +51,7 @@ export default class FileManager extends React.PureComponent { } public getFiles(): Files { - const { - active, - files, - } = this.state; + const { active, files } = this.state; return { local: active === 'local' ? files.local : [], share: active === 'share' ? files.share : [], @@ -62,15 +59,14 @@ export default class FileManager extends React.PureComponent { }; } - private loadData = (key: string): Promise => new Promise( - (resolve, reject): void => { + private loadData = (key: string): Promise => + new Promise((resolve, reject): void => { const { onLoadData } = this.props; const success = (): void => resolve(); const failure = (): void => reject(); onLoadData(key, success, failure); - }, - ); + }); public reset(): void { this.setState({ @@ -93,9 +89,11 @@ export default class FileManager extends React.PureComponent { multiple listType='text' fileList={files.local as any[]} - showUploadList={files.local.length < 5 && { - showRemoveIcon: false, - }} + showUploadList={ + files.local.length < 5 && { + showRemoveIcon: false, + } + } beforeUpload={(_: RcFile, newLocalFiles: RcFile[]): boolean => { this.setState({ files: { @@ -110,19 +108,14 @@ export default class FileManager extends React.PureComponent {

    Click or drag files to this area

    -

    - Support for a bulk images or a single video -

    +

    Support for a bulk images or a single video

    - { files.local.length >= 5 - && ( - <> -
    - - {`${files.local.length} files selected`} - - - )} + {files.local.length >= 5 && ( + <> +
    + {`${files.local.length} files selected`} + + )} ); } @@ -134,12 +127,7 @@ export default class FileManager extends React.PureComponent { return data.map((item: TreeNodeNormal) => { if (item.children) { return ( - + {renderTreeNodes(item.children)} ); @@ -151,59 +139,55 @@ export default class FileManager extends React.PureComponent { const { SHARE_MOUNT_GUIDE_URL } = consts; const { treeData } = this.props; - const { - expandedKeys, - files, - } = this.state; + const { expandedKeys, files } = this.state; return ( - { treeData[0].children && treeData[0].children.length - ? ( - => this.loadData( - node.props.dataRef.key, - )} - onExpand={(newExpandedKeys: string[]): void => { - this.setState({ - expandedKeys: newExpandedKeys, - }); - }} - onCheck={ - (checkedKeys: string[] | { - checked: string[]; - halfChecked: string[]; - }): void => { - const keys = checkedKeys as string[]; - this.setState({ - files: { - ...files, - share: keys, - }, - }); - } - } - > - { renderTreeNodes(treeData) } - - ) : ( -
    - - - Please, be sure you had - - mounted - - share before you built CVAT and the shared storage contains files - -
    - )} + {treeData[0].children && treeData[0].children.length ? ( + => this.loadData(node.props.dataRef.key)} + onExpand={(newExpandedKeys: string[]): void => { + this.setState({ + expandedKeys: newExpandedKeys, + }); + }} + onCheck={( + checkedKeys: + | string[] + | { + checked: string[]; + halfChecked: string[]; + }, + ): void => { + const keys = checkedKeys as string[]; + this.setState({ + files: { + ...files, + share: keys, + }, + }); + }} + > + {renderTreeNodes(treeData)} + + ) : ( +
    + + + Please, be sure you had + + mounted + + share before you built CVAT and the shared storage contains files + +
    + )}
    ); } @@ -240,15 +224,15 @@ export default class FileManager extends React.PureComponent { type='card' activeKey={active} tabBarGutter={5} - onChange={ - (activeKey: string): void => this.setState({ + onChange={(activeKey: string): void => + this.setState({ active: activeKey as any, }) } > - { this.renderLocalSelector() } - { this.renderShareSelector() } - { withRemote && this.renderRemoteSelector() } + {this.renderLocalSelector()} + {this.renderShareSelector()} + {withRemote && this.renderRemoteSelector()} ); diff --git a/cvat-ui/src/components/global-error-boundary/global-error-boundary.tsx b/cvat-ui/src/components/global-error-boundary/global-error-boundary.tsx index 9be93aae..6192eca9 100644 --- a/cvat-ui/src/components/global-error-boundary/global-error-boundary.tsx +++ b/cvat-ui/src/components/global-error-boundary/global-error-boundary.tsx @@ -40,14 +40,9 @@ interface State { function mapStateToProps(state: CombinedState): StateToProps { const { annotation: { - job: { - instance: job, - }, - }, - about: { - server, - packageVersion, + job: { instance: job }, }, + about: { server, packageVersion }, } = state; return { @@ -67,7 +62,6 @@ function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps { }; } - type Props = StateToProps & DispatchToProps; class GlobalErrorBoundary extends React.PureComponent { public constructor(props: Props) { @@ -106,14 +100,7 @@ class GlobalErrorBoundary extends React.PureComponent { } public render(): React.ReactNode { - const { - restore, - job, - serverVersion, - coreVersion, - canvasVersion, - uiVersion, - } = this.props; + const { restore, job, serverVersion, coreVersion, canvasVersion, uiVersion } = this.props; const { hasError, error } = this.state; @@ -142,7 +129,11 @@ class GlobalErrorBoundary extends React.PureComponent { -