diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d03274a..9f8c3eda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,36 @@ 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 +## [1.2.0-beta] - Unreleased + +### Added + +- + +### Changed + +- + +### Deprecated + +- + +### Removed + +- + +### Fixed + +- + +### Security + +- + +## [1.2.0-alpha] - 2020-11-09 ### Added -- Removed Z-Order flag from task creation process - Ability to login into CVAT-UI with token from api/v1/auth/login () - Added layout grids toggling ('ctrl + alt + Enter') - Added password reset functionality () @@ -29,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ability to upload prepared meta information along with a video when creating a task () - Optional chaining plugin for cvat-canvas and cvat-ui () - MOTS png mask format support () +- Ability to correct upload video with a rotation record in the metadata () - User search field for assignee fields () ### Changed @@ -41,14 +67,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated `docker-compose` file version from `2.3` to `3.3`() - Added auto inference of url schema from host in CLI, if provided () - Track frames in skips between annotation is presented in MOT and MOTS formats are marked `outside` () - -### Deprecated - -- +- UI packages installation with `npm ci` instead of `npm install` () ### Removed -- +- Removed Z-Order flag from task creation process ### Fixed @@ -70,10 +93,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Cannot read property 'label' of undefined (Fixed?) () - Excluded track frames marked `outside` in `CVAT for Images` export () - 'List of tasks' Kibana visualization () - -### Security - -- +- An error on exporting not `jpg` or `png` images in TF Detection API format () ## [1.1.0] - 2020-08-31 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb17d973..277d2bc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,9 +67,9 @@ patches and features. - Install npm packages for UI and start UI debug server (run the following command from CVAT root directory): ```sh - npm install && \ - cd cvat-core && npm install && \ - cd ../cvat-ui && npm install && npm start + npm ci && \ + cd cvat-core && npm ci && \ + cd ../cvat-ui && npm ci && npm start ``` > Note for Mac users diff --git a/Dockerfile.ui b/Dockerfile.ui index 6af7763c..a12b60d8 100644 --- a/Dockerfile.ui +++ b/Dockerfile.ui @@ -25,19 +25,19 @@ RUN npm config set loglevel info # Install cvat-data dependencies WORKDIR /tmp/cvat-data/ -RUN npm install +RUN npm ci # Install cvat-core dependencies WORKDIR /tmp/cvat-core/ -RUN npm install +RUN npm ci # Install cvat-canvas dependencies WORKDIR /tmp/cvat-canvas/ -RUN npm install +RUN npm ci # Install cvat-ui dependencies WORKDIR /tmp/cvat-ui/ -RUN npm install +RUN npm ci # Build source code COPY cvat-data/ /tmp/cvat-data/ diff --git a/README.md b/README.md index 0ca054f4..33901c51 100644 --- a/README.md +++ b/README.md @@ -122,3 +122,7 @@ Other ways to ask questions and get our support: - [Intel AI blog: New Computer Vision Tool Accelerates Annotation of Digital Images and Video](https://www.intel.ai/introducing-cvat) - [Intel Software: Computer Vision Annotation Tool: A Universal Approach to Data Annotation](https://software.intel.com/en-us/articles/computer-vision-annotation-tool-a-universal-approach-to-data-annotation) - [VentureBeat: Intel open-sources CVAT, a toolkit for data labeling](https://venturebeat.com/2019/03/05/intel-open-sources-cvat-a-toolkit-for-data-labeling/) + +## Projects using CVAT + +- [Onepanel](https://github.com/onepanelio/core) - Onepanel is an open source vision AI platform that fully integrates CVAT with scalable data processing and parallelized training pipelines. diff --git a/cvat-core/README.md b/cvat-core/README.md index 487de911..22d8faa9 100644 --- a/cvat-core/README.md +++ b/cvat-core/README.md @@ -18,7 +18,7 @@ If you make changes in this package, please do following: - Dependencies installation ```bash -npm install +npm ci ``` - Building the module from sources in the `dist` directory: diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json index 6fc9b5a1..dd367461 100644 --- a/cvat-core/package-lock.json +++ b/cvat-core/package-lock.json @@ -2822,10 +2822,6 @@ }, "cvat-data": { "version": "file:../cvat-data", - "requires": { - "async-mutex": "^0.2.4", - "jszip": "3.5.0" - }, "dependencies": { "@babel/cli": { "version": "7.6.4", @@ -9637,9 +9633,9 @@ } }, "detect-browser": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.1.1.tgz", - "integrity": "sha512-5n2aWI57qC3kZaK4j2zYsG6L1LrxgLptGCNhMQgdKhVn6cSdcq43pp6xHPfTHG3TYM6myF4tIPWiZtfdVDgb9w==" + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.2.0.tgz", + "integrity": "sha512-tr7XntDAu50BVENgQfajMLzacmSe34D+qZc4zjnniz0ZVuw/TZcLcyxHQjYpJTM36sGEkZZlYLnIM1hH7alTMA==" }, "detect-file": { "version": "1.0.0", diff --git a/cvat-core/package.json b/cvat-core/package.json index e7e86118..84140dde 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -36,7 +36,7 @@ "axios": "^0.20.0", "browser-or-node": "^1.2.1", "cvat-data": "../cvat-data", - "detect-browser": "^5.0.0", + "detect-browser": "^5.2.0", "error-stack-parser": "^2.0.2", "form-data": "^2.5.0", "jest-config": "^24.8.0", diff --git a/cvat-ui/README.md b/cvat-ui/README.md index d020214a..c32879e6 100644 --- a/cvat-ui/README.md +++ b/cvat-ui/README.md @@ -20,7 +20,7 @@ you also need to do `npm install` to update `package-lock.json` - Installing dependencies: ```bash -cd ../cvat-core && npm install && cd - && npm install +cd ../cvat-core && npm ci && cd - && npm ci ``` - Running development UI server with autorebuild on change diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 1d67a279..c8a305a6 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1213,9 +1213,9 @@ "dev": true }, "@types/react": { - "version": "16.9.51", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.51.tgz", - "integrity": "sha512-lQa12IyO+DMlnSZ3+AGHRUiUcpK47aakMMoBG8f7HGxJT8Yfe+WE128HIXaHOHVPReAW0oDS3KAI0JI2DDe1PQ==", + "version": "16.9.53", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.53.tgz", + "integrity": "sha512-4nW60Sd4L7+WMXH1D6jCdVftuW7j4Za6zdp6tJ33Rqv0nk1ZAmQKML9ZLD4H0dehA3FZxXR/GM8gXplf82oNGw==", "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -12879,7 +12879,6 @@ "requires": { "axios": "^0.20.0", "browser-or-node": "^1.2.1", - "cvat-data": "file:../cvat-data", "detect-browser": "^5.0.0", "error-stack-parser": "^2.0.2", "form-data": "^2.5.0", @@ -28856,9 +28855,9 @@ } }, "react": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", - "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -28889,9 +28888,9 @@ } }, "react-dom": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", - "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==", + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", diff --git a/cvat-ui/package.json b/cvat-ui/package.json index f07fbf3d..1afed2a4 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -49,7 +49,7 @@ "dependencies": { "@types/lodash": "^4.14.165", "@types/platform": "^1.3.3", - "@types/react": "^16.9.51", + "@types/react": "^16.9.53", "@types/react-color": "^3.0.4", "@types/react-dom": "^16.9.0", "@types/react-redux": "^7.1.2", @@ -67,10 +67,10 @@ "moment": "^2.29.1", "platform": "^1.3.6", "prop-types": "^15.7.2", - "react": "^16.13.1", + "react": "^16.14.0", "react-color": "^2.18.1", "react-cookie": "^4.0.3", - "react-dom": "^16.13.1", + "react-dom": "^16.14.0", "react-hotkeys": "^2.0.0", "react-redux": "^7.1.1", "react-router": "^5.1.0", diff --git a/cvat/__init__.py b/cvat/__init__.py index f96a1724..f4cd65b4 100644 --- a/cvat/__init__.py +++ b/cvat/__init__.py @@ -4,6 +4,6 @@ from cvat.utils.version import get_version -VERSION = (1, 2, 0, 'alpha', 0) +VERSION = (1, 2, 0, 'beta', 0) __version__ = get_version(VERSION) diff --git a/cvat/apps/dataset_manager/formats/README.md b/cvat/apps/dataset_manager/formats/README.md index c3a9b6d0..b4728701 100644 --- a/cvat/apps/dataset_manager/formats/README.md +++ b/cvat/apps/dataset_manager/formats/README.md @@ -18,6 +18,7 @@ - [PASCAL VOC and mask](#voc) - [YOLO](#yolo) - [TF detection API](#tfrecord) + - [ImageNet](#imagenet) ## How to add a new annotation format support @@ -802,3 +803,35 @@ taskname.zip/ ``` - supported annotations: Rectangles, Polygons, Masks (as polygons) + +### [ImageNet](http://www.image-net.org) + +#### ImageNet Dumper + +Downloaded file: a zip archive of the following structure: + +```bash +# if we save images: +taskname.zip/ +└── label1/ + ├── label1_image1.jpg + └── label1_image2.jpg +└── label2/ + ├── label2_image1.jpg + ├── label2_image3.jpg + └── label2_image4.jpg + +# if we keep only annotation: +taskname.zip/ +└── .txt +└── synsets.txt + +``` + +- supported annotations: Labels + +#### ImageNet Loader + +Uploaded file: a zip archive of the structure above + +- supported annotations: Labels diff --git a/cvat/apps/dataset_manager/formats/imagenet.py b/cvat/apps/dataset_manager/formats/imagenet.py new file mode 100644 index 00000000..d9847549 --- /dev/null +++ b/cvat/apps/dataset_manager/formats/imagenet.py @@ -0,0 +1,41 @@ +# Copyright (C) 2020 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import os.path as osp +from glob import glob + +import zipfile +from tempfile import TemporaryDirectory + +from datumaro.components.project import Dataset +from cvat.apps.dataset_manager.bindings import CvatTaskDataExtractor, \ + import_dm_annotations +from cvat.apps.dataset_manager.util import make_zip_archive + +from .registry import dm_env, exporter, importer + + +@exporter(name='ImageNet', ext='ZIP', version='1.0') +def _export(dst_file, task_data, save_images=False): + extractor = CvatTaskDataExtractor(task_data, include_images=save_images) + extractor = Dataset.from_extractors(extractor) # apply lazy transform + with TemporaryDirectory() as temp_dir: + if save_images: + dm_env.converters.get('imagenet').convert(extractor, + save_dir=temp_dir, save_images=save_images) + else: + dm_env.converters.get('imagenet_txt').convert(extractor, + save_dir=temp_dir, save_images=save_images) + + make_zip_archive(temp_dir, dst_file) + +@importer(name='ImageNet', ext='ZIP', version='1.0') +def _import(src_file, task_data): + with TemporaryDirectory() as tmp_dir: + zipfile.ZipFile(src_file).extractall(tmp_dir) + if glob(osp.join(tmp_dir, '*.txt')): + dataset = dm_env.make_importer('imagenet_txt')(tmp_dir).make_dataset() + else: + dataset = dm_env.make_importer('imagenet')(tmp_dir).make_dataset() + import_dm_annotations(dataset, task_data) \ No newline at end of file diff --git a/cvat/apps/dataset_manager/formats/registry.py b/cvat/apps/dataset_manager/formats/registry.py index c84d60fc..c175a42b 100644 --- a/cvat/apps/dataset_manager/formats/registry.py +++ b/cvat/apps/dataset_manager/formats/registry.py @@ -90,4 +90,5 @@ import cvat.apps.dataset_manager.formats.mot import cvat.apps.dataset_manager.formats.mots import cvat.apps.dataset_manager.formats.pascal_voc import cvat.apps.dataset_manager.formats.tfrecord -import cvat.apps.dataset_manager.formats.yolo \ No newline at end of file +import cvat.apps.dataset_manager.formats.yolo +import cvat.apps.dataset_manager.formats.imagenet \ No newline at end of file diff --git a/cvat/apps/dataset_manager/tests/test_formats.py b/cvat/apps/dataset_manager/tests/test_formats.py index 54f5d286..d41e253c 100644 --- a/cvat/apps/dataset_manager/tests/test_formats.py +++ b/cvat/apps/dataset_manager/tests/test_formats.py @@ -269,6 +269,7 @@ class TaskExportTest(_DbTestBase): 'Segmentation mask 1.1', 'TFRecord 1.0', 'YOLO 1.1', + 'ImageNet 1.0', }) def test_import_formats_query(self): @@ -285,6 +286,7 @@ class TaskExportTest(_DbTestBase): 'Segmentation mask 1.1', 'TFRecord 1.0', 'YOLO 1.1', + 'ImageNet 1.0', }) def test_exports(self): @@ -320,6 +322,7 @@ class TaskExportTest(_DbTestBase): ('Segmentation mask 1.1', 'voc'), ('TFRecord 1.0', 'tf_detection_api'), ('YOLO 1.1', 'yolo'), + ('ImageNet 1.0', 'imagenet_txt'), ]: with self.subTest(format=format_name): if not dm.formats.registry.EXPORT_FORMATS[format_name].ENABLED: diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 21430838..b58bf98c 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -14,6 +14,7 @@ import av import numpy as np from pyunpack import Archive from PIL import Image, ImageFile +from cvat.apps.engine.utils import rotate_image # fixes: "OSError:broken data stream" when executing line 72 while loading images downloaded from the web # see: https://stackoverflow.com/questions/42462431/oserror-broken-data-stream-when-reading-image-file @@ -228,6 +229,16 @@ class VideoReader(IMediaReader): for image in packet.decode(): frame_num += 1 if self._has_frame(frame_num - 1): + if packet.stream.metadata.get('rotate'): + old_image = image + image = av.VideoFrame().from_ndarray( + rotate_image( + image.to_ndarray(format='bgr24'), + 360 - int(container.streams.video[0].metadata.get('rotate')) + ), + format ='bgr24' + ) + image.pts = old_image.pts yield (image, self._source_path[0], image.pts) def __iter__(self): @@ -252,7 +263,15 @@ class VideoReader(IMediaReader): container = self._get_av_container() stream = container.streams.video[0] preview = next(container.decode(stream)) - return self._get_preview(preview.to_image()) + return self._get_preview(preview.to_image() if not stream.metadata.get('rotate') \ + else av.VideoFrame().from_ndarray( + rotate_image( + preview.to_ndarray(format='bgr24'), + 360 - int(container.streams.video[0].metadata.get('rotate')) + ), + format ='bgr24' + ).to_image() + ) def get_image_size(self, i): image = (next(iter(self)))[0] diff --git a/cvat/apps/engine/prepare.py b/cvat/apps/engine/prepare.py index 9465b680..9ee54630 100644 --- a/cvat/apps/engine/prepare.py +++ b/cvat/apps/engine/prepare.py @@ -6,6 +6,7 @@ import av from collections import OrderedDict import hashlib import os +from cvat.apps.engine.utils import rotate_image class WorkWithVideo: def __init__(self, **kwargs): @@ -24,7 +25,6 @@ class WorkWithVideo: video_stream.thread_type = 'AUTO' return video_stream - class AnalyzeVideo(WorkWithVideo): def check_type_first_frame(self): container = self._open_video_container(self.source_path, mode='r') @@ -76,7 +76,17 @@ class PrepareInfo(WorkWithVideo): @property def frame_sizes(self): + container = self._open_video_container(self.source_path, 'r') frame = next(iter(self.key_frames.values())) + if container.streams.video[0].metadata.get('rotate'): + frame = av.VideoFrame().from_ndarray( + rotate_image( + frame.to_ndarray(format='bgr24'), + 360 - int(container.streams.video[0].metadata.get('rotate')) + ), + format ='bgr24' + ) + self._close_video_container(container) return (frame.width, frame.height) def check_key_frame(self, container, video_stream, key_frame): @@ -150,6 +160,14 @@ class PrepareInfo(WorkWithVideo): if frame_number < start_chunk_frame_number: continue elif frame_number < end_chunk_frame_number and not ((frame_number - start_chunk_frame_number) % step): + if video_stream.metadata.get('rotate'): + frame = av.VideoFrame().from_ndarray( + rotate_image( + frame.to_ndarray(format='bgr24'), + 360 - int(container.streams.video[0].metadata.get('rotate')) + ), + format ='bgr24' + ) yield frame elif (frame_number - start_chunk_frame_number) % step: continue @@ -177,6 +195,14 @@ class UploadedMeta(PrepareInfo): container.seek(offset=next(iter(self.key_frames.values())), stream=video_stream) for packet in container.demux(video_stream): for frame in packet.decode(): + if video_stream.metadata.get('rotate'): + frame = av.VideoFrame().from_ndarray( + rotate_image( + frame.to_ndarray(format='bgr24'), + 360 - int(container.streams.video[0].metadata.get('rotate')) + ), + format ='bgr24' + ) self._close_video_container(container) return (frame.width, frame.height) diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index fad3654f..e724d242 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -294,6 +294,7 @@ def _create_thread(tid, data): if settings.USE_CACHE and db_data.storage_method == StorageMethodChoice.CACHE: for media_type, media_files in media.items(): + if not media_files: continue diff --git a/cvat/apps/engine/tests/assets/test_rotated_90_video.mp4 b/cvat/apps/engine/tests/assets/test_rotated_90_video.mp4 new file mode 100644 index 00000000..31bd0737 Binary files /dev/null and b/cvat/apps/engine/tests/assets/test_rotated_90_video.mp4 differ diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py index 2b3d7378..02635a70 100644 --- a/cvat/apps/engine/tests/test_rest_api.py +++ b/cvat/apps/engine/tests/test_rest_api.py @@ -1553,6 +1553,16 @@ class TaskDataAPITestCase(APITestCase): video.write(data.read()) cls._image_sizes[filename] = img_sizes + filename = "test_rotated_90_video.mp4" + path = os.path.join(os.path.dirname(__file__), 'assets', 'test_rotated_90_video.mp4') + container = av.open(path, 'r') + for frame in container.decode(video=0): + # pyav ignores rotation record in metadata when decoding frames + img_sizes = [(frame.height, frame.width)] * container.streams.video[0].frames + break + container.close() + cls._image_sizes[filename] = img_sizes + filename = os.path.join("videos", "test_video_1.mp4") path = os.path.join(settings.SHARE_ROOT, filename) os.makedirs(os.path.dirname(path)) @@ -2008,7 +2018,7 @@ class TaskDataAPITestCase(APITestCase): os.path.join(settings.SHARE_ROOT, "videos") ) task_spec = { - "name": "my video with meta info task #11", + "name": "my video with meta info task #13", "overlap": 0, "segment_size": 0, "labels": [ @@ -2027,6 +2037,47 @@ class TaskDataAPITestCase(APITestCase): 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": "my cached video task #14", + "overlap": 0, + "segment_size": 0, + "labels": [ + {"name": "car"}, + {"name": "person"}, + ] + } + + task_data = { + "client_files[0]": open(os.path.join(os.path.dirname(__file__), 'assets', 'test_rotated_90_video.mp4'), 'rb'), + "image_quality": 70, + "use_zip_chunks": True + } + + image_sizes = self._image_sizes['test_rotated_90_video.mp4'] + self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, + self.ChunkType.VIDEO, image_sizes, StorageMethodChoice.FILE_SYSTEM) + + task_spec = { + "name": "my video task #15", + "overlap": 0, + "segment_size": 0, + "labels": [ + {"name": "car"}, + {"name": "person"}, + ] + } + + task_data = { + "client_files[0]": open(os.path.join(os.path.dirname(__file__), 'assets', 'test_rotated_90_video.mp4'), 'rb'), + "image_quality": 70, + "use_cache": True, + "use_zip_chunks": True + } + + image_sizes = self._image_sizes['test_rotated_90_video.mp4'] + self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, + self.ChunkType.VIDEO, image_sizes, StorageMethodChoice.CACHE) + def test_api_v1_tasks_id_data_admin(self): self._test_api_v1_tasks_id_data(self.admin) @@ -3360,6 +3411,9 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase): + polygon_shapes_with_attrs annotations["tags"] = tags_with_attrs + tags_wo_attrs + elif annotation_format == "ImageNet 1.0": + annotations["tags"] = tags_wo_attrs + else: raise Exception("Unknown format {}".format(annotation_format)) diff --git a/cvat/apps/engine/utils.py b/cvat/apps/engine/utils.py index e1ad9ef8..854393cf 100644 --- a/cvat/apps/engine/utils.py +++ b/cvat/apps/engine/utils.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: MIT import ast +import cv2 as cv from collections import namedtuple import importlib import sys @@ -74,3 +75,16 @@ def av_scan_paths(*paths): res = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if res.returncode: raise ValidationError(res.stdout) + +def rotate_image(image, angle): + height, width = image.shape[:2] + image_center = (width/2, height/2) + matrix = cv.getRotationMatrix2D(image_center, angle, 1.) + abs_cos = abs(matrix[0,0]) + abs_sin = abs(matrix[0,1]) + bound_w = int(height * abs_sin + width * abs_cos) + bound_h = int(height * abs_cos + width * abs_sin) + matrix[0, 2] += bound_w/2 - image_center[0] + matrix[1, 2] += bound_h/2 - image_center[1] + matrix = cv.warpAffine(image, matrix, (bound_w, bound_h)) + return matrix diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index 60613b43..4d9dabf1 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -44,4 +44,4 @@ tensorflow==2.2.1 # Optional requirement of Datumaro # archives. Don't use as a python module because it has GPL license. patool==1.12 diskcache==5.0.2 -git+https://github.com/openvinotoolkit/datumaro@v0.1.2 \ No newline at end of file +git+https://github.com/openvinotoolkit/datumaro@v0.1.3 \ No newline at end of file diff --git a/tests/cypress/integration/actions_tasks_objects/case_13_merge_split_features.js b/tests/cypress/integration/actions_tasks_objects/case_13_merge_split_features.js new file mode 100644 index 00000000..70139bc8 --- /dev/null +++ b/tests/cypress/integration/actions_tasks_objects/case_13_merge_split_features.js @@ -0,0 +1,155 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +/// + +import { taskName } from '../../support/const'; + +context('Merge/split features', () => { + const caseId = '13'; + const createRectangleShape2Points = { + points: 'By 2 Points', + type: 'Shape', + switchLabel: false, + firstX: 250, + firstY: 350, + secondX: 350, + secondY: 450, + }; + const createRectangleShape2PointsSecond = { + points: 'By 2 Points', + type: 'Shape', + switchLabel: false, + firstX: createRectangleShape2Points.firstX + 300, + firstY: createRectangleShape2Points.firstY, + secondX: createRectangleShape2Points.secondX + 300, + secondY: createRectangleShape2Points.secondY, + }; + const frameNum = 0; + // Check the 'X' coordinate. 'Y' coordinate is the same. + let xCoordinatesObjectFirstFrame = 0; + let xCoordinatesObjectThirdFrame = 0; + + before(() => { + cy.openTaskJob(taskName); + }); + + function goCheckFrameNumber(frameNum) { + cy.get('.cvat-player-frame-selector').within(() => { + cy.get('input[role="spinbutton"]').clear().type(`${frameNum}{Enter}`).should('have.value', frameNum); + }); + } + + describe(`Testing case "${caseId}"`, () => { + it('Create rectangle shape on first frame', () => { + goCheckFrameNumber(frameNum); + cy.createRectangle(createRectangleShape2Points); + cy.get('#cvat_canvas_shape_1') + .should('have.attr', 'x') + .then((xCoords) => { + xCoordinatesObjectFirstFrame = Math.floor(xCoords); + }); + }); + it('Create rectangle shape on third frame with another position', () => { + goCheckFrameNumber(frameNum + 2); + cy.createRectangle(createRectangleShape2PointsSecond); + cy.get('#cvat_canvas_shape_2') + .should('have.attr', 'x') + .then((xCoords) => { + xCoordinatesObjectThirdFrame = Math.floor(xCoords); + }); + }); + it('Merge the objects with "Merge button"', () => { + cy.get('.cvat-merge-control').click(); + cy.get('#cvat_canvas_shape_2').click(); + goCheckFrameNumber(frameNum); + cy.get('#cvat_canvas_shape_1').click(); + cy.get('.cvat-merge-control').click(); + }); + it('Get a track with keyframes on first and third frame', () => { + cy.get('#cvat_canvas_shape_3').should('exist').and('be.visible'); + cy.get('#cvat-objects-sidebar-state-item-3') + .should('contain', '3') + .and('contain', 'RECTANGLE TRACK') + .within(() => { + cy.get('.cvat-object-item-button-keyframe-enabled').should('exist'); + }); + goCheckFrameNumber(frameNum + 2); + cy.get('#cvat_canvas_shape_3').should('exist').and('be.visible'); + cy.get('#cvat-objects-sidebar-state-item-3') + .should('contain', '3') + .and('contain', 'RECTANGLE TRACK') + .within(() => { + cy.get('.cvat-object-item-button-keyframe-enabled').should('exist'); + }); + }); + it('On the second frame and on the fourth frame the track is invisible', () => { + goCheckFrameNumber(frameNum + 1); + cy.get('#cvat_canvas_shape_3').should('exist').and('be.hidden'); + goCheckFrameNumber(frameNum + 3); + cy.get('#cvat_canvas_shape_3').should('exist').and('be.hidden'); + }); + it('Go to the second frame and remove "outside" flag from the track. The track now visible.', () => { + goCheckFrameNumber(frameNum + 1); + cy.get('#cvat-objects-sidebar-state-item-3') + .should('contain', '3') + .and('contain', 'RECTANGLE TRACK') + .within(() => { + cy.get('.cvat-object-item-button-outside').click(); + cy.get('.cvat-object-item-button-outside-enabled').should('not.exist'); + }); + cy.get('#cvat_canvas_shape_3').should('exist').and('be.visible'); + }); + it('Remove "keyframe" flag from the track. Track now interpolated between position on the first and the third frames.', () => { + cy.get('#cvat-objects-sidebar-state-item-3') + .should('contain', '3') + .and('contain', 'RECTANGLE TRACK') + .within(() => { + cy.get('.cvat-object-item-button-keyframe').click(); + cy.get('.cvat-object-item-button-keyframe-enabled').should('not.exist'); + }); + cy.get('#cvat_canvas_shape_3') + .should('have.attr', 'x') + .then((xCoords) => { + // expected 9785 to be within 9642..9928 + expect(Math.floor(xCoords)).to.be.within( + xCoordinatesObjectFirstFrame, + xCoordinatesObjectThirdFrame, + ); + }); + }); + it('On the fourth frame remove "keyframe" flag from the track. The track now visible and "outside" flag is disabled.', () => { + goCheckFrameNumber(frameNum + 3); + cy.get('#cvat-objects-sidebar-state-item-3') + .should('contain', '3') + .and('contain', 'RECTANGLE TRACK') + .within(() => { + cy.get('.cvat-object-item-button-keyframe').click(); + cy.get('.cvat-object-item-button-keyframe-enabled').should('not.exist'); + cy.get('.cvat-object-item-button-outside-enabled').should('not.exist'); + }); + cy.get('#cvat_canvas_shape_3').should('exist').and('be.visible'); + }); + it('Split a track with "split" button. Previous track became invisible (has "outside" flag). One more track and it is visible.', () => { + cy.get('.cvat-split-track-control').click(); + // A single click does not reproduce the split a track scenario in cypress test. + cy.get('#cvat_canvas_shape_3').click().click(); + cy.get('#cvat_canvas_shape_4').should('exist').and('be.hidden'); + cy.get('#cvat-objects-sidebar-state-item-4') + .should('contain', '4') + .and('contain', 'RECTANGLE TRACK') + .within(() => { + cy.get('.cvat-object-item-button-outside-enabled').should('exist'); + }); + cy.get('#cvat_canvas_shape_5').should('exist').and('be.visible'); + cy.get('#cvat-objects-sidebar-state-item-5') + .should('contain', '5') + .and('contain', 'RECTANGLE TRACK') + .within(() => { + cy.get('.cvat-object-item-button-outside-enabled').should('not.exist'); + cy.get('.cvat-object-item-button-keyframe-enabled').should('exist'); + }); + }); + }); +}); diff --git a/tests/cypress/integration/actions_tasks_objects/issue_1425_highlighted_attribute_correspond_chosen_attribute.js b/tests/cypress/integration/actions_tasks_objects/issue_1425_highlighted_attribute_correspond_chosen_attribute.js index 13926213..991f91d6 100644 --- a/tests/cypress/integration/actions_tasks_objects/issue_1425_highlighted_attribute_correspond_chosen_attribute.js +++ b/tests/cypress/integration/actions_tasks_objects/issue_1425_highlighted_attribute_correspond_chosen_attribute.js @@ -1,8 +1,6 @@ -/* - * Copyright (C) 2020 Intel Corporation - * - * SPDX-License-Identifier: MIT - */ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT /// @@ -30,12 +28,7 @@ context('The highlighted attribute in AAM should correspond to the chosen attrib cy.createRectangle(createRectangleShape2Points); }); it('Go to AAM', () => { - cy.changeAnnotationMode('Attribute annotation'); - // Select the necessary label in any case - cy.get('.attribute-annotation-sidebar-basics-editor').within(() => { - cy.get('.ant-select-selection').click(); - }); - cy.get('.ant-select-dropdown-menu-item').contains(labelName).click(); + cy.changeWorkspace('Attribute annotation', labelName); }); it('Check if highlighted attribute correspond to the chosen attribute in right panel', () => { cy.get('.cvat_canvas_text').within(() => { diff --git a/tests/cypress/integration/actions_tasks_objects/issue_1540_add_remove_tag.js b/tests/cypress/integration/actions_tasks_objects/issue_1540_add_remove_tag.js index 3f5d83be..f5a402ad 100644 --- a/tests/cypress/integration/actions_tasks_objects/issue_1540_add_remove_tag.js +++ b/tests/cypress/integration/actions_tasks_objects/issue_1540_add_remove_tag.js @@ -17,11 +17,11 @@ context('Check if the UI not to crash after remove a tag', () => { describe(`Testing issue "${issueId}"`, () => { it('Add a tag', () => { - cy.changeAnnotationMode('Tag annotation'); + cy.changeWorkspace('Tag annotation'); cy.get('.cvat-tag-annotation-sidebar-buttons').within(() => { cy.get('button').contains('Add tag').click({ force: true }); }); - cy.changeAnnotationMode('Standard'); + cy.changeWorkspace('Standard'); }); it('Remove the tag', () => { cy.get('#cvat-objects-sidebar-state-item-1') diff --git a/tests/cypress/integration/actions_tasks_objects/issue_1750_err_aam_switch_frames.js b/tests/cypress/integration/actions_tasks_objects/issue_1750_err_aam_switch_frames.js index ee7652cd..150911ba 100644 --- a/tests/cypress/integration/actions_tasks_objects/issue_1750_err_aam_switch_frames.js +++ b/tests/cypress/integration/actions_tasks_objects/issue_1750_err_aam_switch_frames.js @@ -1,8 +1,6 @@ -/* - * Copyright (C) 2020 Intel Corporation - * - * SPDX-License-Identifier: MIT - */ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT /// @@ -39,11 +37,7 @@ context('An error occurs in AAM when switching to 2 frames, if the frames have o cy.createRectangle(createRectangleShape2PointsSecond); }); it('Go to AAM', () => { - cy.get('.cvat-workspace-selector').click(); - cy.get('.ant-select-dropdown-menu-item') - .contains('Attribute annotation') - .click() - .should('contain.text', 'Attribute annotation'); + cy.changeWorkspace('Attribute annotation', labelName); }); it('Go to next frame', () => { cy.get('.cvat-player-next-button').click(); @@ -65,6 +59,7 @@ context('An error occurs in AAM when switching to 2 frames, if the frames have o }); it('Page with the error is missing', () => { cy.contains('Oops, something went wrong').should('not.exist'); + cy.changeLabelAAM(labelName); cy.get('.attribute-annotation-sidebar-object-switcher').should('contain', `${labelName} 2 [2/2]`); }); }); diff --git a/tests/cypress/plugins/index.js b/tests/cypress/plugins/index.js index 00a306e2..64540537 100644 --- a/tests/cypress/plugins/index.js +++ b/tests/cypress/plugins/index.js @@ -16,5 +16,13 @@ module.exports = (on, config) => { return null; }, }); + // Try to resolve "Cypress failed to make a connection to the Chrome DevTools Protocol" + // https://github.com/cypress-io/cypress/issues/7450 + on('before:browser:launch', (browser, launchOptions) => { + if (browser.name === 'chrome' && browser.isHeadless) { + launchOptions.args.push('--disable-gpu'); + return launchOptions; + } + }); return config; }; diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 889551c4..5ee311eb 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -1,8 +1,6 @@ -/* - * Copyright (C) 2020 Intel Corporation - * - * SPDX-License-Identifier: MIT - */ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT /// @@ -102,13 +100,14 @@ Cypress.Commands.add('createRectangle', (createRectangleParams) => { cy.switchLabel(createRectangleParams.labelName); } cy.contains('Draw new rectangle') - .parents('.cvat-draw-shape-popover-content').within(() => { - cy.get('.ant-select-selection-selected-value').then(($labelValue) => { - selectedValueGlobal = $labelValue.text(); + .parents('.cvat-draw-shape-popover-content') + .within(() => { + cy.get('.ant-select-selection-selected-value').then(($labelValue) => { + selectedValueGlobal = $labelValue.text(); + }); + cy.get('.ant-radio-wrapper').contains(createRectangleParams.points).click(); + cy.get('button').contains(createRectangleParams.type).click({ force: true }); }); - cy.get('.ant-radio-wrapper').contains(createRectangleParams.points).click(); - cy.get('button').contains(createRectangleParams.type).click({ force: true }); - }) cy.get('.cvat-canvas-container').click(createRectangleParams.firstX, createRectangleParams.firstY); cy.get('.cvat-canvas-container').click(createRectangleParams.secondX, createRectangleParams.secondY); if (createRectangleParams.points === 'By 4 Points') { @@ -124,12 +123,20 @@ Cypress.Commands.add('switchLabel', (labelName) => { }); Cypress.Commands.add('checkObjectParameters', (objectParameters, objectType) => { - cy.get('.cvat-objects-sidebar-state-item').then((objectSidebar) => { - cy.get(`#cvat_canvas_shape_${objectSidebar.length}`).should('exist').and('be.visible'); - cy.get(`#cvat-objects-sidebar-state-item-${objectSidebar.length}`) - .should('contain', objectSidebar.length).and('contain', `${objectType} ${objectParameters.type.toUpperCase()}`).within(() => { - cy.get('.ant-select-selection-selected-value').should('have.text', selectedValueGlobal); - }); + let listCanvasShapeId = []; + cy.document().then((doc) => { + const listCanvasShape = Array.from(doc.querySelectorAll('.cvat_canvas_shape')); + for (let i = 0; i < listCanvasShape.length; i++) { + listCanvasShapeId.push(listCanvasShape[i].id.match(/\d+$/)); + } + const maxId = Math.max(...listCanvasShapeId); + cy.get(`#cvat_canvas_shape_${maxId}`).should('exist').and('be.visible'); + cy.get(`#cvat-objects-sidebar-state-item-${maxId}`) + .should('contain', maxId) + .and('contain', `${objectType} ${objectParameters.type.toUpperCase()}`) + .within(() => { + cy.get('.ant-select-selection-selected-value').should('have.text', selectedValueGlobal); + }); }); }); @@ -217,10 +224,23 @@ Cypress.Commands.add('closeSettings', () => { }); }); -Cypress.Commands.add('changeAnnotationMode', (mode) => { +Cypress.Commands.add('changeWorkspace', (mode, labelName) => { 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); + cy.changeLabelAAM(labelName); +}); + +Cypress.Commands.add('changeLabelAAM', (labelName) => { + cy.get('.cvat-workspace-selector').then((value) => { + const cvatWorkspaceSelectorValue = value.text(); + if (cvatWorkspaceSelectorValue === 'Attribute annotation') { + cy.get('.attribute-annotation-sidebar-basics-editor').within(() => { + cy.get('.ant-select-selection').click(); + }); + cy.get('.ant-select-dropdown-menu-item').contains(labelName).click(); + } + }); }); Cypress.Commands.add('createCuboid', (createCuboidParams) => {