From 457454821f7c1214c776e42c95aa6f27a80a0856 Mon Sep 17 00:00:00 2001 From: zhiltsov-max Date: Mon, 6 Apr 2020 11:54:44 +0300 Subject: [PATCH 01/25] Refactor frame provider (#1355) * Refactor frame provider * fix --- cvat/apps/engine/frame_provider.py | 145 ++++++++++++----------------- 1 file changed, 58 insertions(+), 87 deletions(-) diff --git a/cvat/apps/engine/frame_provider.py b/cvat/apps/engine/frame_provider.py index 9228ccc6..7bd60a8a 100644 --- a/cvat/apps/engine/frame_provider.py +++ b/cvat/apps/engine/frame_provider.py @@ -1,21 +1,21 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2020 Intel Corporation # # SPDX-License-Identifier: MIT +import itertools import math -from io import BytesIO from enum import Enum -import itertools +from io import BytesIO import numpy as np from PIL import Image from cvat.apps.engine.media_extractors import VideoReader, ZipReader -from cvat.apps.engine.models import DataChoice from cvat.apps.engine.mime_types import mimetypes +from cvat.apps.engine.models import DataChoice -class FrameProvider(): +class FrameProvider: class Quality(Enum): COMPRESSED = 0 ORIGINAL = 100 @@ -25,26 +25,33 @@ class FrameProvider(): PIL = 1 NUMPY_ARRAY = 2 - def __init__(self, db_data): - self._db_data = db_data - if db_data.compressed_chunk_type == DataChoice.IMAGESET: - self._compressed_chunk_reader_class = ZipReader - elif db_data.compressed_chunk_type == DataChoice.VIDEO: - self._compressed_chunk_reader_class = VideoReader - else: - raise Exception('Unsupported chunk type') + class ChunkLoader: + def __init__(self, reader_class, path_getter): + self.chunk_id = None + self.chunk_reader = None + self.reader_class = reader_class + self.get_chunk_path = path_getter - if db_data.original_chunk_type == DataChoice.IMAGESET: - self._original_chunk_reader_class = ZipReader - elif db_data.original_chunk_type == DataChoice.VIDEO: - self._original_chunk_reader_class = VideoReader - else: - raise Exception('Unsupported chunk type') + def load(self, chunk_id): + if self.chunk_id != chunk_id: + self.chunk_id = chunk_id + self.chunk_reader = self.reader_class([self.get_chunk_path(chunk_id)]) + return self.chunk_reader - self._extracted_compressed_chunk = None - self._compressed_chunk_reader = None - self._extracted_original_chunk = None - self._original_chunk_reader = None + def __init__(self, db_data): + self._db_data = db_data + self._loaders = {} + + reader_class = { + 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) def __len__(self): return self._db_data.size @@ -74,77 +81,41 @@ class FrameProvider(): buf.seek(0) return buf - def _get_frame(self, frame_number, chunk_path_getter, extracted_chunk, chunk_reader, reader_class): - _, chunk_number, frame_offset = self._validate_frame_number(frame_number) - chunk_path = chunk_path_getter(chunk_number) - if chunk_number != extracted_chunk: - extracted_chunk = chunk_number - chunk_reader = reader_class([chunk_path]) - - frame, frame_name, _ = next(itertools.islice(chunk_reader, frame_offset, None)) - if reader_class is VideoReader: - return (self._av_frame_to_png_bytes(frame), 'image/png') - - return (frame, mimetypes.guess_type(frame_name)) - - def _get_frames(self, chunk_path_getter, reader_class, out_type): - for chunk_idx in range(math.ceil(self._db_data.size / self._db_data.chunk_size)): - chunk_path = chunk_path_getter(chunk_idx) - chunk_reader = reader_class([chunk_path]) - for frame, _, _ in chunk_reader: - if out_type == self.Type.BUFFER: - yield self._av_frame_to_png_bytes(frame) if reader_class is VideoReader else frame - elif out_type == self.Type.PIL: - yield frame.to_image() if reader_class is VideoReader else Image.open(frame) - elif out_type == self.Type.NUMPY_ARRAY: - if reader_class is VideoReader: - image = np.array(frame.to_image()) - else: - image = np.array(Image.open(frame)) - if len(image.shape) == 3 and image.shape[2] in {3, 4}: - image[:, :, :3] = image[:, :, 2::-1] # RGB to BGR - yield image - else: - raise Exception('unsupported output type') + def _convert_frame(self, frame, reader_class, out_type): + if out_type == self.Type.BUFFER: + return self._av_frame_to_png_bytes(frame) if reader_class is VideoReader else frame + elif out_type == self.Type.PIL: + return frame.to_image() if reader_class is VideoReader else Image.open(frame) + elif out_type == self.Type.NUMPY_ARRAY: + if reader_class is VideoReader: + image = np.array(frame.to_image()) + else: + image = np.array(Image.open(frame)) + if len(image.shape) == 3 and image.shape[2] in {3, 4}: + image[:, :, :3] = image[:, :, 2::-1] # RGB to BGR + return image + else: + raise Exception('unsupported output type') def get_preview(self): return self._db_data.get_preview_path() def get_chunk(self, chunk_number, quality=Quality.ORIGINAL): chunk_number = self._validate_chunk_number(chunk_number) - if quality == self.Quality.ORIGINAL: - return self._db_data.get_original_chunk_path(chunk_number) - elif quality == self.Quality.COMPRESSED: - return self._db_data.get_compressed_chunk_path(chunk_number) + return self._loaders[quality].get_chunk_path(chunk_number) def get_frame(self, frame_number, quality=Quality.ORIGINAL): - if quality == self.Quality.ORIGINAL: - return self._get_frame( - frame_number=frame_number, - chunk_path_getter=self._db_data.get_original_chunk_path, - extracted_chunk=self._extracted_original_chunk, - chunk_reader=self._original_chunk_reader, - reader_class=self._original_chunk_reader_class, - ) - elif quality == self.Quality.COMPRESSED: - return self._get_frame( - frame_number=frame_number, - chunk_path_getter=self._db_data.get_compressed_chunk_path, - extracted_chunk=self._extracted_compressed_chunk, - chunk_reader=self._compressed_chunk_reader, - reader_class=self._compressed_chunk_reader_class, - ) + _, chunk_number, frame_offset = self._validate_frame_number(frame_number) + + chunk_reader = self._loaders[quality].load(chunk_number) + + frame, frame_name, _ = next(itertools.islice(chunk_reader, frame_offset, None)) + if self._loaders[quality].reader_class is VideoReader: + return (self._av_frame_to_png_bytes(frame), 'image/png') + return (frame, mimetypes.guess_type(frame_name)) def get_frames(self, quality=Quality.ORIGINAL, out_type=Type.BUFFER): - if quality == self.Quality.ORIGINAL: - return self._get_frames( - chunk_path_getter=self._db_data.get_original_chunk_path, - reader_class=self._original_chunk_reader_class, - out_type=out_type, - ) - elif quality == self.Quality.COMPRESSED: - return self._get_frames( - chunk_path_getter=self._db_data.get_compressed_chunk_path, - reader_class=self._compressed_chunk_reader_class, - out_type=out_type, - ) + loader = self._loaders[quality] + for chunk_idx in range(math.ceil(self._db_data.size / self._db_data.chunk_size)): + for frame, _, _ in loader.load(chunk_idx): + yield self._convert_frame(frame, loader.reader_class, out_type) From 411a3df57ae2ce7021bdb5c07eaee142b2ff7dff Mon Sep 17 00:00:00 2001 From: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> Date: Mon, 6 Apr 2020 11:55:16 +0300 Subject: [PATCH 02/25] Add CODEOWNERS file (#1360) * Add CODEOWNERS file * Removed the outdated file. See https://github.com/opencv/cvat/graphs/contributors * Change codeowners --- .github/CODEOWNERS | 40 ++++++++++++++++++++++++++++++++++++++++ CONTRIBUTORS.md | 38 -------------------------------------- 2 files changed, 40 insertions(+), 38 deletions(-) create mode 100644 .github/CODEOWNERS delete mode 100644 CONTRIBUTORS.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..2ae57678 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,40 @@ +# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, they will +# be requested for review when someone opens a pull request. +* @nmanovic + +# Order is important; the last matching pattern takes the most +# precedence. When someone opens a pull request that only +# modifies components below, only the list of owners and not +# the global owner(s) will be requested for a review. + +# Component: Server +/cvat/ @nmanovic @azhavoro + +# Component: CVAT UI +/cvat-ui/ @bsekachev @ActiveChooN +/cvat-data/ @azhavoro @bsekachev +/cvat-canvas/ @bsekachev @ActiveChooN +/cvat-core/ @bsekachev @ActiveChooN + +# Component: Datumaro +/datumaro/ @zhiltsov-max @nmanovic +/cvat/apps/dataset_manager/ @zhiltsov-max @nmanovic + +# Advanced components (e.g. OpenVINO) +/components/ @azhavoro + +# Infrastructure +Dockerfile* @azhavoro +docker-compose* @azhavoro +.* @azhavoro +*.conf @azhavoro +*.sh @azhavoro +/cvat_proxy/ @azhavoro +/tests/ @azhavoro +/utils/ @azhavoro +/*.md @nmanovic +/LICENSE @nmanovic +/.github/ @nmanovic \ No newline at end of file diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md deleted file mode 100644 index f21998f1..00000000 --- a/CONTRIBUTORS.md +++ /dev/null @@ -1,38 +0,0 @@ -# Core support team -- **[Nikita Manovich](https://github.com/nmanovic)** - - * Project lead - * Developer - * Author and maintainer - -- **[Boris Sekachev](https://github.com/bsekachev)** - - * Primary developer - * Author and maintainer - -- **[Andrey Zhavoronkov](https://github.com/azhavoro)** - - * Developer - * Author and maintainer - -# Contributors - -- **[Victor Salimonov](https://github.com/VikTorSalimonov)** - - * Documentation, screencasts - -- **[Dmitry Sidnev](https://github.com/DmitriySidnev)** - - * [convert_to_coco.py](utils/coco) - an utility for converting annotation from CVAT to COCO data annotation format - -- **[Sebastián Yonekura](https://github.com/syonekura)** - - * [convert_to_voc.py](utils/voc) - an utility for converting CVAT XML to PASCAL VOC data annotation format. - -- **[ITLab Team](https://github.com/itlab-vision/cvat):** - **[Vasily Danilin](https://github.com/DanVev)**, - **[Eugene Shashkin](https://github.com/EvgenyShashkin)**, - **[Dmitry Silenko](https://github.com/DimaSilenko)**, - **[Alina Bykovskaya](https://github.com/alinaut)**, - **[Yanina Koltushkina](https://github.com/YaniKolt)** - * Integrating CI tools as Travis CI, Codacy and Coveralls.io From 76fc8e442d74ae8a733fa722c894d33bc037ddb6 Mon Sep 17 00:00:00 2001 From: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> Date: Mon, 6 Apr 2020 15:58:55 +0300 Subject: [PATCH 03/25] Add pull request and issue templates (#1359) * Add initial version of pull request template * Fix links * Fix codacy issues * Slightly improve titles of sections * Add a note about strikethough for the checklist. * Fix progress of a pull request (each checkbox is an issue) * Add the license header, checkboxes about the license. * Updated the license * Update the license to met https://github.com/licensee/licensee/blob/master/vendor/choosealicense.com/_licenses/mit.txt restrictions. * Fix the pull request template name * Make explaination text as comments (it will be visible when you edit the PR message) * Add initial version of the issue template. --- .github/ISSUE_TEMPLATE.md | 53 ++++++++++++++++++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 46 +++++++++++++++++++++++++++ LICENSE | 5 +-- 3 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..d9506af8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,53 @@ + + +### My actions before raising this issue +- [ ] Read/searched [the docs](https://github.com/opencv/cvat/tree/master#documentation) +- [ ] Searched [past issues](/issues) + + + +### Expected Behaviour + + +### Current Behaviour + + +### Possible Solution + + +### Steps to Reproduce (for bugs) + +1. +1. +1. +1. + +### Context + + +### Your Environment + +- Git hash commit (`git log -1`): +- Docker version `docker version` (e.g. Docker 17.0.05): +- Are you using Docker Swarm or Kubernetes? +- Operating System and version (e.g. Linux, Windows, MacOS): +- Code example or link to GitHub repo or gist to reproduce problem: +- Other diagnostic information / logs: +
+ Logs from `cvat` container +
+ +### Next steps +You may [join our Gitter](https://gitter.im/opencv-cvat/public) channel for community support. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..23b9bde8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,46 @@ + + + + +### Motivation and context + + +### How has this been tested? + + +### Checklist + + +- [ ] I have raised an issue to propose this change ([required](https://github.com/opencv/cvat/issues)) +- [ ] My issue has received approval from the maintainers +- [ ] I've read the [CONTRIBUTION](https://github.com/opencv/cvat/blob/develop/CONTRIBUTING.md) guide +- [ ] I have added description of my changes into [CHANGELOG](https://github.com/opencv/cvat/blob/develop/CHANGELOG.md) file +- [ ] I have updated the [documentation]( + https://github.com/opencv/cvat/blob/develop/README.md#documentation) accordingly +- [ ] I have added tests to cover my changes +- [ ] I have linked related issues ([read github docs]( + https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)) + +### License + +- [ ] I submit _my code changes_ under the same [MIT License]( + https://github.com/opencv/cvat/blob/develop/LICENSE) that covers the project. + Feel free to contact the maintainers if that's a concern. +- [ ] I have updated the license header for each file (see an example below) + +```python +# Copyright (C) 2020 Intel Corporation +# +# SPDX-License-Identifier: MIT +``` diff --git a/LICENSE b/LICENSE index aae0a08e..46056e4f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,6 @@ -Copyright (C) 2018 Intel Corporation +MIT License + +Copyright (C) 2018-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"), @@ -18,4 +20,3 @@ 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.   -SPDX-License-Identifier: MIT From 1d78c540297fad3cf20abf371269bb34121ed529 Mon Sep 17 00:00:00 2001 From: Boris Sekachev <40690378+bsekachev@users.noreply.github.com> Date: Tue, 7 Apr 2020 15:21:33 +0300 Subject: [PATCH 04/25] Batch of fixes (#1370) * Some margins were change to paddings * Removed extra selected * Fix: added outside shapes when merge polyshapes * Fixed double scroll bars * Updated canvas table * Fixed setup methodf * Disabled change frame during drag, resize and editing * Fixed: hidden points are visible * Fixed: Merge is allowed for points, but clicks on points conflict with frame dragging logic * Fixed: do not filter removed objects * Updated CHANGELOG.md * Couple of headers updated --- CHANGELOG.md | 11 ++- cvat-canvas/README.md | 40 +++++----- cvat-canvas/src/typescript/canvasModel.ts | 4 + cvat-canvas/src/typescript/canvasView.ts | 6 +- cvat-core/src/annotations-collection.js | 4 +- .../annotation-page/annotation-page.tsx | 12 ++- .../components/create-task-page/styles.scss | 2 +- cvat-ui/src/components/tasks-page/styles.scss | 6 +- .../objects-side-bar/object-item.tsx | 60 ++++++--------- .../objects-side-bar/objects-list.tsx | 11 ++- .../annotation-page/top-bar/top-bar.tsx | 73 +++++++++++-------- cvat-ui/src/cvat-canvas.ts | 6 ++ cvat-ui/src/styles.scss | 1 - .../engine/static/engine/js/shapeMerger.js | 4 +- .../engine/templates/engine/annotation.html | 4 +- 15 files changed, 139 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a3778c..4678c31d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Increase preview size of a task till 256, 256 on the server +- Minor style updates ### Deprecated - @@ -23,8 +24,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - New shape is added when press ``esc`` when drawing instead of cancellation -- Fixed dextr segmentation. -- Fixed `FileNotFoundError` during dump after moving format files +- Dextr segmentation doesn't work. +- `FileNotFoundError` during dump after moving format files +- CVAT doesn't append outside shapes when merge polyshapes in old UI +- Layout sometimes shows double scroll bars on create task, dashboard and settings pages +- UI fails after trying to change frame during resizing, dragging, editing +- Hidden points (or outsided) are visible after changing a frame +- Merge is allowed for points, but clicks on points conflict with frame dragging logic +- Removed objects are visible for search ### Security - diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md index 59806746..d9cabfa9 100644 --- a/cvat-canvas/README.md +++ b/cvat-canvas/README.md @@ -179,23 +179,23 @@ Standard JS events are used. ## API Reaction -| | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING | DRAG | ZOOM | -|--------------|------|----------|-----------|---------|---------|---------|------|------| -| html() | + | + | + | + | + | + | + | + | -| 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 | +|--------------|------|-------|-------|------|-------|------|------|--------|-------------|-------------| +| html() | + | + | + | + | + | + | + | + | + | + | +| setup() | + | + | + | + | + | - | - | - | + | + | +| activate() | + | - | - | - | - | - | - | - | - | - | +| rotate() | + | + | + | + | + | + | + | + | + | + | +| focus() | + | + | + | + | + | + | + | + | + | + | +| fit() | + | + | + | + | + | + | + | + | + | + | +| grid() | + | + | + | + | + | + | + | + | + | + | +| draw() | + | - | - | - | - | - | - | - | - | - | +| split() | + | - | + | - | - | - | - | - | - | - | +| group() | + | + | - | - | - | - | - | - | - | - | +| merge() | + | - | - | - | + | - | - | - | - | - | +| fitCanvas() | + | + | + | + | + | + | + | + | + | + | +| dragCanvas() | + | - | - | - | - | - | + | - | - | + | +| zoomCanvas() | + | - | - | - | - | - | - | + | + | - | +| cancel() | - | + | + | + | + | + | + | + | + | + | +| configure() | + | - | - | - | - | - | - | - | - | - | +| bitmap() | + | + | + | + | + | + | + | + | + | + | +| setZLayer() | + | + | + | + | + | + | + | + | + | + | diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index 13f99be1..da7d112a 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -327,6 +327,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { } public setup(frameData: any, objectStates: any[]): void { + if ([Mode.EDIT, Mode.DRAG, Mode.RESIZE].includes(this.data.mode)) { + throw Error(`Canvas is busy. Action: ${this.data.mode}`); + } + if (frameData.number === this.data.imageID) { this.data.objects = objectStates; this.notify(UpdateReasons.OBJECTS_UPDATED); diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 27160436..1de8fa52 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1041,7 +1041,7 @@ export class CanvasViewImpl implements CanvasView, Listener { (shape as any).clear(); shape.attr('points', stringified); - if (state.shapeType === 'points') { + if (state.shapeType === 'points' && !state.hidden) { this.selectize(false, shape); this.setupPoints(shape as SVG.PolyLine, state); } @@ -1187,7 +1187,7 @@ export class CanvasViewImpl implements CanvasView, Listener { (shape as any).off('resizestart'); (shape as any).off('resizing'); (shape as any).off('resizedone'); - (shape as any).resize(false); + (shape as any).resize('stop'); // TODO: Hide text only if it is hidden by settings const text = this.svgTexts[clientID]; @@ -1543,6 +1543,8 @@ export class CanvasViewImpl implements CanvasView, Listener { group.on('click.canvas', (event: MouseEvent): void => { // Need to redispatch the event on another element basicPolyline.fire(new MouseEvent('click', event)); + // redispatch event to canvas to be able merge points clicking them + this.content.dispatchEvent(new MouseEvent('click', event)); }); group.bbox = basicPolyline.bbox.bind(basicPolyline); diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js index e3ba2735..e06e60f4 100644 --- a/cvat-core/src/annotations-collection.js +++ b/cvat-core/src/annotations-collection.js @@ -871,8 +871,10 @@ // In particular consider first and last frame as keyframes for all frames const statesData = [].concat( (frame in this.shapes ? this.shapes[frame] : []) + .filter((shape) => !shape.removed) .map((shape) => shape.get(frame)), (frame in this.tags ? this.tags[frame] : []) + .filter((tag) => !tag.removed) .map((tag) => tag.get(frame)), ); const tracks = Object.values(this.tracks) @@ -880,7 +882,7 @@ frame in track.shapes || frame === frameFrom || frame === frameTo - )); + )).filter((track) => !track.removed); statesData.push( ...tracks.map((track) => track.get(frame)) .filter((state) => !state.outside), diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index db5b6500..529ea77e 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -36,7 +36,17 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { useEffect(() => { saveLogs(); - return saveLogs; + const root = window.document.getElementById('root'); + if (root) { + root.style.minHeight = '768px'; + } + + return () => { + saveLogs(); + if (root) { + root.style.minHeight = ''; + } + }; }, []); if (job === null) { diff --git a/cvat-ui/src/components/create-task-page/styles.scss b/cvat-ui/src/components/create-task-page/styles.scss index 3419d860..df2bcc45 100644 --- a/cvat-ui/src/components/create-task-page/styles.scss +++ b/cvat-ui/src/components/create-task-page/styles.scss @@ -6,7 +6,7 @@ .cvat-create-task-form-wrapper { text-align: center; - margin-top: 40px; + padding-top: 40px; overflow-y: auto; height: 90%; diff --git a/cvat-ui/src/components/tasks-page/styles.scss b/cvat-ui/src/components/tasks-page/styles.scss index b2fbc9be..66cd8d4c 100644 --- a/cvat-ui/src/components/tasks-page/styles.scss +++ b/cvat-ui/src/components/tasks-page/styles.scss @@ -9,7 +9,7 @@ height: 100%; > div:nth-child(1) { - margin-bottom: 10px; + padding-bottom: 10px; div > { span { @@ -36,11 +36,11 @@ > div:nth-child(3) { height: 83%; - margin-top: 10px; + padding-top: 10px; } > div:nth-child(4) { - margin-top: 10px; + padding-top: 10px; } } 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 b80fcbbc..d7855e5d 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 @@ -7,6 +7,7 @@ import copy from 'copy-to-clipboard'; import { connect } from 'react-redux'; import { LogType } from 'cvat-logger'; +import { Canvas, isAbleToChangeFrame } from 'cvat-canvas'; import { ActiveControl, CombinedState, ColorBy } from 'reducers/interfaces'; import { collapseObjectItems, @@ -24,7 +25,6 @@ import { import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item'; - interface OwnProps { clientID: number; } @@ -44,6 +44,7 @@ interface StateToProps { minZLayer: number; maxZLayer: number; normalizedKeyMap: Record; + canvasInstance: Canvas; } interface DispatchToProps { @@ -84,6 +85,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { canvas: { ready, activeControl, + instance: canvasInstance, }, colors, }, @@ -119,6 +121,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { minZLayer, maxZLayer, normalizedKeyMap, + canvasInstance, }; } @@ -166,72 +169,44 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { type Props = StateToProps & DispatchToProps; class ObjectItemContainer extends React.PureComponent { private navigateFirstKeyframe = (): void => { - const { - objectState, - changeFrame, - frameNumber, - } = this.props; - + const { objectState, frameNumber } = this.props; const { first } = objectState.keyframes; if (first !== frameNumber) { - changeFrame(first); + this.changeFrame(first); } }; private navigatePrevKeyframe = (): void => { - const { - objectState, - changeFrame, - frameNumber, - } = this.props; - + const { objectState, frameNumber } = this.props; const { prev } = objectState.keyframes; if (prev !== null && prev !== frameNumber) { - changeFrame(prev); + this.changeFrame(prev); } }; private navigateNextKeyframe = (): void => { - const { - objectState, - changeFrame, - frameNumber, - } = this.props; - + const { objectState, frameNumber } = this.props; const { next } = objectState.keyframes; if (next !== null && next !== frameNumber) { - changeFrame(next); + this.changeFrame(next); } }; private navigateLastKeyframe = (): void => { - const { - objectState, - changeFrame, - frameNumber, - } = this.props; - + const { objectState, frameNumber } = this.props; const { last } = objectState.keyframes; if (last !== frameNumber) { - changeFrame(last); + this.changeFrame(last); } }; private copy = (): void => { - const { - objectState, - copyShape, - } = this.props; - + const { objectState, copyShape } = this.props; copyShape(objectState); }; private propagate = (): void => { - const { - objectState, - propagateObject, - } = this.props; - + const { objectState, propagateObject } = this.props; propagateObject(objectState); }; @@ -422,6 +397,13 @@ class ObjectItemContainer extends React.PureComponent { this.commit(); }; + private changeFrame(frame: number): void { + const { changeFrame, canvasInstance } = this.props; + if (isAbleToChangeFrame(canvasInstance)) { + changeFrame(frame); + } + } + private commit(): void { const { objectState, 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 49e61a27..df533b87 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 @@ -15,6 +15,7 @@ import { copyShape as copyShapeAction, propagateObject as propagateObjectAction, } from 'actions/annotation-actions'; +import { Canvas, isAbleToChangeFrame } from 'cvat-canvas'; import { CombinedState, StatesOrdering, ObjectType } from 'reducers/interfaces'; interface StateToProps { @@ -32,6 +33,7 @@ interface StateToProps { annotationsFiltersHistory: string[]; keyMap: Record; normalizedKeyMap: Record; + canvasInstance: Canvas; } interface DispatchToProps { @@ -65,6 +67,9 @@ function mapStateToProps(state: CombinedState): StateToProps { number: frameNumber, }, }, + canvas: { + instance: canvasInstance, + }, tabContentHeight: listHeight, }, shortcuts: { @@ -104,6 +109,7 @@ function mapStateToProps(state: CombinedState): StateToProps { annotationsFiltersHistory, keyMap, normalizedKeyMap, + canvasInstance, }; } @@ -254,6 +260,7 @@ class ObjectsListContainer extends React.PureComponent { minZLayer, keyMap, normalizedKeyMap, + canvasInstance, } = this.props; const { sortedStatesID, @@ -388,7 +395,7 @@ class ObjectsListContainer extends React.PureComponent { if (state && state.objectType === ObjectType.TRACK) { const frame = typeof (state.keyframes.next) === 'number' ? state.keyframes.next : null; - if (frame !== null) { + if (frame !== null && isAbleToChangeFrame(canvasInstance)) { changeFrame(frame); } } @@ -399,7 +406,7 @@ class ObjectsListContainer extends React.PureComponent { if (state && state.objectType === ObjectType.TRACK) { const frame = typeof (state.keyframes.prev) === 'number' ? state.keyframes.prev : null; - if (frame !== null) { + if (frame !== null && isAbleToChangeFrame(canvasInstance)) { changeFrame(frame); } } diff --git a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx index 21226ce9..fcfe943b 100644 --- a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx @@ -23,6 +23,7 @@ import { changeWorkspace as changeWorkspaceAction, activateObject, } from 'actions/annotation-actions'; +import { Canvas, isAbleToChangeFrame } from 'cvat-canvas'; import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar'; import { CombinedState, FrameSpeed, Workspace } from 'reducers/interfaces'; @@ -45,6 +46,7 @@ interface StateToProps { workspace: Workspace; keyMap: Record; normalizedKeyMap: Record; + canvasInstance: Canvas; } interface DispatchToProps { @@ -81,6 +83,7 @@ function mapStateToProps(state: CombinedState): StateToProps { }, canvas: { ready: canvasIsReady, + instance: canvasInstance, }, workspace, }, @@ -118,6 +121,7 @@ function mapStateToProps(state: CombinedState): StateToProps { workspace, keyMap, normalizedKeyMap, + canvasInstance, }; } @@ -197,6 +201,7 @@ class AnnotationTopBarContainer extends React.PureComponent { frameDelay, playing, canvasIsReady, + canvasInstance, onSwitchPlay, onChangeFrame, } = this.props; @@ -217,10 +222,14 @@ class AnnotationTopBarContainer extends React.PureComponent { setTimeout(() => { const { playing: stillPlaying } = this.props; if (stillPlaying) { - onChangeFrame( - frameNumber + 1 + framesSkiped, - stillPlaying, framesSkiped + 1, - ); + if (isAbleToChangeFrame(canvasInstance)) { + onChangeFrame( + frameNumber + 1 + framesSkiped, + stillPlaying, framesSkiped + 1, + ); + } else { + onSwitchPlay(false); + } } }, frameDelay); } else { @@ -240,9 +249,12 @@ class AnnotationTopBarContainer extends React.PureComponent { undo, jobInstance, frameNumber, + canvasInstance, } = this.props; - undo(jobInstance, frameNumber); + if (isAbleToChangeFrame(canvasInstance)) { + undo(jobInstance, frameNumber); + } }; private redo = (): void => { @@ -250,9 +262,12 @@ class AnnotationTopBarContainer extends React.PureComponent { redo, jobInstance, frameNumber, + canvasInstance, } = this.props; - redo(jobInstance, frameNumber); + if (isAbleToChangeFrame(canvasInstance)) { + redo(jobInstance, frameNumber); + } }; private showStatistics = (): void => { @@ -285,7 +300,6 @@ class AnnotationTopBarContainer extends React.PureComponent { jobInstance, playing, onSwitchPlay, - onChangeFrame, } = this.props; const newFrame = jobInstance.startFrame; @@ -293,7 +307,7 @@ class AnnotationTopBarContainer extends React.PureComponent { if (playing) { onSwitchPlay(false); } - onChangeFrame(newFrame); + this.changeFrame(newFrame); } }; @@ -304,7 +318,6 @@ class AnnotationTopBarContainer extends React.PureComponent { jobInstance, playing, onSwitchPlay, - onChangeFrame, } = this.props; const newFrame = Math @@ -313,7 +326,7 @@ class AnnotationTopBarContainer extends React.PureComponent { if (playing) { onSwitchPlay(false); } - onChangeFrame(newFrame); + this.changeFrame(newFrame); } }; @@ -323,7 +336,6 @@ class AnnotationTopBarContainer extends React.PureComponent { jobInstance, playing, onSwitchPlay, - onChangeFrame, } = this.props; const newFrame = Math @@ -332,7 +344,7 @@ class AnnotationTopBarContainer extends React.PureComponent { if (playing) { onSwitchPlay(false); } - onChangeFrame(newFrame); + this.changeFrame(newFrame); } }; @@ -342,7 +354,6 @@ class AnnotationTopBarContainer extends React.PureComponent { jobInstance, playing, onSwitchPlay, - onChangeFrame, } = this.props; const newFrame = Math @@ -351,7 +362,7 @@ class AnnotationTopBarContainer extends React.PureComponent { if (playing) { onSwitchPlay(false); } - onChangeFrame(newFrame); + this.changeFrame(newFrame); } }; @@ -362,7 +373,6 @@ class AnnotationTopBarContainer extends React.PureComponent { jobInstance, playing, onSwitchPlay, - onChangeFrame, } = this.props; const newFrame = Math @@ -371,7 +381,7 @@ class AnnotationTopBarContainer extends React.PureComponent { if (playing) { onSwitchPlay(false); } - onChangeFrame(newFrame); + this.changeFrame(newFrame); } }; @@ -381,7 +391,6 @@ class AnnotationTopBarContainer extends React.PureComponent { jobInstance, playing, onSwitchPlay, - onChangeFrame, } = this.props; const newFrame = jobInstance.stopFrame; @@ -389,7 +398,7 @@ class AnnotationTopBarContainer extends React.PureComponent { if (playing) { onSwitchPlay(false); } - onChangeFrame(newFrame); + this.changeFrame(newFrame); } }; @@ -403,22 +412,16 @@ class AnnotationTopBarContainer extends React.PureComponent { }; private onChangePlayerSliderValue = (value: SliderValue): void => { - const { - playing, - onSwitchPlay, - onChangeFrame, - } = this.props; - + const { playing, onSwitchPlay } = this.props; if (playing) { onSwitchPlay(false); } - onChangeFrame(value as number); + this.changeFrame(value as number); }; private onChangePlayerInputValue = (value: number): void => { const { onSwitchPlay, - onChangeFrame, playing, frameNumber, } = this.props; @@ -427,7 +430,7 @@ class AnnotationTopBarContainer extends React.PureComponent { if (playing) { onSwitchPlay(false); } - onChangeFrame(value); + this.changeFrame(value); } }; @@ -441,6 +444,13 @@ class AnnotationTopBarContainer extends React.PureComponent { copy(url); }; + private changeFrame(frame: number): void { + const { onChangeFrame, canvasInstance } = this.props; + if (isAbleToChangeFrame(canvasInstance)) { + onChangeFrame(frame); + } + } + private beforeUnloadCallback(event: BeforeUnloadEvent): any { const { jobInstance } = this.props; if (jobInstance.annotations.hasUnsavedChanges()) { @@ -472,6 +482,7 @@ class AnnotationTopBarContainer extends React.PureComponent { changeWorkspace, keyMap, normalizedKeyMap, + canvasInstance, } = this.props; const preventDefault = (event: KeyboardEvent | undefined): void => { @@ -537,13 +548,17 @@ class AnnotationTopBarContainer extends React.PureComponent { }, SEARCH_FORWARD: (event: KeyboardEvent | undefined) => { preventDefault(event); - if (frameNumber + 1 <= stopFrame && canvasIsReady) { + if (frameNumber + 1 <= stopFrame && canvasIsReady + && isAbleToChangeFrame(canvasInstance) + ) { searchAnnotations(jobInstance, frameNumber + 1, stopFrame); } }, SEARCH_BACKWARD: (event: KeyboardEvent | undefined) => { preventDefault(event); - if (frameNumber - 1 >= startFrame && canvasIsReady) { + if (frameNumber - 1 >= startFrame && canvasIsReady + && isAbleToChangeFrame(canvasInstance) + ) { searchAnnotations(jobInstance, frameNumber - 1, startFrame); } }, diff --git a/cvat-ui/src/cvat-canvas.ts b/cvat-ui/src/cvat-canvas.ts index 6317c435..1a21be5a 100644 --- a/cvat-ui/src/cvat-canvas.ts +++ b/cvat-ui/src/cvat-canvas.ts @@ -9,9 +9,15 @@ import { RectDrawingMethod, } from '../../cvat-canvas/src/typescript/canvas'; +function isAbleToChangeFrame(canvas: Canvas): boolean { + return ![CanvasMode.DRAG, CanvasMode.EDIT, CanvasMode.RESIZE] + .includes(canvas.mode()); +} + export { Canvas, CanvasMode, CanvasVersion, RectDrawingMethod, + isAbleToChangeFrame, }; diff --git a/cvat-ui/src/styles.scss b/cvat-ui/src/styles.scss index 965ea756..cbd6a4c2 100644 --- a/cvat-ui/src/styles.scss +++ b/cvat-ui/src/styles.scss @@ -48,5 +48,4 @@ hr { height: 100%; display: grid; min-width: 1280px; - min-height: 768px; } diff --git a/cvat/apps/engine/static/engine/js/shapeMerger.js b/cvat/apps/engine/static/engine/js/shapeMerger.js index c7fc478c..f0485329 100644 --- a/cvat/apps/engine/static/engine/js/shapeMerger.js +++ b/cvat/apps/engine/static/engine/js/shapeMerger.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 Intel Corporation + * Copyright (C) 2018-2020 Intel Corporation * * SPDX-License-Identifier: MIT */ @@ -145,7 +145,7 @@ class ShapeMergerModel extends Listener { let nextFrame = frame + 1; let stopFrame = window.cvat.player.frames.stop; let type = shapeDict[frame].shape.type; - if (type === 'annotation_box' && !(nextFrame in shapeDict) && nextFrame <= stopFrame) { + if (type.startsWith('annotation_') && !(nextFrame in shapeDict) && nextFrame <= stopFrame) { let copy = Object.assign({}, object.shapes[object.shapes.length - 1]); copy.outside = true; copy.frame += 1; diff --git a/cvat/apps/engine/templates/engine/annotation.html b/cvat/apps/engine/templates/engine/annotation.html index 66cf69fa..d78b4ecf 100644 --- a/cvat/apps/engine/templates/engine/annotation.html +++ b/cvat/apps/engine/templates/engine/annotation.html @@ -1,5 +1,5 @@ @@ -451,7 +451,7 @@