diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 877a095f..9374a975 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,8 +5,7 @@ on: - 'master' - 'develop' pull_request: - branches: - - '*' + jobs: Unit_testing: runs-on: ubuntu-latest diff --git a/.vscode/launch.json b/.vscode/launch.json index 4b65fad0..55811c53 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "version": "0.2.0", "configurations": [ { - "type": "chrome", + "type": "pwa-chrome", "request": "launch", "preLaunchTask": "npm: start - cvat-ui", "name": "ui.js: debug", @@ -59,7 +59,7 @@ }, { "name": "server: chrome", - "type": "chrome", + "type": "pwa-chrome", "request": "launch", "url": "http://localhost:7000/", "disableNetworkCache":true, diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e24fe57..de2884d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,63 @@ 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.7.0] - Unreleased + +### Added + +- TDB + +### Changed + +- TDB + +### Deprecated + +- TDB + +### Removed + +- TDB + +### Fixed + +- TDB + +### Security + +- TDB + +## \[1.6.0] - 2021-09-17 + +### Added + +- Added ability to import data from share with cli without copying the data () +- Notification if the browser does not support nesassary API +- Added ability to export project as a dataset () + and project with 3D tasks () +- Additional inline tips in interactors with demo gifs () +- Added intelligent scissors blocking feature () +- Support cloud storage status () +- Support cloud storage preview () +- cvat-core: support cloud storages () + +### Changed + +- Non-blocking UI when using interactors () +- "Selected opacity" slider now defines opacity level for shapes being drawnSelected opacity () +- Cloud storage creating and updating () +- Way of working with cloud storage content () + +### Removed + +- Support TEMP_KEY_SECRET_KEY_TOKEN_SET for AWS S3 cloud storage () + +### Fixed + +- Fixed multiple tasks moving () +- Fixed task creating CLI parameter () +- Fixed import for MOTS format () + ## \[1.5.0] - 2021-08-02 ### Added @@ -17,6 +74,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Explicit "Done" button when drawing any polyshapes () - Histogram equalization with OpenCV javascript () - Client-side polyshapes approximation when using semi-automatic interactors & scissors () +- Support of Google Cloud Storage for cloud storage () ### Changed @@ -49,6 +107,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Context image disappears after undo/redo () - Using combined data sources (directory and image) when create a task () - Creating task with labels in project () +- Move task and autoannotation modals were invisible from project page () ## \[1.4.0] - 2021-05-18 @@ -844,22 +903,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Template ``` -## [Unreleased] +## \[Unreleased] ### Added -- +- TDB ### Changed -- +- TDB ### Deprecated -- +- TDB ### Removed -- +- TDB ### Fixed -- +- TDB ### Security -- +- TDB ``` diff --git a/Dockerfile.ui b/Dockerfile.ui index 3bf5b66b..0dc9a44a 100644 --- a/Dockerfile.ui +++ b/Dockerfile.ui @@ -50,7 +50,7 @@ COPY cvat-canvas/ /tmp/cvat-canvas/ COPY cvat-ui/ /tmp/cvat-ui/ RUN npm run build -FROM nginx:stable-alpine +FROM nginx:mainline-alpine # Replace default.conf configuration to remove unnecessary rules RUN sed -i "s/}/application\/wasm wasm;\n}/g" /etc/nginx/mime.types COPY cvat-ui/react_nginx.conf /etc/nginx/conf.d/default.conf diff --git a/components/analytics/logstash/logstash.conf b/components/analytics/logstash/logstash.conf index 5afd6e3f..67ef24b3 100644 --- a/components/analytics/logstash/logstash.conf +++ b/components/analytics/logstash/logstash.conf @@ -124,7 +124,7 @@ output { if [type] == "client" { elasticsearch { hosts => ["${LOGSTASH_OUTPUT_HOST}"] - index => "cvat.client" + index => "%{[@metadata][target_index_client]}" user => "${LOGSTASH_OUTPUT_USER:}" password => "${LOGSTASH_OUTPUT_PASS:}" manage_template => false @@ -132,7 +132,7 @@ output { } else if [type] == "server" { elasticsearch { hosts => ["${LOGSTASH_OUTPUT_HOST}"] - index => "cvat.server" + index => "%{[@metadata][target_index_server]}" user => "${LOGSTASH_OUTPUT_USER:}" password => "${LOGSTASH_OUTPUT_PASS:}" manage_template => false diff --git a/cvat-canvas/package-lock.json b/cvat-canvas/package-lock.json index b0ec3bcb..8fde0d74 100644 --- a/cvat-canvas/package-lock.json +++ b/cvat-canvas/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.5.0", + "version": "2.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3513,14 +3513,26 @@ } }, "enhanced-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", - "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", "dev": true, "requires": { "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", + "memory-fs": "^0.5.0", "tapable": "^1.0.0" + }, + "dependencies": { + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + } } }, "errno": { @@ -5605,9 +5617,9 @@ } }, "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true }, "invariant": { @@ -5619,12 +5631,6 @@ "loose-envify": "^1.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -6165,15 +6171,6 @@ "package-json": "^6.3.0" } }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -6309,15 +6306,6 @@ "semver": "^5.6.0" } }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -6345,17 +6333,6 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -6525,12 +6502,6 @@ "mime-db": "1.40.0" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -7276,17 +7247,6 @@ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -7309,24 +7269,12 @@ "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", "dev": true }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "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-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, "p-limit": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", @@ -7453,9 +7401,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "path-to-regexp": { @@ -10275,9 +10223,9 @@ } }, "url-parse": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", - "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", + "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", "dev": true, "requires": { "querystringify": "^2.1.1", @@ -10535,112 +10483,46 @@ } }, "webpack-cli": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.6.tgz", - "integrity": "sha512-0vEa83M7kJtxK/jUhlpZ27WHIOndz5mghWL2O53kiDoA9DIxSKnfqB92LoqEn77cT4f3H2cZm1BMEat/6AZz3A==", - "dev": true, - "requires": { - "chalk": "2.4.2", - "cross-spawn": "6.0.5", - "enhanced-resolve": "4.1.0", - "findup-sync": "3.0.0", - "global-modules": "2.0.0", - "import-local": "2.0.0", - "interpret": "1.2.0", - "loader-utils": "1.2.3", - "supports-color": "6.1.0", - "v8-compile-cache": "2.0.3", - "yargs": "13.2.4" + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.12.tgz", + "integrity": "sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "cross-spawn": "^6.0.5", + "enhanced-resolve": "^4.1.1", + "findup-sync": "^3.0.0", + "global-modules": "^2.0.0", + "import-local": "^2.0.0", + "interpret": "^1.4.0", + "loader-utils": "^1.4.0", + "supports-color": "^6.1.0", + "v8-compile-cache": "^2.1.1", + "yargs": "^13.3.2" }, "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", "dev": true, "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" } }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.0" - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index 9fa50052..ce264aaa 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.5.0", + "version": "2.7.0", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", "scripts": { @@ -9,6 +9,12 @@ }, "author": "Intel", "license": "MIT", + "browserslist": [ + "Chrome >= 63", + "Firefox > 58", + "not IE 11", + "> 2%" + ], "dependencies": { "svg.draggable.js": "2.2.2", "svg.draw.js": "^2.0.4", @@ -41,7 +47,7 @@ "style-loader": "^1.0.0", "typescript": "^3.5.3", "webpack": "^5.20.2", - "webpack-cli": "^3.3.6", + "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0" } } diff --git a/cvat-canvas/postcss.config.js b/cvat-canvas/postcss.config.js deleted file mode 100644 index 3a833a37..00000000 --- a/cvat-canvas/postcss.config.js +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (C) 2019-2020 Intel Corporation -// -// SPDX-License-Identifier: MIT - -module.exports = { - parser: false, - plugins: { - 'postcss-preset-env': { - browsers: '> 2.5%', // https://github.com/browserslist/browserslist - }, - }, -}; diff --git a/cvat-canvas/src/scss/canvas.scss b/cvat-canvas/src/scss/canvas.scss index 993745ba..5e21171d 100644 --- a/cvat-canvas/src/scss/canvas.scss +++ b/cvat-canvas/src/scss/canvas.scss @@ -24,7 +24,6 @@ polyline.cvat_shape_action_opacity { } .cvat_shape_drawing_opacity { - fill-opacity: 0.2; stroke-opacity: 1; } @@ -161,9 +160,8 @@ polyline.cvat_canvas_shape_splitting { .cvat_canvas_removable_interaction_point { cursor: - url( - 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K' - ) 10 10, + url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K') + 10 10, auto; } diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index 0da3bce4..e59461f0 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -59,6 +59,7 @@ export interface Configuration { forceDisableEditing?: boolean; intelligentPolygonCrop?: boolean; forceFrameUpdate?: boolean; + creationOpacity?: number; } export interface DrawData { @@ -86,6 +87,7 @@ export interface InteractionData { shapeType: string; points: number[]; }; + onChangeToolsBlockerState?: (event: string) => void; } export interface InteractionResult { @@ -547,6 +549,16 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { } } + // install default values for drawing method + if (drawData.enabled) { + if (drawData.shapeType === 'rectangle') { + this.data.drawData.rectDrawingMethod = drawData.rectDrawingMethod || RectDrawingMethod.CLASSIC; + } + if (drawData.shapeType === 'cuboid') { + this.data.drawData.cuboidDrawingMethod = drawData.cuboidDrawingMethod || CuboidDrawingMethod.CLASSIC; + } + } + this.notify(UpdateReasons.DRAW); } @@ -554,15 +566,14 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { if (![Mode.IDLE, Mode.INTERACT].includes(this.data.mode)) { throw Error(`Canvas is busy. Action: ${this.data.mode}`); } - - if (interactionData.enabled && !interactionData.intermediateShape) { + const thresholdChanged = this.data.interactionData.enableThreshold !== interactionData.enableThreshold; + if (interactionData.enabled && !interactionData.intermediateShape && !thresholdChanged) { 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; @@ -656,6 +667,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { this.data.configuration.forceFrameUpdate = configuration.forceFrameUpdate; } + if (typeof configuration.creationOpacity === 'number') { + this.data.configuration.creationOpacity = configuration.creationOpacity; + } + this.notify(UpdateReasons.CONFIG_UPDATED); } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index f1a9f466..40f864ed 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -998,6 +998,8 @@ export class CanvasViewImpl implements CanvasView, Listener { this.adoptedContent, this.adoptedText, this.autoborderHandler, + this.geometry, + this.configuration, ); this.editHandler = new EditHandlerImpl(this.onEditDone.bind(this), this.adoptedContent, this.autoborderHandler); this.mergeHandler = new MergeHandlerImpl( @@ -1026,6 +1028,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.onInteraction.bind(this), this.adoptedContent, this.geometry, + this.configuration, ); // Setup event handlers @@ -1117,6 +1120,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.activate(activeElement); this.editHandler.configurate(this.configuration); this.drawHandler.configurate(this.configuration); + this.interactionHandler.configurate(this.configuration); // remove if exist and not enabled // this.setupObjects([]); @@ -1275,7 +1279,9 @@ export class CanvasViewImpl implements CanvasView, Listener { } this.interactionHandler.interact(data); } else { - this.canvas.style.cursor = ''; + if (!data.enabled) { + this.canvas.style.cursor = ''; + } if (this.mode !== Mode.IDLE) { this.interactionHandler.interact(data); } @@ -1565,7 +1571,6 @@ export class CanvasViewImpl implements CanvasView, Listener { private addObjects(states: any[]): void { const { displayAllText } = this.configuration; - for (const state of states) { const points: number[] = state.points as number[]; const translatedPoints: number[] = this.translateToCanvas(points); diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index 8bf38eee..847375a6 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -50,6 +50,7 @@ export class DrawHandlerImpl implements DrawHandler { private crosshair: Crosshair; private drawData: DrawData; private geometry: Geometry; + private configuration: Configuration; private autoborderHandler: AutoborderHandler; private autobordersEnabled: boolean; @@ -371,6 +372,7 @@ export class DrawHandlerImpl implements DrawHandler { .addClass('cvat_canvas_shape_drawing') .attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, + 'fill-opacity': this.configuration.creationOpacity, }); } @@ -527,6 +529,7 @@ export class DrawHandlerImpl implements DrawHandler { .addClass('cvat_canvas_shape_drawing') .attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, + 'fill-opacity': this.configuration.creationOpacity, }); this.drawPolyshape(); @@ -597,6 +600,7 @@ export class DrawHandlerImpl implements DrawHandler { .addClass('cvat_canvas_shape_drawing') .attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, + 'fill-opacity': this.configuration.creationOpacity, }); } @@ -654,6 +658,7 @@ export class DrawHandlerImpl implements DrawHandler { .addClass('cvat_canvas_shape_drawing') .attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, + 'fill-opacity': this.configuration.creationOpacity, }); this.pasteShape(); @@ -686,6 +691,7 @@ export class DrawHandlerImpl implements DrawHandler { .addClass('cvat_canvas_shape_drawing') .attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, + 'fill-opacity': this.configuration.creationOpacity, }); this.pasteShape(); this.pastePolyshape(); @@ -709,6 +715,7 @@ export class DrawHandlerImpl implements DrawHandler { .attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'face-stroke': 'black', + 'fill-opacity': this.configuration.creationOpacity, }); this.pasteShape(); this.pastePolyshape(); @@ -845,6 +852,8 @@ export class DrawHandlerImpl implements DrawHandler { canvas: SVG.Container, text: SVG.Container, autoborderHandler: AutoborderHandler, + geometry: Geometry, + configuration: Configuration, ) { this.autoborderHandler = autoborderHandler; this.autobordersEnabled = false; @@ -855,7 +864,8 @@ export class DrawHandlerImpl implements DrawHandler { this.initialized = false; this.canceled = false; this.drawData = null; - this.geometry = null; + this.geometry = geometry; + this.configuration = configuration; this.crosshair = new Crosshair(); this.drawInstance = null; this.pointsGroup = null; @@ -874,6 +884,20 @@ export class DrawHandlerImpl implements DrawHandler { } public configurate(configuration: Configuration): void { + this.configuration = configuration; + + const isFillableRect = this.drawData + && this.drawData.shapeType === 'rectangle' + && (this.drawData.rectDrawingMethod === RectDrawingMethod.CLASSIC || this.drawData.initialState); + const isFillableCuboid = this.drawData + && this.drawData.shapeType === 'cuboid' + && (this.drawData.cuboidDrawingMethod === CuboidDrawingMethod.CLASSIC || this.drawData.initialState); + const isFilalblePolygon = this.drawData && this.drawData.shapeType === 'polygon'; + + if (this.drawInstance && (isFillableRect || isFillableCuboid || isFilalblePolygon)) { + this.drawInstance.fill({ opacity: configuration.creationOpacity }); + } + if (typeof configuration.autoborders === 'boolean') { this.autobordersEnabled = configuration.autoborders; if (this.drawInstance) { diff --git a/cvat-canvas/src/typescript/interactionHandler.ts b/cvat-canvas/src/typescript/interactionHandler.ts index dcb8101e..37cedd98 100644 --- a/cvat-canvas/src/typescript/interactionHandler.ts +++ b/cvat-canvas/src/typescript/interactionHandler.ts @@ -8,16 +8,21 @@ import Crosshair from './crosshair'; import { translateToSVG, PropType, stringifyPoints, translateToCanvas, } from './shared'; -import { InteractionData, InteractionResult, Geometry } from './canvasModel'; + +import { + InteractionData, InteractionResult, Geometry, Configuration, +} from './canvasModel'; export interface InteractionHandler { transform(geometry: Geometry): void; interact(interactData: InteractionData): void; + configurate(config: Configuration): void; cancel(): void; } export class InteractionHandlerImpl implements InteractionHandler { private onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void; + private configuration: Configuration; private geometry: Geometry; private canvas: SVG.Container; private interactionData: InteractionData; @@ -30,6 +35,7 @@ export class InteractionHandlerImpl implements InteractionHandler { private thresholdRectSize: number; private intermediateShape: PropType; private drawnIntermediateShape: SVG.Shape; + private thresholdWasModified: boolean; private prepareResult(): InteractionResult[] { return this.interactionShapes.map( @@ -137,14 +143,15 @@ export class InteractionHandlerImpl implements InteractionHandler { _e.preventDefault(); _e.stopPropagation(); self.remove(); + this.shapesWereUpdated = true; + const shouldRaiseEvent = this.shouldRaiseEvent(_e.ctrlKey); this.interactionShapes = this.interactionShapes.filter( (shape: SVG.Shape): boolean => shape !== self, ); if (this.interactionData.startWithBox && this.interactionShapes.length === 1) { this.interactionShapes[0].style({ visibility: '' }); } - this.shapesWereUpdated = true; - if (this.shouldRaiseEvent(_e.ctrlKey)) { + if (shouldRaiseEvent) { this.onInteraction(this.prepareResult(), true, false); } }); @@ -196,16 +203,21 @@ export class InteractionHandlerImpl implements InteractionHandler { .addClass('cvat_canvas_shape_drawing') .attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, - }); + }) + .fill({ opacity: this.configuration.creationOpacity, color: 'white' }); } private initInteraction(): void { if (this.interactionData.crosshair) { this.addCrosshair(); + } else if (this.crosshair) { + this.removeCrosshair(); } - if (this.interactionData.enableThreshold) { this.addThreshold(); + } else if (this.threshold) { + this.threshold.remove(); + this.threshold = null; } } @@ -286,8 +298,8 @@ export class InteractionHandlerImpl implements InteractionHandler { 'shape-rendering': 'geometricprecision', 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, stroke: erroredShape ? 'red' : 'black', - fill: 'none', }) + .fill({ opacity: this.configuration.creationOpacity, color: 'white' }) .addClass('cvat_canvas_interact_intermediate_shape'); this.selectize(true, this.drawnIntermediateShape, erroredShape); } else { @@ -327,7 +339,25 @@ export class InteractionHandlerImpl implements InteractionHandler { const handler = shape.remember('_selectHandler'); if (handler && handler.nested) { handler.nested.fill(shape.attr('fill')); + // move green circle group(anchors) and polygon(lastChild) to the top of svg to make anchors hoverable + handler.parent.node.prepend(handler.nested.node); + handler.parent.node.prepend(handler.parent.node.lastChild); + } + } + + private visualComponentsChanged(interactionData: InteractionData): boolean { + const allowedKeys = ['enabled', 'crosshair', 'enableThreshold', 'onChangeToolsBlockerState']; + if (Object.keys(interactionData).every((key: string): boolean => allowedKeys.includes(key))) { + if (this.interactionData.enableThreshold !== undefined && interactionData.enableThreshold !== undefined + && this.interactionData.enableThreshold !== interactionData.enableThreshold) { + return true; + } + if (this.interactionData.crosshair !== undefined && interactionData.crosshair !== undefined + && this.interactionData.crosshair !== interactionData.crosshair) { + return true; + } } + return false; } public constructor( @@ -339,12 +369,14 @@ export class InteractionHandlerImpl implements InteractionHandler { ) => void, canvas: SVG.Container, geometry: Geometry, + configuration: Configuration, ) { this.onInteraction = (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean): void => { this.shapesWereUpdated = false; onInteraction(shapes, shapesUpdated, isDone, this.threshold ? this.thresholdRectSize / 2 : null); }; this.canvas = canvas; + this.configuration = configuration; this.geometry = geometry; this.shapesWereUpdated = false; this.interactionShapes = []; @@ -369,7 +401,6 @@ export class InteractionHandlerImpl implements InteractionHandler { if (this.threshold) { this.threshold.center(x, y); } - if (this.interactionData.enableSliding && this.interactionShapes.length) { if (this.isWithinFrame(x, y)) { if (this.interactionData.enableThreshold && !this.isWithinThreshold(x, y)) return; @@ -392,6 +423,7 @@ export class InteractionHandlerImpl implements InteractionHandler { this.canvas.on('wheel.interaction', (e: WheelEvent): void => { if (e.ctrlKey) { if (this.threshold) { + this.thresholdWasModified = true; const { x, y } = this.cursorPosition; e.preventDefault(); if (e.deltaY > 0) { @@ -405,10 +437,24 @@ export class InteractionHandlerImpl implements InteractionHandler { } }); - document.body.addEventListener('keyup', (e: KeyboardEvent): void => { - if (e.keyCode === 17 && this.shouldRaiseEvent(false)) { - // 17 is ctrl - this.onInteraction(this.prepareResult(), true, false); + window.addEventListener('keyup', (e: KeyboardEvent): void => { + if (this.interactionData.enabled && e.keyCode === 17) { + if (this.interactionData.onChangeToolsBlockerState && !this.thresholdWasModified) { + this.interactionData.onChangeToolsBlockerState('keyup'); + } + if (this.shouldRaiseEvent(false)) { + // 17 is ctrl + this.onInteraction(this.prepareResult(), true, false); + } + } + }); + + window.addEventListener('keydown', (e: KeyboardEvent): void => { + if (this.interactionData.enabled && e.keyCode === 17) { + if (this.interactionData.onChangeToolsBlockerState && !this.thresholdWasModified) { + this.interactionData.onChangeToolsBlockerState('keydown'); + } + this.thresholdWasModified = false; } }); } @@ -454,6 +500,9 @@ export class InteractionHandlerImpl implements InteractionHandler { if (this.interactionData.startWithBox) { this.interactionShapes[0].style({ visibility: 'hidden' }); } + } else if (interactionData.enabled && this.visualComponentsChanged(interactionData)) { + this.interactionData = { ...this.interactionData, ...interactionData }; + this.initInteraction(); } else if (interactionData.enabled) { this.interactionData = interactionData; this.initInteraction(); @@ -465,6 +514,25 @@ export class InteractionHandlerImpl implements InteractionHandler { } } + public configurate(configuration: Configuration): void { + this.configuration = configuration; + if (this.drawnIntermediateShape) { + this.drawnIntermediateShape.fill({ + opacity: configuration.creationOpacity, + }); + } + + // when interactRectangle + if (this.currentInteractionShape && this.currentInteractionShape.type === 'rect') { + this.currentInteractionShape.fill({ opacity: configuration.creationOpacity }); + } + + // when interactPoints with startwithbbox + if (this.interactionShapes[0] && this.interactionShapes[0].type === 'rect') { + this.interactionShapes[0].fill({ opacity: configuration.creationOpacity }); + } + } + public cancel(): void { this.release(); this.onInteraction(null); diff --git a/cvat-canvas/src/typescript/svg.patch.ts b/cvat-canvas/src/typescript/svg.patch.ts index de863194..debebfd1 100644 --- a/cvat-canvas/src/typescript/svg.patch.ts +++ b/cvat-canvas/src/typescript/svg.patch.ts @@ -10,8 +10,8 @@ import 'svg.select.js'; import 'svg.draw.js'; import consts from './consts'; -import { Point, Equation, CuboidModel, Orientation, Edge } from './cuboid'; -import { parsePoints, clamp } from './shared'; +import { Equation, CuboidModel, Orientation, Edge } from './cuboid'; +import { Point, parsePoints, clamp } from './shared'; // Update constructor const originalDraw = SVG.Element.prototype.draw; @@ -958,8 +958,12 @@ function getTopDown(edgeIndex: EdgeIndex): number[] { }, paintOrientationLines() { - const fillColor = this.attr('fill'); - const strokeColor = this.attr('stroke'); + // style has higher priority than attr, so then try to fetch it if exists + // https://stackoverflow.com/questions/47088409/svg-attributes-beaten-by-cssstyle-in-priority] + // we use getComputedStyle to get actual, not-inlined css property (come from the corresponding css class) + const computedStyles = getComputedStyle(this.node); + const fillColor = computedStyles['fill'] || this.attr('fill'); + const strokeColor = computedStyles['stroke'] || this.attr('stroke'); const selectedColor = this.attr('face-stroke') || '#b0bec5'; this.frontTopEdge.stroke({ color: selectedColor }); this.frontLeftEdge.stroke({ color: selectedColor }); diff --git a/cvat-canvas/webpack.config.js b/cvat-canvas/webpack.config.js index 0b414c96..fe972099 100644 --- a/cvat-canvas/webpack.config.js +++ b/cvat-canvas/webpack.config.js @@ -8,6 +8,23 @@ const path = require('path'); // eslint-disable-next-line @typescript-eslint/no-var-requires const DtsBundleWebpack = require('dts-bundle-webpack'); +const styleLoaders = [ + 'style-loader', + { + loader: 'css-loader', + options: { + importLoaders: 2, + }, + }, + { + loader: 'postcss-loader', + options: { + plugins: [require('postcss-preset-env')], + }, + }, + 'sass-loader', +]; + const nodeConfig = { target: 'node', mode: 'production', @@ -34,7 +51,7 @@ const nodeConfig = { '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-optional-chaining', ], - presets: [['@babel/preset-env'], ['@babel/typescript']], + presets: [['@babel/preset-env', { targets: 'node > 10' }], '@babel/typescript'], sourceType: 'unambiguous', }, }, @@ -42,17 +59,7 @@ const nodeConfig = { { test: /\.(css|scss)$/, exclude: /node_modules/, - use: [ - 'style-loader', - { - loader: 'css-loader', - options: { - importLoaders: 2, - }, - }, - 'postcss-loader', - 'sass-loader', - ], + use: styleLoaders, }, ], }, @@ -96,15 +103,7 @@ const webConfig = { loader: 'babel-loader', options: { plugins: ['@babel/plugin-proposal-class-properties'], - presets: [ - [ - '@babel/preset-env', - { - targets: '> 2.5%', // https://github.com/browserslist/browserslist - }, - ], - ['@babel/typescript'], - ], + presets: ['@babel/preset-env', '@babel/typescript'], sourceType: 'unambiguous', }, }, @@ -112,17 +111,7 @@ const webConfig = { { test: /\.scss$/, exclude: /node_modules/, - use: [ - 'style-loader', - { - loader: 'css-loader', - options: { - importLoaders: 2, - }, - }, - 'postcss-loader', - 'sass-loader', - ], + use: styleLoaders, }, ], }, diff --git a/cvat-canvas3d/package-lock.json b/cvat-canvas3d/package-lock.json index a7f1f80e..1443f31f 100644 --- a/cvat-canvas3d/package-lock.json +++ b/cvat-canvas3d/package-lock.json @@ -3585,14 +3585,26 @@ } }, "enhanced-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", - "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", "dev": true, "requires": { "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", + "memory-fs": "^0.5.0", "tapable": "^1.0.0" + }, + "dependencies": { + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + } } }, "errno": { @@ -5726,9 +5738,9 @@ } }, "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true }, "invariant": { @@ -5740,12 +5752,6 @@ "loose-envify": "^1.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -6210,15 +6216,6 @@ "package-json": "^4.0.0" } }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -6363,15 +6360,6 @@ "semver": "^5.6.0" } }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -6410,17 +6398,6 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -6602,12 +6579,6 @@ "mime-db": "1.40.0" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -7348,17 +7319,6 @@ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -7375,24 +7335,12 @@ "os-tmpdir": "^1.0.0" } }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "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-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, "p-limit": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", @@ -10772,112 +10720,46 @@ } }, "webpack-cli": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.6.tgz", - "integrity": "sha512-0vEa83M7kJtxK/jUhlpZ27WHIOndz5mghWL2O53kiDoA9DIxSKnfqB92LoqEn77cT4f3H2cZm1BMEat/6AZz3A==", - "dev": true, - "requires": { - "chalk": "2.4.2", - "cross-spawn": "6.0.5", - "enhanced-resolve": "4.1.0", - "findup-sync": "3.0.0", - "global-modules": "2.0.0", - "import-local": "2.0.0", - "interpret": "1.2.0", - "loader-utils": "1.2.3", - "supports-color": "6.1.0", - "v8-compile-cache": "2.0.3", - "yargs": "13.2.4" + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.12.tgz", + "integrity": "sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "cross-spawn": "^6.0.5", + "enhanced-resolve": "^4.1.1", + "findup-sync": "^3.0.0", + "global-modules": "^2.0.0", + "import-local": "^2.0.0", + "interpret": "^1.4.0", + "loader-utils": "^1.4.0", + "supports-color": "^6.1.0", + "v8-compile-cache": "^2.1.1", + "yargs": "^13.3.2" }, "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", "dev": true, "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" } }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.0" - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, diff --git a/cvat-canvas3d/package.json b/cvat-canvas3d/package.json index a8eb1fcf..d99ebe51 100644 --- a/cvat-canvas3d/package.json +++ b/cvat-canvas3d/package.json @@ -9,6 +9,12 @@ }, "author": "Intel", "license": "MIT", + "browserslist": [ + "Chrome >= 63", + "Firefox > 58", + "not IE 11", + "> 2%" + ], "devDependencies": { "@babel/cli": "^7.13.16", "@babel/core": "^7.5.5", @@ -34,7 +40,7 @@ "style-loader": "^1.0.0", "typescript": "^3.5.3", "webpack": "^4.44.2", - "webpack-cli": "^3.3.6", + "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0" }, "dependencies": { diff --git a/cvat-canvas3d/postcss.config.js b/cvat-canvas3d/postcss.config.js deleted file mode 100644 index bea48ce1..00000000 --- a/cvat-canvas3d/postcss.config.js +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (C) 2021 Intel Corporation -// -// SPDX-License-Identifier: MIT - -module.exports = { - parser: false, - plugins: { - 'postcss-preset-env': { - browsers: '> 2.5%', // https://github.com/browserslist/browserslist - }, - }, -}; diff --git a/cvat-canvas3d/webpack.config.js b/cvat-canvas3d/webpack.config.js index 6b0b8c64..bfdfc248 100644 --- a/cvat-canvas3d/webpack.config.js +++ b/cvat-canvas3d/webpack.config.js @@ -8,6 +8,23 @@ const path = require('path'); // eslint-disable-next-line @typescript-eslint/no-var-requires const DtsBundleWebpack = require('dts-bundle-webpack'); +const styleLoaders = [ + 'style-loader', + { + loader: 'css-loader', + options: { + importLoaders: 2, + }, + }, + { + loader: 'postcss-loader', + options: { + plugins: [require('postcss-preset-env')], + }, + }, + 'sass-loader', +]; + const nodeConfig = { target: 'node', mode: 'production', @@ -34,7 +51,7 @@ const nodeConfig = { '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-optional-chaining', ], - presets: [['@babel/preset-env'], ['@babel/typescript']], + presets: [['@babel/preset-env', { targets: 'node > 10' }], '@babel/typescript'], sourceType: 'unambiguous', }, }, @@ -42,17 +59,7 @@ const nodeConfig = { { test: /\.(css|scss)$/, exclude: /node_modules/, - use: [ - 'style-loader', - { - loader: 'css-loader', - options: { - importLoaders: 2, - }, - }, - 'postcss-loader', - 'sass-loader', - ], + use: styleLoaders, }, ], }, @@ -96,15 +103,7 @@ const webConfig = { loader: 'babel-loader', options: { plugins: ['@babel/plugin-proposal-class-properties'], - presets: [ - [ - '@babel/preset-env', - { - targets: '> 2.5%', // https://github.com/browserslist/browserslist - }, - ], - ['@babel/typescript'], - ], + presets: ['@babel/preset-env', '@babel/typescript'], sourceType: 'unambiguous', }, }, @@ -112,17 +111,7 @@ const webConfig = { { test: /\.scss$/, exclude: /node_modules/, - use: [ - 'style-loader', - { - loader: 'css-loader', - options: { - importLoaders: 2, - }, - }, - 'postcss-loader', - 'sass-loader', - ], + use: styleLoaders, }, ], }, diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json index 6daac702..f0a18dc0 100644 --- a/cvat-core/package-lock.json +++ b/cvat-core/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "3.13.3", + "version": "3.16.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3785,11 +3785,11 @@ "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==" }, "axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.3.tgz", + "integrity": "sha512-JtoZ3Ndke/+Iwt5n+BgSli/3idTvpt5OjKyoCmz4LX5+lPiY5l7C1colYezhlxThjNa/NhngCUWZSZFypIFuaA==", "requires": { - "follow-redirects": "^1.10.0" + "follow-redirects": "^1.14.0" } }, "babel-code-frame": { @@ -13562,9 +13562,9 @@ } }, "follow-redirects": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", - "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.3.tgz", + "integrity": "sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==" }, "for-in": { "version": "1.0.2", diff --git a/cvat-core/package.json b/cvat-core/package.json index e71098c0..2ca27952 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "3.13.3", + "version": "3.16.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "babel.config.js", "scripts": { @@ -11,6 +11,12 @@ }, "author": "Intel", "license": "MIT", + "browserslist": [ + "Chrome >= 63", + "Firefox > 58", + "not IE 11", + "> 2%" + ], "devDependencies": { "@babel/cli": "^7.4.4", "@babel/core": "^7.4.4", @@ -33,7 +39,7 @@ "webpack-cli": "^3.3.2" }, "dependencies": { - "axios": "^0.21.1", + "axios": "^0.21.3", "browser-or-node": "^1.2.1", "cvat-data": "../cvat-data", "detect-browser": "^5.2.0", diff --git a/cvat-core/src/annotations.js b/cvat-core/src/annotations.js index 991ee33e..07ce90f9 100644 --- a/cvat-core/src/annotations.js +++ b/cvat-core/src/annotations.js @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2020 Intel Corporation +// Copyright (C) 2019-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -8,8 +8,9 @@ const AnnotationsSaver = require('./annotations-saver'); const AnnotationsHistory = require('./annotations-history'); const { checkObjectType } = require('./common'); - const { Task } = require('./session'); - const { Loader, Dumper } = require('./annotation-formats'); + const { Project } = require('./project'); + const { Task, Job } = require('./session'); + const { Loader } = require('./annotation-formats'); const { ScriptingError, DataError, ArgumentError } = require('./exceptions'); const jobCache = new WeakMap(); @@ -50,6 +51,7 @@ stopFrame, frameMeta, }); + // eslint-disable-next-line no-unsanitized/method collection.import(rawAnnotations); const saver = new AnnotationsSaver(rawAnnotations.version, collection, session); @@ -232,27 +234,12 @@ await serverProxy.annotations.uploadAnnotations(sessionType, session.id, file, loader.name); } - async function dumpAnnotations(session, name, dumper) { - if (!(dumper instanceof Dumper)) { - throw new ArgumentError('A dumper must be instance of Dumper class'); - } - - let result = null; - const sessionType = session instanceof Task ? 'task' : 'job'; - if (sessionType === 'job') { - result = await serverProxy.annotations.dumpAnnotations(session.task.id, name, dumper.name); - } else { - result = await serverProxy.annotations.dumpAnnotations(session.id, name, dumper.name); - } - - return result; - } - function importAnnotations(session, data) { const sessionType = session instanceof Task ? 'task' : 'job'; const cache = getCache(sessionType); if (cache.has(session)) { + // eslint-disable-next-line no-unsanitized/method return cache.get(session).collection.import(data); } @@ -274,16 +261,25 @@ ); } - async function exportDataset(session, format) { + async function exportDataset(instance, format, name, saveImages = false) { if (!(format instanceof String || typeof format === 'string')) { throw new ArgumentError('Format must be a string'); } - if (!(session instanceof Task)) { - throw new ArgumentError('A dataset can only be created from a task'); + if (!(instance instanceof Task || instance instanceof Project || instance instanceof Job)) { + throw new ArgumentError('A dataset can only be created from a job, task or project'); + } + if (typeof saveImages !== 'boolean') { + throw new ArgumentError('Save images parameter must be a boolean'); } let result = null; - result = await serverProxy.tasks.exportDataset(session.id, format); + if (instance instanceof Task) { + result = await serverProxy.tasks.exportDataset(instance.id, format, name, saveImages); + } else if (instance instanceof Job) { + result = await serverProxy.tasks.exportDataset(instance.task.id, format, name, saveImages); + } else { + result = await serverProxy.projects.exportDataset(instance.id, format, name, saveImages); + } return result; } @@ -367,7 +363,6 @@ annotationsStatistics, selectObject, uploadAnnotations, - dumpAnnotations, importAnnotations, exportAnnotations, exportDataset, diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index 787c4303..0301fdec 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -16,13 +16,20 @@ camelToSnake, } = require('./common'); - const { TaskStatus, TaskMode, DimensionType } = require('./enums'); + const { + TaskStatus, + TaskMode, + DimensionType, + CloudStorageProviderType, + CloudStorageCredentialsType, + } = require('./enums'); const User = require('./user'); const { AnnotationFormats } = require('./annotation-formats'); const { ArgumentError } = require('./exceptions'); const { Task } = require('./session'); const { Project } = require('./project'); + const { CloudStorage } = require('./cloud-storage'); function implementAPI(cvat) { cvat.plugins.list.implementation = PluginRegistry.list; @@ -262,6 +269,49 @@ cvat.projects.searchNames.implementation = async (search, limit) => serverProxy.projects.searchNames(search, limit); + cvat.cloudStorages.get.implementation = async (filter) => { + checkFilter(filter, { + page: isInteger, + displayName: isString, + resourceName: isString, + description: isString, + id: isInteger, + owner: isString, + search: isString, + providerType: isEnum.bind(CloudStorageProviderType), + credentialsType: isEnum.bind(CloudStorageCredentialsType), + }); + + checkExclusiveFields(filter, ['id', 'search'], ['page']); + + const searchParams = new URLSearchParams(); + for (const field of [ + 'displayName', + 'credentialsType', + 'providerType', + 'owner', + 'search', + 'id', + 'page', + 'description', + ]) { + if (Object.prototype.hasOwnProperty.call(filter, field)) { + searchParams.set(camelToSnake(field), filter[field]); + } + } + + if (Object.prototype.hasOwnProperty.call(filter, 'resourceName')) { + searchParams.set('resource', filter.resourceName); + } + + const cloudStoragesData = await serverProxy.cloudStorages.get(searchParams.toString()); + const cloudStorages = cloudStoragesData.map((cloudStorage) => new CloudStorage(cloudStorage)); + + cloudStorages.count = cloudStoragesData.count; + + return cloudStorages; + }; + return cvat; } diff --git a/cvat-core/src/api.js b/cvat-core/src/api.js index 7067d560..e50cdbc8 100644 --- a/cvat-core/src/api.js +++ b/cvat-core/src/api.js @@ -18,9 +18,12 @@ function build() { const Review = require('./review'); const { Job, Task } = require('./session'); const { Project } = require('./project'); + const implementProject = require('./project-implementation'); const { Attribute, Label } = require('./labels'); const MLModel = require('./ml-model'); const { FrameData } = require('./frames'); + const { CloudStorage } = require('./cloud-storage'); + const enums = require('./enums'); @@ -747,6 +750,41 @@ function build() { PluginError, ServerError, }, + /** + * Namespace is used for getting cloud storages + * @namespace cloudStorages + * @memberof module:API.cvat + */ + cloudStorages: { + /** + * @typedef {Object} CloudStorageFilter + * @property {string} displayName Check if displayName contains this value + * @property {string} resourceName Check if resourceName contains this value + * @property {module:API.cvat.enums.ProviderType} providerType Check if providerType equal this value + * @property {integer} id Check if id equals this value + * @property {integer} page Get specific page + * (default REST API returns 20 clouds storages per request. + * In order to get more, it is need to specify next page) + * @property {string} owner Check if an owner name contains this value + * @property {string} search Combined search of contains among all the fields + * @global + */ + + /** + * Method returns a list of cloud storages corresponding to a filter + * @method get + * @async + * @memberof module:API.cvat.cloudStorages + * @param {CloudStorageFilter} [filter={}] cloud storage filter + * @returns {module:API.cvat.classes.CloudStorage[]} + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} + */ + async get(filter = {}) { + const result = await PluginRegistry.apiWrapper(cvat.cloudStorages.get, filter); + return result; + }, + }, /** * Namespace is used for access to classes * @namespace classes @@ -754,7 +792,7 @@ function build() { */ classes: { User, - Project, + Project: implementProject(Project), Task, Job, Log, @@ -767,6 +805,7 @@ function build() { Issue, Review, FrameData, + CloudStorage, }, }; @@ -779,6 +818,7 @@ function build() { cvat.lambda = Object.freeze(cvat.lambda); cvat.client = Object.freeze(cvat.client); cvat.enums = Object.freeze(cvat.enums); + cvat.cloudStorages = Object.freeze(cvat.cloudStorages); const implementAPI = require('./api-implementation'); diff --git a/cvat-core/src/cloud-storage.js b/cvat-core/src/cloud-storage.js new file mode 100644 index 00000000..4fd8bd3a --- /dev/null +++ b/cvat-core/src/cloud-storage.js @@ -0,0 +1,520 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +(() => { + const PluginRegistry = require('./plugins'); + const serverProxy = require('./server-proxy'); + const { isBrowser, isNode } = require('browser-or-node'); + const { ArgumentError } = require('./exceptions'); + const { CloudStorageCredentialsType, CloudStorageProviderType } = require('./enums'); + + /** + * Class representing a cloud storage + * @memberof module:API.cvat.classes + */ + class CloudStorage { + // TODO: add storage availability status (avaliable/unavaliable) + constructor(initialData) { + const data = { + id: undefined, + display_name: undefined, + description: undefined, + credentials_type: undefined, + provider_type: undefined, + resource: undefined, + account_name: undefined, + key: undefined, + secret_key: undefined, + session_token: undefined, + specific_attributes: undefined, + owner: undefined, + created_date: undefined, + updated_date: undefined, + manifest_path: undefined, + manifests: undefined, + }; + + for (const property in data) { + if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { + data[property] = initialData[property]; + } + } + + Object.defineProperties( + this, + Object.freeze({ + /** + * @name id + * @type {integer} + * @memberof module:API.cvat.classes.CloudStorage + * @readonly + * @instance + */ + id: { + get: () => data.id, + }, + /** + * Storage name + * @name displayName + * @type {string} + * @memberof module:API.cvat.classes.CloudStorage + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + displayName: { + get: () => data.display_name, + set: (value) => { + if (typeof value !== 'string') { + throw new ArgumentError(`Value must be string. ${typeof value} was found`); + } else if (!value.trim().length) { + throw new ArgumentError('Value must not be empty string'); + } + data.display_name = value; + }, + }, + /** + * Storage description + * @name description + * @type {string} + * @memberof module:API.cvat.classes.CloudStorage + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + description: { + get: () => data.description, + set: (value) => { + if (typeof value !== 'string') { + throw new ArgumentError('Value must be string'); + } + data.description = value; + }, + }, + /** + * Azure account name + * @name accountName + * @type {string} + * @memberof module:API.cvat.classes.CloudStorage + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + accountName: { + get: () => data.account_name, + set: (value) => { + if (typeof value === 'string') { + if (value.trim().length) { + data.account_name = value; + } else { + throw new ArgumentError('Value must not be empty'); + } + } else { + throw new ArgumentError(`Value must be a string. ${typeof value} was found`); + } + }, + }, + /** + * AWS access key id + * @name accessKey + * @type {string} + * @memberof module:API.cvat.classes.CloudStorage + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + accessKey: { + get: () => data.key, + set: (value) => { + if (typeof value === 'string') { + if (value.trim().length) { + data.key = value; + } else { + throw new ArgumentError('Value must not be empty'); + } + } else { + throw new ArgumentError(`Value must be a string. ${typeof value} was found`); + } + }, + }, + /** + * AWS secret key + * @name secretKey + * @type {string} + * @memberof module:API.cvat.classes.CloudStorage + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + secretKey: { + get: () => data.secret_key, + set: (value) => { + if (typeof value === 'string') { + if (value.trim().length) { + data.secret_key = value; + } else { + throw new ArgumentError('Value must not be empty'); + } + } else { + throw new ArgumentError(`Value must be a string. ${typeof value} was found`); + } + }, + }, + /** + * Session token + * @name token + * @type {string} + * @memberof module:API.cvat.classes.CloudStorage + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + token: { + get: () => data.session_token, + set: (value) => { + if (typeof value === 'string') { + if (value.trim().length) { + data.session_token = value; + } else { + throw new ArgumentError('Value must not be empty'); + } + } else { + throw new ArgumentError(`Value must be a string. ${typeof value} was found`); + } + }, + }, + /** + * Unique resource name + * @name resourceName + * @type {string} + * @memberof module:API.cvat.classes.CloudStorage + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + resourceName: { + get: () => data.resource, + set: (value) => { + if (typeof value !== 'string') { + throw new ArgumentError(`Value must be string. ${typeof value} was found`); + } else if (!value.trim().length) { + throw new ArgumentError('Value must not be empty'); + } + data.resource = value; + }, + }, + /** + * @name manifestPath + * @type {string} + * @memberof module:API.cvat.classes.CloudStorage + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + manifestPath: { + get: () => data.manifest_path, + set: (value) => { + if (typeof value === 'string') { + data.manifest_path = value; + } else { + throw new ArgumentError('Value must be a string'); + } + }, + }, + /** + * @name providerType + * @type {module:API.cvat.enums.ProviderType} + * @memberof module:API.cvat.classes.CloudStorage + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + providerType: { + get: () => data.provider_type, + set: (key) => { + if (key !== undefined && !!CloudStorageProviderType[key]) { + data.provider_type = CloudStorageProviderType[key]; + } else { + throw new ArgumentError('Value must be one CloudStorageProviderType keys'); + } + }, + }, + /** + * @name credentialsType + * @type {module:API.cvat.enums.CloudStorageCredentialsType} + * @memberof module:API.cvat.classes.CloudStorage + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + credentialsType: { + get: () => data.credentials_type, + set: (key) => { + if (key !== undefined && !!CloudStorageCredentialsType[key]) { + data.credentials_type = CloudStorageCredentialsType[key]; + } else { + throw new ArgumentError('Value must be one CloudStorageCredentialsType keys'); + } + }, + }, + /** + * @name specificAttributes + * @type {string} + * @memberof module:API.cvat.classes.CloudStorage + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + specificAttributes: { + get: () => data.specific_attributes, + set: (attributesValue) => { + if (typeof attributesValue === 'string') { + const attrValues = new URLSearchParams( + Array.from(new URLSearchParams(attributesValue).entries()).filter( + ([key, value]) => !!key && !!value, + ), + ).toString(); + if (!attrValues) { + throw new ArgumentError('Value must match the key1=value1&key2=value2'); + } + data.specific_attributes = attributesValue; + } else { + throw new ArgumentError('Value must be a string'); + } + }, + }, + /** + * Instance of a user who has created the cloud storage + * @name owner + * @type {module:API.cvat.classes.User} + * @memberof module:API.cvat.classes.CloudStorage + * @readonly + * @instance + */ + owner: { + get: () => data.owner, + }, + /** + * @name createdDate + * @type {string} + * @memberof module:API.cvat.classes.CloudStorage + * @readonly + * @instance + */ + createdDate: { + get: () => data.created_date, + }, + /** + * @name updatedDate + * @type {string} + * @memberof module:API.cvat.classes.CloudStorage + * @readonly + * @instance + */ + updatedDate: { + get: () => data.updated_date, + }, + /** + * @name manifests + * @type {string[]} + * @memberof module:API.cvat.classes.CloudStorage + * @instance + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + manifests: { + get: () => data.manifests, + set: (manifests) => { + if (Array.isArray(manifests)) { + for (const elem of manifests) { + if (typeof elem !== 'string') { + throw new ArgumentError('Each element of the manifests array must be a string'); + } + } + data.manifests = manifests; + } else { + throw new ArgumentError('Value must be an array'); + } + }, + }, + }), + ); + } + + /** + * Method updates data of a created cloud storage or creates new cloud storage + * @method save + * @returns {module:API.cvat.classes.CloudStorage} + * @memberof module:API.cvat.classes.CloudStorage + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async save() { + const result = await PluginRegistry.apiWrapper.call(this, CloudStorage.prototype.save); + return result; + } + + /** + * Method deletes a cloud storage from a server + * @method delete + * @memberof module:API.cvat.classes.CloudStorage + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async delete() { + const result = await PluginRegistry.apiWrapper.call(this, CloudStorage.prototype.delete); + return result; + } + + /** + * Method returns cloud storage content + * @method getContent + * @memberof module:API.cvat.classes.CloudStorage + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async getContent() { + const result = await PluginRegistry.apiWrapper.call(this, CloudStorage.prototype.getContent); + return result; + } + + /** + * Method returns the cloud storage preview + * @method getPreview + * @memberof module:API.cvat.classes.CloudStorage + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async getPreview() { + const result = await PluginRegistry.apiWrapper.call(this, CloudStorage.prototype.getPreview); + return result; + } + + /** + * Method returns cloud storage status + * @method getStatus + * @memberof module:API.cvat.classes.CloudStorage + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.PluginError} + */ + async getStatus() { + const result = await PluginRegistry.apiWrapper.call(this, CloudStorage.prototype.getStatus); + return result; + } + } + + CloudStorage.prototype.save.implementation = async function () { + function prepareOptionalFields(cloudStorageInstance) { + const data = {}; + if (cloudStorageInstance.description) { + data.description = cloudStorageInstance.description; + } + + if (cloudStorageInstance.accountName) { + data.account_name = cloudStorageInstance.accountName; + } + + if (cloudStorageInstance.accessKey) { + data.key = cloudStorageInstance.accessKey; + } + + if (cloudStorageInstance.secretKey) { + data.secret_key = cloudStorageInstance.secretKey; + } + + if (cloudStorageInstance.token) { + data.session_token = cloudStorageInstance.token; + } + + if (cloudStorageInstance.specificAttributes) { + data.specific_attributes = cloudStorageInstance.specificAttributes; + } + return data; + } + // update + if (typeof this.id !== 'undefined') { + // providr_type and recource should not change; + // send to the server only the values that have changed + const initialData = {}; + if (this.displayName) { + initialData.display_name = this.displayName; + } + if (this.credentialsType) { + initialData.credentials_type = this.credentialsType; + } + + if (this.manifests) { + initialData.manifests = this.manifests; + } + + const cloudStorageData = { + ...initialData, + ...prepareOptionalFields(this), + }; + + await serverProxy.cloudStorages.update(this.id, cloudStorageData); + return this; + } + + // create + const initialData = { + display_name: this.displayName, + credentials_type: this.credentialsType, + provider_type: this.providerType, + resource: this.resourceName, + manifests: this.manifests, + }; + + const cloudStorageData = { + ...initialData, + ...prepareOptionalFields(this), + }; + + const cloudStorage = await serverProxy.cloudStorages.create(cloudStorageData); + return new CloudStorage(cloudStorage); + }; + + CloudStorage.prototype.delete.implementation = async function () { + const result = await serverProxy.cloudStorages.delete(this.id); + return result; + }; + + CloudStorage.prototype.getContent.implementation = async function () { + const result = await serverProxy.cloudStorages.getContent(this.id, this.manifestPath); + return result; + }; + + CloudStorage.prototype.getPreview.implementation = async function getPreview() { + return new Promise((resolve, reject) => { + serverProxy.cloudStorages + .getPreview(this.id) + .then((result) => { + if (isNode) { + resolve(global.Buffer.from(result, 'binary').toString('base64')); + } else if (isBrowser) { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(result); + } + }) + .catch((error) => { + reject(error); + }); + }); + }; + + CloudStorage.prototype.getStatus.implementation = async function () { + const result = await serverProxy.cloudStorages.getStatus(this.id); + return result; + }; + + module.exports = { + CloudStorage, + }; +})(); diff --git a/cvat-core/src/enums.js b/cvat-core/src/enums.js index 4ce6d80c..c8ecab55 100644 --- a/cvat-core/src/enums.js +++ b/cvat-core/src/enums.js @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2020 Intel Corporation +// Copyright (C) 2019-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -333,6 +333,36 @@ '#733380', ]; + /** + * Types of cloud storage providers + * @enum {string} + * @name CloudStorageProviderType + * @memberof module:API.cvat.enums + * @property {string} AWS_S3 'AWS_S3_BUCKET' + * @property {string} AZURE 'AZURE_CONTAINER' + * @readonly + */ + const CloudStorageProviderType = Object.freeze({ + AWS_S3_BUCKET: 'AWS_S3_BUCKET', + AZURE_CONTAINER: 'AZURE_CONTAINER', + }); + + /** + * Types of cloud storage credentials + * @enum {string} + * @name CloudStorageCredentialsType + * @memberof module:API.cvat.enums + * @property {string} KEY_SECRET_KEY_PAIR 'KEY_SECRET_KEY_PAIR' + * @property {string} ACCOUNT_NAME_TOKEN_PAIR 'ACCOUNT_NAME_TOKEN_PAIR' + * @property {string} ANONYMOUS_ACCESS 'ANONYMOUS_ACCESS' + * @readonly + */ + const CloudStorageCredentialsType = Object.freeze({ + KEY_SECRET_KEY_PAIR: 'KEY_SECRET_KEY_PAIR', + ACCOUNT_NAME_TOKEN_PAIR: 'ACCOUNT_NAME_TOKEN_PAIR', + ANONYMOUS_ACCESS: 'ANONYMOUS_ACCESS', + }); + module.exports = { ShareFileType, TaskStatus, @@ -348,5 +378,7 @@ colors, Source, DimensionType, + CloudStorageProviderType, + CloudStorageCredentialsType, }; })(); diff --git a/cvat-core/src/ml-model.js b/cvat-core/src/ml-model.js index e16cf24e..05aa0140 100644 --- a/cvat-core/src/ml-model.js +++ b/cvat-core/src/ml-model.js @@ -14,6 +14,10 @@ class MLModel { this._framework = data.framework; this._description = data.description; this._type = data.type; + this._tip = { + message: data.help_message, + gif: data.animated_gif, + }; this._params = { canvas: { minPosVertices: data.min_pos_points, @@ -84,6 +88,25 @@ class MLModel { canvas: { ...this._params.canvas }, }; } + + /** + * @typedef {Object} MlModelTip + * @property {string} message A short message for a user about the model + * @property {string} gif A gif URL to be shawn to a user as an example + * @returns {MlModelTip} + * @readonly + */ + get tip() { + return { ...this._tip }; + } + + /** + * @param {(event:string)=>void} onChangeToolsBlockerState Set canvas onChangeToolsBlockerState callback + * @returns {void} + */ + set onChangeToolsBlockerState(onChangeToolsBlockerState) { + this._params.canvas.onChangeToolsBlockerState = onChangeToolsBlockerState; + } } module.exports = MLModel; diff --git a/cvat-core/src/project-implementation.js b/cvat-core/src/project-implementation.js new file mode 100644 index 00000000..c5bb2387 --- /dev/null +++ b/cvat-core/src/project-implementation.js @@ -0,0 +1,74 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +(() => { + const serverProxy = require('./server-proxy'); + const { getPreview } = require('./frames'); + + const { Project } = require('./project'); + const { exportDataset } = require('./annotations'); + + function implementProject(projectClass) { + projectClass.prototype.save.implementation = async function () { + const trainingProjectCopy = this.trainingProject; + if (typeof this.id !== 'undefined') { + // project has been already created, need to update some data + const projectData = { + name: this.name, + assignee_id: this.assignee ? this.assignee.id : null, + bug_tracker: this.bugTracker, + labels: [...this._internalData.labels.map((el) => el.toJSON())], + }; + + if (trainingProjectCopy) { + projectData.training_project = trainingProjectCopy; + } + + await serverProxy.projects.save(this.id, projectData); + return this; + } + + // initial creating + const projectSpec = { + name: this.name, + labels: [...this.labels.map((el) => el.toJSON())], + }; + + if (this.bugTracker) { + projectSpec.bug_tracker = this.bugTracker; + } + + if (trainingProjectCopy) { + projectSpec.training_project = trainingProjectCopy; + } + + const project = await serverProxy.projects.create(projectSpec); + return new Project(project); + }; + + projectClass.prototype.delete.implementation = async function () { + const result = await serverProxy.projects.delete(this.id); + return result; + }; + + projectClass.prototype.preview.implementation = async function () { + if (!this._internalData.task_ids.length) { + return ''; + } + const frameData = await getPreview(this._internalData.task_ids[0]); + return frameData; + }; + + projectClass.prototype.annotations.exportDataset.implementation = async function ( + format, saveImages, customName, + ) { + const result = exportDataset(this, format, customName, saveImages); + return result; + }; + + return projectClass; + } + + module.exports = implementProject; +})(); diff --git a/cvat-core/src/project.js b/cvat-core/src/project.js index b66eab49..7e324498 100644 --- a/cvat-core/src/project.js +++ b/cvat-core/src/project.js @@ -4,11 +4,9 @@ (() => { const PluginRegistry = require('./plugins'); - const serverProxy = require('./server-proxy'); const { ArgumentError } = require('./exceptions'); const { Task } = require('./session'); const { Label } = require('./labels'); - const { getPreview } = require('./frames'); const User = require('./user'); /** @@ -36,6 +34,7 @@ task_subsets: undefined, training_project: undefined, task_ids: undefined, + dimension: undefined, }; for (const property in data) { @@ -155,7 +154,7 @@ /** * @name createdDate * @type {string} - * @memberof module:API.cvat.classes.Task + * @memberof module:API.cvat.classes.Project * @readonly * @instance */ @@ -165,13 +164,24 @@ /** * @name updatedDate * @type {string} - * @memberof module:API.cvat.classes.Task + * @memberof module:API.cvat.classes.Project * @readonly * @instance */ updatedDate: { get: () => data.updated_date, }, + /** + * Dimesion of the tasks in the project, if no task dimension is null + * @name dimension + * @type {string} + * @memberof module:API.cvat.classes.Project + * @readonly + * @instance + */ + dimension: { + get: () => data.dimension, + }, /** * After project has been created value can be appended only. * @name labels @@ -203,7 +213,7 @@ }, }, /** - * Tasks linked with the project + * Tasks related with the project * @name tasks * @type {module:API.cvat.classes.Task[]} * @memberof module:API.cvat.classes.Project @@ -214,7 +224,7 @@ get: () => [...data.tasks], }, /** - * Subsets array for linked tasks + * Subsets array for related tasks * @name subsets * @type {string[]} * @memberof module:API.cvat.classes.Project @@ -254,6 +264,13 @@ }, }), ); + + // When we call a function, for example: project.annotations.get() + // In the method get we lose the project context + // So, we need return it + this.annotations = { + exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this), + }; } /** @@ -289,7 +306,7 @@ } /** - * Method deletes a task from a server + * Method deletes a project from a server * @method delete * @memberof module:API.cvat.classes.Project * @readonly @@ -304,57 +321,28 @@ } } + Object.defineProperties( + Project.prototype, + Object.freeze({ + annotations: Object.freeze({ + value: { + async exportDataset(format, saveImages, customName = '') { + const result = await PluginRegistry.apiWrapper.call( + this, + Project.prototype.annotations.exportDataset, + format, + saveImages, + customName, + ); + return result; + }, + }, + writable: true, + }), + }), + ); + module.exports = { Project, }; - - Project.prototype.save.implementation = async function () { - const trainingProjectCopy = this.trainingProject; - if (typeof this.id !== 'undefined') { - // project has been already created, need to update some data - const projectData = { - name: this.name, - assignee_id: this.assignee ? this.assignee.id : null, - bug_tracker: this.bugTracker, - labels: [...this._internalData.labels.map((el) => el.toJSON())], - }; - - if (trainingProjectCopy) { - projectData.training_project = trainingProjectCopy; - } - - await serverProxy.projects.save(this.id, projectData); - return this; - } - - // initial creating - const projectSpec = { - name: this.name, - labels: [...this.labels.map((el) => el.toJSON())], - }; - - if (this.bugTracker) { - projectSpec.bug_tracker = this.bugTracker; - } - - if (trainingProjectCopy) { - projectSpec.training_project = trainingProjectCopy; - } - - const project = await serverProxy.projects.create(projectSpec); - return new Project(project); - }; - - Project.prototype.delete.implementation = async function () { - const result = await serverProxy.projects.delete(this.id); - return result; - }; - - Project.prototype.preview.implementation = async function () { - if (!this._internalData.task_ids.length) { - return ''; - } - const frameData = await getPreview(this._internalData.task_ids[0]); - return frameData; - }; })(); diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index 524ceeaf..cdc98940 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -465,29 +465,39 @@ } } - async function exportDataset(id, format) { - const { backendAPI } = config; - let url = `${backendAPI}/tasks/${id}/dataset?format=${format}`; + function exportDataset(instanceType) { + return async function (id, format, name, saveImages) { + const { backendAPI } = config; + const baseURL = `${backendAPI}/${instanceType}/${id}/${saveImages ? 'dataset' : 'annotations'}`; + let query = `format=${encodeURIComponent(format)}`; + if (name) { + const filename = name.replace(/\//g, '_'); + query += `&filename=${encodeURIComponent(filename)}`; + } + let url = `${baseURL}?${query}`; - return new Promise((resolve, reject) => { - async function request() { - try { - const response = await Axios.get(`${url}`, { + return new Promise((resolve, reject) => { + async function request() { + Axios.get(`${url}`, { proxy: config.proxy, - }); - if (response.status === 202) { - setTimeout(request, 3000); - } else { - url = `${url}&action=download`; - resolve(url); - } - } catch (errorData) { - reject(generateError(errorData)); + }) + .then((response) => { + if (response.status === 202) { + setTimeout(request, 3000); + } else { + query = `${query}&action=download`; + url = `${baseURL}?${query}`; + resolve(url); + } + }) + .catch((errorData) => { + reject(generateError(errorData)); + }); } - } - setTimeout(request); - }); + setTimeout(request); + }); + }; } async function exportTask(id) { @@ -1169,6 +1179,121 @@ } } + async function createCloudStorage(storageDetail) { + const { backendAPI } = config; + + try { + const response = await Axios.post(`${backendAPI}/cloudstorages`, JSON.stringify(storageDetail), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.data; + } catch (errorData) { + throw generateError(errorData); + } + } + + async function updateCloudStorage(id, cloudStorageData) { + const { backendAPI } = config; + + try { + await Axios.patch(`${backendAPI}/cloudstorages/${id}`, JSON.stringify(cloudStorageData), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); + } + } + + async function getCloudStorages(filter = '') { + const { backendAPI } = config; + + let response = null; + try { + response = await Axios.get(`${backendAPI}/cloudstorages?page_size=12&${filter}`, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); + } + + response.data.results.count = response.data.count; + return response.data.results; + } + + async function getCloudStorageContent(id, manifestPath) { + const { backendAPI } = config; + + let response = null; + try { + const url = `${backendAPI}/cloudstorages/${id}/content${ + manifestPath ? `?manifest_path=${manifestPath}` : '' + }`; + response = await Axios.get(url, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); + } + + return response.data; + } + + async function getCloudStoragePreview(id) { + const { backendAPI } = config; + + let response = null; + try { + const url = `${backendAPI}/cloudstorages/${id}/preview`; + response = await workerAxios.get(url, { + proxy: config.proxy, + responseType: 'arraybuffer', + }); + } catch (errorData) { + throw generateError({ + ...errorData, + message: '', + response: { + ...errorData.response, + data: String.fromCharCode.apply(null, new Uint8Array(errorData.response.data)), + }, + }); + } + + return new Blob([new Uint8Array(response)]); + } + + async function getCloudStorageStatus(id) { + const { backendAPI } = config; + + let response = null; + try { + const url = `${backendAPI}/cloudstorages/${id}/status`; + response = await Axios.get(url, { + proxy: config.proxy, + }); + } catch (errorData) { + throw generateError(errorData); + } + + return response.data; + } + + async function deleteCloudStorage(id) { + const { backendAPI } = config; + + try { + await Axios.delete(`${backendAPI}/cloudstorages/${id}`); + } catch (errorData) { + throw generateError(errorData); + } + } + Object.defineProperties( this, Object.freeze({ @@ -1199,6 +1324,7 @@ save: saveProject, create: createProject, delete: deleteProject, + exportDataset: exportDataset('projects'), }), writable: false, }, @@ -1209,7 +1335,7 @@ saveTask, createTask, deleteTask, - exportDataset, + exportDataset: exportDataset('tasks'), exportTask, importTask, }), @@ -1297,6 +1423,19 @@ }), writable: false, }, + + cloudStorages: { + value: Object.freeze({ + get: getCloudStorages, + getContent: getCloudStorageContent, + getPreview: getCloudStoragePreview, + getStatus: getCloudStorageStatus, + create: createCloudStorage, + delete: deleteCloudStorage, + update: updateCloudStorage, + }), + writable: false, + }, }), ); } diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index a1136030..eaba48bc 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -42,16 +42,6 @@ return result; }, - async dump(dumper, name = null) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.annotations.dump, - dumper, - name, - ); - return result; - }, - async statistics() { const result = await PluginRegistry.apiWrapper.call(this, prototype.annotations.statistics); return result; @@ -148,11 +138,13 @@ return result; }, - async exportDataset(format) { + async exportDataset(format, saveImages, customName = '') { const result = await PluginRegistry.apiWrapper.call( this, prototype.annotations.exportDataset, format, + saveImages, + customName, ); return result; }, @@ -329,21 +321,6 @@ * @instance * @async */ - /** - * Dump of annotations to a file. - * Method always dumps annotations for a whole task. - * @method dump - * @memberof Session.annotations - * @param {module:API.cvat.classes.Dumper} dumper - a dumper - * @param {string} [name = null] - a name of a file with annotations - * which will be used to dump - * @returns {string} URL which can be used in order to get a dump file - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ServerError} - * @throws {module:API.cvat.exceptions.ArgumentError} - * @instance - * @async - */ /** * Collect short statistics about a task or a job. * @method statistics @@ -877,7 +854,6 @@ get: Object.getPrototypeOf(this).annotations.get.bind(this), put: Object.getPrototypeOf(this).annotations.put.bind(this), save: Object.getPrototypeOf(this).annotations.save.bind(this), - dump: Object.getPrototypeOf(this).annotations.dump.bind(this), merge: Object.getPrototypeOf(this).annotations.merge.bind(this), split: Object.getPrototypeOf(this).annotations.split.bind(this), group: Object.getPrototypeOf(this).annotations.group.bind(this), @@ -1036,6 +1012,7 @@ use_cache: undefined, copy_data: undefined, dimension: undefined, + cloud_storage_id: undefined, }; const updatedFields = new FieldUpdateTrigger({ @@ -1397,7 +1374,7 @@ get: () => [...data.jobs], }, /** - * List of files from shared resource + * List of files from shared resource or list of cloud storage files * @name serverFiles * @type {string[]} * @memberof module:API.cvat.classes.Task @@ -1559,6 +1536,15 @@ */ get: () => data.dimension, }, + /** + * @name cloudStorageId + * @type {integer|null} + * @memberof module:API.cvat.classes.Task + * @instance + */ + cloudStorageId: { + get: () => data.cloud_storage_id, + }, _internalData: { get: () => data, }, @@ -1575,7 +1561,6 @@ get: Object.getPrototypeOf(this).annotations.get.bind(this), put: Object.getPrototypeOf(this).annotations.put.bind(this), save: Object.getPrototypeOf(this).annotations.save.bind(this), - dump: Object.getPrototypeOf(this).annotations.dump.bind(this), merge: Object.getPrototypeOf(this).annotations.merge.bind(this), split: Object.getPrototypeOf(this).annotations.split.bind(this), group: Object.getPrototypeOf(this).annotations.group.bind(this), @@ -1715,7 +1700,6 @@ selectObject, annotationsStatistics, uploadAnnotations, - dumpAnnotations, importAnnotations, exportAnnotations, exportDataset, @@ -1948,13 +1932,8 @@ return result; }; - Job.prototype.annotations.dump.implementation = async function (dumper, name) { - const result = await dumpAnnotations(this, name, dumper); - return result; - }; - - Job.prototype.annotations.exportDataset.implementation = async function (format) { - const result = await exportDataset(this.task, format); + Job.prototype.annotations.exportDataset.implementation = async function (format, saveImages, customName) { + const result = await exportDataset(this.task, format, customName, saveImages); return result; }; @@ -2093,6 +2072,9 @@ if (typeof this.copyData !== 'undefined') { taskDataSpec.copy_data = this.copyData; } + if (typeof this.cloudStorageId !== 'undefined') { + taskDataSpec.cloud_storage_id = this.cloudStorageId; + } const task = await serverProxy.tasks.createTask(taskSpec, taskDataSpec, onUpdate); return new Task(task); @@ -2252,11 +2234,6 @@ return result; }; - Task.prototype.annotations.dump.implementation = async function (dumper, name) { - const result = await dumpAnnotations(this, name, dumper); - return result; - }; - Task.prototype.annotations.import.implementation = function (data) { const result = importAnnotations(this, data); return result; @@ -2267,8 +2244,8 @@ return result; }; - Task.prototype.annotations.exportDataset.implementation = async function (format) { - const result = await exportDataset(this, format); + Task.prototype.annotations.exportDataset.implementation = async function (format, saveImages, customName) { + const result = await exportDataset(this, format, customName, saveImages); return result; }; diff --git a/cvat-core/tests/api/cloud-storages.js b/cvat-core/tests/api/cloud-storages.js new file mode 100644 index 00000000..7b5d1bf4 --- /dev/null +++ b/cvat-core/tests/api/cloud-storages.js @@ -0,0 +1,178 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +// Setup mock for a server +jest.mock('../../src/server-proxy', () => { + const mock = require('../mocks/server-proxy.mock'); + return mock; +}); + +// Initialize api +window.cvat = require('../../src/api'); + +const { CloudStorage } = require('../../src/cloud-storage'); +const { cloudStoragesDummyData } = require('../mocks/dummy-data.mock'); + +describe('Feature: get cloud storages', () => { + test('get all cloud storages', async () => { + const result = await window.cvat.cloudStorages.get(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(cloudStoragesDummyData.count); + for (const item of result) { + expect(item).toBeInstanceOf(CloudStorage); + } + }); + + test('get cloud storage by id', async () => { + const result = await window.cvat.cloudStorages.get({ + id: 1, + }); + const cloudStorage = result[0]; + + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(1); + expect(cloudStorage).toBeInstanceOf(CloudStorage); + expect(cloudStorage.id).toBe(1); + expect(cloudStorage.providerType).toBe('AWS_S3_BUCKET'); + expect(cloudStorage.credentialsType).toBe('KEY_SECRET_KEY_PAIR'); + expect(cloudStorage.resourceName).toBe('bucket'); + expect(cloudStorage.displayName).toBe('Demonstration bucket'); + expect(cloudStorage.manifests).toHaveLength(1); + expect(cloudStorage.manifests[0]).toBe('manifest.jsonl'); + expect(cloudStorage.specificAttributes).toBe(''); + expect(cloudStorage.description).toBe('It is first bucket'); + }); + + test('get a cloud storage by an unknown id', async () => { + const result = await window.cvat.cloudStorages.get({ + id: 10, + }); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(0); + }); + + test('get a cloud storage by an invalid id', async () => { + expect( + window.cvat.cloudStorages.get({ + id: '1', + }), + ).rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); + + test('get cloud storages by filters', async () => { + const filters = new Map([ + ['providerType', 'AWS_S3_BUCKET'], + ['resourceName', 'bucket'], + ['displayName', 'Demonstration bucket'], + ['credentialsType', 'KEY_SECRET_KEY_PAIR'], + ['description', 'It is first bucket'], + ]); + + const result = await window.cvat.cloudStorages.get(Object.fromEntries(filters)); + + const [cloudStorage] = result; + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(1); + expect(cloudStorage).toBeInstanceOf(CloudStorage); + expect(cloudStorage.id).toBe(1); + filters.forEach((value, key) => { + expect(cloudStorage[key]).toBe(value); + }); + }); + + test('get cloud storage by invalid filters', async () => { + expect( + window.cvat.cloudStorages.get({ + unknown: '5', + }), + ).rejects.toThrow(window.cvat.exceptions.ArgumentError); + }); +}); + +describe('Feature: create a cloud storage', () => { + test('create new cloud storage without an id', async () => { + const cloudStorage = new window.cvat.classes.CloudStorage({ + display_name: 'new cloud storage', + provider_type: 'AZURE_CONTAINER', + resource: 'newcontainer', + credentials_type: 'ACCOUNT_NAME_TOKEN_PAIR', + account_name: 'accountname', + session_token: 'x'.repeat(135), + manifests: ['manifest.jsonl'], + }); + + const result = await cloudStorage.save(); + expect(typeof result.id).toBe('number'); + }); +}); + +describe('Feature: update a cloud storage', () => { + test('update cloud storage with some new field values', async () => { + const newValues = new Map([ + ['displayName', 'new display name'], + ['credentialsType', 'ANONYMOUS_ACCESS'], + ['description', 'new description'], + ['specificAttributes', 'region=eu-west-1'], + ]); + + let result = await window.cvat.cloudStorages.get({ + id: 1, + }); + + let [cloudStorage] = result; + + for (const [key, value] of newValues) { + cloudStorage[key] = value; + } + + cloudStorage.save(); + + result = await window.cvat.cloudStorages.get({ + id: 1, + }); + [cloudStorage] = result; + + newValues.forEach((value, key) => { + expect(cloudStorage[key]).toBe(value); + }); + }); + + test('Update manifests in a cloud storage', async () => { + const newManifests = [ + 'sub1/manifest.jsonl', + 'sub2/manifest.jsonl', + ]; + + let result = await window.cvat.cloudStorages.get({ + id: 1, + }); + let [cloudStorage] = result; + + cloudStorage.manifests = newManifests; + cloudStorage.save(); + + result = await window.cvat.cloudStorages.get({ + id: 1, + }); + [cloudStorage] = result; + + expect(cloudStorage.manifests).toEqual(newManifests); + }); +}); + +describe('Feature: delete a cloud storage', () => { + test('delete a cloud storage', async () => { + let result = await window.cvat.cloudStorages.get({ + id: 2, + }); + + await result[0].delete(); + result = await window.cvat.cloudStorages.get({ + id: 2, + }); + + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(0); + }); +}); diff --git a/cvat-core/tests/mocks/dummy-data.mock.js b/cvat-core/tests/mocks/dummy-data.mock.js index 17e886ca..7b67d00e 100644 --- a/cvat-core/tests/mocks/dummy-data.mock.js +++ b/cvat-core/tests/mocks/dummy-data.mock.js @@ -2547,6 +2547,56 @@ const frameMetaDummyData = { }, }; +const cloudStoragesDummyData = { + count: 2, + next: null, + previous: null, + results: [ + { + id: 2, + owner: { + url: 'http://localhost:7000/api/v1/users/1', + id: 1, + username: 'maya', + first_name: '', + last_name: '' + }, + manifests: [ + 'manifest.jsonl' + ], + provider_type: 'AZURE_CONTAINER', + resource: 'container', + display_name: 'Demonstration container', + created_date: '2021-09-01T09:29:47.094244Z', + updated_date: '2021-09-01T09:29:47.103264Z', + credentials_type: 'ACCOUNT_NAME_TOKEN_PAIR', + specific_attributes: '', + description: 'It is first container' + }, + { + id: 1, + owner: { + url: 'http://localhost:7000/api/v1/users/1', + id: 1, + username: 'maya', + first_name: '', + last_name: '' + }, + manifests: [ + 'manifest.jsonl' + ], + provider_type: 'AWS_S3_BUCKET', + resource: 'bucket', + display_name: 'Demonstration bucket', + created_date: '2021-08-31T09:03:09.350817Z', + updated_date: '2021-08-31T15:16:21.394773Z', + credentials_type: 'KEY_SECRET_KEY_PAIR', + specific_attributes: '', + description: 'It is first bucket' + } + ] +}; + module.exports = { tasksDummyData, projectsDummyData, @@ -2557,4 +2607,5 @@ module.exports = { jobAnnotationsDummyData, frameMetaDummyData, formatsDummyData, + cloudStoragesDummyData, }; diff --git a/cvat-core/tests/mocks/server-proxy.mock.js b/cvat-core/tests/mocks/server-proxy.mock.js index a5111756..7c9e2e15 100644 --- a/cvat-core/tests/mocks/server-proxy.mock.js +++ b/cvat-core/tests/mocks/server-proxy.mock.js @@ -12,6 +12,7 @@ const { taskAnnotationsDummyData, jobAnnotationsDummyData, frameMetaDummyData, + cloudStoragesDummyData, } = require('./dummy-data.mock'); function QueryStringToJSON(query, ignoreList = []) { @@ -318,6 +319,63 @@ class ServerProxy { return null; } + async function getCloudStorages(filter = '') { + const queries = QueryStringToJSON(filter); + const result = cloudStoragesDummyData.results.filter((item) => { + for (const key in queries) { + if (Object.prototype.hasOwnProperty.call(queries, key)) { + if (queries[key] !== item[key]) { + return false; + } + } + } + return true; + }); + return result; + } + + async function updateCloudStorage(id, cloudStorageData) { + const cloudStorage = cloudStoragesDummyData.results.find((item) => item.id === id); + if (cloudStorage) { + for (const prop in cloudStorageData) { + if ( + Object.prototype.hasOwnProperty.call(cloudStorageData, prop) + && Object.prototype.hasOwnProperty.call(cloudStorage, prop) + ) { + cloudStorage[prop] = cloudStorageData[prop]; + } + } + } + } + + async function createCloudStorage(cloudStorageData) { + const id = Math.max(...cloudStoragesDummyData.results.map((item) => item.id)) + 1; + cloudStoragesDummyData.results.push({ + id, + provider_type: cloudStorageData.provider_type, + resource: cloudStorageData.resource, + display_name: cloudStorageData.display_name, + credentials_type: cloudStorageData.credentials_type, + specific_attributes: cloudStorageData.specific_attributes, + description: cloudStorageData.description, + owner: 1, + created_date: '2021-09-01T09:29:47.094244+03:00', + updated_date: '2021-09-01T09:29:47.103264+03:00', + }); + + const result = await getCloudStorages(`?id=${id}`); + return result[0]; + } + + async function deleteCloudStorage(id) { + const cloudStorages = cloudStoragesDummyData.results; + const cloudStorageId = cloudStorages.findIndex((item) => item.id === id); + if (cloudStorageId !== -1) { + cloudStorages.splice(cloudStorageId); + } + } + + Object.defineProperties( this, Object.freeze({ @@ -384,6 +442,16 @@ class ServerProxy { getAnnotations, }, }, + + cloudStorages: { + value: Object.freeze({ + get: getCloudStorages, + update: updateCloudStorage, + create: createCloudStorage, + delete: deleteCloudStorage, + }), + writable: false, + }, }), ); } diff --git a/cvat-core/webpack.config.js b/cvat-core/webpack.config.js index 6fcc47f5..1234ee72 100644 --- a/cvat-core/webpack.config.js +++ b/cvat-core/webpack.config.js @@ -52,14 +52,7 @@ const webConfig = { use: { loader: 'babel-loader', options: { - presets: [ - [ - '@babel/preset-env', - { - targets: '> 2.5%', - }, - ], - ], + presets: ['@babel/preset-env'], sourceType: 'unambiguous', }, }, diff --git a/cvat-data/package-lock.json b/cvat-data/package-lock.json index 1f432adb..ebbc20b5 100644 --- a/cvat-data/package-lock.json +++ b/cvat-data/package-lock.json @@ -4832,9 +4832,9 @@ "dev": true }, "jszip": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.0.tgz", - "integrity": "sha512-Y2OlFIzrDOPWUnpU0LORIcDn2xN7rC9yKffFM/7pGhQuhO+SUhfm2trkJ/S5amjFvem0Y+1EALz/MEPkvHXVNw==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", + "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", "requires": { "lie": "~3.3.0", "pako": "~1.0.2", diff --git a/cvat-data/package.json b/cvat-data/package.json index 57244c52..390c133f 100644 --- a/cvat-data/package.json +++ b/cvat-data/package.json @@ -3,6 +3,19 @@ "version": "1.0.2", "description": "", "main": "src/js/cvat-data.js", + "scripts": { + "patch": "cd src/js && patch --dry-run --forward -p0 < 3rdparty_patch.diff >> /dev/null && patch -p0 < 3rdparty_patch.diff; true", + "build": "npm run patch; webpack --config ./webpack.config.js", + "server": "npm run patch; nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'" + }, + "author": "Intel", + "license": "MIT", + "browserslist": [ + "Chrome >= 63", + "Firefox > 58", + "not IE 11", + "> 2%" + ], "devDependencies": { "@babel/cli": "^7.13.16", "@babel/core": "^7.4.4", @@ -22,13 +35,6 @@ }, "dependencies": { "async-mutex": "^0.3.1", - "jszip": "3.7.0" - }, - "scripts": { - "patch": "cd src/js && patch --dry-run --forward -p0 < 3rdparty_patch.diff >> /dev/null && patch -p0 < 3rdparty_patch.diff; true", - "build": "npm run patch; webpack --config ./webpack.config.js", - "server": "npm run patch; nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'" - }, - "author": "Intel", - "license": "MIT" + "jszip": "3.7.1" + } } diff --git a/cvat-data/webpack.config.js b/cvat-data/webpack.config.js index a286cab0..1b90f2e0 100644 --- a/cvat-data/webpack.config.js +++ b/cvat-data/webpack.config.js @@ -29,14 +29,7 @@ const cvatData = { use: { loader: 'babel-loader', options: { - presets: [ - [ - '@babel/preset-env', - { - targets: '> 2.5%', // https://github.com/browserslist/browserslist - }, - ], - ], + presets: ['@babel/preset-env'], sourceType: 'unambiguous', }, }, diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index e1a4936e..f7934f42 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.21.1", + "version": "1.23.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1244,9 +1244,9 @@ "dev": true }, "@types/lodash": { - "version": "4.14.170", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.170.tgz", - "integrity": "sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==" + "version": "4.14.172", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz", + "integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==" }, "@types/minimatch": { "version": "3.0.3", @@ -1267,9 +1267,9 @@ "dev": true }, "@types/platform": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/platform/-/platform-1.3.3.tgz", - "integrity": "sha512-1fuOulBHWIxAPLBtLms+UtbeRDt6rL7gP5R+Yugfzdg+poCLxXqXTE8i+FpYeiytGRLUEtnFkjsY/j+usbQBqw==" + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/platform/-/platform-1.3.4.tgz", + "integrity": "sha512-U0o4K+GNiK0PNxoDwd8xRnvLVe4kzei6opn3/FCjAriqaP+rfrDdSl1kP/hLL6Y3/Y3hhGnBwD4dCkkAqs1W/Q==" }, "@types/prop-types": { "version": "15.7.4", @@ -1283,9 +1283,9 @@ "dev": true }, "@types/react": { - "version": "16.14.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.10.tgz", - "integrity": "sha512-QadBsMyF6ldjEAXEhsmEW/L0uBDJT8yw7Qoe5sRnEKVrzMkiYoJwqoL5TKJOlArsn/wvIJM/XdVzkdL6+AS64Q==", + "version": "16.14.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.12.tgz", + "integrity": "sha512-7nOJgNsRbARhZhvwPm7cnzahtzEi5VJ9OvcQk8ExEEb1t+zaFklwLVkJz7G1kfxX4X/mDa/icTmzE0vTmqsqBg==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1293,26 +1293,26 @@ } }, "@types/react-color": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.4.tgz", - "integrity": "sha512-EswbYJDF1kkrx93/YU+BbBtb46CCtDMvTiGmcOa/c5PETnwTiSWoseJ1oSWeRl/4rUXkhME9bVURvvPg0W5YQw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.5.tgz", + "integrity": "sha512-0VZy8Uq5x04cW5QFz24Qw8MMMlsMi8Bb+XG5h59ATqPnWVq6OheHtrwv5LeakdTRDaECQnExJNSFOsSe4Eo/zQ==", "requires": { "@types/react": "*", "@types/reactcss": "*" } }, "@types/react-dom": { - "version": "16.9.13", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.13.tgz", - "integrity": "sha512-34Hr3XnmUSJbUVDxIw/e7dhQn2BJZhJmlAaPyPwfTQyuVS9mV/CeyghFcXyvkJXxI7notQJz8mF8FeCVvloJrA==", + "version": "16.9.14", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.14.tgz", + "integrity": "sha512-FIX2AVmPTGP30OUJ+0vadeIFJJ07Mh1m+U0rxfgyW34p3rTlXI+nlenvAxNn4BP36YyI9IJ/+UJ7Wu22N1pI7A==", "requires": { "@types/react": "^16" } }, "@types/react-redux": { - "version": "7.1.16", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.16.tgz", - "integrity": "sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==", + "version": "7.1.18", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.18.tgz", + "integrity": "sha512-9iwAsPyJ9DLTRH+OFeIrm9cAbIj1i2ANL3sKQFATqnPWRbg+jEFXyZOKHiQK/N86pNRXbb4HRxAxo0SIX1XwzQ==", "requires": { "@types/hoist-non-react-statics": "^3.3.0", "@types/react": "*", @@ -1321,26 +1321,26 @@ } }, "@types/react-resizable": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-1.7.2.tgz", - "integrity": "sha512-6c6L94+VOksr9838LDrlYeucic2+0qkGnwolGE77YJztYHCWSucQV0e9+Qyl+uHpJTBRS95A5JESBg5NgCAC3A==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-1.7.3.tgz", + "integrity": "sha512-DAx+hdnHFMJHgl8geiKo3jLt1GCT838SwQixjCtbRRfqCBawAKriVLCZ1nvp7B/2Pxd94MWod8NyJEnAAmNHNA==", "requires": { "@types/react": "*" } }, "@types/react-router": { - "version": "5.1.15", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.15.tgz", - "integrity": "sha512-z3UlMG/x91SFEVmmvykk9FLTliDvfdIUky4k2rCfXWQ0NKbrP8o9BTCaCTPuYsB8gDkUnUmkcA2vYlm2DR+HAA==", + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz", + "integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==", "requires": { "@types/history": "*", "@types/react": "*" } }, "@types/react-router-dom": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.7.tgz", - "integrity": "sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.8.tgz", + "integrity": "sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw==", "requires": { "@types/history": "*", "@types/react": "*", @@ -1356,25 +1356,25 @@ } }, "@types/reactcss": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.3.tgz", - "integrity": "sha512-d2gQQ0IL6hXLnoRfVYZukQNWHuVsE75DzFTLPUuyyEhJS8G2VvlE+qfQQ91SJjaMqlURRCNIsX7Jcsw6cEuJlA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.4.tgz", + "integrity": "sha512-1rhVqteMSD6KQjO+dPBObE1OkKadw00HVJkG5WCYsyvMwGgdTZ530wF7Bkrg/4TWxB2AtINIzFotjW51eViw+w==", "requires": { "@types/react": "*" } }, "@types/redux-logger": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.8.tgz", - "integrity": "sha512-zM+cxiSw6nZtRbxpVp9SE3x/X77Z7e7YAfHD1NkxJyJbAGSXJGF0E9aqajZfPOa/sTYnuwutmlCldveExuCeLw==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.9.tgz", + "integrity": "sha512-cwYhVbYNgH01aepeMwhd0ABX6fhVB2rcQ9m80u8Fl50ZODhsZ8RhQArnLTkE7/Zrfq4Sz/taNoF7DQy9pCZSKg==", "requires": { "redux": "^4.0.0" } }, "@types/resize-observer-browser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.5.tgz", - "integrity": "sha512-8k/67Z95Goa6Lznuykxkfhq9YU3l1Qe6LNZmwde1u7802a3x8v44oq0j91DICclxatTr0rNnhXx7+VTIetSrSQ==" + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.6.tgz", + "integrity": "sha512-61IfTac0s9jvNtBCpyo86QeaN8qqpMGHdK0uGKCCIy2dt5/Yk84VduHIdWAcmkC5QvdkPL0p5eWYgUZtHKKUVg==" }, "@types/scheduler": { "version": "0.16.2", @@ -7664,8 +7664,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" + "is-core-module": "^2.2.0" } } } @@ -7759,8 +7758,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" + "is-core-module": "^2.2.0" } } } @@ -11289,11 +11287,6 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -12296,10 +12289,7 @@ "resolve": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", - "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", - "requires": { - "path-parse": "^1.0.6" - } + "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==" }, "resolve-cwd": { "version": "2.0.0", @@ -21051,11 +21041,6 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -22054,10 +22039,7 @@ "resolve": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", - "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", - "requires": { - "path-parse": "^1.0.6" - } + "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==" }, "resolve-cwd": { "version": "2.0.0", @@ -27255,8 +27237,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", "requires": { - "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" + "is-core-module": "^2.1.0" } }, "semver": { @@ -35054,11 +35035,6 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, "path-type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", @@ -35512,10 +35488,7 @@ "resolve": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", - "requires": { - "path-parse": "^1.0.6" - } + "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==" }, "resolve-cwd": { "version": "2.0.0", @@ -40741,8 +40714,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", "requires": { - "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" + "is-core-module": "^2.1.0" } }, "semver": { @@ -42211,8 +42183,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", "requires": { - "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" + "is-core-module": "^2.1.0" } }, "slash": { @@ -44212,11 +44183,6 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, "pbkdf2": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", @@ -44769,10 +44735,7 @@ "resolve": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "requires": { - "path-parse": "^1.0.6" - } + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==" }, "resolve-cwd": { "version": "2.0.0", @@ -52297,9 +52260,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "path-to-regexp": { @@ -54169,9 +54132,9 @@ } }, "react-cookie": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.0.3.tgz", - "integrity": "sha512-cmi6IpdVgTSvjqssqIEvo779Gfqc4uPGHRrKMEdHcqkmGtPmxolGfsyKj95bhdLEKqMdbX8MLBCwezlnhkHK0g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.1.1.tgz", + "integrity": "sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==", "requires": { "@types/hoist-non-react-statics": "^3.0.1", "hoist-non-react-statics": "^3.0.0", @@ -54481,25 +54444,25 @@ } }, "redux": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.0.tgz", - "integrity": "sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", + "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", "requires": { "@babel/runtime": "^7.9.2" }, "dependencies": { "@babel/runtime": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz", - "integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==", + "version": "7.15.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.3.tgz", + "integrity": "sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==", "requires": { "regenerator-runtime": "^0.13.4" } }, "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" } } }, diff --git a/cvat-ui/package.json b/cvat-ui/package.json index b3bb6360..5b347dd5 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.21.1", + "version": "1.23.1", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { @@ -11,6 +11,12 @@ "lint": "eslint './src/**/*.{ts,tsx}'", "lint:fix": "eslint './src/**/*.{ts,tsx}' --fix" }, + "browserslist": [ + "Chrome >= 63", + "Firefox > 58", + "not IE 11", + "> 2%" + ], "author": "Intel", "license": "MIT", "devDependencies": { @@ -49,18 +55,18 @@ }, "dependencies": { "@ant-design/icons": "^4.6.2", - "@types/lodash": "^4.14.170", - "@types/platform": "^1.3.3", - "@types/react": "^16.14.10", - "@types/react-color": "^3.0.4", - "@types/react-dom": "^16.9.13", - "@types/react-redux": "^7.1.16", - "@types/react-resizable": "^1.7.2", - "@types/react-router": "^5.1.15", - "@types/react-router-dom": "^5.1.7", + "@types/lodash": "^4.14.172", + "@types/platform": "^1.3.4", + "@types/react": "^16.14.12", + "@types/react-color": "^3.0.5", + "@types/react-dom": "^16.9.14", + "@types/react-redux": "^7.1.18", + "@types/react-resizable": "^1.7.3", + "@types/react-router": "^5.1.16", + "@types/react-router-dom": "^5.1.8", "@types/react-share": "^3.0.3", - "@types/redux-logger": "^3.0.8", - "@types/resize-observer-browser": "^0.1.5", + "@types/redux-logger": "^3.0.9", + "@types/resize-observer-browser": "^0.1.6", "antd": "^4.13.0", "copy-to-clipboard": "^3.3.1", "cvat-canvas": "file:../cvat-canvas", @@ -77,7 +83,7 @@ "react": "^16.14.0", "react-awesome-query-builder": "^3.0.0", "react-color": "^2.19.3", - "react-cookie": "^4.0.3", + "react-cookie": "^4.1.1", "react-dom": "^16.14.0", "react-moment": "^1.1.1", "react-redux": "^7.2.4", @@ -85,7 +91,7 @@ "react-router": "^5.1.0", "react-router-dom": "^5.1.0", "react-share": "^3.0.1", - "redux": "^4.1.0", + "redux": "^4.1.1", "redux-devtools-extension": "^2.13.9", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0" diff --git a/cvat-ui/postcss.config.js b/cvat-ui/postcss.config.js deleted file mode 100644 index 6493a9ea..00000000 --- a/cvat-ui/postcss.config.js +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (C) 2020 Intel Corporation -// -// SPDX-License-Identifier: MIT - -/* eslint-disable */ -module.exports = { - parser: false, - plugins: { - 'postcss-preset-env': { - browsers: '> 2.5%', // https://github.com/browserslist/browserslist - }, - }, -}; diff --git a/cvat-ui/src/actions/export-actions.ts b/cvat-ui/src/actions/export-actions.ts new file mode 100644 index 00000000..d5a2b801 --- /dev/null +++ b/cvat-ui/src/actions/export-actions.ts @@ -0,0 +1,49 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; + +export enum ExportActionTypes { + OPEN_EXPORT_MODAL = 'OPEN_EXPORT_MODAL', + CLOSE_EXPORT_MODAL = 'CLOSE_EXPORT_MODAL', + EXPORT_DATASET = 'EXPORT_DATASET', + EXPORT_DATASET_SUCCESS = 'EXPORT_DATASET_SUCCESS', + EXPORT_DATASET_FAILED = 'EXPORT_DATASET_FAILED', +} + +export const exportActions = { + openExportModal: (instance: any) => createAction(ExportActionTypes.OPEN_EXPORT_MODAL, { instance }), + closeExportModal: () => createAction(ExportActionTypes.CLOSE_EXPORT_MODAL), + exportDataset: (instance: any, format: string) => + createAction(ExportActionTypes.EXPORT_DATASET, { instance, format }), + exportDatasetSuccess: (instance: any, format: string) => + createAction(ExportActionTypes.EXPORT_DATASET_SUCCESS, { instance, format }), + exportDatasetFailed: (instance: any, format: string, error: any) => + createAction(ExportActionTypes.EXPORT_DATASET_FAILED, { + instance, + format, + error, + }), +}; + +export const exportDatasetAsync = ( + instance: any, + format: string, + name: string, + saveImages: boolean, +): ThunkAction => async (dispatch) => { + dispatch(exportActions.exportDataset(instance, format)); + + try { + const url = await instance.annotations.exportDataset(format, saveImages, name); + const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement; + downloadAnchor.href = url; + downloadAnchor.click(); + dispatch(exportActions.exportDatasetSuccess(instance, format)); + } catch (error) { + dispatch(exportActions.exportDatasetFailed(instance, format, error)); + } +}; + +export type ExportActions = ActionUnion; diff --git a/cvat-ui/src/actions/settings-actions.ts b/cvat-ui/src/actions/settings-actions.ts index 966743c4..6e9b5bfb 100644 --- a/cvat-ui/src/actions/settings-actions.ts +++ b/cvat-ui/src/actions/settings-actions.ts @@ -3,7 +3,9 @@ // SPDX-License-Identifier: MIT import { AnyAction } from 'redux'; -import { GridColor, ColorBy, SettingsState } from 'reducers/interfaces'; +import { + GridColor, ColorBy, SettingsState, ToolsBlockerState, +} from 'reducers/interfaces'; export enum SettingsActionTypes { SWITCH_ROTATE_ALL = 'SWITCH_ROTATE_ALL', @@ -34,6 +36,7 @@ export enum SettingsActionTypes { CHANGE_CANVAS_BACKGROUND_COLOR = 'CHANGE_CANVAS_BACKGROUND_COLOR', SWITCH_SETTINGS_DIALOG = 'SWITCH_SETTINGS_DIALOG', SET_SETTINGS = 'SET_SETTINGS', + SWITCH_TOOLS_BLOCKER_STATE = 'SWITCH_TOOLS_BLOCKER_STATE', } export function changeShapesOpacity(opacity: number): AnyAction { @@ -280,6 +283,15 @@ export function changeDefaultApproxPolyAccuracy(approxPolyAccuracy: number): Any }; } +export function switchToolsBlockerState(toolsBlockerState: ToolsBlockerState): AnyAction { + return { + type: SettingsActionTypes.SWITCH_TOOLS_BLOCKER_STATE, + payload: { + toolsBlockerState, + }, + }; +} + export function setSettings(settings: Partial): AnyAction { return { type: SettingsActionTypes.SET_SETTINGS, diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index c0fabf97..468bfe8c 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -18,12 +18,6 @@ export enum TasksActionTypes { LOAD_ANNOTATIONS = 'LOAD_ANNOTATIONS', LOAD_ANNOTATIONS_SUCCESS = 'LOAD_ANNOTATIONS_SUCCESS', LOAD_ANNOTATIONS_FAILED = 'LOAD_ANNOTATIONS_FAILED', - DUMP_ANNOTATIONS = 'DUMP_ANNOTATIONS', - DUMP_ANNOTATIONS_SUCCESS = 'DUMP_ANNOTATIONS_SUCCESS', - DUMP_ANNOTATIONS_FAILED = 'DUMP_ANNOTATIONS_FAILED', - EXPORT_DATASET = 'EXPORT_DATASET', - EXPORT_DATASET_SUCCESS = 'EXPORT_DATASET_SUCCESS', - EXPORT_DATASET_FAILED = 'EXPORT_DATASET_FAILED', DELETE_TASK = 'DELETE_TASK', DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS', DELETE_TASK_FAILED = 'DELETE_TASK_FAILED', @@ -108,60 +102,6 @@ export function getTasksAsync(query: TasksQuery): ThunkAction, {}, }; } -function dumpAnnotation(task: any, dumper: any): AnyAction { - const action = { - type: TasksActionTypes.DUMP_ANNOTATIONS, - payload: { - task, - dumper, - }, - }; - - return action; -} - -function dumpAnnotationSuccess(task: any, dumper: any): AnyAction { - const action = { - type: TasksActionTypes.DUMP_ANNOTATIONS_SUCCESS, - payload: { - task, - dumper, - }, - }; - - return action; -} - -function dumpAnnotationFailed(task: any, dumper: any, error: any): AnyAction { - const action = { - type: TasksActionTypes.DUMP_ANNOTATIONS_FAILED, - payload: { - task, - dumper, - error, - }, - }; - - return action; -} - -export function dumpAnnotationsAsync(task: any, dumper: any): ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - try { - dispatch(dumpAnnotation(task, dumper)); - const url = await task.annotations.dump(dumper); - const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement; - downloadAnchor.href = url; - downloadAnchor.click(); - } catch (error) { - dispatch(dumpAnnotationFailed(task, dumper, error)); - return; - } - - dispatch(dumpAnnotationSuccess(task, dumper)); - }; -} - function loadAnnotations(task: any, loader: any): AnyAction { const action = { type: TasksActionTypes.LOAD_ANNOTATIONS, @@ -263,60 +203,6 @@ export function importTaskAsync(file: File): ThunkAction, {}, {}, }; } -function exportDataset(task: any, exporter: any): AnyAction { - const action = { - type: TasksActionTypes.EXPORT_DATASET, - payload: { - task, - exporter, - }, - }; - - return action; -} - -function exportDatasetSuccess(task: any, exporter: any): AnyAction { - const action = { - type: TasksActionTypes.EXPORT_DATASET_SUCCESS, - payload: { - task, - exporter, - }, - }; - - return action; -} - -function exportDatasetFailed(task: any, exporter: any, error: any): AnyAction { - const action = { - type: TasksActionTypes.EXPORT_DATASET_FAILED, - payload: { - task, - exporter, - error, - }, - }; - - return action; -} - -export function exportDatasetAsync(task: any, exporter: any): ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - dispatch(exportDataset(task, exporter)); - - try { - const url = await task.annotations.exportDataset(exporter.name); - const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement; - downloadAnchor.href = url; - downloadAnchor.click(); - } catch (error) { - dispatch(exportDatasetFailed(task, exporter, error)); - } - - dispatch(exportDatasetSuccess(task, exporter)); - }; -} - function exportTask(taskID: number): AnyAction { const action = { type: TasksActionTypes.EXPORT_TASK, diff --git a/cvat-ui/src/components/actions-menu/actions-menu.tsx b/cvat-ui/src/components/actions-menu/actions-menu.tsx index aa4d2acf..d208ef68 100644 --- a/cvat-ui/src/components/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/components/actions-menu/actions-menu.tsx @@ -9,9 +9,7 @@ import Modal from 'antd/lib/modal'; import { LoadingOutlined } from '@ant-design/icons'; // eslint-disable-next-line import/no-extraneous-dependencies import { MenuInfo } from 'rc-menu/lib/interface'; -import DumpSubmenu from './dump-submenu'; import LoadSubmenu from './load-submenu'; -import ExportSubmenu from './export-submenu'; import { DimensionType } from '../../reducers/interfaces'; interface Props { @@ -21,8 +19,6 @@ interface Props { loaders: any[]; dumpers: any[]; loadActivity: string | null; - dumpActivities: string[] | null; - exportActivities: string[] | null; inferenceIsActive: boolean; taskDimension: DimensionType; onClickMenu: (params: MenuInfo, file?: File) => void; @@ -30,7 +26,6 @@ interface Props { } export enum Actions { - DUMP_TASK_ANNO = 'dump_task_anno', LOAD_TASK_ANNO = 'load_task_anno', EXPORT_TASK_DATASET = 'export_task_dataset', DELETE_TASK = 'delete_task', @@ -43,14 +38,10 @@ export enum Actions { export default function ActionsMenuComponent(props: Props): JSX.Element { const { taskID, - taskMode, bugTracker, inferenceIsActive, - dumpers, loaders, onClickMenu, - dumpActivities, - exportActivities, loadActivity, taskDimension, exportIsActive, @@ -106,13 +97,6 @@ export default function ActionsMenuComponent(props: Props): JSX.Element { return ( - {DumpSubmenu({ - taskMode, - dumpers, - dumpActivities, - menuKey: Actions.DUMP_TASK_ANNO, - taskDimension, - })} {LoadSubmenu({ loaders, loadActivity, @@ -122,19 +106,14 @@ export default function ActionsMenuComponent(props: Props): JSX.Element { menuKey: Actions.LOAD_TASK_ANNO, taskDimension, })} - {ExportSubmenu({ - exporters: dumpers, - exportActivities, - menuKey: Actions.EXPORT_TASK_DATASET, - taskDimension, - })} + Export task dataset {!!bugTracker && Open bug tracker} Automatic annotation {exportIsActive && } - Export Task + Export task
Move to project diff --git a/cvat-ui/src/components/actions-menu/dump-submenu.tsx b/cvat-ui/src/components/actions-menu/dump-submenu.tsx deleted file mode 100644 index 91721ac6..00000000 --- a/cvat-ui/src/components/actions-menu/dump-submenu.tsx +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (C) 2020 Intel Corporation -// -// SPDX-License-Identifier: MIT - -import React from 'react'; -import Menu from 'antd/lib/menu'; -import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons'; -import Text from 'antd/lib/typography/Text'; -import { DimensionType } from '../../reducers/interfaces'; - -function isDefaultFormat(dumperName: string, taskMode: string): boolean { - return ( - (dumperName === 'CVAT for video 1.1' && taskMode === 'interpolation') || - (dumperName === 'CVAT for images 1.1' && taskMode === 'annotation') - ); -} - -interface Props { - taskMode: string; - menuKey: string; - dumpers: any[]; - dumpActivities: string[] | null; - taskDimension: DimensionType; -} - -export default function DumpSubmenu(props: Props): JSX.Element { - const { - taskMode, menuKey, dumpers, dumpActivities, taskDimension, - } = props; - - return ( - - {dumpers - .sort((a: any, b: any) => a.name.localeCompare(b.name)) - .filter((dumper: any): boolean => dumper.dimension === taskDimension) - .map( - (dumper: any): JSX.Element => { - const pending = (dumpActivities || []).includes(dumper.name); - const disabled = !dumper.enabled || pending; - const isDefault = isDefaultFormat(dumper.name, taskMode); - return ( - - - - {dumper.name} - - {pending && } - - ); - }, - )} - - ); -} diff --git a/cvat-ui/src/components/actions-menu/export-submenu.tsx b/cvat-ui/src/components/actions-menu/export-submenu.tsx deleted file mode 100644 index 68356500..00000000 --- a/cvat-ui/src/components/actions-menu/export-submenu.tsx +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (C) 2020 Intel Corporation -// -// SPDX-License-Identifier: MIT - -import React from 'react'; -import Menu from 'antd/lib/menu'; -import Text from 'antd/lib/typography/Text'; -import { ExportOutlined, LoadingOutlined } from '@ant-design/icons'; -import { DimensionType } from '../../reducers/interfaces'; - -interface Props { - menuKey: string; - exporters: any[]; - exportActivities: string[] | null; - taskDimension: DimensionType; -} - -export default function ExportSubmenu(props: Props): JSX.Element { - const { - menuKey, exporters, exportActivities, taskDimension, - } = props; - - return ( - - {exporters - .sort((a: any, b: any) => a.name.localeCompare(b.name)) - .filter((exporter: any): boolean => exporter.dimension === taskDimension) - .map( - (exporter: any): JSX.Element => { - const pending = (exportActivities || []).includes(exporter.name); - const disabled = !exporter.enabled || pending; - return ( - - - {exporter.name} - {pending && } - - ); - }, - )} - - ); -} diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index 5bedffaa..973e8ceb 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -44,7 +44,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { saveLogs(); const root = window.document.getElementById('root'); if (root) { - root.style.minHeight = '600px'; + root.style.minHeight = '720px'; } return () => { diff --git a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx index fb21708f..1afcc0c8 100644 --- a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx @@ -106,6 +106,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { showObjectsTextAlways, workspace, showProjections, + selectedOpacity, } = this.props; const { canvasInstance } = this.props as { canvasInstance: Canvas }; @@ -121,6 +122,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { forceDisableEditing: workspace === Workspace.REVIEW_WORKSPACE, intelligentPolygonCrop, showProjections, + creationOpacity: selectedOpacity, }); this.initialSetup(); @@ -166,7 +168,8 @@ export default class CanvasWrapperComponent extends React.PureComponent { prevProps.showObjectsTextAlways !== showObjectsTextAlways || prevProps.automaticBordering !== automaticBordering || prevProps.showProjections !== showProjections || - prevProps.intelligentPolygonCrop !== intelligentPolygonCrop + prevProps.intelligentPolygonCrop !== intelligentPolygonCrop || + prevProps.selectedOpacity !== selectedOpacity ) { canvasInstance.configure({ undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE, @@ -174,6 +177,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { autoborders: automaticBordering, showProjections, intelligentPolygonCrop, + creationOpacity: selectedOpacity, }); } @@ -198,7 +202,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.activate(null); const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`); if (el) { - (el as any).instance.fill({ opacity: opacity / 100 }); + (el as any).instance.fill({ opacity }); } } @@ -214,7 +218,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { } if (gridPattern) { gridPattern.style.stroke = gridColor.toLowerCase(); - gridPattern.style.opacity = `${gridOpacity / 100}`; + gridPattern.style.opacity = `${gridOpacity}`; } } @@ -225,10 +229,8 @@ export default class CanvasWrapperComponent extends React.PureComponent { ) { const backgroundElement = window.document.getElementById('cvat_canvas_background'); if (backgroundElement) { - backgroundElement.style.filter = - `brightness(${brightnessLevel / 100})` + - `contrast(${contrastLevel / 100})` + - `saturate(${saturationLevel / 100})`; + const filter = `brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`; + backgroundElement.style.filter = filter; } } @@ -619,7 +621,7 @@ 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}`); } } } @@ -648,7 +650,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { handler.nested.fill({ color: shapeColor }); } - (shapeView as any).instance.fill({ color: shapeColor, opacity: opacity / 100 }); + (shapeView as any).instance.fill({ color: shapeColor, opacity }); (shapeView as any).instance.stroke({ color: outlined ? outlineColor : shapeColor }); } } @@ -710,17 +712,15 @@ export default class CanvasWrapperComponent extends React.PureComponent { } if (gridPattern) { gridPattern.style.stroke = gridColor.toLowerCase(); - gridPattern.style.opacity = `${gridOpacity / 100}`; + gridPattern.style.opacity = `${gridOpacity}`; } canvasInstance.grid(gridSize, gridSize); // Filters const backgroundElement = window.document.getElementById('cvat_canvas_background'); if (backgroundElement) { - backgroundElement.style.filter = - `brightness(${brightnessLevel / 100})` + - `contrast(${contrastLevel / 100})` + - `saturate(${saturationLevel / 100})`; + const filter = `brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`; + backgroundElement.style.filter = filter; } const canvasWrapperElement = window.document @@ -823,7 +823,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { - }> + }> diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/interactor-tooltips.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/interactor-tooltips.tsx new file mode 100644 index 00000000..6ee62ed1 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/interactor-tooltips.tsx @@ -0,0 +1,48 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import Image from 'antd/lib/image'; +import Paragraph from 'antd/lib/typography/Paragraph'; +import Text from 'antd/lib/typography/Text'; + +interface Props { + name?: string; + gif?: string; + message?: string; + withNegativePoints?: boolean; +} + +function InteractorTooltips(props: Props): JSX.Element { + const { + name, gif, message, withNegativePoints, + } = props; + const UNKNOWN_MESSAGE = 'Selected interactor does not have a help message'; + const desc = message || UNKNOWN_MESSAGE; + return ( +
+ {name ? ( + <> + {desc} + + You can prevent server requests holding + {' Ctrl '} + key + + + Positive points can be added by left-clicking the image. + {withNegativePoints ? ( + Negative points can be added by right-clicking the image. + ) : null} + + {gif ? Example gif : null} + + ) : ( + Select an interactor to see help message + )} +
+ ); +} + +export default React.memo(InteractorTooltips); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx index 2543a3ef..3afbecd8 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx @@ -19,7 +19,7 @@ import getCore from 'cvat-core-wrapper'; import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper'; import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors'; import { - CombinedState, ActiveControl, OpenCVTool, ObjectType, ShapeType, + CombinedState, ActiveControl, OpenCVTool, ObjectType, ShapeType, ToolsBlockerState, } from 'reducers/interfaces'; import { interactWithCanvas, @@ -34,6 +34,7 @@ import ApproximationAccuracy, { thresholdFromAccuracy, } from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy'; import { ImageProcessing } from 'utils/opencv-wrapper/opencv-interfaces'; +import { switchToolsBlockerState } from 'actions/settings-actions'; import withVisibilityHandling from './handle-popover-visibility'; interface Props { @@ -46,6 +47,8 @@ interface Props { curZOrder: number; defaultApproxPolyAccuracy: number; frameData: any; + toolsBlockerState: ToolsBlockerState; + activeControl: ActiveControl; } interface DispatchToProps { @@ -54,6 +57,7 @@ interface DispatchToProps { createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void; fetchAnnotations(): void; changeFrame(toFrame: number, fillBuffer?: boolean, frameStep?: number, forceUpdate?: boolean):void; + onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState):void; } interface State { @@ -87,12 +91,13 @@ function mapStateToProps(state: CombinedState): Props { }, }, settings: { - workspace: { defaultApproxPolyAccuracy }, + workspace: { defaultApproxPolyAccuracy, toolsBlockerState }, }, } = state; return { isActivated: activeControl === ActiveControl.OPENCV_TOOLS, + activeControl, canvasInstance: canvasInstance as Canvas, defaultApproxPolyAccuracy, jobInstance, @@ -101,6 +106,7 @@ function mapStateToProps(state: CombinedState): Props { states, frame, frameData, + toolsBlockerState, }; } @@ -110,6 +116,7 @@ const mapDispatchToProps = { fetchAnnotations: fetchAnnotationsAsync, createAnnotations: createAnnotationsAsync, changeFrame: changeFrameAsync, + onSwitchToolsBlockerState: switchToolsBlockerState, }; class OpenCVControlComponent extends React.PureComponent { @@ -142,7 +149,10 @@ class OpenCVControlComponent extends React.PureComponent => { const { approxPolyAccuracy } = this.state; const { - createAnnotations, isActivated, jobInstance, frame, labels, curZOrder, canvasInstance, + createAnnotations, isActivated, jobInstance, frame, labels, curZOrder, canvasInstance, toolsBlockerState, } = this.props; const { activeLabelID } = this.state; if (!isActivated || !this.activeTool) { @@ -191,27 +206,41 @@ class OpenCVControlComponent extends React.PureComponent 2) { + // disable approximation for lastest two points to disable fickering + const [x, y] = this.latestPoints.slice(-2); + this.latestPoints.splice(this.latestPoints.length - 2, 2); + points = openCVWrapper.contours.approxPoly( + this.latestPoints, + thresholdFromAccuracy(approxPolyAccuracy), + false, + ); + points.push([x, y]); + } else { + points = openCVWrapper.contours.approxPoly( + this.latestPoints, + thresholdFromAccuracy(approxPolyAccuracy), + false, + ); + } canvasInstance.interact({ enabled: true, intermediateShape: { shapeType: ShapeType.POLYGON, - points: approx.flat(), + points: points.flat(), }, }); } if (isDone) { // need to recalculate without the latest sliding point - const finalPoints = await this.runCVAlgorithm(pressedPoints, threshold); + const finalPoints = await this.runCVAlgorithm(pressedPoints, + toolsBlockerState.algorithmsLocked ? 0 : threshold); const finalObject = new core.classes.ObjectState({ frame, objectType: ObjectType.SHAPE, @@ -229,6 +258,22 @@ class OpenCVControlComponent extends React.PureComponent { + const { + isActivated, toolsBlockerState, onSwitchToolsBlockerState, canvasInstance, + } = this.props; + if (isActivated && event === 'keyup') { + onSwitchToolsBlockerState({ algorithmsLocked: !toolsBlockerState.algorithmsLocked }); + canvasInstance.interact({ + enabled: true, + crosshair: toolsBlockerState.algorithmsLocked, + enableThreshold: toolsBlockerState.algorithmsLocked, + onChangeToolsBlockerState: this.onChangeToolsBlockerState, }); } }; @@ -263,6 +308,7 @@ class OpenCVControlComponent extends React.PureComponent ) : null} + {includesToolsBlockerButton ? ( + + + + ) : null} ); } 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 5c2867b9..39742c3c 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 @@ -6,7 +6,9 @@ import React from 'react'; import Input from 'antd/lib/input'; import { Col, Row } from 'antd/lib/grid'; -import { ActiveControl, PredictorState, Workspace } from 'reducers/interfaces'; +import { + ActiveControl, PredictorState, ToolsBlockerState, Workspace, +} from 'reducers/interfaces'; import LeftGroup from './left-group'; import PlayerButtons from './player-buttons'; import PlayerNavigation from './player-navigation'; @@ -28,6 +30,7 @@ interface Props { undoShortcut: string; redoShortcut: string; drawShortcut: string; + switchToolsBlockerShortcut: string; playPauseShortcut: string; nextFrameShortcut: string; previousFrameShortcut: string; @@ -39,6 +42,7 @@ interface Props { predictor: PredictorState; isTrainingActive: boolean; activeControl: ActiveControl; + toolsBlockerState: ToolsBlockerState; changeWorkspace(workspace: Workspace): void; switchPredictor(predictorEnabled: boolean): void; showStatistics(): void; @@ -59,6 +63,7 @@ interface Props { onUndoClick(): void; onRedoClick(): void; onFinishDraw(): void; + onSwitchToolsBlockerState(): void; jobInstance: any; } @@ -79,6 +84,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { undoShortcut, redoShortcut, drawShortcut, + switchToolsBlockerShortcut, playPauseShortcut, nextFrameShortcut, previousFrameShortcut, @@ -89,6 +95,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { predictor, focusFrameInputShortcut, activeControl, + toolsBlockerState, showStatistics, switchPredictor, showFilters, @@ -109,6 +116,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { onUndoClick, onRedoClick, onFinishDraw, + onSwitchToolsBlockerState, jobInstance, isTrainingActive, } = props; @@ -125,10 +133,13 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { redoShortcut={redoShortcut} activeControl={activeControl} drawShortcut={drawShortcut} + switchToolsBlockerShortcut={switchToolsBlockerShortcut} + toolsBlockerState={toolsBlockerState} onSaveAnnotation={onSaveAnnotation} onUndoClick={onUndoClick} onRedoClick={onRedoClick} onFinishDraw={onFinishDraw} + onSwitchToolsBlockerState={onSwitchToolsBlockerState} /> diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index ae46e76c..2b82934d 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -33,7 +33,11 @@ import getCore from 'cvat-core-wrapper'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import { NotificationsState } from 'reducers/interfaces'; import { customWaViewHit } from 'utils/enviroment'; -import showPlatformNotification, { platformInfo, stopNotifications } from 'utils/platform-checker'; +import showPlatformNotification, { + platformInfo, + stopNotifications, + showUnsupportedNotification, +} from 'utils/platform-checker'; import '../styles.scss'; interface CVATAppProps { @@ -87,6 +91,50 @@ class CVATApplication extends React.PureComponent + + + + {`The browser you are using is ${name} ${version} based on ${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 ${os}`} + + + + ), + onOk: () => stopNotifications(true), + }); + } else if (showUnsupportedNotification()) { + stopNotifications(false); + Modal.warning({ + title: 'Unsupported features detected', + className: 'cvat-modal-unsupported-features-warning', + content: ( + + {`${name} v${version} does not support API, which is used by CVAT. `} + It is strongly recommended to update your browser. + + ), + onOk: () => stopNotifications(true), + }); + } } public componentDidUpdate(): void { @@ -269,37 +317,6 @@ class CVATApplication extends React.PureComponent - - - - {`The browser you are using is ${name} ${version} based on ${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 ${os}`} - - - - ), - onOk: () => stopNotifications(true), - }); - } - if (readyForRender) { if (user && user.isVerified) { return ( diff --git a/cvat-ui/src/components/export-dataset/export-dataset-modal.tsx b/cvat-ui/src/components/export-dataset/export-dataset-modal.tsx new file mode 100644 index 00000000..8ae93059 --- /dev/null +++ b/cvat-ui/src/components/export-dataset/export-dataset-modal.tsx @@ -0,0 +1,150 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React, { useState, useEffect, useCallback } from 'react'; +import Modal from 'antd/lib/modal'; +import Notification from 'antd/lib/notification'; +import { useSelector, useDispatch } from 'react-redux'; +import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons'; +import Text from 'antd/lib/typography/Text'; +import Select from 'antd/lib/select'; +import Checkbox from 'antd/lib/checkbox'; +import Input from 'antd/lib/input'; +import Form from 'antd/lib/form'; + +import { CombinedState } from 'reducers/interfaces'; +import { exportActions, exportDatasetAsync } from 'actions/export-actions'; +import getCore from 'cvat-core-wrapper'; + +const core = getCore(); + +type FormValues = { + selectedFormat: string | undefined; + saveImages: boolean; + customName: string | undefined; +}; + +function ExportDatasetModal(): JSX.Element { + const [instanceType, setInstanceType] = useState(''); + const [activities, setActivities] = useState([]); + const [form] = Form.useForm(); + const dispatch = useDispatch(); + const instance = useSelector((state: CombinedState) => state.export.instance); + const modalVisible = useSelector((state: CombinedState) => state.export.modalVisible); + const dumpers = useSelector((state: CombinedState) => state.formats.annotationFormats.dumpers); + const { tasks: taskExportActivities, projects: projectExportActivities } = useSelector( + (state: CombinedState) => state.export, + ); + + const initActivities = (): void => { + if (instance instanceof core.classes.Project) { + setInstanceType('project'); + setActivities(projectExportActivities[instance.id] || []); + } else if (instance instanceof core.classes.Task) { + setInstanceType('task'); + setActivities(taskExportActivities[instance.id] || []); + if (instance.mode === 'interpolation' && instance.dimension === '2d') { + form.setFieldsValue({ selectedFormat: 'CVAT for video 1.1' }); + } else if (instance.mode === 'annotation' && instance.dimension === '2d') { + form.setFieldsValue({ selectedFormat: 'CVAT for images 1.1' }); + } + } + }; + + useEffect(() => { + initActivities(); + }, [instance?.id, instance instanceof core.classes.Project]); + + const closeModal = (): void => { + form.resetFields(); + dispatch(exportActions.closeExportModal()); + }; + + const handleExport = useCallback( + (values: FormValues): void => { + // have to validate format before so it would not be undefined + dispatch( + exportDatasetAsync( + instance, + values.selectedFormat as string, + values.customName ? `${values.customName}.zip` : '', + values.saveImages, + ), + ); + closeModal(); + Notification.info({ + message: 'Dataset export started', + description: + `Dataset export was started for ${instanceType} #${instance?.id}. ` + + 'Download will start automaticly as soon as the dataset is ready.', + className: `cvat-notification-notice-export-${instanceType}-start`, + }); + }, + [instance?.id, instance instanceof core.classes.Project, instanceType], + ); + + return ( + form.submit()} + className={`cvat-modal-export-${instanceType}`} + > +
+ + + + + Save images + + + + +
+
+ ); +} + +export default React.memo(ExportDatasetModal); diff --git a/cvat-ui/src/components/export-dataset/styles.scss b/cvat-ui/src/components/export-dataset/styles.scss new file mode 100644 index 00000000..26946bd0 --- /dev/null +++ b/cvat-ui/src/components/export-dataset/styles.scss @@ -0,0 +1,13 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +@import '../../base.scss'; + +.cvat-modal-export-option-item > .ant-select-item-option-content, +.cvat-modal-export-select .ant-select-selection-item { + > span[role='img'] { + color: $info-icon-color; + margin-right: $grid-unit-size; + } +} diff --git a/cvat-ui/src/components/login-page/cookie-policy-drawer.tsx b/cvat-ui/src/components/login-page/cookie-policy-drawer.tsx deleted file mode 100644 index f1c6238b..00000000 --- a/cvat-ui/src/components/login-page/cookie-policy-drawer.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (C) 2020 Intel Corporation -// -// SPDX-License-Identifier: MIT - -import React, { useState, useEffect } from 'react'; -import Drawer from 'antd/lib/drawer'; -import Paragraph from 'antd/lib/typography/Paragraph'; -import Button from 'antd/lib/button/button'; - -import { isPublic } from 'utils/enviroment'; - -function CookieDrawer(): JSX.Element { - const [drawerVisible, setDrawerVisible] = useState(false); - - useEffect(() => { - const cookiePolicyAccepted = localStorage.getItem('cookiePolicyAccepted'); - if (cookiePolicyAccepted === null && isPublic()) { - setDrawerVisible(true); - } - }, []); - - const onClose = (): void => { - localStorage.setItem('cookiePolicyAccepted', 'true'); - setDrawerVisible(false); - }; - - return ( - - - This site uses cookies for functionality, analytics, and advertising purposes as described in our Cookie - and Similar Technologies Notice. To see what cookies we serve and set your preferences, please visit our -
Cookie Consent Tool. By continuing to use our website, you - agree to our use of cookies. - - - - ); -} - -export default CookieDrawer; diff --git a/cvat-ui/src/components/login-page/intel-footer-drawer.tsx b/cvat-ui/src/components/login-page/intel-footer-drawer.tsx new file mode 100644 index 00000000..2d14e457 --- /dev/null +++ b/cvat-ui/src/components/login-page/intel-footer-drawer.tsx @@ -0,0 +1,27 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { Layout } from 'antd'; + +import { isPublic } from 'utils/enviroment'; +import consts from 'consts'; + +function FooterDrawer(): JSX.Element | null { + const { Footer } = Layout; + const { INTEL_TERMS_OF_USE_URL, INTEL_COOKIES_URL, INTEL_PRIVACY_URL } = consts; + + return isPublic() ? ( + + ) : null; +} + +export default React.memo(FooterDrawer); diff --git a/cvat-ui/src/components/login-page/login-page.tsx b/cvat-ui/src/components/login-page/login-page.tsx index 7fb3daa5..66505c7f 100644 --- a/cvat-ui/src/components/login-page/login-page.tsx +++ b/cvat-ui/src/components/login-page/login-page.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -8,9 +8,11 @@ import { Link, withRouter } from 'react-router-dom'; import Title from 'antd/lib/typography/Title'; import Text from 'antd/lib/typography/Text'; import { Row, Col } from 'antd/lib/grid'; +import { Layout } from 'antd'; + +import FooterDrawer from 'components/login-page/intel-footer-drawer'; import LoginForm, { LoginData } from './login-form'; -import CookieDrawer from './cookie-policy-drawer'; interface LoginPageComponentProps { fetching: boolean; @@ -27,40 +29,44 @@ function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps xl: { span: 4 }, }; + const { Content } = Layout; + const { fetching, onLogin, renderResetPassword } = props; return ( - <> - - - Login - { - onLogin(loginData.username, loginData.password); - }} - /> - - - - New to CVAT? Create - an account - - - - {renderResetPassword && ( + + + + + Login + { + onLogin(loginData.username, loginData.password); + }} + /> - Forgot your password? + New to CVAT? Create + an account - )} - - - - + {renderResetPassword && ( + + + + Forgot your password? + + + + )} + + + + + ); } diff --git a/cvat-ui/src/components/models-page/styles.scss b/cvat-ui/src/components/models-page/styles.scss index ddcaef8a..5d93e670 100644 --- a/cvat-ui/src/components/models-page/styles.scss +++ b/cvat-ui/src/components/models-page/styles.scss @@ -5,7 +5,8 @@ @import '../../base.scss'; .cvat-models-page { - padding-top: 30px; + padding-top: $grid-unit-size * 2; + padding-bottom: $grid-unit-size; height: 90%; overflow: auto; position: fixed; diff --git a/cvat-ui/src/components/move-task-modal/move-task-modal.tsx b/cvat-ui/src/components/move-task-modal/move-task-modal.tsx index fe356899..18a78d79 100644 --- a/cvat-ui/src/components/move-task-modal/move-task-modal.tsx +++ b/cvat-ui/src/components/move-task-modal/move-task-modal.tsx @@ -34,8 +34,8 @@ export default function MoveTaskModal(): JSX.Element { const [labelMap, setLabelMap] = useState<{ [key: string]: LabelMapperItemValue }>({}); const initValues = (): void => { + const labelValues: { [key: string]: LabelMapperItemValue } = {}; if (task) { - const labelValues: { [key: string]: LabelMapperItemValue } = {}; task.labels.forEach((label: any) => { labelValues[label.id] = { labelId: label.id, @@ -43,8 +43,8 @@ export default function MoveTaskModal(): JSX.Element { clearAttributes: true, }; }); - setLabelMap(labelValues); } + setLabelMap(labelValues); }; const onCancel = (): void => { diff --git a/cvat-ui/src/components/project-page/project-page.tsx b/cvat-ui/src/components/project-page/project-page.tsx index 385d58c5..b2bb7a62 100644 --- a/cvat-ui/src/components/project-page/project-page.tsx +++ b/cvat-ui/src/components/project-page/project-page.tsx @@ -16,7 +16,10 @@ import { PlusOutlined } from '@ant-design/icons'; import { CombinedState, Task } from 'reducers/interfaces'; import { getProjectsAsync } from 'actions/projects-actions'; import { cancelInferenceAsync } from 'actions/models-actions'; +import ExportDatasetModal from 'components/export-dataset/export-dataset-modal'; import TaskItem from 'components/tasks-page/task-item'; +import MoveTaskModal from 'components/move-task-modal/move-task-modal'; +import ModelRunnerDialog from 'components/model-runner-modal/model-runner-dialog'; import DetailsComponent from './details'; import ProjectTopBar from './top-bar'; @@ -109,6 +112,9 @@ export default function ProjectPageComponent(): JSX.Element { ))} + + + ); } diff --git a/cvat-ui/src/components/project-page/top-bar.tsx b/cvat-ui/src/components/project-page/top-bar.tsx index dde03ecb..aa5720a7 100644 --- a/cvat-ui/src/components/project-page/top-bar.tsx +++ b/cvat-ui/src/components/project-page/top-bar.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-ui/src/components/projects-page/actions-menu.tsx b/cvat-ui/src/components/projects-page/actions-menu.tsx index 35fefea5..c85684ad 100644 --- a/cvat-ui/src/components/projects-page/actions-menu.tsx +++ b/cvat-ui/src/components/projects-page/actions-menu.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -8,6 +8,7 @@ import Modal from 'antd/lib/modal'; import Menu from 'antd/lib/menu'; import { deleteProjectAsync } from 'actions/projects-actions'; +import { exportActions } from 'actions/export-actions'; interface Props { projectInstance: any; @@ -36,6 +37,12 @@ export default function ProjectActionsMenuComponent(props: Props): JSX.Element { return ( + dispatch(exportActions.openExportModal(projectInstance))} + > + Export project dataset + +
Delete
); diff --git a/cvat-ui/src/components/projects-page/project-item.tsx b/cvat-ui/src/components/projects-page/project-item.tsx index 2ca8523f..d2f6a9e7 100644 --- a/cvat-ui/src/components/projects-page/project-item.tsx +++ b/cvat-ui/src/components/projects-page/project-item.tsx @@ -15,18 +15,27 @@ import Button from 'antd/lib/button'; import { MoreOutlined } from '@ant-design/icons'; import { CombinedState, Project } from 'reducers/interfaces'; +import { useCardHeightHOC } from 'utils/hooks'; import ProjectActionsMenuComponent from './actions-menu'; interface Props { projectInstance: Project; } +const useCardHeight = useCardHeightHOC({ + containerClassName: 'cvat-projects-page', + siblingClassNames: ['cvat-projects-pagination', 'cvat-projects-top-bar'], + paddings: 40, + numberOfRows: 3, +}); + export default function ProjectItemComponent(props: Props): JSX.Element { const { projectInstance: { instance, preview }, } = props; const history = useHistory(); + const height = useCardHeight(); const ownerName = instance.owner ? instance.owner.username : null; const updated = moment(instance.updatedDate).fromNow(); const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes); @@ -36,8 +45,7 @@ export default function ProjectItemComponent(props: Props): JSX.Element { history.push(`/projects/${instance.id}`); }; - const style: React.CSSProperties = {}; - + const style: React.CSSProperties = { height }; if (deleted) { style.pointerEvents = 'none'; style.opacity = 0.5; diff --git a/cvat-ui/src/components/projects-page/project-list.tsx b/cvat-ui/src/components/projects-page/project-list.tsx index 494b501f..79685975 100644 --- a/cvat-ui/src/components/projects-page/project-list.tsx +++ b/cvat-ui/src/components/projects-page/project-list.tsx @@ -15,7 +15,7 @@ export default function ProjectListComponent(): JSX.Element { const dispatch = useDispatch(); const projectsCount = useSelector((state: CombinedState) => state.projects.count); const { page } = useSelector((state: CombinedState) => state.projects.gettingQuery); - const projectInstances = useSelector((state: CombinedState) => state.projects.current); + const projects = useSelector((state: CombinedState) => state.projects.current); const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery); function changePage(p: number): void { @@ -27,28 +27,13 @@ export default function ProjectListComponent(): JSX.Element { ); } - const projects = projectInstances.reduce((rows, key, index) => { - if (index % 4 === 0) { - rows.push([key]); - } else { - rows[rows.length - 1].push(key); - } - return rows; - }, []); - return ( <> {projects.map( - (row: Project[]): JSX.Element => ( - - {row.map((project: Project) => ( - - - - ))} - + (project: Project): JSX.Element => ( + ), )} diff --git a/cvat-ui/src/components/projects-page/projects-page.tsx b/cvat-ui/src/components/projects-page/projects-page.tsx index 5d4d129a..a2842348 100644 --- a/cvat-ui/src/components/projects-page/projects-page.tsx +++ b/cvat-ui/src/components/projects-page/projects-page.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -8,9 +8,10 @@ import { useDispatch, useSelector } from 'react-redux'; import { useLocation, useHistory } from 'react-router'; import Spin from 'antd/lib/spin'; -import FeedbackComponent from 'components/feedback/feedback'; import { CombinedState, ProjectsQuery } from 'reducers/interfaces'; import { getProjectsAsync } from 'actions/projects-actions'; +import FeedbackComponent from 'components/feedback/feedback'; +import ExportDatasetModal from 'components/export-dataset/export-dataset-modal'; import EmptyListComponent from './empty-list'; import TopBarComponent from './top-bar'; import ProjectListComponent from './project-list'; @@ -55,6 +56,7 @@ export default function ProjectsPageComponent(): JSX.Element { {projectsCount ? : } + ); } diff --git a/cvat-ui/src/components/projects-page/styles.scss b/cvat-ui/src/components/projects-page/styles.scss index dd60013d..f665de5a 100644 --- a/cvat-ui/src/components/projects-page/styles.scss +++ b/cvat-ui/src/components/projects-page/styles.scss @@ -6,9 +6,8 @@ .cvat-projects-page { padding-top: $grid-unit-size * 2; - padding-bottom: $grid-unit-size * 5; + padding-bottom: $grid-unit-size; height: 100%; - position: fixed; width: 100%; > div:nth-child(1) { @@ -71,28 +70,6 @@ justify-content: center; } -.cvat-projects-project-item-title, -.cvat-projects-project-item-card-preview { - cursor: pointer; -} - -.cvat-porjects-project-item-description { - display: flex; - justify-content: space-between; - - // actions button - > div:nth-child(2) { - display: flex; - align-self: flex-end; - justify-content: center; - - > button { - color: $text-color; - width: inherit; - } - } -} - .ant-menu.cvat-project-actions-menu { box-shadow: 0 0 17px rgba(0, 0, 0, 0.2); @@ -107,17 +84,62 @@ } .cvat-projects-project-item-card { - .ant-empty { - margin: $grid-unit-size; - height: $grid-unit-size * 16; - } + .cvat-projects-project-item-card-preview { + .ant-empty { + margin: $grid-unit-size; + height: inherit; + display: grid; + + > div:first-child { + margin: auto; + } + } - img { - height: $grid-unit-size * 18; + height: 100%; + display: flex; + align-items: center; + justify-content: space-around; object-fit: cover; + cursor: pointer; } + + .cvat-projects-project-item-title { + cursor: pointer; + } + + .cvat-porjects-project-item-description { + display: flex; + justify-content: space-between; + + // actions button + > div:nth-child(2) { + display: flex; + align-self: flex-end; + justify-content: center; + + > button { + color: $text-color; + width: inherit; + } + } + } + + .ant-card-cover { + flex: 1; + height: 0; + } + + width: 25%; + border-width: 4px; + display: flex; + flex-direction: column; } .cvat-project-list-content { padding-bottom: $grid-unit-size; } + +.cvat-projects-list { + display: flex; + flex-wrap: wrap; +} diff --git a/cvat-ui/src/components/register-page/register-page.tsx b/cvat-ui/src/components/register-page/register-page.tsx index b3a299a5..8776bee6 100644 --- a/cvat-ui/src/components/register-page/register-page.tsx +++ b/cvat-ui/src/components/register-page/register-page.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -9,9 +9,10 @@ import { Link, withRouter } from 'react-router-dom'; import Title from 'antd/lib/typography/Title'; import Text from 'antd/lib/typography/Text'; import { Row, Col } from 'antd/lib/grid'; +import { Layout } from 'antd'; import { UserAgreement } from 'reducers/interfaces'; -import CookieDrawer from 'components/login-page/cookie-policy-drawer'; +import FooterDrawer from 'components/login-page/intel-footer-drawer'; import RegisterForm, { RegisterData, UserConfirmation } from './register-form'; interface RegisterPageComponentProps { @@ -38,39 +39,42 @@ function RegisterPageComponent(props: RegisterPageComponentProps & RouteComponen }; const { fetching, userAgreements, onRegister } = props; + const { Content } = Layout; return ( - <> - - - Create an account - { - onRegister( - registerData.username, - registerData.firstName, - registerData.lastName, - registerData.email, - registerData.password1, - registerData.password2, - registerData.confirmations, - ); - }} - /> - - - - Already have an account? - Login - - - - - - - + + + + + Create an account + { + onRegister( + registerData.username, + registerData.firstName, + registerData.lastName, + registerData.email, + registerData.password1, + registerData.password2, + registerData.confirmations, + ); + }} + /> + + + + Already have an account? + Login + + + + + + + + ); } 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 index 24c4735b..723d78d0 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -6,10 +6,12 @@ import React from 'react'; import { connect } from 'react-redux'; import Title from 'antd/lib/typography/Title'; import { Row, Col } from 'antd/lib/grid'; +import { Layout } from 'antd'; import { CombinedState } from 'reducers/interfaces'; import { resetPasswordAsync } from 'actions/auth-actions'; +import FooterDrawer from 'components/login-page/intel-footer-drawer'; import ResetPasswordConfirmForm, { ResetPasswordConfirmData } from './reset-password-confirm-form'; interface StateToProps { @@ -46,23 +48,30 @@ function ResetPasswordPagePageComponent(props: ResetPasswordConfirmPageComponent const { fetching, onResetPasswordConfirm } = props; + const { Content } = Layout; + return ( - - - Change password - { - onResetPasswordConfirm( - resetPasswordConfirmData.newPassword1, - resetPasswordConfirmData.newPassword2, - resetPasswordConfirmData.uid, - resetPasswordConfirmData.token, - ); - }} - /> - - + + + + + Change password + { + onResetPasswordConfirm( + resetPasswordConfirmData.newPassword1, + resetPasswordConfirmData.newPassword2, + resetPasswordConfirmData.uid, + resetPasswordConfirmData.token, + ); + }} + /> + + + + + ); } 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 index f02b5a75..3b343865 100644 --- a/cvat-ui/src/components/reset-password-page/reset-password-page.tsx +++ b/cvat-ui/src/components/reset-password-page/reset-password-page.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -8,9 +8,11 @@ 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 { Layout } from 'antd'; import { requestPasswordResetAsync } from 'actions/auth-actions'; import { CombinedState } from 'reducers/interfaces'; +import FooterDrawer from 'components/login-page/intel-footer-drawer'; import ResetPasswordForm, { ResetPasswordData } from './reset-password-form'; interface StateToProps { @@ -46,27 +48,33 @@ function ResetPasswordPagePageComponent(props: ResetPasswordPageComponentProps): }; const { fetching, onResetPassword } = props; + const { Content } = Layout; return ( - - - Reset password - { - onResetPassword(resetPasswordData.email); - }} - /> - - - - Go to - login page - + + + + + Reset password + { + onResetPassword(resetPasswordData.email); + }} + /> + + + + Go to + login page + + + - - + + + ); } diff --git a/cvat-ui/src/components/task-page/styles.scss b/cvat-ui/src/components/task-page/styles.scss index 1a381e89..3b6d6a84 100644 --- a/cvat-ui/src/components/task-page/styles.scss +++ b/cvat-ui/src/components/task-page/styles.scss @@ -88,9 +88,7 @@ .cvat-task-preview-wrapper { overflow: hidden; margin-bottom: 20px; - width: $grid-unit-size * 32; height: $grid-unit-size * 18; - display: table-cell; text-align: center; vertical-align: middle; background-color: $background-color-2; @@ -98,7 +96,6 @@ .cvat-task-parameters { margin-top: $grid-unit-size * 2; - width: $grid-unit-size * 32; } .cvat-open-bug-tracker-button { diff --git a/cvat-ui/src/components/task-page/task-page.tsx b/cvat-ui/src/components/task-page/task-page.tsx index 627dbf9d..0465fa95 100644 --- a/cvat-ui/src/components/task-page/task-page.tsx +++ b/cvat-ui/src/components/task-page/task-page.tsx @@ -14,6 +14,7 @@ import DetailsContainer from 'containers/task-page/details'; import JobListContainer from 'containers/task-page/job-list'; import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog'; import MoveTaskModal from 'components/move-task-modal/move-task-modal'; +import ExportDatasetModal from 'components/export-dataset/export-dataset-modal'; import { Task } from 'reducers/interfaces'; import TopBarComponent from './top-bar'; @@ -85,6 +86,7 @@ class TaskPageComponent extends React.PureComponent { + {updating && } ); diff --git a/cvat-ui/src/components/tasks-page/styles.scss b/cvat-ui/src/components/tasks-page/styles.scss index 2c6dce8c..9ce429cc 100644 --- a/cvat-ui/src/components/tasks-page/styles.scss +++ b/cvat-ui/src/components/tasks-page/styles.scss @@ -6,8 +6,8 @@ @import '../../styles.scss'; .cvat-tasks-page { - padding-top: 15px; - padding-bottom: 40px; + padding-top: $grid-unit-size * 2; + padding-bottom: $grid-unit-size; height: 100%; width: 100%; @@ -33,6 +33,18 @@ padding-top: 10px; } + @media screen and (min-height: 900px) { + > div:nth-child(2) { + height: 88%; + } + } + + @media screen and (min-height: 1200px) { + > div:nth-child(2) { + height: 93%; + } + } + > div:nth-child(3) { padding-top: 10px; } diff --git a/cvat-ui/src/components/tasks-page/tasks-page.tsx b/cvat-ui/src/components/tasks-page/tasks-page.tsx index 6e0df457..1420822b 100644 --- a/cvat-ui/src/components/tasks-page/tasks-page.tsx +++ b/cvat-ui/src/components/tasks-page/tasks-page.tsx @@ -14,6 +14,7 @@ import Text from 'antd/lib/typography/Text'; import { TasksQuery } from 'reducers/interfaces'; import FeedbackComponent from 'components/feedback/feedback'; import TaskListContainer from 'containers/tasks-page/tasks-list'; +import ExportDatasetModal from 'components/export-dataset/export-dataset-modal'; import TopBar from './top-bar'; import EmptyListComponent from './empty-list'; @@ -221,6 +222,7 @@ class TasksPageComponent extends React.PureComponent )} + ); } diff --git a/cvat-ui/src/consts.ts b/cvat-ui/src/consts.ts index e5df214c..a062480e 100644 --- a/cvat-ui/src/consts.ts +++ b/cvat-ui/src/consts.ts @@ -22,6 +22,9 @@ const LATEST_COMMENTS_SHOWN_QUICK_ISSUE = 3; const QUICK_ISSUE_INCORRECT_POSITION_TEXT = 'Wrong position'; const QUICK_ISSUE_INCORRECT_ATTRIBUTE_TEXT = 'Wrong attribute'; const DEFAULT_PROJECT_SUBSETS = ['Train', 'Test', 'Validation']; +const INTEL_TERMS_OF_USE_URL = 'https://www.intel.com/content/www/us/en/legal/terms-of-use.html'; +const INTEL_COOKIES_URL = 'https://www.intel.com/content/www/us/en/privacy/intel-cookie-notice.html'; +const INTEL_PRIVACY_URL = 'https://www.intel.com/content/www/us/en/privacy/intel-privacy-notice.html'; export default { UNDEFINED_ATTRIBUTE_VALUE, @@ -41,4 +44,7 @@ export default { QUICK_ISSUE_INCORRECT_POSITION_TEXT, QUICK_ISSUE_INCORRECT_ATTRIBUTE_TEXT, DEFAULT_PROJECT_SUBSETS, + INTEL_TERMS_OF_USE_URL, + INTEL_COOKIES_URL, + INTEL_PRIVACY_URL, }; diff --git a/cvat-ui/src/containers/actions-menu/actions-menu.tsx b/cvat-ui/src/containers/actions-menu/actions-menu.tsx index 5923928c..5c38825c 100644 --- a/cvat-ui/src/containers/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/containers/actions-menu/actions-menu.tsx @@ -12,13 +12,12 @@ import { CombinedState } from 'reducers/interfaces'; import { modelsActions } from 'actions/models-actions'; import { - dumpAnnotationsAsync, loadAnnotationsAsync, - exportDatasetAsync, deleteTaskAsync, exportTaskAsync, switchMoveTaskModalVisible, } from 'actions/tasks-actions'; +import { exportActions } from 'actions/export-actions'; interface OwnProps { taskInstance: any; @@ -27,16 +26,13 @@ interface OwnProps { interface StateToProps { annotationFormats: any; loadActivity: string | null; - dumpActivities: string[] | null; - exportActivities: string[] | null; inferenceIsActive: boolean; exportIsActive: boolean; } interface DispatchToProps { loadAnnotations: (taskInstance: any, loader: any, file: File) => void; - dumpAnnotations: (taskInstance: any, dumper: any) => void; - exportDataset: (taskInstance: any, exporter: any) => void; + showExportModal: (taskInstance: any) => void; deleteTask: (taskInstance: any) => void; openRunModelWindow: (taskInstance: any) => void; exportTask: (taskInstance: any) => void; @@ -52,14 +48,12 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { formats: { annotationFormats }, tasks: { activities: { - dumps, loads, exports: activeExports, backups, + loads, backups, }, }, } = state; return { - dumpActivities: tid in dumps ? dumps[tid] : null, - exportActivities: tid in activeExports ? activeExports[tid] : null, loadActivity: tid in loads ? loads[tid] : null, annotationFormats, inferenceIsActive: tid in state.models.inferences, @@ -72,11 +66,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { loadAnnotations: (taskInstance: any, loader: any, file: File): void => { dispatch(loadAnnotationsAsync(taskInstance, loader, file)); }, - dumpAnnotations: (taskInstance: any, dumper: any): void => { - dispatch(dumpAnnotationsAsync(taskInstance, dumper)); - }, - exportDataset: (taskInstance: any, exporter: any): void => { - dispatch(exportDatasetAsync(taskInstance, exporter)); + showExportModal: (taskInstance: any): void => { + dispatch(exportActions.openExportModal(taskInstance)); }, deleteTask: (taskInstance: any): void => { dispatch(deleteTaskAsync(taskInstance)); @@ -98,14 +89,11 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps): taskInstance, annotationFormats: { loaders, dumpers }, loadActivity, - dumpActivities, - exportActivities, inferenceIsActive, exportIsActive, loadAnnotations, - dumpAnnotations, - exportDataset, + showExportModal, deleteTask, openRunModelWindow, exportTask, @@ -115,28 +103,18 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps): function onClickMenu(params: MenuInfo, file?: File): void { if (params.keyPath.length > 1) { const [additionalKey, action] = params.keyPath; - if (action === Actions.DUMP_TASK_ANNO) { - const format = additionalKey; - const [dumper] = dumpers.filter((_dumper: any): boolean => _dumper.name === format); - if (dumper) { - dumpAnnotations(taskInstance, dumper); - } - } else if (action === Actions.LOAD_TASK_ANNO) { + if (action === Actions.LOAD_TASK_ANNO) { const format = additionalKey; const [loader] = loaders.filter((_loader: any): boolean => _loader.name === format); if (loader && file) { loadAnnotations(taskInstance, loader, file); } - } else if (action === Actions.EXPORT_TASK_DATASET) { - const format = additionalKey; - const [exporter] = dumpers.filter((_exporter: any): boolean => _exporter.name === format); - if (exporter) { - exportDataset(taskInstance, exporter); - } } } else { const [action] = params.keyPath; - if (action === Actions.DELETE_TASK) { + if (action === Actions.EXPORT_TASK_DATASET) { + showExportModal(taskInstance); + } else if (action === Actions.DELETE_TASK) { deleteTask(taskInstance); } else if (action === Actions.OPEN_BUG_TRACKER) { window.open(`${taskInstance.bugTracker}`, '_blank'); @@ -158,8 +136,6 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps): loaders={loaders} dumpers={dumpers} loadActivity={loadActivity} - dumpActivities={dumpActivities} - exportActivities={exportActivities} inferenceIsActive={inferenceIsActive} onClickMenu={onClickMenu} taskDimension={taskInstance.dimension} diff --git a/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx b/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx index cc92a231..d7d52d9a 100644 --- a/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx +++ b/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx @@ -90,6 +90,7 @@ interface StateToProps { maxZLayer: number; curZLayer: number; automaticBordering: boolean; + intelligentPolygonCrop: boolean; switchableAutomaticBordering: boolean; keyMap: KeyMap; canvasBackgroundColor: string; @@ -188,9 +189,9 @@ function mapStateToProps(state: CombinedState): StateToProps { activatedAttributeID, selectedStatesID, annotations, - opacity, + opacity: opacity / 100, colorBy, - selectedOpacity, + selectedOpacity: selectedOpacity / 100, outlined, outlineColor, showBitmap, @@ -198,12 +199,12 @@ function mapStateToProps(state: CombinedState): StateToProps { grid, gridSize, gridColor, - gridOpacity, + gridOpacity: gridOpacity / 100, activeLabelID, activeObjectType, - brightnessLevel, - contrastLevel, - saturationLevel, + brightnessLevel: brightnessLevel / 100, + contrastLevel: contrastLevel / 100, + saturationLevel: saturationLevel / 100, resetZoom, aamZoomMargin, showObjectsTextAlways, diff --git a/cvat-ui/src/containers/annotation-page/top-bar/annotation-menu.tsx b/cvat-ui/src/containers/annotation-page/top-bar/annotation-menu.tsx index a52d3e11..60d5fef6 100644 --- a/cvat-ui/src/containers/annotation-page/top-bar/annotation-menu.tsx +++ b/cvat-ui/src/containers/annotation-page/top-bar/annotation-menu.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -10,7 +10,7 @@ import { MenuInfo } from 'rc-menu/lib/interface'; import { CombinedState, TaskStatus } from 'reducers/interfaces'; import AnnotationMenuComponent, { Actions } from 'components/annotation-page/top-bar/annotation-menu'; -import { dumpAnnotationsAsync, exportDatasetAsync, updateJobAsync } from 'actions/tasks-actions'; +import { updateJobAsync } from 'actions/tasks-actions'; import { uploadJobAnnotationsAsync, removeAnnotationsAsync, @@ -19,20 +19,18 @@ import { switchSubmitReviewDialog as switchSubmitReviewDialogAction, setForceExitAnnotationFlag as setForceExitAnnotationFlagAction, } from 'actions/annotation-actions'; +import { exportActions } from 'actions/export-actions'; interface StateToProps { annotationFormats: any; jobInstance: any; loadActivity: string | null; - dumpActivities: string[] | null; - exportActivities: string[] | null; user: any; } interface DispatchToProps { loadAnnotations(job: any, loader: any, file: File): void; - dumpAnnotations(task: any, dumper: any): void; - exportDataset(task: any, exporter: any): void; + showExportModal(task: any): void; removeAnnotations(sessionInstance: any): void; switchRequestReviewDialog(visible: boolean): void; switchSubmitReviewDialog(visible: boolean): void; @@ -49,7 +47,7 @@ function mapStateToProps(state: CombinedState): StateToProps { }, formats: { annotationFormats }, tasks: { - activities: { dumps, loads, exports: activeExports }, + activities: { loads }, }, auth: { user }, } = state; @@ -58,8 +56,6 @@ function mapStateToProps(state: CombinedState): StateToProps { const jobID = jobInstance.id; return { - dumpActivities: taskID in dumps ? dumps[taskID] : null, - exportActivities: taskID in activeExports ? activeExports[taskID] : null, loadActivity: taskID in loads || jobID in jobLoads ? loads[taskID] || jobLoads[jobID] : null, jobInstance, annotationFormats, @@ -72,11 +68,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { loadAnnotations(job: any, loader: any, file: File): void { dispatch(uploadJobAnnotationsAsync(job, loader, file)); }, - dumpAnnotations(task: any, dumper: any): void { - dispatch(dumpAnnotationsAsync(task, dumper)); - }, - exportDataset(task: any, exporter: any): void { - dispatch(exportDatasetAsync(task, exporter)); + showExportModal(task: any): void { + dispatch(exportActions.openExportModal(task)); }, removeAnnotations(sessionInstance: any): void { dispatch(removeAnnotationsAsync(sessionInstance)); @@ -108,11 +101,8 @@ function AnnotationMenuContainer(props: Props): JSX.Element { annotationFormats: { loaders, dumpers }, history, loadActivity, - dumpActivities, - exportActivities, loadAnnotations, - dumpAnnotations, - exportDataset, + showExportModal, removeAnnotations, switchRequestReviewDialog, switchSubmitReviewDialog, @@ -124,28 +114,18 @@ function AnnotationMenuContainer(props: Props): JSX.Element { const onClickMenu = (params: MenuInfo, file?: File): void => { if (params.keyPath.length > 1) { const [additionalKey, action] = params.keyPath; - if (action === Actions.DUMP_TASK_ANNO) { - const format = additionalKey; - const [dumper] = dumpers.filter((_dumper: any): boolean => _dumper.name === format); - if (dumper) { - dumpAnnotations(jobInstance.task, dumper); - } - } else if (action === Actions.LOAD_JOB_ANNO) { + if (action === Actions.LOAD_JOB_ANNO) { const format = additionalKey; const [loader] = loaders.filter((_loader: any): boolean => _loader.name === format); if (loader && file) { loadAnnotations(jobInstance, loader, file); } - } else if (action === Actions.EXPORT_TASK_DATASET) { - const format = additionalKey; - const [exporter] = dumpers.filter((_exporter: any): boolean => _exporter.name === format); - if (exporter) { - exportDataset(jobInstance.task, exporter); - } } } else { const [action] = params.keyPath; - if (action === Actions.REMOVE_ANNO) { + if (action === Actions.EXPORT_TASK_DATASET) { + showExportModal(jobInstance.task); + } else if (action === Actions.REMOVE_ANNO) { removeAnnotations(jobInstance); } else if (action === Actions.REQUEST_REVIEW) { switchRequestReviewDialog(true); @@ -173,8 +153,6 @@ function AnnotationMenuContainer(props: Props): JSX.Element { loaders={loaders} dumpers={dumpers} loadActivity={loadActivity} - dumpActivities={dumpActivities} - exportActivities={exportActivities} onClickMenu={onClickMenu} setForceExitAnnotationFlag={setForceExitAnnotationFlag} saveAnnotations={saveAnnotations} 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 d14e382c..d922a000 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 @@ -30,9 +30,10 @@ import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-ba import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas3d } from 'cvat-canvas3d-wrapper'; import { - CombinedState, FrameSpeed, Workspace, PredictorState, DimensionType, ActiveControl, + CombinedState, FrameSpeed, Workspace, PredictorState, DimensionType, ActiveControl, ToolsBlockerState, } from 'reducers/interfaces'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; +import { switchToolsBlockerState } from 'actions/settings-actions'; interface StateToProps { jobInstance: any; @@ -49,6 +50,7 @@ interface StateToProps { redoAction?: string; autoSave: boolean; autoSaveInterval: number; + toolsBlockerState: ToolsBlockerState; workspace: Workspace; keyMap: KeyMap; normalizedKeyMap: Record; @@ -72,6 +74,7 @@ interface DispatchToProps { setForceExitAnnotationFlag(forceExit: boolean): void; changeWorkspace(workspace: Workspace): void; switchPredictor(predictorEnabled: boolean): void; + onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState): void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -92,7 +95,7 @@ function mapStateToProps(state: CombinedState): StateToProps { }, settings: { player: { frameSpeed, frameStep }, - workspace: { autoSave, autoSaveInterval }, + workspace: { autoSave, autoSaveInterval, toolsBlockerState }, }, shortcuts: { keyMap, normalizedKeyMap }, plugins: { list }, @@ -113,6 +116,7 @@ function mapStateToProps(state: CombinedState): StateToProps { redoAction: history.redo.length ? history.redo[history.redo.length - 1][0] : undefined, autoSave, autoSaveInterval, + toolsBlockerState, workspace, keyMap, normalizedKeyMap, @@ -167,6 +171,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { dispatch(getPredictionsAsync()); } }, + onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState):void{ + dispatch(switchToolsBlockerState(toolsBlockerState)); + }, }; } @@ -431,6 +438,22 @@ class AnnotationTopBarContainer extends React.PureComponent { canvasInstance.draw({ enabled: false }); }; + private onSwitchToolsBlockerState = (): void => { + const { + toolsBlockerState, onSwitchToolsBlockerState, canvasInstance, activeControl, + } = this.props; + if (canvasInstance instanceof Canvas) { + if (activeControl.includes(ActiveControl.OPENCV_TOOLS)) { + canvasInstance.interact({ + enabled: true, + crosshair: toolsBlockerState.algorithmsLocked, + enableThreshold: toolsBlockerState.algorithmsLocked, + }); + } + } + onSwitchToolsBlockerState({ algorithmsLocked: !toolsBlockerState.algorithmsLocked }); + }; + private onURLIconClick = (): void => { const { frameNumber } = this.props; const { origin, pathname } = window.location; @@ -546,6 +569,7 @@ class AnnotationTopBarContainer extends React.PureComponent { searchAnnotations, changeWorkspace, switchPredictor, + toolsBlockerState, } = this.props; const preventDefault = (event: KeyboardEvent | undefined): void => { @@ -672,6 +696,8 @@ class AnnotationTopBarContainer extends React.PureComponent { undoShortcut={normalizedKeyMap.UNDO} redoShortcut={normalizedKeyMap.REDO} drawShortcut={normalizedKeyMap.SWITCH_DRAW_MODE} + // this shortcut is handled in interactionHandler.ts separatelly + switchToolsBlockerShortcut={normalizedKeyMap.SWITCH_TOOLS_BLOCKER_STATE} playPauseShortcut={normalizedKeyMap.PLAY_PAUSE} nextFrameShortcut={normalizedKeyMap.NEXT_FRAME} previousFrameShortcut={normalizedKeyMap.PREV_FRAME} @@ -683,6 +709,8 @@ class AnnotationTopBarContainer extends React.PureComponent { onUndoClick={this.undo} onRedoClick={this.redo} onFinishDraw={this.onFinishDraw} + onSwitchToolsBlockerState={this.onSwitchToolsBlockerState} + toolsBlockerState={toolsBlockerState} jobInstance={jobInstance} isTrainingActive={isTrainingActive} activeControl={activeControl} diff --git a/cvat-ui/src/reducers/export-reducer.ts b/cvat-ui/src/reducers/export-reducer.ts new file mode 100644 index 00000000..a7a02352 --- /dev/null +++ b/cvat-ui/src/reducers/export-reducer.ts @@ -0,0 +1,67 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import { ExportActions, ExportActionTypes } from 'actions/export-actions'; +import getCore from 'cvat-core-wrapper'; +import deepCopy from 'utils/deep-copy'; + +import { ExportState } from './interfaces'; + +const core = getCore(); + +const defaultState: ExportState = { + tasks: {}, + projects: {}, + instance: null, + modalVisible: false, +}; + +export default (state: ExportState = defaultState, action: ExportActions): ExportState => { + switch (action.type) { + case ExportActionTypes.OPEN_EXPORT_MODAL: + return { + ...state, + modalVisible: true, + instance: action.payload.instance, + }; + case ExportActionTypes.CLOSE_EXPORT_MODAL: + return { + ...state, + modalVisible: false, + instance: null, + }; + case ExportActionTypes.EXPORT_DATASET: { + const { instance, format } = action.payload; + const activities = deepCopy(instance instanceof core.classes.Project ? state.projects : state.tasks); + + activities[instance.id] = + instance.id in activities && !activities[instance.id].includes(format) ? + [...activities[instance.id], format] : + activities[instance.id] || [format]; + + return { + ...state, + tasks: instance instanceof core.classes.Task ? activities : state.tasks, + projects: instance instanceof core.classes.Project ? activities : state.projects, + }; + } + case ExportActionTypes.EXPORT_DATASET_FAILED: + case ExportActionTypes.EXPORT_DATASET_SUCCESS: { + const { instance, format } = action.payload; + const activities = deepCopy(instance instanceof core.classes.Project ? state.projects : state.tasks); + + activities[instance.id] = activities[instance.id].filter( + (exporterName: string): boolean => exporterName !== format, + ); + + return { + ...state, + tasks: instance instanceof core.classes.Task ? activities : state.tasks, + projects: instance instanceof core.classes.Project ? activities : state.projects, + }; + } + default: + return state; + } +}; diff --git a/cvat-ui/src/reducers/formats-reducer.ts b/cvat-ui/src/reducers/formats-reducer.ts index 855caaa5..8260e976 100644 --- a/cvat-ui/src/reducers/formats-reducer.ts +++ b/cvat-ui/src/reducers/formats-reducer.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 7da382b8..ef11a820 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -86,14 +86,6 @@ export interface TasksState { count: number; current: Task[]; activities: { - dumps: { - // dumps in different formats at the same time - [tid: number]: string[]; // dumper names - }; - exports: { - // exports in different formats at the same time - [tid: number]: string[]; // dumper names - }; loads: { // only one loading simultaneously [tid: number]: string; // loader name @@ -112,6 +104,17 @@ export interface TasksState { }; } +export interface ExportState { + tasks: { + [tid: number]: string[]; + }; + projects: { + [pid: number]: string[]; + }; + instance: any; + modalVisible: boolean; +} + export interface FormatsState { annotationFormats: any; fetching: boolean; @@ -182,12 +185,23 @@ export interface Model { framework: string; description: string; type: string; + onChangeToolsBlockerState: (event:string) => void; + tip: { + message: string; + gif: string; + }; params: { - canvas: Record; + canvas: Record; }; } export type OpenCVTool = IntelligentScissors; + +export interface ToolsBlockerState { + algorithmsLocked?: boolean; + buttonVisible?: boolean; +} + export enum TaskStatus { ANNOTATION = 'annotation', REVIEW = 'validation', @@ -557,6 +571,7 @@ export interface WorkspaceSettingsState { showAllInterpolationTracks: boolean; intelligentPolygonCrop: boolean; defaultApproxPolyAccuracy: number; + toolsBlockerState: ToolsBlockerState; } export interface ShapesSettingsState { @@ -617,6 +632,7 @@ export interface CombinedState { settings: SettingsState; shortcuts: ShortcutsState; review: ReviewState; + export: ExportState; } export enum DimensionType { diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index e09db0b2..34907060 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -16,9 +16,13 @@ import { NotificationsActionType } from 'actions/notification-actions'; import { BoundariesActionTypes } from 'actions/boundaries-actions'; import { UserAgreementsActionTypes } from 'actions/useragreements-actions'; import { ReviewActionTypes } from 'actions/review-actions'; +import { ExportActionTypes } from 'actions/export-actions'; +import getCore from 'cvat-core-wrapper'; import { NotificationsState } from './interfaces'; +const core = getCore(); + const defaultState: NotificationsState = { errors: { auth: { @@ -308,8 +312,9 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } - case TasksActionTypes.EXPORT_DATASET_FAILED: { - const taskID = action.payload.task.id; + case ExportActionTypes.EXPORT_DATASET_FAILED: { + const instanceID = action.payload.instance.id; + const instanceType = action.payload.instance instanceof core.classes.Project ? 'project' : 'task'; return { ...state, errors: { @@ -319,7 +324,8 @@ export default function (state = defaultState, action: AnyAction): Notifications exportingAsDataset: { message: 'Could not export dataset for the ' + - `task ${taskID}`, + `` + + `${instanceType} ${instanceID}`, reason: action.payload.error.toString(), }, }, @@ -392,24 +398,6 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } - case TasksActionTypes.DUMP_ANNOTATIONS_FAILED: { - const taskID = action.payload.task.id; - return { - ...state, - errors: { - ...state.errors, - tasks: { - ...state.errors.tasks, - dumping: { - message: - 'Could not dump annotations for the ' + - `task ${taskID}`, - reason: action.payload.error.toString(), - }, - }, - }, - }; - } case TasksActionTypes.DELETE_TASK_FAILED: { const { taskID } = action.payload; return { diff --git a/cvat-ui/src/reducers/root-reducer.ts b/cvat-ui/src/reducers/root-reducer.ts index 04358b44..b1219b7a 100644 --- a/cvat-ui/src/reducers/root-reducer.ts +++ b/cvat-ui/src/reducers/root-reducer.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -17,6 +17,7 @@ import settingsReducer from './settings-reducer'; import shortcutsReducer from './shortcuts-reducer'; import userAgreementsReducer from './useragreements-reducer'; import reviewReducer from './review-reducer'; +import exportReducer from './export-reducer'; export default function createRootReducer(): Reducer { return combineReducers({ @@ -34,5 +35,6 @@ export default function createRootReducer(): Reducer { shortcuts: shortcutsReducer, userAgreements: userAgreementsReducer, review: reviewReducer, + export: exportReducer, }); } diff --git a/cvat-ui/src/reducers/settings-reducer.ts b/cvat-ui/src/reducers/settings-reducer.ts index 301ff9a7..69a945d6 100644 --- a/cvat-ui/src/reducers/settings-reducer.ts +++ b/cvat-ui/src/reducers/settings-reducer.ts @@ -32,6 +32,10 @@ const defaultState: SettingsState = { showAllInterpolationTracks: false, intelligentPolygonCrop: true, defaultApproxPolyAccuracy: 9, + toolsBlockerState: { + algorithmsLocked: false, + buttonVisible: false, + }, }, player: { canvasBackgroundColor: '#ffffff', @@ -287,6 +291,15 @@ export default (state = defaultState, action: AnyAction): SettingsState => { }, }; } + case SettingsActionTypes.SWITCH_TOOLS_BLOCKER_STATE: { + return { + ...state, + workspace: { + ...state.workspace, + toolsBlockerState: { ...state.workspace.toolsBlockerState, ...action.payload.toolsBlockerState }, + }, + }; + } case SettingsActionTypes.SWITCH_SETTINGS_DIALOG: { return { ...state, @@ -320,6 +333,18 @@ export default (state = defaultState, action: AnyAction): SettingsState => { }, }; } + case AnnotationActionTypes.INTERACT_WITH_CANVAS: { + return { + ...state, + workspace: { + ...state.workspace, + toolsBlockerState: { + buttonVisible: true, + algorithmsLocked: false, + }, + }, + }; + } case AuthActionTypes.LOGOUT_SUCCESS: { return { ...defaultState }; } diff --git a/cvat-ui/src/reducers/shortcuts-reducer.ts b/cvat-ui/src/reducers/shortcuts-reducer.ts index ed219f9d..1af6d08a 100644 --- a/cvat-ui/src/reducers/shortcuts-reducer.ts +++ b/cvat-ui/src/reducers/shortcuts-reducer.ts @@ -322,6 +322,13 @@ const defaultKeyMap = ({ action: 'keydown', applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D], }, + SWITCH_TOOLS_BLOCKER_STATE: { + name: 'Switch algorithm blocker', + description: 'Postpone running the algorithm for interaction tools', + sequences: ['сtrl'], + action: 'keydown', + applicable: [DimensionType.DIM_2D], + }, CHANGE_OBJECT_COLOR: { name: 'Change color', description: 'Set the next color for an activated shape', diff --git a/cvat-ui/src/reducers/tasks-reducer.ts b/cvat-ui/src/reducers/tasks-reducer.ts index 78236132..592286d1 100644 --- a/cvat-ui/src/reducers/tasks-reducer.ts +++ b/cvat-ui/src/reducers/tasks-reducer.ts @@ -32,8 +32,6 @@ const defaultState: TasksState = { mode: null, }, activities: { - dumps: {}, - exports: {}, loads: {}, deletes: {}, creates: { @@ -85,84 +83,6 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState initialized: true, fetching: false, }; - case TasksActionTypes.DUMP_ANNOTATIONS: { - const { task } = action.payload; - const { dumper } = action.payload; - const { dumps } = state.activities; - - dumps[task.id] = - task.id in dumps && !dumps[task.id].includes(dumper.name) ? - [...dumps[task.id], dumper.name] : - dumps[task.id] || [dumper.name]; - - return { - ...state, - activities: { - ...state.activities, - dumps: { - ...dumps, - }, - }, - }; - } - case TasksActionTypes.DUMP_ANNOTATIONS_FAILED: - case TasksActionTypes.DUMP_ANNOTATIONS_SUCCESS: { - const { task } = action.payload; - const { dumper } = action.payload; - const { dumps } = state.activities; - - dumps[task.id] = dumps[task.id].filter((dumperName: string): boolean => dumperName !== dumper.name); - - return { - ...state, - activities: { - ...state.activities, - dumps: { - ...dumps, - }, - }, - }; - } - case TasksActionTypes.EXPORT_DATASET: { - const { task } = action.payload; - const { exporter } = action.payload; - const { exports: activeExports } = state.activities; - - activeExports[task.id] = - task.id in activeExports && !activeExports[task.id].includes(exporter.name) ? - [...activeExports[task.id], exporter.name] : - activeExports[task.id] || [exporter.name]; - - return { - ...state, - activities: { - ...state.activities, - exports: { - ...activeExports, - }, - }, - }; - } - case TasksActionTypes.EXPORT_DATASET_FAILED: - case TasksActionTypes.EXPORT_DATASET_SUCCESS: { - const { task } = action.payload; - const { exporter } = action.payload; - const { exports: activeExports } = state.activities; - - activeExports[task.id] = activeExports[task.id].filter( - (exporterName: string): boolean => exporterName !== exporter.name, - ); - - return { - ...state, - activities: { - ...state.activities, - exports: { - ...activeExports, - }, - }, - }; - } case TasksActionTypes.LOAD_ANNOTATIONS: { const { task } = action.payload; const { loader } = action.payload; diff --git a/cvat-ui/src/styles.scss b/cvat-ui/src/styles.scss index c00a4236..18d102ec 100644 --- a/cvat-ui/src/styles.scss +++ b/cvat-ui/src/styles.scss @@ -57,7 +57,7 @@ hr { width: 100%; height: 100%; display: grid; - min-width: 1280px; + min-width: 1024px; } #layout-grid { diff --git a/cvat-ui/src/utils/deep-copy.ts b/cvat-ui/src/utils/deep-copy.ts new file mode 100644 index 00000000..986c4dbe --- /dev/null +++ b/cvat-ui/src/utils/deep-copy.ts @@ -0,0 +1,21 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +function deepCopy(obj: T): T { + if (typeof obj !== 'object') { + return obj; + } + if (!obj) { + return obj; + } + const container: any = (obj instanceof Array) ? [] : {}; + for (const i in obj) { + if (Object.prototype.hasOwnProperty.call(obj, i)) { + container[i] = deepCopy(obj[i]); + } + } + return container; +} + +export default deepCopy; diff --git a/cvat-ui/src/utils/enviroment.ts b/cvat-ui/src/utils/enviroment.ts index 41c03315..c2edca5c 100644 --- a/cvat-ui/src/utils/enviroment.ts +++ b/cvat-ui/src/utils/enviroment.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-ui/src/utils/hooks.ts b/cvat-ui/src/utils/hooks.ts index 74b58a1d..9107317d 100644 --- a/cvat-ui/src/utils/hooks.ts +++ b/cvat-ui/src/utils/hooks.ts @@ -1,7 +1,7 @@ // Copyright (C) 2021 Intel Corporation // // SPDX-License-Identifier: MIT -import { useRef, useEffect } from 'react'; +import { useRef, useEffect, useState } from 'react'; // eslint-disable-next-line import/prefer-default-export export function usePrevious(value: T): T | undefined { @@ -11,3 +11,51 @@ export function usePrevious(value: T): T | undefined { }); return ref.current; } + +export interface ICardHeightHOC { + numberOfRows: number; + paddings: number; + containerClassName: string; + siblingClassNames: string[]; +} + +export function useCardHeightHOC(params: ICardHeightHOC): () => string { + const { + numberOfRows, paddings, containerClassName, siblingClassNames, + } = params; + + return (): string => { + const [height, setHeight] = useState('auto'); + useEffect(() => { + const resize = (): void => { + const container = window.document.getElementsByClassName(containerClassName)[0]; + const siblings = siblingClassNames.map( + (classname: string): Element | undefined => window.document.getElementsByClassName(classname)[0], + ); + + if (container) { + const { clientHeight: containerHeight } = container; + const othersHeight = siblings.reduce((acc: number, el: Element | undefined): number => { + if (el) { + return acc + el.clientHeight; + } + + return acc; + }, 0); + + const cardHeight = (containerHeight - (othersHeight + paddings)) / numberOfRows; + setHeight(`${Math.round(cardHeight)}px`); + } + }; + + resize(); + window.addEventListener('resize', resize); + + return () => { + window.removeEventListener('resize', resize); + }; + }, []); + + return height; + }; +} diff --git a/cvat-ui/src/utils/opencv-wrapper/histogram-equalization.ts b/cvat-ui/src/utils/opencv-wrapper/histogram-equalization.ts index 7bf33966..ff7687de 100644 --- a/cvat-ui/src/utils/opencv-wrapper/histogram-equalization.ts +++ b/cvat-ui/src/utils/opencv-wrapper/histogram-equalization.ts @@ -60,8 +60,7 @@ export default class HistogramEqualizationImplementation implements HistogramEqu this.hashFrame(imgData, frameNumber); return imgData; } catch (e) { - console.log('Histogram equalization error', e); - return src; + throw new Error(e.toString()); } finally { if (matImage) matImage.delete(); if (channels) channels.delete(); diff --git a/cvat-ui/src/utils/opencv-wrapper/intelligent-scissors.ts b/cvat-ui/src/utils/opencv-wrapper/intelligent-scissors.ts index 8fca2301..49483f73 100644 --- a/cvat-ui/src/utils/opencv-wrapper/intelligent-scissors.ts +++ b/cvat-ui/src/utils/opencv-wrapper/intelligent-scissors.ts @@ -14,6 +14,7 @@ export interface IntelligentScissorsParams { enableSliding: boolean; allowRemoveOnlyLast: boolean; minPosVertices: number; + onChangeToolsBlockerState: (event:string)=>void; }; } @@ -21,6 +22,7 @@ export interface IntelligentScissors { reset(): void; run(points: number[], image: ImageData, offsetX: number, offsetY: number): number[]; params: IntelligentScissorsParams; + switchBlockMode(mode?:boolean):void; } function applyOffset(points: Point[], offsetX: number, offsetY: number): Point[] { @@ -34,6 +36,7 @@ function applyOffset(points: Point[], offsetX: number, offsetY: number): Point[] export default class IntelligentScissorsImplementation implements IntelligentScissors { private cv: any; + private onChangeToolsBlockerState: (event:string)=>void; private scissors: { tool: any; state: { @@ -46,14 +49,20 @@ export default class IntelligentScissorsImplementation implements IntelligentSci } >; // point index : start index in path image: any | null; + blocked: boolean; }; }; - public constructor(cv: any) { + public constructor(cv: any, onChangeToolsBlockerState:(event:string)=>void) { this.cv = cv; + this.onChangeToolsBlockerState = onChangeToolsBlockerState; this.reset(); } + public switchBlockMode(mode:boolean): void { + this.scissors.state.blocked = mode; + } + public reset(): void { if (this.scissors && this.scissors.tool) { this.scissors.tool.delete(); @@ -66,6 +75,7 @@ export default class IntelligentScissorsImplementation implements IntelligentSci path: [], anchors: {}, image: null, + blocked: false, }, }; @@ -88,7 +98,6 @@ export default class IntelligentScissorsImplementation implements IntelligentSci const { tool, state } = scissors; const points = applyOffset(numberArrayToPoints(coordinates), offsetX, offsetY); - if (points.length > 1) { let matImage = null; const contour = new cv.Mat(); @@ -108,7 +117,6 @@ export default class IntelligentScissorsImplementation implements IntelligentSci delete state.anchors[+i]; } } - return [...state.path]; } @@ -118,14 +126,17 @@ export default class IntelligentScissorsImplementation implements IntelligentSci state.path = state.path.slice(0, state.anchors[points.length - 1].start); delete state.anchors[points.length - 1]; } - - tool.applyImage(matImage); - tool.buildMap(new cv.Point(prevX, prevY)); - tool.getContour(new cv.Point(curX, curY), contour); - const pathSegment = []; - for (let row = 0; row < contour.rows; row++) { - pathSegment.push(contour.intAt(row, 0) + offsetX, contour.intAt(row, 1) + offsetY); + if (!state.blocked) { + tool.applyImage(matImage); + tool.buildMap(new cv.Point(prevX, prevY)); + tool.getContour(new cv.Point(curX, curY), contour); + + for (let row = 0; row < contour.rows; row++) { + pathSegment.push(contour.intAt(row, 0) + offsetX, contour.intAt(row, 1) + offsetY); + } + } else { + pathSegment.push(curX + offsetX, curY + offsetY); } state.anchors[points.length - 1] = { point: cur, @@ -140,13 +151,13 @@ export default class IntelligentScissorsImplementation implements IntelligentSci contour.delete(); } } else { + state.path = []; state.path.push(...pointsToNumberArray(applyOffset(points.slice(-1), -offsetX, -offsetY))); state.anchors[0] = { point: points[0], start: 0, }; } - return [...state.path]; } @@ -167,6 +178,7 @@ export default class IntelligentScissorsImplementation implements IntelligentSci enableSliding: true, allowRemoveOnlyLast: true, minPosVertices: 1, + onChangeToolsBlockerState: this.onChangeToolsBlockerState, }, }; } diff --git a/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts b/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts index cc18cecb..c946fb19 100644 --- a/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts +++ b/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts @@ -12,7 +12,7 @@ const core = getCore(); const baseURL = core.config.backendAPI.slice(0, -7); export interface Segmentation { - intelligentScissorsFactory: () => IntelligentScissors; + intelligentScissorsFactory: (onChangeToolsBlockerState:(event:string)=>void) => IntelligentScissors; } export interface Contours { @@ -20,7 +20,7 @@ export interface Contours { } export interface ImgProc { - hist: () => HistogramEqualization + hist: () => HistogramEqualization; } export class OpenCVWrapper { @@ -41,10 +41,6 @@ export class OpenCVWrapper { const contentLength = response.headers.get('Content-Length'); const { body } = response; - if (contentLength === null) { - throw new Error('Content length is null, but necessary'); - } - if (body === null) { throw new Error('Response body is null, but necessary'); } @@ -64,7 +60,9 @@ export class OpenCVWrapper { if (value instanceof Uint8Array) { decodedScript += decoder.decode(value); receivedLength += value.length; - const percentage = (receivedLength * 100) / +(contentLength as string); + // Cypress workaround: content-length is always zero in cypress, it is done optional here + // Just progress bar will be disabled + const percentage = contentLength ? (receivedLength * 100) / +(contentLength as string) : 0; onProgress(+percentage.toFixed(0)); } } @@ -128,7 +126,8 @@ export class OpenCVWrapper { } return { - intelligentScissorsFactory: () => new IntelligentScissorsImplementation(this.cv), + intelligentScissorsFactory: (onChangeToolsBlockerState:(event:string)=>void) => + new IntelligentScissorsImplementation(this.cv, onChangeToolsBlockerState), }; } diff --git a/cvat-ui/src/utils/platform-checker.ts b/cvat-ui/src/utils/platform-checker.ts index 71335db6..81cbfe7c 100644 --- a/cvat-ui/src/utils/platform-checker.ts +++ b/cvat-ui/src/utils/platform-checker.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -9,6 +9,7 @@ const name = platform.name || 'unknown'; const version = platform.version || 'unknown'; const os = platform.os ? platform.os.toString() : 'unknown'; let platformNotificationShown = window.localStorage.getItem('platformNotiticationShown') !== null; +let featuresNotificationShown = window.localStorage.getItem('featuresNotificationShown') !== null; export function platformInfo(): { engine: string; @@ -26,8 +27,10 @@ export function platformInfo(): { export function stopNotifications(saveInStorage: boolean): void { platformNotificationShown = true; + featuresNotificationShown = true; if (saveInStorage) { window.localStorage.setItem('platformNotiticationShown', 'shown'); + window.localStorage.setItem('featuresNotificationShown', 'shown'); } } @@ -36,5 +39,12 @@ export default function showPlatformNotification(): boolean { // Gecko is engine of Firefox, supported but works worse than in Chrome (let's show the message) // WebKit is engine of Apple Safary, not supported const unsupportedPlatform = !['Blink'].includes(engine); - return unsupportedPlatform && !platformNotificationShown; + return !platformNotificationShown && unsupportedPlatform; +} + +export function showUnsupportedNotification(): boolean { + const nesassaryFeatures = [window.ResizeObserver, Object.fromEntries]; + + const unsupportedFeatures = nesassaryFeatures.some((feature) => typeof feature === 'undefined'); + return !featuresNotificationShown && unsupportedFeatures; } diff --git a/cvat-ui/webpack.config.js b/cvat-ui/webpack.config.js index f99c03cb..213b0d92 100644 --- a/cvat-ui/webpack.config.js +++ b/cvat-ui/webpack.config.js @@ -28,6 +28,7 @@ module.exports = (env) => ({ contentBase: path.join(__dirname, 'dist'), compress: false, inline: true, + host: process.env.CVAT_UI_HOST || 'localhost', port: 3000, historyApiFallback: true, proxy: [ @@ -63,16 +64,7 @@ module.exports = (env) => ({ }, ], ], - presets: [ - [ - '@babel/preset-env', - { - targets: '> 2.5%', // https://github.com/browserslist/browserslist - }, - ], - ['@babel/preset-react'], - ['@babel/typescript'], - ], + presets: ['@babel/preset-env', '@babel/preset-react', '@babel/typescript'], sourceType: 'unambiguous', }, }, @@ -87,7 +79,12 @@ module.exports = (env) => ({ importLoaders: 2, }, }, - 'postcss-loader', + { + loader: 'postcss-loader', + options: { + plugins: [require('postcss-preset-env')], + }, + }, 'sass-loader', ], }, diff --git a/cvat/__init__.py b/cvat/__init__.py index 52ec08c4..e01f98d3 100644 --- a/cvat/__init__.py +++ b/cvat/__init__.py @@ -4,6 +4,6 @@ from cvat.utils.version import get_version -VERSION = (1, 5, 0, 'final', 0) +VERSION = (1, 6, 0, 'final', 0) __version__ = get_version(VERSION) diff --git a/cvat/apps/dataset_manager/annotation.py b/cvat/apps/dataset_manager/annotation.py index 0fbd6bf0..88989dbb 100644 --- a/cvat/apps/dataset_manager/annotation.py +++ b/cvat/apps/dataset_manager/annotation.py @@ -42,6 +42,9 @@ class AnnotationIR: def __getitem__(self, key): return getattr(self, key) + def __setitem__(self, key, value): + return setattr(self, key, value) + @data.setter def data(self, data): self.version = data['version'] diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 82716123..fba124b5 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -3,52 +3,34 @@ # # SPDX-License-Identifier: MIT +import sys import os.path as osp -from collections import OrderedDict, namedtuple +from collections import namedtuple +from typing import Any, Callable, DefaultDict, Dict, List, Literal, Mapping, NamedTuple, OrderedDict, Tuple, Union from pathlib import Path from django.utils import timezone import datumaro.components.extractor as datumaro from cvat.apps.engine.frame_provider import FrameProvider -from cvat.apps.engine.models import AttributeType, ShapeType, DimensionType, Image as Img +from cvat.apps.engine.models import AttributeType, ShapeType, Project, Task, Label, DimensionType, Image as Img from datumaro.util import cast from datumaro.util.image import ByteImage, Image -from .annotation import AnnotationManager, TrackManager +from .annotation import AnnotationManager, TrackManager, AnnotationIR -class TaskData: - Attribute = namedtuple('Attribute', 'name, value') - Shape = namedtuple("Shape", 'id, label_id') # 3d - LabeledShape = namedtuple( - 'LabeledShape', 'type, frame, label, points, occluded, attributes, source, group, z_order') - LabeledShape.__new__.__defaults__ = (0, 0) - TrackedShape = namedtuple( - 'TrackedShape', 'type, frame, points, occluded, outside, keyframe, attributes, source, group, z_order, label, track_id') - TrackedShape.__new__.__defaults__ = ('manual', 0, 0, None, 0) - Track = namedtuple('Track', 'label, group, source, shapes') - Tag = namedtuple('Tag', 'frame, label, attributes, source, group') - Tag.__new__.__defaults__ = (0, ) - Frame = namedtuple( - 'Frame', 'idx, id, frame, name, width, height, labeled_shapes, tags, shapes, labels') - Labels = namedtuple('Label', 'id, name, color') +class InstanceLabelData: + Attribute = NamedTuple('Attribute', [('name', str), ('value', Any)]) - def __init__(self, annotation_ir, db_task, host='', create_callback=None): - self._annotation_ir = annotation_ir - self._db_task = db_task - self._host = host - self._create_callback = create_callback - self._MAX_ANNO_SIZE = 30000 - self._frame_info = {} - self._frame_mapping = {} - self._frame_step = db_task.data.get_frame_step() + def __init__(self, instance: Union[Task, Project]) -> None: + instance = instance.project if isinstance(instance, Task) and instance.project_id is not None else instance - db_labels = (self._db_task.project if self._db_task.project_id else self._db_task).label_set.all().prefetch_related( - 'attributespec_set').order_by('pk') + db_labels = instance.label_set.all().prefetch_related('attributespec_set').order_by('pk') - self._label_mapping = OrderedDict( - (db_label.id, db_label) for db_label in db_labels) + self._label_mapping = OrderedDict[int, Label]( + ((db_label.id, db_label) for db_label in db_labels), + ) self._attribute_mapping = {db_label.id: { 'mutable': {}, 'immutable': {}, 'spec': {}} @@ -69,9 +51,6 @@ class TaskData: **attr_mapping['immutable'], } - self._init_frame_info() - self._init_meta() - def _get_label_id(self, label_name): for db_label in self._label_mapping.values(): if label_name == db_label.name: @@ -103,6 +82,71 @@ class TaskData: def _get_immutable_attribute_id(self, label_id, attribute_name): return self._get_attribute_id(label_id, attribute_name, 'immutable') + def _import_attribute(self, label_id, attribute): + spec_id = self._get_attribute_id(label_id, attribute.name) + value = attribute.value + + if spec_id: + spec = self._attribute_mapping[label_id]['spec'][spec_id] + + try: + if spec.input_type == AttributeType.NUMBER: + pass # no extra processing required + elif spec.input_type == AttributeType.CHECKBOX: + if isinstance(value, str): + value = value.lower() + assert value in {'true', 'false'} + elif isinstance(value, (bool, int, float)): + value = 'true' if value else 'false' + else: + raise ValueError("Unexpected attribute value") + except Exception as e: + raise Exception("Failed to convert attribute '%s'='%s': %s" % + (self._get_label_name(label_id), value, e)) + + return { 'spec_id': spec_id, 'value': value } + + def _export_attributes(self, attributes): + exported_attributes = [] + for attr in attributes: + attribute_name = self._get_attribute_name(attr["spec_id"]) + exported_attributes.append(InstanceLabelData.Attribute( + name=attribute_name, + value=attr["value"], + )) + return exported_attributes + + +class TaskData(InstanceLabelData): + Shape = namedtuple("Shape", 'id, label_id') # 3d + LabeledShape = namedtuple( + 'LabeledShape', 'type, frame, label, points, occluded, attributes, source, group, z_order') + LabeledShape.__new__.__defaults__ = (0, 0) + TrackedShape = namedtuple( + 'TrackedShape', 'type, frame, points, occluded, outside, keyframe, attributes, source, group, z_order, label, track_id') + TrackedShape.__new__.__defaults__ = ('manual', 0, 0, None, 0) + Track = namedtuple('Track', 'label, group, source, shapes') + Tag = namedtuple('Tag', 'frame, label, attributes, source, group') + Tag.__new__.__defaults__ = (0, ) + Frame = namedtuple( + 'Frame', 'idx, id, frame, name, width, height, labeled_shapes, tags, shapes, labels') + Labels = namedtuple('Label', 'id, name, color') + + def __init__(self, annotation_ir, db_task, host='', create_callback=None): + self._annotation_ir = annotation_ir + self._db_task = db_task + self._host = host + self._create_callback = create_callback + self._MAX_ANNO_SIZE = 30000 + self._frame_info = {} + self._frame_mapping = {} + self._frame_step = db_task.data.get_frame_step() + + InstanceLabelData.__init__(self, db_task) + + self._init_frame_info() + self._init_meta() + def abs_frame_id(self, relative_id): if relative_id not in range(0, self._db_task.data.size): raise ValueError("Unknown internal frame id %s" % relative_id) @@ -135,79 +179,80 @@ class TaskData: for frame_number, info in self._frame_info.items() } - def _init_meta(self): - db_segments = self._db_task.segment_set.all().prefetch_related('job_set') - self._meta = OrderedDict([ - ("task", OrderedDict([ - ("id", str(self._db_task.id)), - ("name", self._db_task.name), - ("size", str(self._db_task.data.size)), - ("mode", self._db_task.mode), - ("overlap", str(self._db_task.overlap)), - ("bugtracker", self._db_task.bug_tracker), - ("created", str(timezone.localtime(self._db_task.created_date))), - ("updated", str(timezone.localtime(self._db_task.updated_date))), - ("start_frame", str(self._db_task.data.start_frame)), - ("stop_frame", str(self._db_task.data.stop_frame)), - ("frame_filter", self._db_task.data.frame_filter), - - ("labels", [ - ("label", OrderedDict([ - ("name", db_label.name), - ("color", db_label.color), - ("attributes", [ - ("attribute", OrderedDict([ - ("name", db_attr.name), - ("mutable", str(db_attr.mutable)), - ("input_type", db_attr.input_type), - ("default_value", db_attr.default_value), - ("values", db_attr.values)])) - for db_attr in db_label.attributespec_set.all()]) - ])) for db_label in self._label_mapping.values() - ]), + @staticmethod + def meta_for_task(db_task, host, label_mapping=None): + db_segments = db_task.segment_set.all().prefetch_related('job_set') + + meta = OrderedDict([ + ("id", str(db_task.id)), + ("name", db_task.name), + ("size", str(db_task.data.size)), + ("mode", db_task.mode), + ("overlap", str(db_task.overlap)), + ("bugtracker", db_task.bug_tracker), + ("created", str(timezone.localtime(db_task.created_date))), + ("updated", str(timezone.localtime(db_task.updated_date))), + ("subset", db_task.subset or datumaro.DEFAULT_SUBSET_NAME), + ("start_frame", str(db_task.data.start_frame)), + ("stop_frame", str(db_task.data.stop_frame)), + ("frame_filter", db_task.data.frame_filter), + + ("segments", [ + ("segment", OrderedDict([ + ("id", str(db_segment.id)), + ("start", str(db_segment.start_frame)), + ("stop", str(db_segment.stop_frame)), + ("url", "{}/?id={}".format( + host, db_segment.job_set.all()[0].id))] + )) for db_segment in db_segments + ]), + + ("owner", OrderedDict([ + ("username", db_task.owner.username), + ("email", db_task.owner.email) + ]) if db_task.owner else ""), + + ("assignee", OrderedDict([ + ("username", db_task.assignee.username), + ("email", db_task.assignee.email) + ]) if db_task.assignee else ""), + ]) - ("segments", [ - ("segment", OrderedDict([ - ("id", str(db_segment.id)), - ("start", str(db_segment.start_frame)), - ("stop", str(db_segment.stop_frame)), - ("url", "{}/?id={}".format( - self._host, db_segment.job_set.all()[0].id))] - )) for db_segment in db_segments - ]), + if label_mapping is not None: + meta['labels'] = [ + ("label", OrderedDict([ + ("name", db_label.name), + ("color", db_label.color), + ("attributes", [ + ("attribute", OrderedDict([ + ("name", db_attr.name), + ("mutable", str(db_attr.mutable)), + ("input_type", db_attr.input_type), + ("default_value", db_attr.default_value), + ("values", db_attr.values)])) + for db_attr in db_label.attributespec_set.all()]) + ])) for db_label in label_mapping.values() + ] + + if hasattr(db_task.data, "video"): + meta["original_size"] = OrderedDict([ + ("width", str(db_task.data.video.width)), + ("height", str(db_task.data.video.height)) + ]) - ("owner", OrderedDict([ - ("username", self._db_task.owner.username), - ("email", self._db_task.owner.email) - ]) if self._db_task.owner else ""), + return meta - ("assignee", OrderedDict([ - ("username", self._db_task.assignee.username), - ("email", self._db_task.assignee.email) - ]) if self._db_task.assignee else ""), - ])), + def _init_meta(self): + self._meta = OrderedDict([ + ("task", self.meta_for_task(self._db_task, self._host, self._label_mapping)), ("dumped", str(timezone.localtime(timezone.now()))) ]) if hasattr(self._db_task.data, "video"): - self._meta["task"]["original_size"] = OrderedDict([ - ("width", str(self._db_task.data.video.width)), - ("height", str(self._db_task.data.video.height)) - ]) # Add source to dumped file self._meta["source"] = str( osp.basename(self._db_task.data.video.path)) - def _export_attributes(self, attributes): - exported_attributes = [] - for attr in attributes: - attribute_name = self._get_attribute_name(attr["spec_id"]) - exported_attributes.append(TaskData.Attribute( - name=attribute_name, - value=attr["value"], - )) - return exported_attributes - def _export_tracked_shape(self, shape): return TaskData.TrackedShape( type=shape["type"], @@ -356,30 +401,6 @@ class TaskData: if self._get_attribute_id(label_id, attrib.name)] return _tag - def _import_attribute(self, label_id, attribute): - spec_id = self._get_attribute_id(label_id, attribute.name) - value = attribute.value - - if spec_id: - spec = self._attribute_mapping[label_id]['spec'][spec_id] - - try: - if spec.input_type == AttributeType.NUMBER: - pass # no extra processing required - elif spec.input_type == AttributeType.CHECKBOX: - if isinstance(value, str): - value = value.lower() - assert value in {'true', 'false'} - elif isinstance(value, (bool, int, float)): - value = 'true' if value else 'false' - else: - raise ValueError("Unexpected attribute value") - except Exception as e: - raise Exception("Failed to convert attribute '%s'='%s': %s" % - (self._get_label_name(label_id), value, e)) - - return { 'spec_id': spec_id, 'value': value } - def _import_shape(self, shape): _shape = shape._asdict() label_id = self._get_label_id(_shape.pop('label')) @@ -482,10 +503,342 @@ class TaskData: return v return None -class CvatTaskDataExtractor(datumaro.SourceExtractor): +class ProjectData(InstanceLabelData): + LabeledShape = NamedTuple('LabledShape', [('type', str), ('frame', int), ('label', str), ('points', List[float]), ('occluded', bool), ('attributes', List[InstanceLabelData.Attribute]), ('source', str), ('group', int), ('z_order', int), ('task_id', int)]) + LabeledShape.__new__.__defaults__ = (0,0) + TrackedShape = NamedTuple('TrackedShape', + [('type', str), ('frame', int), ('points', List[float]), ('occluded', bool), ('outside', bool), ('keyframe', bool), ('attributes', List[InstanceLabelData.Attribute]), ('source', str), ('group', int), ('z_order', int), ('label', str), ('track_id', int)], + ) + TrackedShape.__new__.__defaults__ = ('manual', 0, 0, None, 0) + Track = NamedTuple('Track', [('label', str), ('group', int), ('source', str), ('shapes', List[TrackedShape]), ('task_id', int)]) + Tag = NamedTuple('Tag', [('frame', int), ('label', str), ('attributes', List[InstanceLabelData.Attribute]), ('source', str), ('group', int), ('task_id', int)]) + Tag.__new__.__defaults__ = (0, ) + Frame = NamedTuple('Frame', [('task_id', int), ('subset', str), ('idx', int), ('id', int), ('frame', int), ('name', str), ('width', int), ('height', int), ('labeled_shapes', List[Union[LabeledShape, TrackedShape]]), ('tags', List[Tag])]) + + def __init__(self, annotation_irs: Mapping[str, AnnotationIR], db_project: Project, host: str, create_callback: Callable = None): + self._annotation_irs = annotation_irs + self._db_project = db_project + self._db_tasks: OrderedDict[int, Task] = OrderedDict( + ((db_task.id, db_task) for db_task in db_project.tasks.order_by("subset","id").all()) + ) + self._subsets = set() + self._host = host + self._create_callback = create_callback + self._MAX_ANNO_SIZE = 30000 + self._frame_info: Dict[Tuple[int, int], Literal["path", "width", "height", "subset"]] = dict() + self._frame_mapping: Dict[Tuple[str, str], Tuple[str, str]] = dict() + self._frame_steps: Dict[int, int] = {task.id: task.data.get_frame_step() for task in self._db_tasks.values()} + + for task in self._db_tasks.values(): + self._subsets.add(task.subset) + self._subsets: List[str] = list(self._subsets) + + InstanceLabelData.__init__(self, db_project) + + self._init_task_frame_offsets() + self._init_frame_info() + self._init_meta() + + def abs_frame_id(self, task_id: int, relative_id: int) -> int: + task = self._db_tasks[task_id] + if relative_id not in range(0, task.data.size): + raise ValueError(f"Unknown internal frame id {relative_id}") + return relative_id * task.data.get_frame_step() + task.data.start_frame + self._task_frame_offsets[task_id] + + def rel_frame_id(self, task_id: int, absolute_id: int) -> int: + task = self._db_tasks[task_id] + d, m = divmod( + absolute_id - task.data.start_frame, task.data.get_frame_step()) + if m or d not in range(0, task.data.size): + raise ValueError(f"Unknown frame {absolute_id}") + return d + + def _init_task_frame_offsets(self): + self._task_frame_offsets: Dict[int, int] = dict() + s = 0 + subset = None + + for task in self._db_tasks.values(): + if subset != task.subset: + s = 0 + subset = task.subset + self._task_frame_offsets[task.id] = s + s += task.data.start_frame + task.data.get_frame_step() * task.data.size + + + def _init_frame_info(self): + self._frame_info = dict() + original_names = DefaultDict[Tuple[str, str], int](int) + for task in self._db_tasks.values(): + defaulted_subset = get_defaulted_subset(task.subset, self._subsets) + if hasattr(task.data, 'video'): + self._frame_info.update({(task.id, frame): { + "path": "frame_{:06d}".format(self.abs_frame_id(task.id, frame)), + "width": task.data.video.width, + "height": task.data.video.height, + "subset": defaulted_subset, + } for frame in range(task.data.size)}) + else: + self._frame_info.update({(task.id, self.rel_frame_id(task.id, db_image.frame)): { + "path": mangle_image_name(db_image.path, defaulted_subset, original_names), + "id": db_image.id, + "width": db_image.width, + "height": db_image.height, + "subset": defaulted_subset + } for db_image in task.data.images.all()}) + + self._frame_mapping = { + (self._db_tasks[frame_ident[0]].subset, self._get_filename(info["path"])): frame_ident + for frame_ident, info in self._frame_info.items() + } + + def _init_meta(self): + self._meta = OrderedDict([ + ('project', OrderedDict([ + ('id', str(self._db_project.id)), + ('name', self._db_project.name), + ("bugtracker", self._db_project.bug_tracker), + ("created", str(timezone.localtime(self._db_project.created_date))), + ("updated", str(timezone.localtime(self._db_project.updated_date))), + ("tasks", [ + ('task', + TaskData.meta_for_task(db_task, self._host) + ) for db_task in self._db_tasks.values() + ]), + + ("labels", [ + ("label", OrderedDict([ + ("name", db_label.name), + ("color", db_label.color), + ("attributes", [ + ("attribute", OrderedDict([ + ("name", db_attr.name), + ("mutable", str(db_attr.mutable)), + ("input_type", db_attr.input_type), + ("default_value", db_attr.default_value), + ("values", db_attr.values)])) + for db_attr in db_label.attributespec_set.all()]) + ])) for db_label in self._label_mapping.values() + ]), + + ("owner", OrderedDict([ + ("username", self._db_project.owner.username), + ("email", self._db_project.owner.email), + ]) if self._db_project.owner else ""), + + ("assignee", OrderedDict([ + ("username", self._db_project.assignee.username), + ("email", self._db_project.assignee.email), + ]) if self._db_project.assignee else ""), + ])), + ("dumped", str(timezone.localtime(timezone.now()))) + ]) + + def _export_tracked_shape(self, shape: dict, task_id: int): + return ProjectData.TrackedShape( + type=shape["type"], + frame=self.abs_frame_id(task_id, shape["frame"]), + label=self._get_label_name(shape["label_id"]), + points=shape["points"], + occluded=shape["occluded"], + z_order=shape.get("z_order", 0), + group=shape.get("group", 0), + outside=shape.get("outside", False), + keyframe=shape.get("keyframe", True), + track_id=shape["track_id"], + source=shape.get("source", "manual"), + attributes=self._export_attributes(shape["attributes"]), + ) + + def _export_labeled_shape(self, shape: dict, task_id: int): + return ProjectData.LabeledShape( + type=shape["type"], + label=self._get_label_name(shape["label_id"]), + frame=self.abs_frame_id(task_id, shape["frame"]), + points=shape["points"], + occluded=shape["occluded"], + z_order=shape.get("z_order", 0), + group=shape.get("group", 0), + source=shape["source"], + attributes=self._export_attributes(shape["attributes"]), + task_id=task_id, + ) + + def _export_tag(self, tag: dict, task_id: int): + return ProjectData.Tag( + frame=self.abs_frame_id(task_id, tag["frame"]), + label=self._get_label_name(tag["label_id"]), + group=tag.get("group", 0), + source=tag["source"], + attributes=self._export_attributes(tag["attributes"]), + task_id=task_id + ) + + def group_by_frame(self, include_empty=False): + frames: Dict[Tuple[str, int], ProjectData.Frame] = {} + def get_frame(task_id: int, idx: int) -> ProjectData.Frame: + frame_info = self._frame_info[(task_id, idx)] + abs_frame = self.abs_frame_id(task_id, idx) + if (frame_info["subset"], abs_frame) not in frames: + frames[(frame_info["subset"], abs_frame)] = ProjectData.Frame( + task_id=task_id, + subset=frame_info["subset"], + idx=idx, + id=frame_info.get('id',0), + frame=abs_frame, + name=frame_info["path"], + height=frame_info["height"], + width=frame_info["width"], + labeled_shapes=[], + tags=[], + ) + return frames[(frame_info["subset"], abs_frame)] + + if include_empty: + for ident in self._frame_info: + get_frame(*ident) + + for task in self._db_tasks.values(): + anno_manager = AnnotationManager(self._annotation_irs[task.id]) + for shape in sorted(anno_manager.to_shapes(task.data.size), + key=lambda shape: shape.get("z_order", 0)): + if (task.id, shape['frame']) not in self._frame_info: + continue + if 'track_id' in shape: + if shape['outside']: + continue + exported_shape = self._export_tracked_shape(shape, task.id) + else: + exported_shape = self._export_labeled_shape(shape, task.id) + get_frame(task.id, shape['frame']).labeled_shapes.append(exported_shape) + + for tag in self._annotation_irs[task.id].tags: + get_frame(task.id, tag['frame']).tags.append(self._export_tag(tag, task.id)) + + return iter(frames.values()) + + @property + def shapes(self): + for task in self._db_tasks.values(): + for shape in self._annotation_irs[task.id].shapes: + yield self._export_labeled_shape(shape, task.id) + + @property + def tracks(self): + idx = 0 + for task in self._db_tasks.values(): + for track in self._annotation_irs[task.id].tracks: + tracked_shapes = TrackManager.get_interpolated_shapes( + track, 0, task.data.size + ) + for tracked_shape in tracked_shapes: + tracked_shape["attributes"] += track["attributes"] + tracked_shape["track_id"] = idx + tracked_shape["group"] = track["group"] + tracked_shape["source"] = track["source"] + tracked_shape["label_id"] = track["label_id"] + yield ProjectData.Track( + label=self._get_label_name(track["label_id"]), + group=track["group"], + source=track["source"], + shapes=[self._export_tracked_shape(shape, task.id) + for shape in tracked_shapes], + task_id=task.id + ) + idx+=1 + + @property + def tags(self): + for task in self._db_tasks.values(): + for tag in self._annotation_irs[task.id].tags: + yield self._export_tag(tag, task.id) + + @property + def meta(self): + return self._meta + + @property + def data(self): + raise NotImplementedError() + + @property + def frame_info(self): + return self._frame_info + + @property + def frame_step(self): + return self._frame_steps + + @property + def db_project(self): + return self._db_project + + @property + def subsets(self) -> List[str]: + return self._subsets + + @property + def tasks(self): + return list(self._db_tasks.values()) + + @property + def task_data(self): + for task_id, task in self._db_tasks.items(): + yield TaskData(self._annotation_irs[task_id], task, self._host) + + @staticmethod + def _get_filename(path): + return osp.splitext(path)[0] + + +class CVATDataExtractorMixin: + def __init__(self): + super().__init__() + + def categories(self) -> dict: + raise NotImplementedError() + + @staticmethod + def _load_categories(labels: list): + categories: Dict[datumaro.AnnotationType, datumaro.Categories] = {} + + label_categories = datumaro.LabelCategories(attributes=['occluded']) + + for _, label in labels: + label_categories.add(label['name']) + for _, attr in label['attributes']: + label_categories.attributes.add(attr['name']) + + + categories[datumaro.AnnotationType.label] = label_categories + + return categories + + @staticmethod + def _load_user_info(meta: dict): + return { + "name": meta['owner']['username'], + "createdAt": meta['created'], + "updatedAt": meta['updated'] + } + + def _read_cvat_anno(self, cvat_frame_anno: Union[ProjectData.Frame, TaskData.Frame], labels: list): + categories = self.categories() + label_cat = categories[datumaro.AnnotationType.label] + def map_label(name): return label_cat.find(name)[0] + label_attrs = { + label['name']: label['attributes'] + for _, label in labels + } + + return convert_cvat_anno_to_dm(cvat_frame_anno, label_attrs, map_label) + + +class CvatTaskDataExtractor(datumaro.SourceExtractor, CVATDataExtractorMixin): def __init__(self, task_data, include_images=False, format_type=None, dimension=DimensionType.DIM_2D): super().__init__() - self._categories, self._user = self._load_categories(task_data, dimension=dimension) + self._categories = self._load_categories(task_data.meta['task']['labels']) + self._user = self._load_user_info(task_data.meta['task']) if dimension == DimensionType.DIM_3D else {} self._dimension = dimension self._format_type = format_type dm_items = [] @@ -537,12 +890,13 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor): dm_image = _make_image(frame_data.idx, **image_args) else: dm_image = Image(**image_args) - dm_anno = self._read_cvat_anno(frame_data, task_data) + dm_anno = self._read_cvat_anno(frame_data, task_data.meta['task']['labels']) if dimension == DimensionType.DIM_2D: dm_item = datumaro.DatasetItem(id=osp.splitext(frame_data.name)[0], - annotations=dm_anno, image=dm_image, - attributes={'frame': frame_data.frame}) + annotations=dm_anno, image=dm_image, + attributes={'frame': frame_data.frame + }) elif dimension == DimensionType.DIM_3D: attributes = {'frame': frame_data.frame} if format_type == "sly_pointcloud": @@ -550,13 +904,11 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor): attributes["createdAt"] = self._user["createdAt"] attributes["updatedAt"] = self._user["updatedAt"] attributes["labels"] = [] - index = 0 - for _, label in task_data.meta['task']['labels']: - attributes["labels"].append({"label_id": index, "name": label["name"], "color": label["color"]}) + for (idx, (_, label)) in enumerate(task_data.meta['task']['labels']): + attributes["labels"].append({"label_id": idx, "name": label["name"], "color": label["color"]}) attributes["track_id"] = -1 - index += 1 - dm_item = datumaro.DatasetItem(id=osp.split(frame_data.name)[-1].split('.')[0], + dm_item = datumaro.DatasetItem(id=osp.splitext(osp.split(frame_data.name)[-1])[0], annotations=dm_anno, point_cloud=dm_image[0], related_images=dm_image[1], attributes=attributes) @@ -564,132 +916,245 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor): self._items = dm_items - def __iter__(self): - for item in self._items: - yield item + def _read_cvat_anno(self, cvat_frame_anno: TaskData.Frame, labels: list): + categories = self.categories() + label_cat = categories[datumaro.AnnotationType.label] + def map_label(name): return label_cat.find(name)[0] + label_attrs = { + label['name']: label['attributes'] + for _, label in labels + } - def __len__(self): - return len(self._items) + return convert_cvat_anno_to_dm(cvat_frame_anno, label_attrs, map_label, self._format_type, self._dimension) - def categories(self): - return self._categories +class CVATProjectDataExtractor(datumaro.Extractor, CVATDataExtractorMixin): + def __init__(self, project_data: ProjectData, include_images: bool = False, format_type: str = None, dimension: DimensionType = DimensionType.DIM_2D): + super().__init__() + self._categories = self._load_categories(project_data.meta['project']['labels']) + self._user = self._load_user_info(project_data.meta['project']) if dimension == DimensionType.DIM_3D else {} + self._dimension = dimension + self._format_type = format_type - @staticmethod - def _load_categories(cvat_anno, dimension): - categories = {} + dm_items: List[datumaro.DatasetItem] = [] + + ext_per_task: Dict[int, str] = {} + image_maker_per_task: Dict[int, Callable] = {} + + for task in project_data.tasks: + is_video = task.mode == 'interpolation' + ext_per_task[task.id] = FrameProvider.VIDEO_FRAME_EXT if is_video else '' + if self._dimension == DimensionType.DIM_3D: + def image_maker_factory(task): + images_query = task.data.images.prefetch_related() + def _make_image(i, **kwargs): + loader = osp.join( + task.data.get_upload_dirname(), kwargs['path'], + ) + related_images = [] + image = images_query.get(id=i) + for i in image.related_files.all(): + path = osp.realpath(str(i.path)) + if osp.isfile(path): + related_images.append(path) + return loader, related_images + return _make_image + image_maker_per_task[task.id] = image_maker_factory(task) + elif include_images: + if is_video: + # optimization for videos: use numpy arrays instead of bytes + # some formats or transforms can require image data + def image_maker_factory(task): + frame_provider = FrameProvider(task.data) + def _make_image(i, **kwargs): + loader = lambda _: frame_provider.get_frame(i, + quality=frame_provider.Quality.ORIGINAL, + out_type=frame_provider.Type.NUMPY_ARRAY)[0] + return Image(loader=loader, **kwargs) + return _make_image + else: + # for images use encoded data to avoid recoding + def image_maker_factory(task): + frame_provider = FrameProvider(task.data) + def _make_image(i, **kwargs): + loader = lambda _: frame_provider.get_frame(i, + quality=frame_provider.Quality.ORIGINAL, + out_type=frame_provider.Type.BUFFER)[0].getvalue() + return ByteImage(data=loader, **kwargs) + return _make_image + image_maker_per_task[task.id] = image_maker_factory(task) + + for frame_data in project_data.group_by_frame(include_empty=True): + image_args = { + 'path': frame_data.name + ext_per_task[frame_data.task_id], + 'size': (frame_data.height, frame_data.width), + } + if self._dimension == DimensionType.DIM_3D: + dm_image = image_maker_per_task[frame_data.task_id](frame_data.id, **image_args) + elif include_images: + dm_image = image_maker_per_task[frame_data.task_id](frame_data.idx, **image_args) + else: + dm_image = Image(**image_args) + dm_anno = self._read_cvat_anno(frame_data, project_data.meta['project']['labels']) + if self._dimension == DimensionType.DIM_2D: + dm_item = datumaro.DatasetItem(id=osp.splitext(frame_data.name)[0], + annotations=dm_anno, image=dm_image, + subset=frame_data.subset, + attributes={'frame': frame_data.frame} + ) + else: + attributes = {'frame': frame_data.frame} + if format_type == "sly_pointcloud": + attributes["name"] = self._user["name"] + attributes["createdAt"] = self._user["createdAt"] + attributes["updatedAt"] = self._user["updatedAt"] + attributes["labels"] = [] + for (idx, (_, label)) in enumerate(project_data.meta['project']['labels']): + attributes["labels"].append({"label_id": idx, "name": label["name"], "color": label["color"]}) + attributes["track_id"] = -1 - label_categories = datumaro.LabelCategories(attributes=['occluded']) + dm_item = datumaro.DatasetItem(id=osp.splitext(osp.split(frame_data.name)[-1])[0], + annotations=dm_anno, point_cloud=dm_image[0], related_images=dm_image[1], + attributes=attributes, subset=frame_data.subset) + dm_items.append(dm_item) - user_info = {} - if dimension == DimensionType.DIM_3D: - user_info = {"name": cvat_anno.meta['task']['owner']['username'], - "createdAt": cvat_anno.meta['task']['created'], - "updatedAt": cvat_anno.meta['task']['updated']} - for _, label in cvat_anno.meta['task']['labels']: - label_categories.add(label['name']) - for _, attr in label['attributes']: - label_categories.attributes.add(attr['name']) + self._items = dm_items + def categories(self): + return self._categories - categories[datumaro.AnnotationType.label] = label_categories + def __iter__(self): + yield from self._items - return categories, user_info + def __len__(self): + return len(self._items) - def _read_cvat_anno(self, cvat_frame_anno, task_data): - item_anno = [] - categories = self.categories() - label_cat = categories[datumaro.AnnotationType.label] - def map_label(name): return label_cat.find(name)[0] - label_attrs = { - label['name']: label['attributes'] - for _, label in task_data.meta['task']['labels'] - } +def GetCVATDataExtractor(instance_data: Union[ProjectData, TaskData], include_images: bool = False, format_type: str = None, dimension: DimensionType = DimensionType.DIM_2D): + if isinstance(instance_data, ProjectData): + return CVATProjectDataExtractor(instance_data, include_images, format_type, dimension) + else: + return CvatTaskDataExtractor(instance_data, include_images, format_type, dimension) + +class CvatImportError(Exception): + pass - def convert_attrs(label, cvat_attrs): - cvat_attrs = {a.name: a.value for a in cvat_attrs} - dm_attr = dict() - for _, a_desc in label_attrs[label]: - a_name = a_desc['name'] - a_value = cvat_attrs.get(a_name, a_desc['default_value']) - try: - if a_desc['input_type'] == AttributeType.NUMBER: - a_value = float(a_value) - elif a_desc['input_type'] == AttributeType.CHECKBOX: - a_value = (a_value.lower() == 'true') - dm_attr[a_name] = a_value - except Exception as e: - raise Exception( - "Failed to convert attribute '%s'='%s': %s" % - (a_name, a_value, e)) - if self._format_type == "sly_pointcloud" and (a_desc.get('input_type') == 'select' or a_desc.get('input_type') == 'radio'): - dm_attr[f"{a_name}__values"] = a_desc["values"] - - return dm_attr - - for tag_obj in cvat_frame_anno.tags: - anno_group = tag_obj.group or 0 - anno_label = map_label(tag_obj.label) - anno_attr = convert_attrs(tag_obj.label, tag_obj.attributes) - - anno = datumaro.Label(label=anno_label, - attributes=anno_attr, group=anno_group) - item_anno.append(anno) - - shapes = [] +def mangle_image_name(name: str, subset: str, names: DefaultDict[Tuple[str, str], int]) -> str: + name, ext = name.rsplit(osp.extsep, maxsplit=1) + + if not names[(subset, name)]: + names[(subset, name)] += 1 + return osp.extsep.join([name, ext]) + else: + image_name = f"{name}_{names[(subset, name)]}" + if not names[(subset, image_name)]: + names[(subset, name)] += 1 + return osp.extsep.join([image_name, ext]) + else: + i = 1 + while i < sys.maxsize: + new_image_name = f"{image_name}_{i}" + if not names[(subset, new_image_name)]: + names[(subset, name)] += 1 + return osp.extsep.join([new_image_name, ext]) + i += 1 + raise Exception('Cannot mangle image name') + +def get_defaulted_subset(subset: str, subsets: List[str]) -> str: + if subset: + return subset + else: + if datumaro.DEFAULT_SUBSET_NAME not in subsets: + return datumaro.DEFAULT_SUBSET_NAME + else: + i = 1 + while i < sys.maxsize: + if f'{datumaro.DEFAULT_SUBSET_NAME}_{i}' not in subsets: + return f'{datumaro.DEFAULT_SUBSET_NAME}_{i}' + i += 1 + raise Exception('Cannot find default name for subset') + + +def convert_cvat_anno_to_dm(cvat_frame_anno, label_attrs, map_label, format_name=None, dimension=DimensionType.DIM_2D): + item_anno = [] + + def convert_attrs(label, cvat_attrs): + cvat_attrs = {a.name: a.value for a in cvat_attrs} + dm_attr = dict() + for _, a_desc in label_attrs[label]: + a_name = a_desc['name'] + a_value = cvat_attrs.get(a_name, a_desc['default_value']) + try: + if a_desc['input_type'] == AttributeType.NUMBER: + a_value = float(a_value) + elif a_desc['input_type'] == AttributeType.CHECKBOX: + a_value = (a_value.lower() == 'true') + dm_attr[a_name] = a_value + except Exception as e: + raise Exception( + "Failed to convert attribute '%s'='%s': %s" % + (a_name, a_value, e)) + return dm_attr + + for tag_obj in cvat_frame_anno.tags: + anno_group = tag_obj.group or 0 + anno_label = map_label(tag_obj.label) + anno_attr = convert_attrs(tag_obj.label, tag_obj.attributes) + + anno = datumaro.Label(label=anno_label, + attributes=anno_attr, group=anno_group) + item_anno.append(anno) + + shapes = [] + if hasattr(cvat_frame_anno, 'shapes'): for shape in cvat_frame_anno.shapes: shapes.append({"id": shape.id, "label_id": shape.label_id}) - for index, shape_obj in enumerate(cvat_frame_anno.labeled_shapes): - anno_group = shape_obj.group or 0 - anno_label = map_label(shape_obj.label) - anno_attr = convert_attrs(shape_obj.label, shape_obj.attributes) - anno_attr['occluded'] = shape_obj.occluded - - if hasattr(shape_obj, 'track_id'): - anno_attr['track_id'] = shape_obj.track_id - anno_attr['keyframe'] = shape_obj.keyframe - - anno_points = shape_obj.points - if shape_obj.type == ShapeType.POINTS: - anno = datumaro.Points(anno_points, - label=anno_label, attributes=anno_attr, group=anno_group, - z_order=shape_obj.z_order) - elif shape_obj.type == ShapeType.POLYLINE: - anno = datumaro.PolyLine(anno_points, - label=anno_label, attributes=anno_attr, group=anno_group, - z_order=shape_obj.z_order) - elif shape_obj.type == ShapeType.POLYGON: - anno = datumaro.Polygon(anno_points, - label=anno_label, attributes=anno_attr, group=anno_group, - z_order=shape_obj.z_order) - elif shape_obj.type == ShapeType.RECTANGLE: - x0, y0, x1, y1 = anno_points - anno = datumaro.Bbox(x0, y0, x1 - x0, y1 - y0, - label=anno_label, attributes=anno_attr, group=anno_group, - z_order=shape_obj.z_order) - elif shape_obj.type == ShapeType.CUBOID: - if self._dimension == DimensionType.DIM_3D: - if self._format_type == "sly_pointcloud": - anno_id = shapes[index]["id"] - anno_attr["label_id"] = shapes[index]["label_id"] - else: - anno_id = index - position, rotation, scale = anno_points[0:3], anno_points[3:6], anno_points[6:9] - anno = datumaro.Cuboid3d(id=anno_id, position=position, rotation=rotation, scale=scale, - label=anno_label, attributes=anno_attr, group=anno_group - ) + for index, shape_obj in enumerate(cvat_frame_anno.labeled_shapes): + anno_group = shape_obj.group or 0 + anno_label = map_label(shape_obj.label) + anno_attr = convert_attrs(shape_obj.label, shape_obj.attributes) + anno_attr['occluded'] = shape_obj.occluded + + if hasattr(shape_obj, 'track_id'): + anno_attr['track_id'] = shape_obj.track_id + anno_attr['keyframe'] = shape_obj.keyframe + + anno_points = shape_obj.points + if shape_obj.type == ShapeType.POINTS: + anno = datumaro.Points(anno_points, + label=anno_label, attributes=anno_attr, group=anno_group, + z_order=shape_obj.z_order) + elif shape_obj.type == ShapeType.POLYLINE: + anno = datumaro.PolyLine(anno_points, + label=anno_label, attributes=anno_attr, group=anno_group, + z_order=shape_obj.z_order) + elif shape_obj.type == ShapeType.POLYGON: + anno = datumaro.Polygon(anno_points, + label=anno_label, attributes=anno_attr, group=anno_group, + z_order=shape_obj.z_order) + elif shape_obj.type == ShapeType.RECTANGLE: + x0, y0, x1, y1 = anno_points + anno = datumaro.Bbox(x0, y0, x1 - x0, y1 - y0, + label=anno_label, attributes=anno_attr, group=anno_group, + z_order=shape_obj.z_order) + elif shape_obj.type == ShapeType.CUBOID: + if dimension == DimensionType.DIM_3D: + if format_name == "sly_pointcloud": + anno_id = shapes[index]["id"] else: - continue + anno_id = index + position, rotation, scale = anno_points[0:3], anno_points[3:6], anno_points[6:9] + anno = datumaro.Cuboid3d(id=anno_id, position=position, rotation=rotation, scale=scale, + label=anno_label, attributes=anno_attr, group=anno_group + ) else: - raise Exception("Unknown shape type '%s'" % shape_obj.type) - - item_anno.append(anno) + continue + else: + raise Exception("Unknown shape type '%s'" % shape_obj.type) - return item_anno + item_anno.append(anno) -class CvatImportError(Exception): - pass + return item_anno def match_dm_item(item, task_data, root_hint=None): is_video = task_data.meta['task']['mode'] == 'interpolation' diff --git a/cvat/apps/dataset_manager/formats/camvid.py b/cvat/apps/dataset_manager/formats/camvid.py index a8fb5059..2522f3fb 100644 --- a/cvat/apps/dataset_manager/formats/camvid.py +++ b/cvat/apps/dataset_manager/formats/camvid.py @@ -7,7 +7,7 @@ from tempfile import TemporaryDirectory from datumaro.components.dataset import Dataset from pyunpack import Archive -from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, +from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations) from cvat.apps.dataset_manager.util import make_zip_archive @@ -16,13 +16,13 @@ from .utils import make_colormap @exporter(name='CamVid', ext='ZIP', version='1.0') -def _export(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) dataset.transform('polygons_to_masks') dataset.transform('boxes_to_masks') dataset.transform('merge_instance_segments') - label_map = make_colormap(task_data) + label_map = make_colormap(instance_data) with TemporaryDirectory() as temp_dir: dataset.export(temp_dir, 'camvid', save_images=save_images, apply_colormap=True, @@ -31,10 +31,10 @@ def _export(dst_file, task_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='CamVid', ext='ZIP', version='1.0') -def _import(src_file, task_data): +def _import(src_file, instance_data): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'camvid', env=dm_env) dataset.transform('masks_to_polygons') - import_dm_annotations(dataset, task_data) + import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/coco.py b/cvat/apps/dataset_manager/formats/coco.py index 3e4fb223..927df2de 100644 --- a/cvat/apps/dataset_manager/formats/coco.py +++ b/cvat/apps/dataset_manager/formats/coco.py @@ -7,7 +7,7 @@ from tempfile import TemporaryDirectory from datumaro.components.dataset import Dataset -from cvat.apps.dataset_manager.bindings import CvatTaskDataExtractor, \ +from cvat.apps.dataset_manager.bindings import GetCVATDataExtractor, \ import_dm_annotations from cvat.apps.dataset_manager.util import make_zip_archive @@ -15,9 +15,9 @@ from .registry import dm_env, exporter, importer @exporter(name='COCO', ext='ZIP', version='1.0') -def _export(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) with TemporaryDirectory() as temp_dir: dataset.export(temp_dir, 'coco_instances', save_images=save_images, merge_images=True) @@ -25,14 +25,14 @@ def _export(dst_file, task_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='COCO', ext='JSON, ZIP', version='1.0') -def _import(src_file, task_data): +def _import(src_file, instance_data): if zipfile.is_zipfile(src_file): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'coco', env=dm_env) - import_dm_annotations(dataset, task_data) + import_dm_annotations(dataset, instance_data) else: dataset = Dataset.import_from(src_file.name, 'coco_instances', env=dm_env) - import_dm_annotations(dataset, task_data) \ No newline at end of file + import_dm_annotations(dataset, instance_data) \ No newline at end of file diff --git a/cvat/apps/dataset_manager/formats/cvat.py b/cvat/apps/dataset_manager/formats/cvat.py index 786a5025..9e7fa514 100644 --- a/cvat/apps/dataset_manager/formats/cvat.py +++ b/cvat/apps/dataset_manager/formats/cvat.py @@ -2,8 +2,10 @@ # # SPDX-License-Identifier: MIT +from io import BufferedWriter import os import os.path as osp +from typing import Callable import zipfile from collections import OrderedDict from glob import glob @@ -11,7 +13,7 @@ from tempfile import TemporaryDirectory from datumaro.components.extractor import DatasetItem -from cvat.apps.dataset_manager.bindings import match_dm_item +from cvat.apps.dataset_manager.bindings import TaskData, match_dm_item, ProjectData, get_defaulted_subset from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.engine.frame_provider import FrameProvider @@ -42,8 +44,10 @@ def create_xml_dumper(file_object): self.xmlgen.characters(self.version) self.xmlgen.endElement("version") - def open_root(self): + def open_document(self): self.xmlgen.startDocument() + + def open_root(self): self.xmlgen.startElement("annotations", {}) self._level += 1 self._add_version() @@ -168,23 +172,34 @@ def create_xml_dumper(file_object): self._level -= 1 self._indent() self.xmlgen.endElement("annotations") + self._indent() + + def close_document(self): self.xmlgen.endDocument() + return XmlAnnotationWriter(file_object) -def dump_as_cvat_annotation(file_object, annotations): - dumper = create_xml_dumper(file_object) +def dump_as_cvat_annotation(dumper, annotations): dumper.open_root() dumper.add_meta(annotations.meta) for frame_annotation in annotations.group_by_frame(include_empty=True): frame_id = frame_annotation.frame - dumper.open_image(OrderedDict([ + image_attrs = OrderedDict([ ("id", str(frame_id)), ("name", frame_annotation.name), + ]) + if isinstance(annotations, ProjectData): + image_attrs.update(OrderedDict([ + ("subset", frame_annotation.subset), + ("task_id", str(frame_annotation.task_id)), + ])) + image_attrs.update(OrderedDict([ ("width", str(frame_annotation.width)), ("height", str(frame_annotation.height)) ])) + dumper.open_image(image_attrs) for shape in frame_annotation.labeled_shapes: dump_data = OrderedDict([ @@ -286,8 +301,7 @@ def dump_as_cvat_annotation(file_object, annotations): dumper.close_image() dumper.close_root() -def dump_as_cvat_interpolation(file_object, annotations): - dumper = create_xml_dumper(file_object) +def dump_as_cvat_interpolation(dumper, annotations): dumper.open_root() dumper.add_meta(annotations.meta) def dump_track(idx, track): @@ -298,6 +312,13 @@ def dump_as_cvat_interpolation(file_object, annotations): ("source", track.source), ]) + if hasattr(track, 'task_id'): + task, = filter(lambda task: task.id == track.task_id, annotations.tasks) + dump_data.update(OrderedDict([ + ('task_id', str(track.task_id)), + ('subset', get_defaulted_subset(task.subset, annotations.subsets)), + ])) + if track.group: dump_data['group_id'] = str(track.group) dumper.open_track(dump_data) @@ -383,11 +404,17 @@ def dump_as_cvat_interpolation(file_object, annotations): counter += 1 for shape in annotations.shapes: - dump_track(counter, annotations.Track( - label=shape.label, - group=shape.group, - source=shape.source, - shapes=[annotations.TrackedShape( + frame_step = annotations.frame_step if isinstance(annotations, TaskData) else annotations.frame_step[shape.task_id] + if isinstance(annotations, TaskData): + stop_frame = int(annotations.meta['task']['stop_frame']) + else: + task_meta = list(filter(lambda task: int(task[1]['id']) == shape.task_id, annotations.meta['project']['tasks']))[0][1] + stop_frame = int(task_meta['stop_frame']) + track = { + 'label': shape.label, + 'group': shape.group, + 'source': shape.source, + 'shapes': [annotations.TrackedShape( type=shape.type, points=shape.points, occluded=shape.occluded, @@ -405,13 +432,15 @@ def dump_as_cvat_interpolation(file_object, annotations): outside=True, keyframe=True, z_order=shape.z_order, - frame=shape.frame + annotations.frame_step, + frame=shape.frame + frame_step, attributes=shape.attributes, - )] if shape.frame + annotations.frame_step < \ - int(annotations.meta['task']['stop_frame']) \ + )] if shape.frame + frame_step < \ + stop_frame \ else [] ), - )) + } + if isinstance(annotations, ProjectData): track['task_id'] = shape.task_id + dump_track(counter, annotations.Track(**track)) counter += 1 dumper.close_root() @@ -527,39 +556,76 @@ def load(file_object, annotations): tag = None el.clear() -def _export(dst_file, task_data, anno_callback, save_images=False): +def dump_task_anno(dst_file, task_data, callback): + dumper = create_xml_dumper(dst_file) + dumper.open_document() + callback(dumper, task_data) + dumper.close_document() + +def dump_project_anno(dst_file: BufferedWriter, project_data: ProjectData, callback: Callable): + dumper = create_xml_dumper(dst_file) + dumper.open_document() + callback(dumper, project_data) + dumper.close_document() + +def dump_media_files(task_data: TaskData, img_dir: str, project_data: ProjectData = None): + ext = '' + if task_data.meta['task']['mode'] == 'interpolation': + ext = FrameProvider.VIDEO_FRAME_EXT + + frame_provider = FrameProvider(task_data.db_task.data) + frames = frame_provider.get_frames( + frame_provider.Quality.ORIGINAL, + frame_provider.Type.BUFFER) + for frame_id, (frame_data, _) in enumerate(frames): + frame_name = task_data.frame_info[frame_id]['path'] if project_data is None \ + else project_data.frame_info[(task_data.db_task.id, frame_id)]['path'] + 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()) + +def _export_task(dst_file, task_data, anno_callback, save_images=False): with TemporaryDirectory() as temp_dir: with open(osp.join(temp_dir, 'annotations.xml'), 'wb') as f: - anno_callback(f, task_data) + dump_task_anno(f, task_data, anno_callback) if save_images: - ext = '' - if task_data.meta['task']['mode'] == 'interpolation': - ext = FrameProvider.VIDEO_FRAME_EXT - - img_dir = osp.join(temp_dir, 'images') - frame_provider = FrameProvider(task_data.db_task.data) - frames = frame_provider.get_frames( - frame_provider.Quality.ORIGINAL, - frame_provider.Type.BUFFER) - for frame_id, (frame_data, _) in enumerate(frames): - frame_name = task_data.frame_info[frame_id]['path'] - 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()) + dump_media_files(task_data, osp.join(temp_dir, 'images')) + + make_zip_archive(temp_dir, dst_file) + +def _export_project(dst_file: str, project_data: ProjectData, anno_callback: Callable, save_images: bool=False): + with TemporaryDirectory() as temp_dir: + with open(osp.join(temp_dir, 'annotations.xml'), 'wb') as f: + dump_project_anno(f, project_data, anno_callback) + + if save_images: + for task_data in project_data.task_data: + subset = get_defaulted_subset(task_data.db_task.subset, project_data.subsets) + subset_dir = osp.join(temp_dir, 'images', subset) + os.makedirs(subset_dir, exist_ok=True) + dump_media_files(task_data, subset_dir, project_data) make_zip_archive(temp_dir, dst_file) @exporter(name='CVAT for video', ext='ZIP', version='1.1') -def _export_video(dst_file, task_data, save_images=False): - _export(dst_file, task_data, - anno_callback=dump_as_cvat_interpolation, save_images=save_images) +def _export_video(dst_file, instance_data, save_images=False): + if isinstance(instance_data, ProjectData): + _export_project(dst_file, instance_data, + anno_callback=dump_as_cvat_interpolation, save_images=save_images) + else: + _export_task(dst_file, instance_data, + anno_callback=dump_as_cvat_interpolation, save_images=save_images) @exporter(name='CVAT for images', ext='ZIP', version='1.1') -def _export_images(dst_file, task_data, save_images=False): - _export(dst_file, task_data, - anno_callback=dump_as_cvat_annotation, save_images=save_images) +def _export_images(dst_file, instance_data, save_images=False): + if isinstance(instance_data, ProjectData): + _export_project(dst_file, instance_data, + anno_callback=dump_as_cvat_annotation, save_images=save_images) + else: + _export_task(dst_file, instance_data, + anno_callback=dump_as_cvat_annotation, save_images=save_images) @importer(name='CVAT', ext='XML, ZIP', version='1.1') def _import(src_file, task_data): diff --git a/cvat/apps/dataset_manager/formats/datumaro/__init__.py b/cvat/apps/dataset_manager/formats/datumaro/__init__.py index 0f351f83..f4fe0423 100644 --- a/cvat/apps/dataset_manager/formats/datumaro/__init__.py +++ b/cvat/apps/dataset_manager/formats/datumaro/__init__.py @@ -8,8 +8,8 @@ import os.path as osp import shutil from tempfile import TemporaryDirectory -from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, - import_dm_annotations) +from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, + import_dm_annotations, ProjectData) from cvat.apps.dataset_manager.util import make_zip_archive from cvat.settings.base import BASE_DIR from datumaro.components.project import Project @@ -23,23 +23,28 @@ class DatumaroProjectExporter: _TEMPLATES_DIR = osp.join(osp.dirname(__file__), 'export_templates') @staticmethod - def _save_image_info(save_dir, task_data): + def _save_image_info(save_dir, instance_data): os.makedirs(save_dir, exist_ok=True) config = { - 'server_url': task_data._host or 'localhost', - 'task_id': task_data.db_task.id, + 'server_url': instance_data._host or 'localhost' } + if isinstance(instance_data, ProjectData): + config['project_id'] = instance_data.db_project.id + else: + config['task_id'] = instance_data.db_task.id images = [] images_meta = { 'images': images, } - for frame_id, frame in task_data.frame_info.items(): - images.append({ + for frame_id, frame in enumerate(instance_data.frame_info.values()): + image_info = { 'id': frame_id, 'name': osp.basename(frame['path']), 'width': frame['width'], 'height': frame['height'], - }) + } + if isinstance(instance_data, ProjectData): + image_info['subset'] = frame['subset'] with open(osp.join(save_dir, 'config.json'), 'w', encoding='utf-8') as config_file: @@ -48,11 +53,12 @@ class DatumaroProjectExporter: 'w', encoding='utf-8') as images_file: json.dump(images_meta, images_file) - def _export(self, task_data, save_dir, save_images=False): - dataset = CvatTaskDataExtractor(task_data, include_images=save_images) + def _export(self, instance_data, save_dir, save_images=False): + dataset = GetCVATDataExtractor(instance_data, include_images=save_images) + db_instance = instance_data.db_project if isinstance(instance_data, ProjectData) else instance_data.db_task dm_env.converters.get('datumaro_project').convert(dataset, save_dir=save_dir, save_images=save_images, - project_config={ 'project_name': task_data.db_task.name, } + project_config={ 'project_name': db_instance.name, } ) project = Project.load(save_dir) @@ -64,13 +70,16 @@ class DatumaroProjectExporter: if not save_images: # add remote links to images - source_name = 'task_%s_images' % task_data.db_task.id + source_name = '{}_{}_images'.format( + 'project' if isinstance(instance_data, ProjectData) else 'task', + db_instance.id, + ) project.add_source(source_name, { 'format': self._REMOTE_IMAGES_EXTRACTOR, }) self._save_image_info( osp.join(save_dir, project.local_source_dir(source_name)), - task_data) + instance_data) project.save() templates_dir = osp.join(self._TEMPLATES_DIR, 'plugins') @@ -87,7 +96,7 @@ class DatumaroProjectExporter: shutil.copytree(osp.join(BASE_DIR, 'utils', 'cli'), osp.join(cvat_utils_dst_dir, 'cli')) - def __call__(self, dst_file, task_data, save_images=False): + def __call__(self, dst_file, instance_data, save_images=False): with TemporaryDirectory() as temp_dir: - self._export(task_data, save_dir=temp_dir, save_images=save_images) + self._export(instance_data, save_dir=temp_dir, save_images=save_images) make_zip_archive(temp_dir, dst_file) diff --git a/cvat/apps/dataset_manager/formats/icdar.py b/cvat/apps/dataset_manager/formats/icdar.py index 03eda245..544e20de 100644 --- a/cvat/apps/dataset_manager/formats/icdar.py +++ b/cvat/apps/dataset_manager/formats/icdar.py @@ -9,7 +9,7 @@ from datumaro.components.dataset import Dataset from datumaro.components.extractor import (AnnotationType, Caption, Label, LabelCategories, ItemTransform) -from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, +from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations) from cvat.apps.dataset_manager.util import make_zip_archive @@ -75,45 +75,45 @@ class LabelToCaption(ItemTransform): return item.wrap(annotations=annotations) @exporter(name='ICDAR Recognition', ext='ZIP', version='1.0') -def _export_recognition(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export_recognition(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) dataset.transform(LabelToCaption) with TemporaryDirectory() as temp_dir: dataset.export(temp_dir, 'icdar_word_recognition', save_images=save_images) make_zip_archive(temp_dir, dst_file) @importer(name='ICDAR Recognition', ext='ZIP', version='1.0') -def _import(src_file, task_data): +def _import(src_file, instance_data): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'icdar_word_recognition', env=dm_env) dataset.transform(CaptionToLabel, 'icdar') - import_dm_annotations(dataset, task_data) + import_dm_annotations(dataset, instance_data) @exporter(name='ICDAR Localization', ext='ZIP', version='1.0') -def _export_localization(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export_localization(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) with TemporaryDirectory() as temp_dir: dataset.export(temp_dir, 'icdar_text_localization', save_images=save_images) make_zip_archive(temp_dir, dst_file) @importer(name='ICDAR Localization', ext='ZIP', version='1.0') -def _import(src_file, task_data): +def _import(src_file, instance_data): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'icdar_text_localization', env=dm_env) dataset.transform(AddLabelToAnns, 'icdar') - import_dm_annotations(dataset, task_data) + import_dm_annotations(dataset, instance_data) @exporter(name='ICDAR Segmentation', ext='ZIP', version='1.0') -def _export_segmentation(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export_segmentation(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) with TemporaryDirectory() as temp_dir: dataset.transform('polygons_to_masks') dataset.transform('boxes_to_masks') @@ -122,10 +122,10 @@ def _export_segmentation(dst_file, task_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='ICDAR Segmentation', ext='ZIP', version='1.0') -def _import(src_file, task_data): +def _import(src_file, instance_data): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'icdar_text_segmentation', env=dm_env) dataset.transform(AddLabelToAnns, 'icdar') dataset.transform('masks_to_polygons') - import_dm_annotations(dataset, task_data) + import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/imagenet.py b/cvat/apps/dataset_manager/formats/imagenet.py index 2ed0cb47..1085ef74 100644 --- a/cvat/apps/dataset_manager/formats/imagenet.py +++ b/cvat/apps/dataset_manager/formats/imagenet.py @@ -9,7 +9,7 @@ from tempfile import TemporaryDirectory from datumaro.components.dataset import Dataset -from cvat.apps.dataset_manager.bindings import CvatTaskDataExtractor, \ +from cvat.apps.dataset_manager.bindings import GetCVATDataExtractor, \ import_dm_annotations from cvat.apps.dataset_manager.util import make_zip_archive @@ -17,9 +17,9 @@ from .registry import dm_env, exporter, importer @exporter(name='ImageNet', ext='ZIP', version='1.0') -def _export(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) with TemporaryDirectory() as temp_dir: if save_images: dataset.export(temp_dir, 'imagenet', save_images=save_images) @@ -29,11 +29,11 @@ def _export(dst_file, task_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='ImageNet', ext='ZIP', version='1.0') -def _import(src_file, task_data): +def _import(src_file, instance_data): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) if glob(osp.join(tmp_dir, '*.txt')): dataset = Dataset.import_from(tmp_dir, 'imagenet_txt', env=dm_env) else: dataset = Dataset.import_from(tmp_dir, 'imagenet', env=dm_env) - import_dm_annotations(dataset, task_data) \ No newline at end of file + import_dm_annotations(dataset, instance_data) \ No newline at end of file diff --git a/cvat/apps/dataset_manager/formats/labelme.py b/cvat/apps/dataset_manager/formats/labelme.py index 744b11fa..2fc1f7f7 100644 --- a/cvat/apps/dataset_manager/formats/labelme.py +++ b/cvat/apps/dataset_manager/formats/labelme.py @@ -7,7 +7,7 @@ from tempfile import TemporaryDirectory from datumaro.components.dataset import Dataset from pyunpack import Archive -from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, +from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations) from cvat.apps.dataset_manager.util import make_zip_archive @@ -15,19 +15,19 @@ from .registry import dm_env, exporter, importer @exporter(name='LabelMe', ext='ZIP', version='3.0') -def _export(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) with TemporaryDirectory() as temp_dir: dataset.export(temp_dir, 'label_me', save_images=save_images) make_zip_archive(temp_dir, dst_file) @importer(name='LabelMe', ext='ZIP', version='3.0') -def _import(src_file, task_data): +def _import(src_file, instance_data): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'label_me', env=dm_env) dataset.transform('masks_to_polygons') - import_dm_annotations(dataset, task_data) + import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/market1501.py b/cvat/apps/dataset_manager/formats/market1501.py index f94d65dc..f578d3ab 100644 --- a/cvat/apps/dataset_manager/formats/market1501.py +++ b/cvat/apps/dataset_manager/formats/market1501.py @@ -9,7 +9,7 @@ from datumaro.components.dataset import Dataset from datumaro.components.extractor import (AnnotationType, Label, LabelCategories, ItemTransform) -from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, +from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations) from cvat.apps.dataset_manager.util import make_zip_archive @@ -60,19 +60,19 @@ class LabelAttrToAttr(ItemTransform): @exporter(name='Market-1501', ext='ZIP', version='1.0') -def _export(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) with TemporaryDirectory() as temp_dir: dataset.transform(LabelAttrToAttr, 'market-1501') dataset.export(temp_dir, 'market1501', save_images=save_images) make_zip_archive(temp_dir, dst_file) @importer(name='Market-1501', ext='ZIP', version='1.0') -def _import(src_file, task_data): +def _import(src_file, instance_data): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'market1501', env=dm_env) dataset.transform(AttrToLabelAttr, 'market-1501') - import_dm_annotations(dataset, task_data) + import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/mask.py b/cvat/apps/dataset_manager/formats/mask.py index 3e3780e8..67d61eed 100644 --- a/cvat/apps/dataset_manager/formats/mask.py +++ b/cvat/apps/dataset_manager/formats/mask.py @@ -7,7 +7,7 @@ from tempfile import TemporaryDirectory from datumaro.components.dataset import Dataset from pyunpack import Archive -from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, +from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations) from cvat.apps.dataset_manager.util import make_zip_archive @@ -16,23 +16,23 @@ from .utils import make_colormap @exporter(name='Segmentation mask', ext='ZIP', version='1.1') -def _export(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) dataset.transform('polygons_to_masks') dataset.transform('boxes_to_masks') dataset.transform('merge_instance_segments') with TemporaryDirectory() as temp_dir: dataset.export(temp_dir, 'voc_segmentation', save_images=save_images, - apply_colormap=True, label_map=make_colormap(task_data)) + apply_colormap=True, label_map=make_colormap(instance_data)) make_zip_archive(temp_dir, dst_file) @importer(name='Segmentation mask', ext='ZIP', version='1.1') -def _import(src_file, task_data): +def _import(src_file, instance_data): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'voc', env=dm_env) dataset.transform('masks_to_polygons') - import_dm_annotations(dataset, task_data) + import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/mot.py b/cvat/apps/dataset_manager/formats/mot.py index 29d5182a..26fc7b0d 100644 --- a/cvat/apps/dataset_manager/formats/mot.py +++ b/cvat/apps/dataset_manager/formats/mot.py @@ -8,16 +8,16 @@ import datumaro.components.extractor as datumaro from datumaro.components.dataset import Dataset from pyunpack import Archive -from cvat.apps.dataset_manager.bindings import CvatTaskDataExtractor +from cvat.apps.dataset_manager.bindings import GetCVATDataExtractor from cvat.apps.dataset_manager.util import make_zip_archive from .registry import dm_env, exporter, importer @exporter(name='MOT', ext='ZIP', version='1.1') -def _export(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) with TemporaryDirectory() as temp_dir: dataset.export(temp_dir, 'mot_seq_gt', save_images=save_images) diff --git a/cvat/apps/dataset_manager/formats/mots.py b/cvat/apps/dataset_manager/formats/mots.py index b8b562ec..9ba5b226 100644 --- a/cvat/apps/dataset_manager/formats/mots.py +++ b/cvat/apps/dataset_manager/formats/mots.py @@ -8,7 +8,7 @@ from datumaro.components.dataset import Dataset from datumaro.components.extractor import AnnotationType, ItemTransform from pyunpack import Archive -from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, +from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, find_dataset_root, match_dm_item) from cvat.apps.dataset_manager.util import make_zip_archive @@ -21,9 +21,9 @@ class KeepTracks(ItemTransform): if 'track_id' in a.attributes]) @exporter(name='MOTS PNG', ext='ZIP', version='1.0') -def _export(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) dataset.transform(KeepTracks) # can only export tracks dataset.transform('polygons_to_masks') dataset.transform('boxes_to_masks') @@ -46,15 +46,27 @@ def _import(src_file, task_data): root_hint = find_dataset_root(dataset, task_data) + shift = 0 for item in dataset: frame_number = task_data.abs_frame_id( match_dm_item(item, task_data, root_hint=root_hint)) + track_ids = set() + for ann in item.annotations: if ann.type != AnnotationType.polygon: continue track_id = ann.attributes['track_id'] + group_id = track_id + + if track_id in track_ids: + # use negative id for tracks with the same id on the same frame + shift -= 1 + track_id = shift + else: + track_ids.add(track_id) + shape = task_data.TrackedShape( type='polygon', points=ann.points, @@ -65,6 +77,7 @@ def _import(src_file, task_data): frame=frame_number, attributes=[], source='manual', + group=group_id ) # build trajectories as lists of shapes in track dict diff --git a/cvat/apps/dataset_manager/formats/pascal_voc.py b/cvat/apps/dataset_manager/formats/pascal_voc.py index 3f10b93a..93504628 100644 --- a/cvat/apps/dataset_manager/formats/pascal_voc.py +++ b/cvat/apps/dataset_manager/formats/pascal_voc.py @@ -11,17 +11,17 @@ from tempfile import TemporaryDirectory from datumaro.components.dataset import Dataset from pyunpack import Archive -from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, - import_dm_annotations) +from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, + ProjectData, import_dm_annotations) from cvat.apps.dataset_manager.util import make_zip_archive from .registry import dm_env, exporter, importer @exporter(name='PASCAL VOC', ext='ZIP', version='1.1') -def _export(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) with TemporaryDirectory() as temp_dir: dataset.export(temp_dir, 'voc', save_images=save_images, label_map='source') @@ -29,15 +29,16 @@ def _export(dst_file, task_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='PASCAL VOC', ext='ZIP', version='1.1') -def _import(src_file, task_data): +def _import(src_file, instance_data): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) # put label map from the task if not present labelmap_file = osp.join(tmp_dir, 'labelmap.txt') if not osp.isfile(labelmap_file): - labels = (label['name'] + ':::' - for _, label in task_data.meta['task']['labels']) + labels_meta = instance_data.meta['project']['labels'] \ + if isinstance(instance_data, ProjectData) else instance_data.meta['task']['labels'] + labels = (label['name'] + ':::' for _, label in labels_meta) with open(labelmap_file, 'w') as f: f.write('\n'.join(labels)) @@ -57,4 +58,4 @@ def _import(src_file, task_data): dataset = Dataset.import_from(tmp_dir, 'voc', env=dm_env) dataset.transform('masks_to_polygons') - import_dm_annotations(dataset, task_data) + import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/pointcloud.py b/cvat/apps/dataset_manager/formats/pointcloud.py index 1fc31e4a..0009cd2f 100644 --- a/cvat/apps/dataset_manager/formats/pointcloud.py +++ b/cvat/apps/dataset_manager/formats/pointcloud.py @@ -7,7 +7,7 @@ from tempfile import TemporaryDirectory from datumaro.components.dataset import Dataset -from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, +from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations) from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.engine.models import DimensionType @@ -18,7 +18,7 @@ from .registry import dm_env, exporter, importer @exporter(name='Sly Point Cloud Format', ext='ZIP', version='1.0', dimension=DimensionType.DIM_3D) def _export_images(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( + dataset = Dataset.from_extractors(GetCVATDataExtractor( task_data, include_images=save_images, format_type='sly_pointcloud', dimension=DimensionType.DIM_3D), env=dm_env) with TemporaryDirectory() as temp_dir: diff --git a/cvat/apps/dataset_manager/formats/tfrecord.py b/cvat/apps/dataset_manager/formats/tfrecord.py index 9847bf61..d9c705a7 100644 --- a/cvat/apps/dataset_manager/formats/tfrecord.py +++ b/cvat/apps/dataset_manager/formats/tfrecord.py @@ -6,7 +6,7 @@ from tempfile import TemporaryDirectory from pyunpack import Archive -from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, +from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations) from cvat.apps.dataset_manager.util import make_zip_archive from datumaro.components.project import Dataset @@ -23,18 +23,18 @@ except ImportError: @exporter(name='TFRecord', ext='ZIP', version='1.0', enabled=tf_available) -def _export(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) with TemporaryDirectory() as temp_dir: dataset.export(temp_dir, 'tf_detection_api', save_images=save_images) make_zip_archive(temp_dir, dst_file) @importer(name='TFRecord', ext='ZIP', version='1.0', enabled=tf_available) -def _import(src_file, task_data): +def _import(src_file, instance_data): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'tf_detection_api', env=dm_env) - import_dm_annotations(dataset, task_data) + import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/utils.py b/cvat/apps/dataset_manager/formats/utils.py index 184a1331..0d545e46 100644 --- a/cvat/apps/dataset_manager/formats/utils.py +++ b/cvat/apps/dataset_manager/formats/utils.py @@ -48,8 +48,9 @@ def rgb2hex(color): def hex2rgb(color): return tuple(int(color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4)) -def make_colormap(task_data): - labels = [label for _, label in task_data.meta['task']['labels']] +def make_colormap(instance_data): + instance_name = 'project' if 'project' in instance_data.meta.keys() else 'task' + labels = [label for _, label in instance_data.meta[instance_name]['labels']] label_names = [label['name'] for label in labels] if 'background' not in label_names: diff --git a/cvat/apps/dataset_manager/formats/velodynepoint.py b/cvat/apps/dataset_manager/formats/velodynepoint.py index 12eafbce..747c4751 100644 --- a/cvat/apps/dataset_manager/formats/velodynepoint.py +++ b/cvat/apps/dataset_manager/formats/velodynepoint.py @@ -7,7 +7,7 @@ from tempfile import TemporaryDirectory from datumaro.components.dataset import Dataset -from cvat.apps.dataset_manager.bindings import CvatTaskDataExtractor, \ +from cvat.apps.dataset_manager.bindings import GetCVATDataExtractor, \ import_dm_annotations from .registry import dm_env @@ -20,7 +20,7 @@ from .registry import exporter, importer @exporter(name='Kitti Raw Format', ext='ZIP', version='1.0', dimension=DimensionType.DIM_3D) def _export_images(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( + dataset = Dataset.from_extractors(GetCVATDataExtractor( task_data, include_images=save_images, format_type="kitti_raw", dimension=DimensionType.DIM_3D), env=dm_env) with TemporaryDirectory() as temp_dir: diff --git a/cvat/apps/dataset_manager/formats/vggface2.py b/cvat/apps/dataset_manager/formats/vggface2.py index 0ae6d9a9..d75f960a 100644 --- a/cvat/apps/dataset_manager/formats/vggface2.py +++ b/cvat/apps/dataset_manager/formats/vggface2.py @@ -7,7 +7,7 @@ from tempfile import TemporaryDirectory from datumaro.components.dataset import Dataset -from cvat.apps.dataset_manager.bindings import CvatTaskDataExtractor, \ +from cvat.apps.dataset_manager.bindings import GetCVATDataExtractor, \ import_dm_annotations from cvat.apps.dataset_manager.util import make_zip_archive @@ -15,19 +15,19 @@ from .registry import dm_env, exporter, importer @exporter(name='VGGFace2', ext='ZIP', version='1.0') -def _export(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) with TemporaryDirectory() as temp_dir: dataset.export(temp_dir, 'vgg_face2', save_images=save_images) make_zip_archive(temp_dir, dst_file) @importer(name='VGGFace2', ext='ZIP', version='1.0') -def _import(src_file, task_data): +def _import(src_file, instance_data): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'vgg_face2', env=dm_env) dataset.transform('rename', r"|([^/]+/)?(.+)|\2|") - import_dm_annotations(dataset, task_data) + import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/widerface.py b/cvat/apps/dataset_manager/formats/widerface.py index 7f120ffe..b578c14c 100644 --- a/cvat/apps/dataset_manager/formats/widerface.py +++ b/cvat/apps/dataset_manager/formats/widerface.py @@ -7,7 +7,7 @@ from tempfile import TemporaryDirectory from datumaro.components.dataset import Dataset -from cvat.apps.dataset_manager.bindings import CvatTaskDataExtractor, \ +from cvat.apps.dataset_manager.bindings import GetCVATDataExtractor, \ import_dm_annotations from cvat.apps.dataset_manager.util import make_zip_archive @@ -15,18 +15,18 @@ from .registry import dm_env, exporter, importer @exporter(name='WiderFace', ext='ZIP', version='1.0') -def _export(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) with TemporaryDirectory() as temp_dir: dataset.export(temp_dir, 'wider_face', save_images=save_images) make_zip_archive(temp_dir, dst_file) @importer(name='WiderFace', ext='ZIP', version='1.0') -def _import(src_file, task_data): +def _import(src_file, instance_data): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'wider_face', env=dm_env) - import_dm_annotations(dataset, task_data) + import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/yolo.py b/cvat/apps/dataset_manager/formats/yolo.py index 0df6f5fe..6327f3c0 100644 --- a/cvat/apps/dataset_manager/formats/yolo.py +++ b/cvat/apps/dataset_manager/formats/yolo.py @@ -8,7 +8,7 @@ from tempfile import TemporaryDirectory from pyunpack import Archive -from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, +from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations, match_dm_item, find_dataset_root) from cvat.apps.dataset_manager.util import make_zip_archive from datumaro.components.extractor import DatasetItem @@ -19,9 +19,9 @@ from .registry import dm_env, exporter, importer @exporter(name='YOLO', ext='ZIP', version='1.1') -def _export(dst_file, task_data, save_images=False): - dataset = Dataset.from_extractors(CvatTaskDataExtractor( - task_data, include_images=save_images), env=dm_env) +def _export(dst_file, instance_data, save_images=False): + dataset = Dataset.from_extractors(GetCVATDataExtractor( + instance_data, include_images=save_images), env=dm_env) with TemporaryDirectory() as temp_dir: dataset.export(temp_dir, 'yolo', save_images=save_images) diff --git a/cvat/apps/dataset_manager/project.py b/cvat/apps/dataset_manager/project.py new file mode 100644 index 00000000..866a75d4 --- /dev/null +++ b/cvat/apps/dataset_manager/project.py @@ -0,0 +1,71 @@ +# Copyright (C) 2021 Intel Corporation +# +# SPDX-License-Identifier: MIT + +from typing import Callable + +from django.db import transaction + +from cvat.apps.engine import models +from cvat.apps.dataset_manager.task import TaskAnnotation + +from .annotation import AnnotationIR +from .bindings import ProjectData +from .formats.registry import make_exporter + +def export_project(project_id, dst_file, format_name, + server_url=None, save_images=False): + # For big tasks dump function may run for a long time and + # we dont need to acquire lock after the task has been initialized from DB. + # But there is the bug with corrupted dump file in case 2 or + # more dump request received at the same time: + # https://github.com/opencv/cvat/issues/217 + with transaction.atomic(): + project = ProjectAnnotation(project_id) + project.init_from_db() + + exporter = make_exporter(format_name) + with open(dst_file, 'wb') as f: + project.export(f, exporter, host=server_url, save_images=save_images) + +class ProjectAnnotation: + def __init__(self, pk: int): + self.db_project = models.Project.objects.get(id=pk) + self.db_tasks = models.Task.objects.filter(project__id=pk).order_by('id') + + self.annotation_irs: dict[int, AnnotationIR] = dict() + + def reset(self): + for annotation_ir in self.annotation_irs.values(): + annotation_ir.reset() + + def put(self, data): + raise NotImplementedError() + + def create(self, data): + raise NotImplementedError() + + def update(self, data): + raise NotImplementedError() + + def delete(self, data=None): + raise NotImplementedError() + + def init_from_db(self): + self.reset() + + for task in self.db_tasks: + annotation = TaskAnnotation(pk=task.id) + annotation.init_from_db() + self.annotation_irs[task.id] = annotation.ir_data + + def export(self, dst_file: str, exporter: Callable, host: str='', **options): + project_data = ProjectData( + annotation_irs=self.annotation_irs, + db_project=self.db_project, + host=host + ) + exporter(dst_file, project_data, **options) + @property + def data(self) -> dict: + raise NotImplementedError() \ No newline at end of file diff --git a/cvat/apps/dataset_manager/tests/assets/projects.json b/cvat/apps/dataset_manager/tests/assets/projects.json new file mode 100644 index 00000000..cef9a4ba --- /dev/null +++ b/cvat/apps/dataset_manager/tests/assets/projects.json @@ -0,0 +1,55 @@ +{ + "main": { + "name": "Main project", + "owner_id": 1, + "assignee_id": 2, + "labels": [ + { + "name": "car", + "color": "#2080c0", + "attributes": [ + { + "name": "select_name", + "mutable": false, + "input_type": "select", + "default_value": "bmw", + "values": ["bmw", "mazda", "renault"] + }, + { + "name": "radio_name", + "mutable": false, + "input_type": "radio", + "default_value": "x1", + "values": ["x1", "x2", "x3"] + }, + { + "name": "check_name", + "mutable": true, + "input_type": "checkbox", + "default_value": "false", + "values": ["false"] + }, + { + "name": "text_name", + "mutable": false, + "input_type": "text", + "default_value": "qwerty", + "values": ["qwerty"] + }, + { + "name": "number_name", + "mutable": false, + "input_type": "number", + "default_value": "-4", + "values": ["-4", "4", "1"] + } + ] + }, + { + "name": "person", + "color": "#c06060", + "attributes": [] + } + ] + } +} diff --git a/cvat/apps/dataset_manager/tests/assets/tasks.json b/cvat/apps/dataset_manager/tests/assets/tasks.json index 09e2d866..23ea55e2 100644 --- a/cvat/apps/dataset_manager/tests/assets/tasks.json +++ b/cvat/apps/dataset_manager/tests/assets/tasks.json @@ -282,17 +282,20 @@ } ] }, - "many jobs": { - "name": "many jobs", + "task in project #1": { + "name": "First task in project", + "project_id": 1, "overlap": 0, - "segment_size": 5, + "segment_size": 100, "owner_id": 1, - "labels": [ - { - "name": "car", - "color": "#2080c0", - "attributes": [] - } - ] + "assignee_id": 2 + }, + "task in project #2": { + "name": "Second task in project", + "project_id": 1, + "overlap": 0, + "segment_size": 100, + "owner_id": 1, + "assignee_id": 2 } } diff --git a/cvat/apps/dataset_manager/tests/test_formats.py b/cvat/apps/dataset_manager/tests/test_formats.py index 8f7edd2f..69c7e347 100644 --- a/cvat/apps/dataset_manager/tests/test_formats.py +++ b/cvat/apps/dataset_manager/tests/test_formats.py @@ -3,22 +3,27 @@ # # SPDX-License-Identifier: MIT - -from io import BytesIO +import numpy as np import os.path as osp import tempfile import zipfile +from io import BytesIO import datumaro +from datumaro.components.dataset import Dataset, DatasetItem +from datumaro.components.extractor import Mask +from django.contrib.auth.models import Group, User from PIL import Image -from django.contrib.auth.models import User, Group -from rest_framework.test import APITestCase, APIClient + from rest_framework import status +from rest_framework.test import APIClient, APITestCase import cvat.apps.dataset_manager as dm from cvat.apps.dataset_manager.annotation import AnnotationIR -from cvat.apps.dataset_manager.bindings import TaskData, find_dataset_root, CvatTaskDataExtractor +from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, + TaskData, find_dataset_root) from cvat.apps.dataset_manager.task import TaskAnnotation +from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.engine.models import Task @@ -501,7 +506,6 @@ class TaskExportTest(_DbTestBase): self.assertTrue(frame.frame in range(6, 10)) self.assertEqual(i + 1, 4) - class FrameMatchingTest(_DbTestBase): def _generate_task_images(self, paths): # pylint: disable=no-self-use f = BytesIO() @@ -598,9 +602,10 @@ class TaskAnnotationsImportTest(_DbTestBase): self._put_api_v1_task_id_annotations(task["id"], annotations) return annotations - def _generate_task_images(self, count, name="image"): + def _generate_task_images(self, count, name="image", **image_params): images = { - "client_files[%d]" % i: generate_image_file("image_%d.jpg" % i) + "client_files[%d]" % i: generate_image_file("%s_%d.jpg" % (name, i), + **image_params) for i in range(count) } images["image_quality"] = 75 @@ -916,3 +921,36 @@ class TaskAnnotationsImportTest(_DbTestBase): self.skipTest("Format is disabled") self._test_can_import_annotations(task, format_name) + + def test_can_import_mots_annotations_with_splited_masks(self): + #https://github.com/openvinotoolkit/cvat/issues/3360 + + format_name = 'MOTS PNG 1.0' + source_dataset = Dataset.from_iterable([ + DatasetItem(id='image_0', + annotations=[ + Mask(np.array([[1, 1, 1, 0, 1, 1, 1]] * 5), + label=0, attributes={'track_id': 0}) + ] + ) + ], categories=['label_0']) + + with tempfile.TemporaryDirectory() as temp_dir: + dataset_dir = osp.join(temp_dir, 'dataset') + source_dataset.export(dataset_dir, 'mots_png') + dataset_path = osp.join(temp_dir, 'annotations.zip') + make_zip_archive(dataset_dir, dataset_path) + + images = self._generate_task_images(1, size=(5, 7)) + task = { + 'name': 'test', + "overlap": 0, + "segment_size": 100, + "labels": [{'name': 'label_0'}] + } + task.update() + task = self._create_task(task, images) + + dm.task.import_task_annotations(task['id'], dataset_path, format_name) + self._test_can_import_annotations(task, format_name) + diff --git a/cvat/apps/dataset_manager/tests/test_rest_api_formats.py b/cvat/apps/dataset_manager/tests/test_rest_api_formats.py index 2d69aee8..313a392b 100644 --- a/cvat/apps/dataset_manager/tests/test_rest_api_formats.py +++ b/cvat/apps/dataset_manager/tests/test_rest_api_formats.py @@ -7,6 +7,7 @@ import json import os.path as osp import os import av +from django.http import response import numpy as np import random import xml.etree.ElementTree as ET @@ -26,6 +27,10 @@ from cvat.apps.dataset_manager.bindings import CvatTaskDataExtractor, TaskData from cvat.apps.dataset_manager.task import TaskAnnotation from cvat.apps.engine.models import Task +projects_path = osp.join(osp.dirname(__file__), 'assets', 'projects.json') +with open(projects_path) as file: + projects = json.load(file) + tasks_path = osp.join(osp.dirname(__file__), 'assets', 'tasks.json') with open(tasks_path) as file: tasks = json.load(file) @@ -133,8 +138,8 @@ class _DbTestBase(APITestCase): return response @staticmethod - def _generate_task_images(count): # pylint: disable=no-self-use - images = {"client_files[%d]" % i: generate_image_file("image_%d.jpg" % i) for i in range(count)} + def _generate_task_images(count, name_offsets = 0): # pylint: disable=no-self-use + images = {"client_files[%d]" % i: generate_image_file("image_%d.jpg" % (i + name_offsets)) for i in range(count)} images["image_quality"] = 75 return images @@ -159,6 +164,14 @@ class _DbTestBase(APITestCase): return task + def _create_project(self, data): + with ForceLogin(self.user, self.client): + response = self.client.post('/api/v1/projects', data=data, format="json") + assert response.status_code == status.HTTP_201_CREATED, response.status_code + project = response.data + + return project + def _get_jobs(self, task_id): with ForceLogin(self.admin, self.client): response = self.client.get("/api/v1/tasks/{}/jobs".format(task_id)) @@ -297,14 +310,25 @@ class _DbTestBase(APITestCase): def _generate_url_upload_job_annotations(self, job_id, upload_format_name): return f"/api/v1/jobs/{job_id}/annotations?format={upload_format_name}" - def _generate_url_dump_dataset(self, task_id): + def _generate_url_dump_task_dataset(self, task_id): return f"/api/v1/tasks/{task_id}/dataset" + def _generate_url_dump_project_annotations(self, project_id, format_name): + return f"/api/v1/projects/{project_id}/annotations?format={format_name}" + + def _generate_url_dump_project_dataset(self, project_id, format_name): + return f"/api/v1/projects/{project_id}/dataset?format={format_name}" + def _remove_annotations(self, url, user): response = self._delete_request(url, user) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) return response + def _delete_project(self, project_id, user): + response = self._delete_request(f'/api/v1/projects/{project_id}', user) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + return response + class TaskDumpUploadTest(_DbTestBase): def test_api_v1_dump_and_upload_annotations_with_objects_type_is_shape(self): @@ -789,7 +813,7 @@ class TaskDumpUploadTest(_DbTestBase): task = self._create_task(tasks["main"], images) task_id = task["id"] # dump annotations - url = self._generate_url_dump_dataset(task_id) + url = self._generate_url_dump_task_dataset(task_id) for user, edata in list(expected.items()): user_name = edata['name'] file_zip_name = osp.join(test_dir, f'{test_name}_{user_name}_{dump_format_name}.zip') @@ -1147,3 +1171,108 @@ class TaskDumpUploadTest(_DbTestBase): # equals annotations data_from_task_after_upload = self._get_data_from_task(task_id, include_images) compare_datasets(self, data_from_task_before_upload, data_from_task_after_upload) + +class ProjectDump(_DbTestBase): + def test_api_v1_export_dataset(self): + test_name = self._testMethodName + dump_formats = dm.views.get_export_formats() + + expected = { + self.admin: {'name': 'admin', 'code': status.HTTP_200_OK, 'create code': status.HTTP_201_CREATED, + 'accept code': status.HTTP_202_ACCEPTED, 'file_exists': True}, + self.user: {'name': 'user', 'code': status.HTTP_200_OK, 'create code': status.HTTP_201_CREATED, + 'accept code': status.HTTP_202_ACCEPTED, 'file_exists': True}, + None: {'name': 'none', 'code': status.HTTP_401_UNAUTHORIZED, 'create code': status.HTTP_401_UNAUTHORIZED, + 'accept code': status.HTTP_401_UNAUTHORIZED, 'file_exists': False}, + } + + with TestDir() as test_dir: + for dump_format in dump_formats: + if not dump_format.ENABLED or dump_format.DIMENSION == dm.bindings.DimensionType.DIM_3D: + continue + dump_format_name = dump_format.DISPLAY_NAME + with self.subTest(format=dump_format_name): + project = self._create_project(projects['main']) + pid = project['id'] + images = self._generate_task_images(3) + tasks['task in project #1']['project_id'] = pid + self._create_task(tasks['task in project #1'], images) + images = self._generate_task_images(3, 3) + tasks['task in project #2']['project_id'] = pid + self._create_task(tasks['task in project #2'], images) + url = self._generate_url_dump_project_dataset(project['id'], dump_format_name) + + for user, edata in list(expected.items()): + user_name = edata['name'] + file_zip_name = osp.join(test_dir, f'{test_name}_{user_name}_{dump_format_name}.zip') + data = { + "format": dump_format_name, + } + response = self._get_request_with_data(url, data, user) + self.assertEqual(response.status_code, edata["accept code"]) + response = self._get_request_with_data(url, data, user) + self.assertEqual(response.status_code, edata["create code"]) + data = { + "format": dump_format_name, + "action": "download", + } + response = self._get_request_with_data(url, data, user) + self.assertEqual(response.status_code, edata["code"]) + if response.status_code == status.HTTP_200_OK: + content = BytesIO(b"".join(response.streaming_content)) + with open(file_zip_name, "wb") as f: + f.write(content.getvalue()) + self.assertEqual(response.status_code, edata['code']) + self.assertEqual(osp.exists(file_zip_name), edata['file_exists']) + + def test_api_v1_export_annotatios(self): + test_name = self._testMethodName + dump_formats = dm.views.get_export_formats() + + expected = { + self.admin: {'name': 'admin', 'code': status.HTTP_200_OK, 'create code': status.HTTP_201_CREATED, + 'accept code': status.HTTP_202_ACCEPTED, 'file_exists': True}, + self.user: {'name': 'user', 'code': status.HTTP_200_OK, 'create code': status.HTTP_201_CREATED, + 'accept code': status.HTTP_202_ACCEPTED, 'file_exists': True}, + None: {'name': 'none', 'code': status.HTTP_401_UNAUTHORIZED, 'create code': status.HTTP_401_UNAUTHORIZED, + 'accept code': status.HTTP_401_UNAUTHORIZED, 'file_exists': False}, + } + + with TestDir() as test_dir: + for dump_format in dump_formats: + if not dump_format.ENABLED or dump_format.DIMENSION == dm.bindings.DimensionType.DIM_3D: + continue + dump_format_name = dump_format.DISPLAY_NAME + with self.subTest(format=dump_format_name): + project = self._create_project(projects['main']) + pid = project['id'] + images = self._generate_task_images(3) + tasks['task in project #1']['project_id'] = pid + self._create_task(tasks['task in project #1'], images) + images = self._generate_task_images(3, 3) + tasks['task in project #2']['project_id'] = pid + self._create_task(tasks['task in project #2'], images) + url = self._generate_url_dump_project_annotations(project['id'], dump_format_name) + + for user, edata in list(expected.items()): + user_name = edata['name'] + file_zip_name = osp.join(test_dir, f'{test_name}_{user_name}_{dump_format_name}.zip') + data = { + "format": dump_format_name, + } + response = self._get_request_with_data(url, data, user) + self.assertEqual(response.status_code, edata["accept code"]) + response = self._get_request_with_data(url, data, user) + self.assertEqual(response.status_code, edata["create code"]) + data = { + "format": dump_format_name, + "action": "download", + } + response = self._get_request_with_data(url, data, user) + self.assertEqual(response.status_code, edata["code"]) + if response.status_code == status.HTTP_200_OK: + content = BytesIO(b"".join(response.streaming_content)) + with open(file_zip_name, "wb") as f: + f.write(content.getvalue()) + self.assertEqual(response.status_code, edata['code']) + self.assertEqual(osp.exists(file_zip_name), edata['file_exists']) diff --git a/cvat/apps/dataset_manager/views.py b/cvat/apps/dataset_manager/views.py index 36fcea63..4f51c69a 100644 --- a/cvat/apps/dataset_manager/views.py +++ b/cvat/apps/dataset_manager/views.py @@ -13,9 +13,10 @@ from datumaro.util import to_snake_case from django.utils import timezone import cvat.apps.dataset_manager.task as task -from cvat.apps.engine.backup import TaskExporter +import cvat.apps.dataset_manager.project as project from cvat.apps.engine.log import slogger -from cvat.apps.engine.models import Task +from cvat.apps.engine.models import Project, Task +from cvat.apps.engine.backup import TaskExporter from .formats.registry import EXPORT_FORMATS, IMPORT_FORMATS from .util import current_function_name @@ -29,22 +30,32 @@ def log_exception(logger=None, exc_info=True): exc_info=exc_info) -def get_export_cache_dir(db_task): - task_dir = osp.abspath(db_task.get_task_dirname()) - if osp.isdir(task_dir): - return osp.join(task_dir, 'export_cache') +def get_export_cache_dir(db_instance): + base_dir = osp.abspath(db_instance.get_project_dirname() if isinstance(db_instance, Project) else db_instance.get_task_dirname()) + if osp.isdir(base_dir): + return osp.join(base_dir, 'export_cache') else: - raise Exception('Task dir {} does not exist'.format(task_dir)) + raise Exception('{} dir {} does not exist'.format("Project" if isinstance(db_instance, Project) else "Task", base_dir)) DEFAULT_CACHE_TTL = timedelta(hours=10) -CACHE_TTL = DEFAULT_CACHE_TTL +TASK_CACHE_TTL = DEFAULT_CACHE_TTL +PROJECT_CACHE_TTL = DEFAULT_CACHE_TTL / 3 -def export_task(task_id, dst_format, server_url=None, save_images=False): +def export(dst_format, task_id=None, project_id=None, server_url=None, save_images=False): try: - db_task = Task.objects.get(pk=task_id) - - cache_dir = get_export_cache_dir(db_task) + if task_id is not None: + db_instance = Task.objects.get(pk=task_id) + logger = slogger.task[task_id] + cache_ttl = TASK_CACHE_TTL + export_fn = task.export_task + else: + db_instance = Project.objects.get(pk=project_id) + logger = slogger.project[project_id] + cache_ttl = PROJECT_CACHE_TTL + export_fn = project.export_project + + cache_dir = get_export_cache_dir(db_instance) exporter = EXPORT_FORMATS[dst_format] output_base = '%s_%s' % ('dataset' if save_images else 'annotations', @@ -52,39 +63,51 @@ def export_task(task_id, dst_format, server_url=None, save_images=False): output_path = '%s.%s' % (output_base, exporter.EXT) output_path = osp.join(cache_dir, output_path) - task_time = timezone.localtime(db_task.updated_date).timestamp() + instance_time = timezone.localtime(db_instance.updated_date).timestamp() + if isinstance(db_instance, Project): + tasks_update = list(map(lambda db_task: timezone.localtime(db_task.updated_date).timestamp(), db_instance.tasks.all())) + instance_time = max(tasks_update + [instance_time]) if not (osp.exists(output_path) and \ - task_time <= osp.getmtime(output_path)): + instance_time <= osp.getmtime(output_path)): os.makedirs(cache_dir, exist_ok=True) with tempfile.TemporaryDirectory(dir=cache_dir) as temp_dir: temp_file = osp.join(temp_dir, 'result') - task.export_task(task_id, temp_file, dst_format, + export_fn(db_instance.id, temp_file, dst_format, server_url=server_url, save_images=save_images) os.replace(temp_file, output_path) archive_ctime = osp.getctime(output_path) scheduler = django_rq.get_scheduler() - cleaning_job = scheduler.enqueue_in(time_delta=CACHE_TTL, + cleaning_job = scheduler.enqueue_in(time_delta=cache_ttl, func=clear_export_cache, task_id=task_id, file_path=output_path, file_ctime=archive_ctime) - slogger.task[task_id].info( - "The task '{}' is exported as '{}' at '{}' " + logger.info( + "The {} '{}' is exported as '{}' at '{}' " "and available for downloading for the next {}. " "Export cache cleaning job is enqueued, id '{}'".format( - db_task.name, dst_format, output_path, CACHE_TTL, - cleaning_job.id)) + "project" if isinstance(db_instance, Project) else 'task', + db_instance.name, dst_format, output_path, cache_ttl, + cleaning_job.id + )) return output_path except Exception: - log_exception(slogger.task[task_id]) + log_exception(logger) raise def export_task_as_dataset(task_id, dst_format=None, server_url=None): - return export_task(task_id, dst_format, server_url=server_url, save_images=True) + return export(dst_format, task_id=task_id, server_url=server_url, save_images=True) def export_task_annotations(task_id, dst_format=None, server_url=None): - return export_task(task_id, dst_format, server_url=server_url, save_images=False) + return export(dst_format,task_id=task_id, server_url=server_url, save_images=False) + +def export_project_as_dataset(project_id, dst_format=None, server_url=None): + return export(dst_format, project_id=project_id, server_url=server_url, save_images=True) + + +def export_project_annotations(project_id, dst_format=None, server_url=None): + return export(dst_format, project_id=project_id, server_url=server_url, save_images=False) def clear_export_cache(task_id, file_path, file_ctime): try: @@ -116,7 +139,7 @@ def backup_task(task_id, output_path): archive_ctime = osp.getctime(output_path) scheduler = django_rq.get_scheduler() - cleaning_job = scheduler.enqueue_in(time_delta=CACHE_TTL, + cleaning_job = scheduler.enqueue_in(time_delta=TASK_CACHE_TTL, func=clear_export_cache, task_id=task_id, file_path=output_path, file_ctime=archive_ctime) @@ -124,7 +147,7 @@ def backup_task(task_id, output_path): "The task '{}' is backuped at '{}' " "and available for downloading for the next {}. " "Export cache cleaning job is enqueued, id '{}'".format( - db_task.name, output_path, CACHE_TTL, + db_task.name, output_path, TASK_CACHE_TTL, cleaning_job.id)) return output_path diff --git a/cvat/apps/engine/cache.py b/cvat/apps/engine/cache.py index 75196ee8..71380f28 100644 --- a/cvat/apps/engine/cache.py +++ b/cvat/apps/engine/cache.py @@ -15,7 +15,7 @@ from cvat.apps.engine.media_extractors import (Mpeg4ChunkWriter, ImageDatasetManifestReader, VideoDatasetManifestReader) from cvat.apps.engine.models import DataChoice, StorageChoice from cvat.apps.engine.models import DimensionType -from cvat.apps.engine.cloud_provider import get_cloud_storage_instance, Credentials +from cvat.apps.engine.cloud_provider import get_cloud_storage_instance, Credentials, Status from cvat.apps.engine.utils import md5_hash class CacheInteraction: def __init__(self, dimension=DimensionType.DIM_2D): @@ -71,6 +71,7 @@ class CacheInteraction: step=db_data.get_frame_step()) if db_data.storage == StorageChoice.CLOUD_STORAGE: db_cloud_storage = db_data.cloud_storage + assert db_cloud_storage, 'Cloud storage instance was deleted' credentials = Credentials() credentials.convert_from_db({ 'type': db_cloud_storage.credentials_type, @@ -81,22 +82,38 @@ class CacheInteraction: 'credentials': credentials, 'specific_attributes': db_cloud_storage.get_specific_attributes() } - cloud_storage_instance = get_cloud_storage_instance(cloud_provider=db_cloud_storage.provider_type, **details) - cloud_storage_instance.initialize_content() - for item in reader: - name = f"{item['name']}{item['extension']}" - if name not in cloud_storage_instance: - raise Exception('{} file was not found on a {} storage'.format(name, cloud_storage_instance.name)) - with NamedTemporaryFile(mode='w+b', prefix='cvat', suffix=name, delete=False) as temp_file: - source_path = temp_file.name - buf = cloud_storage_instance.download_fileobj(name) - temp_file.write(buf.getvalue()) - checksum = item.get('checksum', None) - if not checksum: - slogger.glob.warning('A manifest file does not contain checksum for image {}'.format(item.get('name'))) - if checksum and not md5_hash(source_path) == checksum: - slogger.glob.warning('Hash sums of files {} do not match'.format(name)) - images.append((source_path, source_path, None)) + try: + cloud_storage_instance = get_cloud_storage_instance(cloud_provider=db_cloud_storage.provider_type, **details) + cloud_storage_instance.initialize_content() + for item in reader: + file_name = f"{item['name']}{item['extension']}" + if file_name not in cloud_storage_instance: + raise Exception('{} file was not found on a {} storage'.format(file_name, cloud_storage_instance.name)) + with NamedTemporaryFile(mode='w+b', prefix='cvat', suffix=file_name.replace(os.path.sep, '#'), delete=False) as temp_file: + source_path = temp_file.name + buf = cloud_storage_instance.download_fileobj(file_name) + temp_file.write(buf.getvalue()) + checksum = item.get('checksum', None) + if not checksum: + slogger.cloud_storage[db_cloud_storage.id].warning('A manifest file does not contain checksum for image {}'.format(item.get('name'))) + if checksum and not md5_hash(source_path) == checksum: + slogger.cloud_storage[db_cloud_storage.id].warning('Hash sums of files {} do not match'.format(file_name)) + images.append((source_path, source_path, None)) + except Exception as ex: + storage_status = cloud_storage_instance.get_status() + if storage_status == Status.FORBIDDEN: + msg = 'The resource {} is no longer available. Access forbidden.'.format(cloud_storage_instance.name) + elif storage_status == Status.NOT_FOUND: + msg = 'The resource {} not found. It may have been deleted.'.format(cloud_storage_instance.name) + else: + # check status of last file + file_status = cloud_storage_instance.get_file_status(file_name) + if file_status == Status.NOT_FOUND: + raise Exception("'{}' not found on the cloud storage '{}'".format(file_name, cloud_storage_instance.name)) + elif file_status == Status.FORBIDDEN: + raise Exception("Access to the file '{}' on the '{}' cloud storage is denied".format(file_name, cloud_storage_instance.name)) + msg = str(ex) + raise Exception(msg) else: for item in reader: source_path = os.path.join(upload_dir, f"{item['name']}{item['extension']}") diff --git a/cvat/apps/engine/cloud_provider.py b/cvat/apps/engine/cloud_provider.py index 017d5f7d..869b2ad1 100644 --- a/cvat/apps/engine/cloud_provider.py +++ b/cvat/apps/engine/cloud_provider.py @@ -1,19 +1,40 @@ -#from dataclasses import dataclass +# Copyright (C) 2021 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import os +import boto3 + from abc import ABC, abstractmethod, abstractproperty +from enum import Enum from io import BytesIO -import boto3 from boto3.s3.transfer import TransferConfig -from botocore.exceptions import WaiterError +from botocore.exceptions import ClientError from botocore.handlers import disable_signing from azure.storage.blob import BlobServiceClient -from azure.core.exceptions import ResourceExistsError +from azure.core.exceptions import ResourceExistsError, HttpResponseError from azure.storage.blob import PublicAccess +from google.cloud import storage +from google.cloud.exceptions import NotFound as GoogleCloudNotFound, Forbidden as GoogleCloudForbidden + from cvat.apps.engine.log import slogger from cvat.apps.engine.models import CredentialsTypeChoice, CloudProviderChoice +class Status(str, Enum): + AVAILABLE = 'AVAILABLE' + NOT_FOUND = 'NOT_FOUND' + FORBIDDEN = 'FORBIDDEN' + + @classmethod + def choices(cls): + return tuple((x.value, x.name) for x in cls) + + def __str__(self): + return self.value + class _CloudStorage(ABC): def __init__(self): @@ -28,7 +49,23 @@ class _CloudStorage(ABC): pass @abstractmethod - def exists(self): + def _head_file(self, key): + pass + + @abstractmethod + def _head(self): + pass + + @abstractmethod + def get_status(self): + pass + + @abstractmethod + def get_file_status(self, key): + pass + + @abstractmethod + def get_file_last_modified(self, key): pass @abstractmethod @@ -42,6 +79,7 @@ class _CloudStorage(ABC): def download_file(self, key, path): file_obj = self.download_fileobj(key) if isinstance(file_obj, BytesIO): + os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, 'wb') as f: f.write(file_obj.getvalue()) else: @@ -77,15 +115,19 @@ def get_cloud_storage_instance(cloud_provider, resource, credentials, specific_a account_name=credentials.account_name, sas_token=credentials.session_token ) + elif cloud_provider == CloudProviderChoice.GOOGLE_CLOUD_STORAGE: + instance = GoogleCloudStorage( + bucket_name=resource, + service_account_json=credentials.key_file_path, + prefix=specific_attributes.get('prefix'), + location=specific_attributes.get('location'), + project=specific_attributes.get('project') + ) else: raise NotImplementedError() return instance class AWS_S3(_CloudStorage): - waiter_config = { - 'Delay': 5, # The amount of time in seconds to wait between attempts. Default: 5 - 'MaxAttempts': 3, # The maximum number of attempts to be made. Default: 20 - } transfer_config = { 'max_io_queue': 10, } @@ -104,6 +146,13 @@ class AWS_S3(_CloudStorage): aws_session_token=session_token, region_name=region ) + elif access_key_id and secret_key: + self._s3 = boto3.resource( + 's3', + aws_access_key_id=access_key_id, + aws_secret_access_key=secret_key, + region_name=region + ) elif any([access_key_id, secret_key, session_token]): raise Exception('Insufficient data for authorization') # anonymous access @@ -122,26 +171,38 @@ class AWS_S3(_CloudStorage): def name(self): return self._bucket.name - def exists(self): - waiter = self._client_s3.get_waiter('bucket_exists') - try: - waiter.wait( - Bucket=self.name, - WaiterConfig=self.waiter_config - ) - except WaiterError: - raise Exception('A resource {} unavailable'.format(self.name)) + def _head(self): + return self._client_s3.head_bucket(Bucket=self.name) - def is_object_exist(self, key_object): - waiter = self._client_s3.get_waiter('object_exists') + def _head_file(self, key): + return self._client_s3.head_object(Bucket=self.name, Key=key) + + def get_status(self): + # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.head_object + # return only 3 codes: 200, 403, 404 try: - waiter.wait( - Bucket=self._bucket, - Key=key_object, - WaiterConfig=self.waiter_config - ) - except WaiterError: - raise Exception('A file {} unavailable'.format(key_object)) + self._head() + return Status.AVAILABLE + except ClientError as ex: + code = ex.response['Error']['Code'] + if code == '403': + return Status.FORBIDDEN + else: + return Status.NOT_FOUND + + def get_file_status(self, key): + try: + self._head_file(key) + return Status.AVAILABLE + except ClientError as ex: + code = ex.response['Error']['Code'] + if code == '403': + return Status.FORBIDDEN + else: + return Status.NOT_FOUND + + def get_file_last_modified(self, key): + return self._head_file(key).get('LastModified') def upload_file(self, file_obj, file_name): self._bucket.upload_fileobj( @@ -221,12 +282,35 @@ class AzureBlobContainer(_CloudStorage): slogger.glob.info(msg) raise Exception(msg) - def exists(self): - return self._container_client.exists(timeout=5) + def _head(self): + return self._container_client.get_container_properties() + + def _head_file(self, key): + blob_client = self.container.get_blob_client(key) + return blob_client.get_blob_properties() + + def get_file_last_modified(self, key): + return self._head_file(key).last_modified - def is_object_exist(self, file_name): - blob_client = self._container_client.get_blob_client(file_name) - return blob_client.exists() + def get_status(self): + try: + self._head() + return Status.AVAILABLE + except HttpResponseError as ex: + if ex.status_code == 403: + return Status.FORBIDDEN + else: + return Status.NOT_FOUND + + def get_file_status(self, key): + try: + self._head_file(key) + return Status.AVAILABLE + except HttpResponseError as ex: + if ex.status_code == 403: + return Status.FORBIDDEN + else: + return Status.NOT_FOUND def upload_file(self, file_obj, file_name): self._container_client.upload_blob(name=file_name, data=file_obj) @@ -256,41 +340,167 @@ class AzureBlobContainer(_CloudStorage): class GOOGLE_DRIVE(_CloudStorage): pass +def _define_gcs_status(func): + def wrapper(self, key=None): + try: + if not key: + func(self) + else: + func(self, key) + return Status.AVAILABLE + except GoogleCloudNotFound: + return Status.NOT_FOUND + except GoogleCloudForbidden: + return Status.FORBIDDEN + return wrapper + +class GoogleCloudStorage(_CloudStorage): + + def __init__(self, bucket_name, prefix=None, service_account_json=None, project=None, location=None): + super().__init__() + if service_account_json: + self._storage_client = storage.Client.from_service_account_json(service_account_json) + else: + self._storage_client = storage.Client() + + bucket = self._storage_client.lookup_bucket(bucket_name) + if bucket is None: + bucket = self._storage_client.bucket(bucket_name, user_project=project) + + self._bucket = bucket + self._bucket_location = location + self._prefix = prefix + + @property + def bucket(self): + return self._bucket + + @property + def name(self): + return self._bucket.name + + def _head(self): + return self._storage_client.get_bucket(bucket_or_name=self.name) + + def _head_file(self, key): + blob = self.bucket.blob(key) + return self._storage_client._get_resource(blob.path) + + @_define_gcs_status + def get_status(self): + self._head() + + @_define_gcs_status + def get_file_status(self, key): + self._head_file(key) + + def initialize_content(self): + self._files = [ + { + 'name': blob.name + } + for blob in self._storage_client.list_blobs( + self.bucket, prefix=self._prefix + ) + ] + + def download_fileobj(self, key): + buf = BytesIO() + blob = self.bucket.blob(key) + self._storage_client.download_blob_to_file(blob, buf) + buf.seek(0) + return buf + + def upload_file(self, file_obj, file_name): + self.bucket.blob(file_name).upload_from_file(file_obj) + + def create(self): + try: + self._bucket = self._storage_client.create_bucket( + self.bucket, + location=self._bucket_location + ) + slogger.glob.info( + 'Bucket {} has been created at {} region for {}'.format( + self.name, + self.bucket.location, + self.bucket.user_project, + )) + except Exception as ex: + msg = str(ex) + slogger.glob.info(msg) + raise Exception(msg) + + def get_file_last_modified(self, key): + blob = self.bucket.blob(key) + blob.reload() + return blob.updated + class Credentials: - __slots__ = ('key', 'secret_key', 'session_token', 'account_name', 'credentials_type') + __slots__ = ('key', 'secret_key', 'session_token', 'account_name', 'key_file_path', 'credentials_type') def __init__(self, **credentials): self.key = credentials.get('key', '') self.secret_key = credentials.get('secret_key', '') self.session_token = credentials.get('session_token', '') self.account_name = credentials.get('account_name', '') + self.key_file_path = credentials.get('key_file_path', '') self.credentials_type = credentials.get('credentials_type', None) def convert_to_db(self): converted_credentials = { - CredentialsTypeChoice.TEMP_KEY_SECRET_KEY_TOKEN_SET : \ - " ".join([self.key, self.secret_key, self.session_token]), + CredentialsTypeChoice.KEY_SECRET_KEY_PAIR : \ + " ".join([self.key, self.secret_key]), CredentialsTypeChoice.ACCOUNT_NAME_TOKEN_PAIR : " ".join([self.account_name, self.session_token]), - CredentialsTypeChoice.ANONYMOUS_ACCESS: "", + CredentialsTypeChoice.KEY_FILE_PATH: self.key_file_path, + CredentialsTypeChoice.ANONYMOUS_ACCESS: "" if not self.account_name else self.account_name, } return converted_credentials[self.credentials_type] def convert_from_db(self, credentials): self.credentials_type = credentials.get('type') - if self.credentials_type == CredentialsTypeChoice.TEMP_KEY_SECRET_KEY_TOKEN_SET: - self.key, self.secret_key, self.session_token = credentials.get('value').split() + if self.credentials_type == CredentialsTypeChoice.KEY_SECRET_KEY_PAIR: + self.key, self.secret_key = credentials.get('value').split() elif self.credentials_type == CredentialsTypeChoice.ACCOUNT_NAME_TOKEN_PAIR: self.account_name, self.session_token = credentials.get('value').split() + elif self.credentials_type == CredentialsTypeChoice.ANONYMOUS_ACCESS: + self.session_token, self.key, self.secret_key = ('', '', '') + # account_name will be in [some_value, ''] + self.account_name = credentials.get('value') + elif self.credentials_type == CredentialsTypeChoice.KEY_FILE_PATH: + self.key_file_path = credentials.get('value') else: - self.account_name, self.session_token, self.key, self.secret_key = ('', '', '', '') - self.credentials_type = None + raise NotImplementedError('Found {} not supported credentials type'.format(self.credentials_type)) def mapping_with_new_values(self, credentials): self.credentials_type = credentials.get('credentials_type', self.credentials_type) - self.key = credentials.get('key', self.key) - self.secret_key = credentials.get('secret_key', self.secret_key) - self.session_token = credentials.get('session_token', self.session_token) - self.account_name = credentials.get('account_name', self.account_name) + if self.credentials_type == CredentialsTypeChoice.ANONYMOUS_ACCESS: + self.key = '' + self.secret_key = '' + self.session_token = '' + self.key_file_path = '' + self.account_name = credentials.get('account_name', self.account_name) + elif self.credentials_type == CredentialsTypeChoice.KEY_SECRET_KEY_PAIR: + self.key = credentials.get('key', self.key) + self.secret_key = credentials.get('secret_key', self.secret_key) + self.session_token = '' + self.account_name = '' + self.key_file_path = '' + elif self.credentials_type == CredentialsTypeChoice.ACCOUNT_NAME_TOKEN_PAIR: + self.session_token = credentials.get('session_token', self.session_token) + self.account_name = credentials.get('account_name', self.account_name) + self.key = '' + self.secret_key = '' + self.key_file_path = '' + elif self.credentials_type == CredentialsTypeChoice.KEY_FILE_PATH: + self.key = '' + self.secret_key = '' + self.session_token = '' + self.account_name = '' + self.key_file_path = credentials.get('key_file_path', self.key_file_path) + else: + raise NotImplementedError('Mapping credentials: unsupported credentials type') + def values(self): - return [self.key, self.secret_key, self.session_token, self.account_name] + return [self.key, self.secret_key, self.session_token, self.account_name, self.key_file_path] diff --git a/cvat/apps/engine/migrations/0041_auto_20210827_0258.py b/cvat/apps/engine/migrations/0041_auto_20210827_0258.py new file mode 100644 index 00000000..0e089c0e --- /dev/null +++ b/cvat/apps/engine/migrations/0041_auto_20210827_0258.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.13 on 2021-08-27 02:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('engine', '0040_cloud_storage'), + ] + + operations = [ + migrations.AlterField( + model_name='cloudstorage', + name='credentials_type', + field=models.CharField(choices=[('TEMP_KEY_SECRET_KEY_TOKEN_SET', 'TEMP_KEY_SECRET_KEY_TOKEN_SET'), ('ACCOUNT_NAME_TOKEN_PAIR', 'ACCOUNT_NAME_TOKEN_PAIR'), ('KEY_FILE_PATH', 'KEY_FILE_PATH'), ('ANONYMOUS_ACCESS', 'ANONYMOUS_ACCESS')], max_length=29), + ), + migrations.AlterField( + model_name='cloudstorage', + name='provider_type', + field=models.CharField(choices=[('AWS_S3_BUCKET', 'AWS_S3'), ('AZURE_CONTAINER', 'AZURE_CONTAINER'), ('GOOGLE_DRIVE', 'GOOGLE_DRIVE'), ('GOOGLE_CLOUD_STORAGE', 'GOOGLE_CLOUD_STORAGE')], max_length=20), + ), + ] diff --git a/cvat/apps/engine/migrations/0042_auto_20210830_1056.py b/cvat/apps/engine/migrations/0042_auto_20210830_1056.py new file mode 100644 index 00000000..7b5a496a --- /dev/null +++ b/cvat/apps/engine/migrations/0042_auto_20210830_1056.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.13 on 2021-08-30 10:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('engine', '0041_auto_20210827_0258'), + ] + + operations = [ + migrations.AlterField( + model_name='cloudstorage', + name='credentials_type', + field=models.CharField(choices=[('KEY_SECRET_KEY_PAIR', 'KEY_SECRET_KEY_PAIR'), ('ACCOUNT_NAME_TOKEN_PAIR', 'ACCOUNT_NAME_TOKEN_PAIR'), ('KEY_FILE_PATH', 'KEY_FILE_PATH'), ('ANONYMOUS_ACCESS', 'ANONYMOUS_ACCESS')], max_length=29), + ), + migrations.CreateModel( + name='Manifest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('filename', models.CharField(default='manifest.jsonl', max_length=1024)), + ('cloud_storage', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='manifests', to='engine.cloudstorage')), + ], + ), + ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 78ec751a..22272423 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -12,6 +12,7 @@ from django.core.files.storage import FileSystemStorage from django.db import models from django.utils.translation import gettext_lazy as _ +from cvat.apps.engine.utils import parse_specific_attributes class SafeCharField(models.CharField): def get_prep_value(self, value): @@ -542,6 +543,7 @@ class CloudProviderChoice(str, Enum): AWS_S3 = 'AWS_S3_BUCKET' AZURE_CONTAINER = 'AZURE_CONTAINER' GOOGLE_DRIVE = 'GOOGLE_DRIVE' + GOOGLE_CLOUD_STORAGE = 'GOOGLE_CLOUD_STORAGE' @classmethod def choices(cls): @@ -556,8 +558,9 @@ class CloudProviderChoice(str, Enum): class CredentialsTypeChoice(str, Enum): # ignore bandit issues because false positives - TEMP_KEY_SECRET_KEY_TOKEN_SET = 'TEMP_KEY_SECRET_KEY_TOKEN_SET' # nosec + KEY_SECRET_KEY_PAIR = 'KEY_SECRET_KEY_PAIR' # nosec ACCOUNT_NAME_TOKEN_PAIR = 'ACCOUNT_NAME_TOKEN_PAIR' # nosec + KEY_FILE_PATH = 'KEY_FILE_PATH' ANONYMOUS_ACCESS = 'ANONYMOUS_ACCESS' @classmethod @@ -571,6 +574,13 @@ class CredentialsTypeChoice(str, Enum): def __str__(self): return self.value +class Manifest(models.Model): + filename = models.CharField(max_length=1024, default='manifest.jsonl') + cloud_storage = models.ForeignKey('CloudStorage', on_delete=models.CASCADE, null=True, related_name='manifests') + + def __str__(self): + return '{}'.format(self.filename) + class CloudStorage(models.Model): # restrictions: # AWS bucket name, Azure container name - 63 @@ -606,11 +616,10 @@ class CloudStorage(models.Model): return os.path.join(self.get_storage_dirname(), 'logs') def get_log_path(self): - return os.path.join(self.get_storage_dirname(), "storage.log") + return os.path.join(self.get_storage_logs_dirname(), "storage.log") + + def get_preview_path(self): + return os.path.join(self.get_storage_dirname(), 'preview.jpeg') def get_specific_attributes(self): - specific_attributes = self.specific_attributes - return { - item.split('=')[0].strip(): item.split('=')[1].strip() - for item in specific_attributes.split('&') - } if specific_attributes else dict() + return parse_specific_attributes(self.specific_attributes) diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 0c0b1130..ae6c9d60 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -11,8 +11,9 @@ from django.contrib.auth.models import User, Group from cvat.apps.dataset_manager.formats.utils import get_label_color from cvat.apps.engine import models -from cvat.apps.engine.cloud_provider import get_cloud_storage_instance, Credentials +from cvat.apps.engine.cloud_provider import get_cloud_storage_instance, Credentials, Status from cvat.apps.engine.log import slogger +from cvat.apps.engine.utils import parse_specific_attributes class BasicUserSerializer(serializers.ModelSerializer): def validate(self, data): @@ -411,6 +412,8 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer): validated_project_id = validated_data.get('project_id', None) if validated_project_id is not None and validated_project_id != instance.project_id: project = models.Project.objects.get(id=validated_data.get('project_id', None)) + if project.tasks.count() and project.tasks.first().dimension != instance.dimension: + raise serializers.ValidationError(f'Dimension ({instance.dimension}) of the task must be the same as other tasks in project ({project.tasks.first().dimension})') if instance.project_id is None: for old_label in instance.label_set.all(): try: @@ -453,8 +456,10 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer): # When moving task labels can be mapped to one, but when not names must be unique if 'project_id' in attrs.keys() and self.instance is not None: project_id = attrs.get('project_id') - if project_id is not None and not models.Project.objects.filter(id=project_id).count(): - raise serializers.ValidationError(f'Cannot find project with ID {project_id}') + if project_id is not None: + project = models.Project.objects.filter(id=project_id).first() + if project is None: + raise serializers.ValidationError(f'Cannot find project with ID {project_id}') # Check that all labels can be mapped new_label_names = set() old_labels = self.instance.project.label_set.all() if self.instance.project_id else self.instance.label_set.all() @@ -500,13 +505,15 @@ class ProjectWithoutTaskSerializer(serializers.ModelSerializer): owner_id = serializers.IntegerField(write_only=True, allow_null=True, required=False) assignee = BasicUserSerializer(allow_null=True, required=False) assignee_id = serializers.IntegerField(write_only=True, allow_null=True, required=False) + task_subsets = serializers.ListField(child=serializers.CharField(), required=False) training_project = TrainingProjectSerializer(required=False, allow_null=True) + dimension = serializers.CharField(max_length=16, required=False) class Meta: model = models.Project fields = ('url', 'id', 'name', 'labels', 'tasks', 'owner', 'assignee', 'owner_id', 'assignee_id', - 'bug_tracker', 'created_date', 'updated_date', 'status', 'training_project') - read_only_fields = ('created_date', 'updated_date', 'status', 'owner', 'asignee') + 'bug_tracker', 'task_subsets', 'created_date', 'updated_date', 'status', 'training_project', 'dimension') + read_only_fields = ('created_date', 'updated_date', 'status', 'owner', 'asignee', 'task_subsets', 'dimension') ordering = ['-id'] @@ -515,6 +522,7 @@ class ProjectWithoutTaskSerializer(serializers.ModelSerializer): task_subsets = set(instance.tasks.values_list('subset', flat=True)) task_subsets.discard('') response['task_subsets'] = list(task_subsets) + response['dimension'] = instance.tasks.first().dimension if instance.tasks.count() else None return response class ProjectSerializer(ProjectWithoutTaskSerializer): @@ -576,7 +584,9 @@ class ProjectSerializer(ProjectWithoutTaskSerializer): return value def to_representation(self, instance): - return serializers.ModelSerializer.to_representation(self, instance) # ignoring subsets here + response = serializers.ModelSerializer.to_representation(self, instance) # ignoring subsets here + response['dimension'] = instance.tasks.first().dimension if instance.tasks.count() else None + return response class ExceptionSerializer(serializers.Serializer): system = serializers.CharField(max_length=255) @@ -771,8 +781,22 @@ class CombinedReviewSerializer(ReviewSerializer): return db_review +class ManifestSerializer(serializers.ModelSerializer): + class Meta: + model = models.Manifest + fields = ('filename', ) + + # pylint: disable=no-self-use + def to_internal_value(self, data): + return {'filename': data } + + # pylint: disable=no-self-use + def to_representation(self, instance): + return instance.filename if instance else instance + class BaseCloudStorageSerializer(serializers.ModelSerializer): owner = BasicUserSerializer(required=False) + manifests = ManifestSerializer(many=True, default=[]) class Meta: model = models.CloudStorage exclude = ['credentials'] @@ -783,14 +807,16 @@ class CloudStorageSerializer(serializers.ModelSerializer): session_token = serializers.CharField(max_length=440, allow_blank=True, required=False) key = serializers.CharField(max_length=20, allow_blank=True, required=False) secret_key = serializers.CharField(max_length=40, allow_blank=True, required=False) + key_file_path = serializers.CharField(max_length=64, allow_blank=True, required=False) account_name = serializers.CharField(max_length=24, allow_blank=True, required=False) + manifests = ManifestSerializer(many=True, default=[]) class Meta: model = models.CloudStorage fields = ( 'provider_type', 'resource', 'display_name', 'owner', 'credentials_type', 'created_date', 'updated_date', 'session_token', 'account_name', 'key', - 'secret_key', 'specific_attributes', 'description' + 'secret_key', 'key_file_path', 'specific_attributes', 'description', 'id', 'manifests', ) read_only_fields = ('created_date', 'updated_date', 'owner') @@ -807,6 +833,9 @@ class CloudStorageSerializer(serializers.ModelSerializer): if attrs.get('provider_type') == models.CloudProviderChoice.AZURE_CONTAINER: if not attrs.get('account_name', ''): raise serializers.ValidationError('Account name for Azure container was not specified') + if attrs.get('provider_type') == models.CloudProviderChoice.GOOGLE_CLOUD_STORAGE: + if not attrs.get('key_file_path', ''): + raise serializers.ValidationError('Key file path for Google cloud storage was not specified') return attrs def create(self, validated_data): @@ -817,31 +846,62 @@ class CloudStorageSerializer(serializers.ModelSerializer): key=validated_data.pop('key', ''), secret_key=validated_data.pop('secret_key', ''), session_token=validated_data.pop('session_token', ''), + key_file_path=validated_data.pop('key_file_path', ''), credentials_type = validated_data.get('credentials_type') ) + details = { + 'resource': validated_data.get('resource'), + 'credentials': credentials, + 'specific_attributes': parse_specific_attributes(validated_data.get('specific_attributes', '')) + } + storage = get_cloud_storage_instance(cloud_provider=provider_type, **details) if should_be_created: - details = { - 'resource': validated_data.get('resource'), - 'credentials': credentials, - 'specific_attributes': { - item.split('=')[0].strip(): item.split('=')[1].strip() - for item in validated_data.get('specific_attributes').split('&') - } if len(validated_data.get('specific_attributes', '')) - else dict() - } - storage = get_cloud_storage_instance(cloud_provider=provider_type, **details) try: storage.create() except Exception as ex: slogger.glob.warning("Failed with creating storage\n{}".format(str(ex))) raise - db_storage = models.CloudStorage.objects.create( - credentials=credentials.convert_to_db(), - **validated_data - ) - db_storage.save() - return db_storage + storage_status = storage.get_status() + if storage_status == Status.AVAILABLE: + manifests = validated_data.pop('manifests') + # check manifest files availability + for manifest in manifests: + file_status = storage.get_file_status(manifest.get('filename')) + if file_status == Status.NOT_FOUND: + raise serializers.ValidationError({ + 'manifests': "The '{}' file does not exist on '{}' cloud storage" \ + .format(manifest.get('filename'), storage.name) + }) + elif file_status == Status.FORBIDDEN: + raise serializers.ValidationError({ + 'manifests': "The '{}' file does not available on '{}' cloud storage. Access denied" \ + .format(manifest.get('filename'), storage.name) + }) + + db_storage = models.CloudStorage.objects.create( + credentials=credentials.convert_to_db(), + **validated_data + ) + db_storage.save() + + manifest_file_instances = [models.Manifest(**manifest, cloud_storage=db_storage) for manifest in manifests] + models.Manifest.objects.bulk_create(manifest_file_instances) + + cloud_storage_path = db_storage.get_storage_dirname() + if os.path.isdir(cloud_storage_path): + shutil.rmtree(cloud_storage_path) + + os.makedirs(db_storage.get_storage_logs_dirname(), exist_ok=True) + return db_storage + elif storage_status == Status.FORBIDDEN: + field = 'credentials' + message = 'Cannot create resource {} with specified credentials. Access forbidden.'.format(storage.name) + else: + field = 'recource' + message = 'The resource {} not found. It may have been deleted.'.format(storage.name) + slogger.glob.error(message) + raise serializers.ValidationError({field: message}) # pylint: disable=no-self-use def update(self, instance, validated_data): @@ -850,15 +910,56 @@ class CloudStorageSerializer(serializers.ModelSerializer): 'type': instance.credentials_type, 'value': instance.credentials, }) - tmp = {k:v for k,v in validated_data.items() if k in {'key', 'secret_key', 'account_name', 'session_token', 'credentials_type'}} + tmp = {k:v for k,v in validated_data.items() if k in {'key', 'secret_key', 'account_name', 'session_token', 'key_file_path', 'credentials_type'}} credentials.mapping_with_new_values(tmp) instance.credentials = credentials.convert_to_db() instance.credentials_type = validated_data.get('credentials_type', instance.credentials_type) instance.resource = validated_data.get('resource', instance.resource) instance.display_name = validated_data.get('display_name', instance.display_name) - - instance.save() - return instance + instance.description = validated_data.get('description', instance.description) + instance.specific_attributes = validated_data.get('specific_attributes', instance.specific_attributes) + + # check cloud storage existing + details = { + 'resource': instance.resource, + 'credentials': credentials, + 'specific_attributes': parse_specific_attributes(instance.specific_attributes) + } + storage = get_cloud_storage_instance(cloud_provider=instance.provider_type, **details) + storage_status = storage.get_status() + if storage_status == Status.AVAILABLE: + new_manifest_names = set(i.get('filename') for i in validated_data.get('manifests', [])) + previos_manifest_names = set(i.filename for i in instance.manifests.all()) + delta_to_delete = tuple(previos_manifest_names - new_manifest_names) + delta_to_create = tuple(new_manifest_names - previos_manifest_names) + if delta_to_delete: + instance.manifests.filter(filename__in=delta_to_delete).delete() + if delta_to_create: + # check manifest files existing + for manifest in delta_to_create: + file_status = storage.get_file_status(manifest) + if file_status == Status.NOT_FOUND: + raise serializers.ValidationError({ + 'manifests': "The '{}' file does not exist on '{}' cloud storage" + .format(manifest, storage.name) + }) + elif file_status == Status.FORBIDDEN: + raise serializers.ValidationError({ + 'manifests': "The '{}' file does not available on '{}' cloud storage. Access denied" \ + .format(manifest.get('filename'), storage.name) + }) + manifest_instances = [models.Manifest(filename=f, cloud_storage=instance) for f in delta_to_create] + models.Manifest.objects.bulk_create(manifest_instances) + instance.save() + return instance + elif storage_status == Status.FORBIDDEN: + field = 'credentials' + message = 'Cannot update resource {} with specified credentials. Access forbidden.'.format(storage.name) + else: + field = 'recource' + message = 'The resource {} not found. It may have been deleted.'.format(storage.name) + slogger.glob.error(message) + raise serializers.ValidationError({field: message}) class RelatedFileSerializer(serializers.ModelSerializer): diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index cc6a5ffa..e3cb293e 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -252,10 +252,20 @@ def _create_thread(tid, data, isImport=False): 'specific_attributes': db_cloud_storage.get_specific_attributes() } cloud_storage_instance = get_cloud_storage_instance(cloud_provider=db_cloud_storage.provider_type, **details) - cloud_storage_instance.download_file(manifest_file[0], db_data.get_manifest_path()) first_sorted_media_image = sorted(media['image'])[0] cloud_storage_instance.download_file(first_sorted_media_image, os.path.join(upload_dir, first_sorted_media_image)) + # prepare task manifest file from cloud storage manifest file + manifest = ImageManifestManager(db_data.get_manifest_path()) + cloud_storage_manifest = ImageManifestManager( + os.path.join(db_data.cloud_storage.get_storage_dirname(), manifest_file[0]) + ) + cloud_storage_manifest.set_index() + media_files = sorted(media['image']) + content = cloud_storage_manifest.get_subset(media_files) + manifest.create(content) + manifest.init_index() + av_scan_paths(upload_dir) job = rq.get_current_job() @@ -311,6 +321,9 @@ def _create_thread(tid, data, isImport=False): validate_dimension.set_path(upload_dir) validate_dimension.validate() + if db_task.project is not None and db_task.project.tasks.count() > 1 and db_task.project.tasks.first().dimension != validate_dimension.dimension: + raise Exception(f'Dimension ({validate_dimension.dimension}) of the task must be the same as other tasks in project ({db_task.project.tasks.first().dimension})') + if validate_dimension.dimension == models.DimensionType.DIM_3D: db_task.dimension = models.DimensionType.DIM_3D @@ -367,8 +380,6 @@ def _create_thread(tid, data, isImport=False): if not (db_data.storage == models.StorageChoice.CLOUD_STORAGE): w, h = extractor.get_image_size(0) else: - manifest = ImageManifestManager(db_data.get_manifest_path()) - manifest.init_index() img_properties = manifest[0] w, h = img_properties['width'], img_properties['height'] area = h * w diff --git a/cvat/apps/engine/utils.py b/cvat/apps/engine/utils.py index 87b7b856..c7d8ed49 100644 --- a/cvat/apps/engine/utils.py +++ b/cvat/apps/engine/utils.py @@ -98,4 +98,11 @@ def md5_hash(frame): frame = frame.to_image() elif isinstance(frame, str): frame = Image.open(frame, 'r') - return hashlib.md5(frame.tobytes()).hexdigest() # nosec \ No newline at end of file + return hashlib.md5(frame.tobytes()).hexdigest() # nosec + +def parse_specific_attributes(specific_attributes): + assert isinstance(specific_attributes, str), 'Specific attributes must be a string' + return { + item.split('=')[0].strip(): item.split('=')[1].strip() + for item in specific_attributes.split('&') + } if specific_attributes else dict() \ No newline at end of file diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 3adaa017..61688290 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -2,16 +2,17 @@ # # SPDX-License-Identifier: MIT +import errno import io -import json import os import os.path as osp +import pytz import shutil import traceback import uuid from datetime import datetime from distutils.util import strtobool -from tempfile import mkstemp, TemporaryDirectory +from tempfile import mkstemp, NamedTemporaryFile import cv2 from django.db.models.query import Prefetch @@ -40,10 +41,12 @@ from sendfile import sendfile import cvat.apps.dataset_manager as dm import cvat.apps.dataset_manager.views # pylint: disable=unused-import from cvat.apps.authentication import auth -from cvat.apps.engine.cloud_provider import get_cloud_storage_instance, Credentials +from cvat.apps.engine.cloud_provider import get_cloud_storage_instance, Credentials, Status from cvat.apps.dataset_manager.bindings import CvatImportError from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer from cvat.apps.engine.frame_provider import FrameProvider +from cvat.apps.engine.media_extractors import ImageListReader +from cvat.apps.engine.mime_types import mimetypes from cvat.apps.engine.models import ( Job, StatusChoice, Task, Project, Review, Issue, Comment, StorageMethodChoice, ReviewStatus, StorageChoice, Image, @@ -206,6 +209,7 @@ class ServerViewSet(viewsets.ViewSet): class ProjectFilter(filters.FilterSet): name = filters.CharFilter(field_name="name", lookup_expr="icontains") owner = filters.CharFilter(field_name="owner__username", lookup_expr="icontains") + assignee = filters.CharFilter(field_name="assignee__username", lookup_expr="icontains") status = filters.CharFilter(field_name="status", lookup_expr="icontains") class Meta: @@ -233,12 +237,14 @@ class ProjectFilter(filters.FilterSet): @method_decorator(name='partial_update', decorator=swagger_auto_schema(operation_summary='Methods does a partial update of chosen fields in a project')) class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet): queryset = models.Project.objects.all().order_by('-id') - search_fields = ("name", "owner__username", "status") + search_fields = ("name", "owner__username", "assignee__username", "status") filterset_class = ProjectFilter ordering_fields = ("id", "name", "owner", "status", "assignee") http_method_names = ['get', 'post', 'head', 'patch', 'delete'] def get_serializer_class(self): + if self.request.path.endswith('tasks'): + return TaskSerializer if self.request.query_params and self.request.query_params.get("names_only") == "true": return ProjectSearchSerializer if self.request.query_params and self.request.query_params.get("without_tasks") == "true": @@ -298,6 +304,76 @@ class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet): return Response(serializer.data) + @swagger_auto_schema(method='get', operation_summary='Export project as a dataset in a specific format', + manual_parameters=[ + openapi.Parameter('format', openapi.IN_QUERY, + description="Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats", + type=openapi.TYPE_STRING, required=True), + openapi.Parameter('filename', openapi.IN_QUERY, + description="Desired output file name", + type=openapi.TYPE_STRING, required=False), + openapi.Parameter('action', in_=openapi.IN_QUERY, + description='Used to start downloading process after annotation file had been created', + type=openapi.TYPE_STRING, required=False, enum=['download']) + ], + responses={'202': openapi.Response(description='Exporting has been started'), + '201': openapi.Response(description='Output file is ready for downloading'), + '200': openapi.Response(description='Download of file started'), + '405': openapi.Response(description='Format is not available'), + } + ) + @action(detail=True, methods=['GET'], serializer_class=None, + url_path='dataset') + def dataset_export(self, request, pk): + db_project = self.get_object() # force to call check_object_permissions + + format_name = request.query_params.get("format", "") + return _export_annotations(db_instance=db_project, + rq_id="/api/v1/project/{}/dataset/{}".format(pk, format_name), + request=request, + action=request.query_params.get("action", "").lower(), + callback=dm.views.export_project_as_dataset, + format_name=format_name, + filename=request.query_params.get("filename", "").lower(), + ) + + @swagger_auto_schema(method='get', operation_summary='Method allows to download project annotations', + manual_parameters=[ + openapi.Parameter('format', openapi.IN_QUERY, + description="Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats", + type=openapi.TYPE_STRING, required=True), + openapi.Parameter('filename', openapi.IN_QUERY, + description="Desired output file name", + type=openapi.TYPE_STRING, required=False), + openapi.Parameter('action', in_=openapi.IN_QUERY, + description='Used to start downloading process after annotation file had been created', + type=openapi.TYPE_STRING, required=False, enum=['download']) + ], + responses={ + '202': openapi.Response(description='Dump of annotations has been started'), + '201': openapi.Response(description='Annotations file is ready to download'), + '200': openapi.Response(description='Download of file started'), + '405': openapi.Response(description='Format is not available'), + '401': openapi.Response(description='Format is not specified'), + } + ) + @action(detail=True, methods=['GET'], + serializer_class=LabeledDataSerializer) + def annotations(self, request, pk): + db_project = self.get_object() # force to call check_object_permissions + format_name = request.query_params.get('format') + if format_name: + return _export_annotations(db_instance=db_project, + rq_id="/api/v1/projects/{}/annotations/{}".format(pk, format_name), + request=request, + action=request.query_params.get("action", "").lower(), + callback=dm.views.export_project_annotations, + format_name=format_name, + filename=request.query_params.get("filename", "").lower(), + ) + else: + return Response("Format is not specified",status=status.HTTP_400_BAD_REQUEST) + class TaskFilter(filters.FilterSet): project = filters.CharFilter(field_name="project__name", lookup_expr="icontains") name = filters.CharFilter(field_name="name", lookup_expr="icontains") @@ -475,7 +551,7 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet): else: return Response(status=status.HTTP_202_ACCEPTED) - ttl = dm.views.CACHE_TTL.total_seconds() + ttl = dm.views.TASK_CACHE_TTL.total_seconds() queue.enqueue_call( func=dm.views.backup_task, args=(pk, 'task_dump.zip'), @@ -679,7 +755,7 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet): if request.method == 'GET': format_name = request.query_params.get('format') if format_name: - return _export_annotations(db_task=db_task, + return _export_annotations(db_instance=db_task, rq_id="/api/v1/tasks/{}/annotations/{}".format(pk, format_name), request=request, action=request.query_params.get("action", "").lower(), @@ -806,7 +882,7 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet): db_task = self.get_object() # force to call check_object_permissions format_name = request.query_params.get("format", "") - return _export_annotations(db_task=db_task, + return _export_annotations(db_instance=db_task, rq_id="/api/v1/tasks/{}/dataset/{}".format(pk, format_name), request=request, action=request.query_params.get("action", "").lower(), @@ -1114,6 +1190,18 @@ class RedefineDescriptionField(FieldInspector): 'supported: range=aws_range' return result +class CloudStorageFilter(filters.FilterSet): + display_name = filters.CharFilter(field_name='display_name', lookup_expr='icontains') + provider_type = filters.CharFilter(field_name='provider_type', lookup_expr='icontains') + resource = filters.CharFilter(field_name='resource', lookup_expr='icontains') + credentials_type = filters.CharFilter(field_name='credentials_type', lookup_expr='icontains') + description = filters.CharFilter(field_name='description', lookup_expr='icontains') + owner = filters.CharFilter(field_name='owner__username', lookup_expr='icontains') + + class Meta: + model = models.CloudStorage + fields = ('id', 'display_name', 'provider_type', 'resource', 'credentials_type', 'description', 'owner') + @method_decorator( name='retrieve', decorator=swagger_auto_schema( @@ -1153,8 +1241,8 @@ class RedefineDescriptionField(FieldInspector): class CloudStorageViewSet(auth.CloudStorageGetQuerySetMixin, viewsets.ModelViewSet): http_method_names = ['get', 'post', 'patch', 'delete'] queryset = CloudStorageModel.objects.all().prefetch_related('data').order_by('-id') - search_fields = ('provider_type', 'display_name', 'resource', 'owner__username') - filterset_fields = ['provider_type', 'display_name', 'resource', 'credentials_type'] + search_fields = ('provider_type', 'display_name', 'resource', 'credentials_type', 'owner__username', 'description') + filterset_class = CloudStorageFilter def get_permissions(self): http_method = self.request.method @@ -1184,36 +1272,7 @@ class CloudStorageViewSet(auth.CloudStorageGetQuerySetMixin, viewsets.ModelViewS return queryset def perform_create(self, serializer): - # check that instance of cloud storage exists - provider_type = serializer.validated_data.get('provider_type') - credentials = Credentials( - session_token=serializer.validated_data.get('session_token', ''), - account_name=serializer.validated_data.get('account_name', ''), - key=serializer.validated_data.get('key', ''), - secret_key=serializer.validated_data.get('secret_key', '') - ) - details = { - 'resource': serializer.validated_data.get('resource'), - 'credentials': credentials, - 'specific_attributes': { - item.split('=')[0].strip(): item.split('=')[1].strip() - for item in serializer.validated_data.get('specific_attributes').split('&') - } if len(serializer.validated_data.get('specific_attributes', '')) - else dict() - } - storage = get_cloud_storage_instance(cloud_provider=provider_type, **details) - try: - storage.exists() - except Exception as ex: - message = str(ex) - slogger.glob.error(message) - raise - - owner = self.request.data.get('owner') - if owner: - serializer.save() - else: - serializer.save(owner=self.request.user) + serializer.save(owner=self.request.user) def perform_destroy(self, instance): cloud_storage_dirname = instance.get_storage_dirname() @@ -1238,7 +1297,7 @@ class CloudStorageViewSet(auth.CloudStorageGetQuerySetMixin, viewsets.ModelViewS msg_body = "" for ex in exceptions.args: for field, ex_msg in ex.items(): - msg_body += ": ".join([field, str(ex_msg[0])]) + msg_body += ': '.join([field, ex_msg if isinstance(ex_msg, str) else str(ex_msg[0])]) msg_body += '\n' return HttpResponseBadRequest(msg_body) except APIException as ex: @@ -1249,14 +1308,14 @@ class CloudStorageViewSet(auth.CloudStorageGetQuerySetMixin, viewsets.ModelViewS @swagger_auto_schema( method='get', - operation_summary='Method returns a mapped names of an available files from a storage and a manifest content', + operation_summary='Method returns a manifest content', manual_parameters=[ openapi.Parameter('manifest_path', openapi.IN_QUERY, description="Path to the manifest file in a cloud storage", type=openapi.TYPE_STRING) ], responses={ - '200': openapi.Response(description='Mapped names of an available files from a storage and a manifest content'), + '200': openapi.Response(description='A manifest content'), }, tags=['cloud storages'] ) @@ -1275,30 +1334,152 @@ class CloudStorageViewSet(auth.CloudStorageGetQuerySetMixin, viewsets.ModelViewS 'specific_attributes': db_storage.get_specific_attributes() } storage = get_cloud_storage_instance(cloud_provider=db_storage.provider_type, **details) - storage.initialize_content() - storage_files = storage.content - + if not db_storage.manifests.count(): + raise Exception('There is no manifest file') manifest_path = request.query_params.get('manifest_path', 'manifest.jsonl') - with TemporaryDirectory(suffix='manifest', prefix='cvat') as tmp_dir: - tmp_manifest_path = os.path.join(tmp_dir, 'manifest.jsonl') - storage.download_file(manifest_path, tmp_manifest_path) - manifest = ImageManifestManager(tmp_manifest_path) - manifest.init_index() - manifest_files = manifest.data - content = {f:[] for f in set(storage_files) | set(manifest_files)} - for key, _ in content.items(): - if key in storage_files: content[key].append('s') # storage - if key in manifest_files: content[key].append('m') # manifest - - data = json.dumps(content) - return Response(data=data, content_type="aplication/json") + file_status = storage.get_file_status(manifest_path) + if file_status == Status.NOT_FOUND: + raise FileNotFoundError(errno.ENOENT, + "Not found on the cloud storage {}".format(db_storage.display_name), manifest_path) + elif file_status == Status.FORBIDDEN: + raise PermissionError(errno.EACCES, + "Access to the file on the '{}' cloud storage is denied".format(db_storage.display_name), manifest_path) + + full_manifest_path = os.path.join(db_storage.get_storage_dirname(), manifest_path) + if not os.path.exists(full_manifest_path) or \ + datetime.utcfromtimestamp(os.path.getmtime(full_manifest_path)).replace(tzinfo=pytz.UTC) < storage.get_file_last_modified(manifest_path): + storage.download_file(manifest_path, full_manifest_path) + manifest = ImageManifestManager(full_manifest_path) + # need to update index + manifest.set_index() + manifest_files = manifest.data + return Response(data=manifest_files, content_type="text/plain") + + except CloudStorageModel.DoesNotExist: + message = f"Storage {pk} does not exist" + slogger.glob.error(message) + return HttpResponseNotFound(message) + except FileNotFoundError as ex: + msg = f"{ex.strerror} {ex.filename}" + slogger.cloud_storage[pk].info(msg) + return Response(data=msg, status=status.HTTP_404_NOT_FOUND) + except Exception as ex: + # check that cloud storage was not deleted + storage_status = storage.get_status() + if storage_status == Status.FORBIDDEN: + msg = 'The resource {} is no longer available. Access forbidden.'.format(storage.name) + elif storage_status == Status.NOT_FOUND: + msg = 'The resource {} not found. It may have been deleted.'.format(storage.name) + else: + msg = str(ex) + return HttpResponseBadRequest(msg) + @swagger_auto_schema( + method='get', + operation_summary='Method returns a preview image from a cloud storage', + responses={ + '200': openapi.Response(description='Preview'), + }, + tags=['cloud storages'] + ) + @action(detail=True, methods=['GET'], url_path='preview') + def preview(self, request, pk): + try: + db_storage = CloudStorageModel.objects.get(pk=pk) + if not os.path.exists(db_storage.get_preview_path()): + credentials = Credentials() + credentials.convert_from_db({ + 'type': db_storage.credentials_type, + 'value': db_storage.credentials, + }) + details = { + 'resource': db_storage.resource, + 'credentials': credentials, + 'specific_attributes': db_storage.get_specific_attributes() + } + storage = get_cloud_storage_instance(cloud_provider=db_storage.provider_type, **details) + if not db_storage.manifests.count(): + raise Exception('Cannot get the cloud storage preview. There is no manifest file') + preview_path = None + for manifest_model in db_storage.manifests.all(): + full_manifest_path = os.path.join(db_storage.get_storage_dirname(), manifest_model.filename) + if not os.path.exists(full_manifest_path) or \ + datetime.utcfromtimestamp(os.path.getmtime(full_manifest_path)).replace(tzinfo=pytz.UTC) < storage.get_file_last_modified(manifest_model.filename): + storage.download_file(manifest_model.filename, full_manifest_path) + manifest = ImageManifestManager(os.path.join(db_storage.get_storage_dirname(), manifest_model.filename)) + # need to update index + manifest.set_index() + if not len(manifest): + continue + preview_info = manifest[0] + preview_path = ''.join([preview_info['name'], preview_info['extension']]) + break + if not preview_path: + msg = 'Cloud storage {} does not contain any images'.format(pk) + slogger.cloud_storage[pk].info(msg) + return HttpResponseBadRequest(msg) + + file_status = storage.get_file_status(preview_path) + if file_status == Status.NOT_FOUND: + raise FileNotFoundError(errno.ENOENT, + "Not found on the cloud storage {}".format(db_storage.display_name), preview_path) + elif file_status == Status.FORBIDDEN: + raise PermissionError(errno.EACCES, + "Access to the file on the '{}' cloud storage is denied".format(db_storage.display_name), preview_path) + with NamedTemporaryFile() as temp_image: + storage.download_file(preview_path, temp_image.name) + reader = ImageListReader([temp_image.name]) + preview = reader.get_preview() + preview.save(db_storage.get_preview_path()) + content_type = mimetypes.guess_type(db_storage.get_preview_path())[0] + return HttpResponse(open(db_storage.get_preview_path(), 'rb').read(), content_type) except CloudStorageModel.DoesNotExist: message = f"Storage {pk} does not exist" slogger.glob.error(message) return HttpResponseNotFound(message) except Exception as ex: - return HttpResponseBadRequest(str(ex)) + # check that cloud storage was not deleted + storage_status = storage.get_status() + if storage_status == Status.FORBIDDEN: + msg = 'The resource {} is no longer available. Access forbidden.'.format(storage.name) + elif storage_status == Status.NOT_FOUND: + msg = 'The resource {} not found. It may have been deleted.'.format(storage.name) + else: + msg = str(ex) + return HttpResponseBadRequest(msg) + + @swagger_auto_schema( + method='get', + operation_summary='Method returns a cloud storage status', + responses={ + '200': openapi.Response(description='Status'), + }, + tags=['cloud storages'] + ) + @action(detail=True, methods=['GET'], url_path='status') + def status(self, request, pk): + try: + db_storage = CloudStorageModel.objects.get(pk=pk) + credentials = Credentials() + credentials.convert_from_db({ + 'type': db_storage.credentials_type, + 'value': db_storage.credentials, + }) + details = { + 'resource': db_storage.resource, + 'credentials': credentials, + 'specific_attributes': db_storage.get_specific_attributes() + } + storage = get_cloud_storage_instance(cloud_provider=db_storage.provider_type, **details) + storage_status = storage.get_status() + return HttpResponse(storage_status) + except CloudStorageModel.DoesNotExist: + message = f"Storage {pk} does not exist" + slogger.glob.error(message) + return HttpResponseNotFound(message) + except Exception as ex: + msg = str(ex) + return HttpResponseBadRequest(msg) def rq_handler(job, exc_type, exc_value, tb): job.exc_info = "".join( @@ -1373,7 +1554,7 @@ def _import_annotations(request, rq_id, rq_func, pk, format_name): return Response(status=status.HTTP_202_ACCEPTED) -def _export_annotations(db_task, rq_id, request, format_name, action, callback, filename): +def _export_annotations(db_instance, rq_id, request, format_name, action, callback, filename): if action not in {"", "download"}: raise serializers.ValidationError( "Unexpected action specified for the request") @@ -1390,9 +1571,12 @@ def _export_annotations(db_task, rq_id, request, format_name, action, callback, rq_job = queue.fetch_job(rq_id) if rq_job: - last_task_update_time = timezone.localtime(db_task.updated_date) + last_instance_update_time = timezone.localtime(db_instance.updated_date) + if isinstance(db_instance, Project): + tasks_update = list(map(lambda db_task: timezone.localtime(db_task.updated_date), db_instance.tasks.all())) + last_instance_update_time = max(tasks_update + [last_instance_update_time]) request_time = rq_job.meta.get('request_time', None) - if request_time is None or request_time < last_task_update_time: + if request_time is None or request_time < last_instance_update_time: rq_job.cancel() rq_job.delete() else: @@ -1401,12 +1585,14 @@ def _export_annotations(db_task, rq_id, request, format_name, action, callback, if action == "download" and osp.exists(file_path): rq_job.delete() - timestamp = datetime.strftime(last_task_update_time, + timestamp = datetime.strftime(last_instance_update_time, "%Y_%m_%d_%H_%M_%S") filename = filename or \ - "task_{}-{}-{}{}".format( - db_task.name, timestamp, - format_name, osp.splitext(file_path)[1]) + "{}_{}-{}-{}{}".format( + "project" if isinstance(db_instance, models.Project) else "task", + db_instance.name, timestamp, + format_name, osp.splitext(file_path)[1] + ) return sendfile(request, file_path, attachment=True, attachment_filename=filename.lower()) else: @@ -1427,11 +1613,9 @@ def _export_annotations(db_task, rq_id, request, format_name, action, callback, except Exception: server_address = None - ttl = dm.views.CACHE_TTL.total_seconds() + ttl = (dm.views.PROJECT_CACHE_TTL if isinstance(db_instance, Project) else dm.views.TASK_CACHE_TTL).total_seconds() queue.enqueue_call(func=callback, - args=(db_task.id, format_name, server_address), job_id=rq_id, + args=(db_instance.id, format_name, server_address), job_id=rq_id, meta={ 'request_time': timezone.localtime() }, result_ttl=ttl, failure_ttl=ttl) return Response(status=status.HTTP_202_ACCEPTED) - - diff --git a/cvat/apps/lambda_manager/views.py b/cvat/apps/lambda_manager/views.py index 53bb2c14..f5a34217 100644 --- a/cvat/apps/lambda_manager/views.py +++ b/cvat/apps/lambda_manager/views.py @@ -113,6 +113,8 @@ class LambdaFunction: self.min_pos_points = int(meta_anno.get('min_pos_points', 1)) self.min_neg_points = int(meta_anno.get('min_neg_points', -1)) self.startswith_box = bool(meta_anno.get('startswith_box', False)) + self.animated_gif = meta_anno.get('animated_gif', '') + self.help_message = meta_anno.get('help_message', '') self.gateway = gateway def to_dict(self): @@ -129,7 +131,9 @@ class LambdaFunction: response.update({ 'min_pos_points': self.min_pos_points, 'min_neg_points': self.min_neg_points, - 'startswith_box': self.startswith_box + 'startswith_box': self.startswith_box, + 'help_message': self.help_message, + 'animated_gif': self.animated_gif }) if self.kind is LambdaType.TRACKER: diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index 2ec26515..60569a23 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -6,7 +6,7 @@ django-cacheops==5.0.1 django-compressor==2.4 django-rq==2.3.2 EasyProcess==0.3 -Pillow==8.3.0 +Pillow==8.3.2 numpy==1.19.5 python-ldap==3.3.1 pytz==2020.1 @@ -14,7 +14,7 @@ pyunpack==0.2.1 rcssmin==1.0.6 redis==3.5.3 rjsmin==1.1.0 -requests==2.24.0 +requests==2.26.0 rq==1.5.1 rq-scheduler==0.10.0 sqlparse==0.3.1 @@ -47,9 +47,11 @@ diskcache==5.0.2 open3d==0.11.2 boto3==1.17.61 azure-storage-blob==12.8.1 +google-cloud-storage==1.42.0 # --no-binary=datumaro: workaround for pip to install # opencv-headless instead of regular opencv, to actually run setup script # --no-binary=pycocotools: workaround for binary incompatibility on numpy 1.20 # of pycocotools and tensorflow 2.4.1 # when pycocotools is installed by wheel in python 3.8+ datumaro==0.1.10.1 --no-binary=datumaro --no-binary=pycocotools +urllib3>=1.26.5 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/docker-compose.yml b/docker-compose.yml index 2ae57d99..4c472246 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,6 +68,7 @@ services: traefik: image: traefik:v2.4 container_name: traefik + restart: always command: - "--providers.docker.exposedByDefault=false" - "--providers.docker.network=cvat" @@ -95,4 +96,4 @@ volumes: cvat_logs: networks: - cvat: \ No newline at end of file + cvat: diff --git a/serverless/openvino/dextr/nuclio/function.yaml b/serverless/openvino/dextr/nuclio/function.yaml index 82559942..b996c0d8 100644 --- a/serverless/openvino/dextr/nuclio/function.yaml +++ b/serverless/openvino/dextr/nuclio/function.yaml @@ -7,6 +7,8 @@ metadata: spec: framework: openvino min_pos_points: 4 + animated_gif: https://raw.githubusercontent.com/openvinotoolkit/cvat/0fbb19ae3846a017853d52e187f0ce149adced7d/site/content/en/images/dextr_example.gif + help_message: The interactor allows to get a mask of an object using its extreme points (more or equal than 4). You can add a point left-clicking the image spec: description: Deep Extreme Cut diff --git a/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml b/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml index 57b78b71..c0c4e6dc 100644 --- a/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml +++ b/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml @@ -8,6 +8,8 @@ metadata: framework: pytorch min_pos_points: 1 min_neg_points: 0 + animated_gif: https://raw.githubusercontent.com/openvinotoolkit/cvat/0fbb19ae3846a017853d52e187f0ce149adced7d/site/content/en/images/fbrs_example.gif + help_message: The interactor allows to get a mask for an object using positive points, and negative points spec: description: f-BRS interactive segmentation diff --git a/serverless/pytorch/shiyinzhang/iog/nuclio/function.yaml b/serverless/pytorch/shiyinzhang/iog/nuclio/function.yaml index a210195f..6535525a 100644 --- a/serverless/pytorch/shiyinzhang/iog/nuclio/function.yaml +++ b/serverless/pytorch/shiyinzhang/iog/nuclio/function.yaml @@ -9,6 +9,8 @@ metadata: min_pos_points: 1 min_neg_points: 0 startswith_box: true + animated_gif: https://raw.githubusercontent.com/openvinotoolkit/cvat/0fbb19ae3846a017853d52e187f0ce149adced7d/site/content/en/images/iog_example.gif + help_message: The interactor allows to get a mask of an object using its wrapping boundig box, positive, and negative points inside it spec: description: Interactive Object Segmentation with Inside-Outside Guidance diff --git a/site/README.md b/site/README.md index c706e583..5e895393 100644 --- a/site/README.md +++ b/site/README.md @@ -44,7 +44,7 @@ To build and preview your site locally, use: cd /cvat/site/ hugo server -By default, your site will be available at . +By default, your site will be available at . Instead of a "hugo server" command, you can use the "hugo" command that generates the site into a "public" folder. diff --git a/site/build_docs.py b/site/build_docs.py index d649af46..37232b8f 100644 --- a/site/build_docs.py +++ b/site/build_docs.py @@ -54,7 +54,8 @@ def generate_docs(repo, output_dir, tags): generate_versioning_config(os.path.join(cwd, 'site', 'versioning.toml'), (t.name for t in tags), '/..') for tag in tags: - repo.git.checkout(tag.name) + repo.git.checkout(tag.name, '--', 'site/content/en/docs') + repo.git.checkout(tag.name, '--', 'site/content/en/images') destination_dir = os.path.join(output_dir, tag.name) os.makedirs(destination_dir) run_hugo(content_loc, destination_dir) diff --git a/site/config.toml b/site/config.toml index 390309ec..b9f6f7ac 100644 --- a/site/config.toml +++ b/site/config.toml @@ -49,11 +49,16 @@ anchor = "smart" name = "Try it now" weight = 50 url = "https://cvat.org" +[[menu.main]] + name = "GitHub" + weight = 60 + url = "https://github.com/openvinotoolkit/cvat" + pre = "" [services] [services.googleAnalytics] # Comment out the next line to disable GA tracking. Also disables the feature described in [params.ui.feedback]. -id = "UA-00000000-0" +# id = "UA-00000000-0" # Language configuration diff --git a/site/content/en/_index.html b/site/content/en/_index.html index 8c5fd7a3..1fe09b59 100644 --- a/site/content/en/_index.html +++ b/site/content/en/_index.html @@ -3,6 +3,8 @@ title = "Home" linkTitle = "Home" +++ + + {{< blocks/section height="full" color="docs" >}}
diff --git a/site/content/en/docs/administration/_index.md b/site/content/en/docs/administration/_index.md index cb85ffa3..215c41aa 100644 --- a/site/content/en/docs/administration/_index.md +++ b/site/content/en/docs/administration/_index.md @@ -3,7 +3,7 @@ title: 'Administration' linkTitle: 'Administration' weight: 3 -description: 'This section contains documents for system administrators' +description: 'This section contains documents for system administrators.' hide_feedback: true --- diff --git a/site/content/en/docs/administration/advanced/_index.md b/site/content/en/docs/administration/advanced/_index.md index a58d2bb5..8f1ad89b 100644 --- a/site/content/en/docs/administration/advanced/_index.md +++ b/site/content/en/docs/administration/advanced/_index.md @@ -3,7 +3,7 @@ title: 'Advanced' linkTitle: 'Advanced' weight: 2 -description: 'This section contains basic documents for system administrators' +description: 'This section contains advanced documents for system administrators.' hide_feedback: true --- diff --git a/site/content/en/docs/administration/advanced/analytics.md b/site/content/en/docs/administration/advanced/analytics.md index 6ef814ef..c52b0365 100644 --- a/site/content/en/docs/administration/advanced/analytics.md +++ b/site/content/en/docs/administration/advanced/analytics.md @@ -2,10 +2,10 @@ --- -title: 'Analytics for Computer Vision Annotation Tool (CVAT)' -linkTitle: 'Analytics' +title: 'Installation Analytics' +linkTitle: 'Installation Analytics' weight: 2 -description: This section on [GitHub](https://github.com/openvinotoolkit/cvat/tree/develop/components/analytics) +description: 'Instructions for deployment and customization of Analytics. This section on [GitHub](https://github.com/openvinotoolkit/cvat/tree/develop/components/analytics).' --- diff --git a/site/content/en/docs/administration/advanced/backup_guide.md b/site/content/en/docs/administration/advanced/backup_guide.md index 316f929f..6fe1e15c 100644 --- a/site/content/en/docs/administration/advanced/backup_guide.md +++ b/site/content/en/docs/administration/advanced/backup_guide.md @@ -2,6 +2,7 @@ title: 'Backup guide' linkTitle: 'Backup guide' weight: 11 +description: 'Instructions on how to backup CVAT data with Docker.' --- diff --git a/site/content/en/docs/administration/advanced/installation_automatic_annotation.md b/site/content/en/docs/administration/advanced/installation_automatic_annotation.md index d75e941a..e7d2c19b 100644 --- a/site/content/en/docs/administration/advanced/installation_automatic_annotation.md +++ b/site/content/en/docs/administration/advanced/installation_automatic_annotation.md @@ -5,8 +5,7 @@ title: 'Semi-automatic and Automatic Annotation' linkTitle: 'Installation Auto Annotation' weight: 5 -description: 'This page provides information about the installation of components needed for -semi-automatic and automatic annotation' +description: 'Information about the installation of components needed for semi-automatic and automatic annotation.' --- @@ -32,7 +31,13 @@ semi-automatic and automatic annotation' - You have to install `nuctl` command line tool to build and deploy serverless functions. Download [version 1.5.16](https://github.com/nuclio/nuclio/releases/tag/1.5.16). It is important that the version you download matches the version in - [docker-compose.serverless.yml](https://github.com/openvinotoolkit/cvat/blob/develop/components/serverless/docker-compose.serverless.yml) + [docker-compose.serverless.yml](https://github.com/openvinotoolkit/cvat/blob/develop/components/serverless/docker-compose.serverless.yml). + For example, using wget. + + ``` + wget https://github.com/nuclio/nuclio/releases/download//nuctl--linux-amd64 + ``` + After downloading the nuclio, give it a proper permission and do a softlink ``` diff --git a/site/content/en/docs/administration/advanced/mounting_cloud_storages.md b/site/content/en/docs/administration/advanced/mounting_cloud_storages.md index c1db66aa..873c5c13 100644 --- a/site/content/en/docs/administration/advanced/mounting_cloud_storages.md +++ b/site/content/en/docs/administration/advanced/mounting_cloud_storages.md @@ -2,6 +2,7 @@ title: 'Mounting cloud storage' linkTitle: 'Mounting cloud storage' weight: 10 +description: 'Instructions on how to mount AWS S3 bucket, Microsoft Azure container or Google Drive as a filesystem.' --- diff --git a/site/content/en/docs/administration/basics/AWS-Deployment-Guide.md b/site/content/en/docs/administration/basics/AWS-Deployment-Guide.md index 12396134..786a070e 100644 --- a/site/content/en/docs/administration/basics/AWS-Deployment-Guide.md +++ b/site/content/en/docs/administration/basics/AWS-Deployment-Guide.md @@ -2,6 +2,7 @@ title: 'AWS-Deployment Guide' linkTitle: 'AWS-Deployment Guide' weight: 4 +description: 'Instructions for deploying CVAT on Nvidia GPU and other AWS machines.' --- There are two ways of deploying the CVAT. diff --git a/site/content/en/docs/administration/basics/REST_API_guide.md b/site/content/en/docs/administration/basics/REST_API_guide.md index 5ba6c657..ee1634ba 100644 --- a/site/content/en/docs/administration/basics/REST_API_guide.md +++ b/site/content/en/docs/administration/basics/REST_API_guide.md @@ -2,6 +2,7 @@ title: 'REST API guide' linkTitle: 'REST API' weight: 12 +description: 'Instructions on how to interact with REST API and getting swagger documentation.' --- To access swagger documentation you need to be authorized. diff --git a/site/content/en/docs/administration/basics/_index.md b/site/content/en/docs/administration/basics/_index.md index c2fb927b..e156be79 100644 --- a/site/content/en/docs/administration/basics/_index.md +++ b/site/content/en/docs/administration/basics/_index.md @@ -3,7 +3,7 @@ title: 'Basics' linkTitle: 'Basics' weight: 1 -description: 'This section contains basic documents for system administrators' +description: 'This section contains basic documents for system administrators.' hide_feedback: true --- diff --git a/site/content/en/docs/administration/basics/installation.md b/site/content/en/docs/administration/basics/installation.md index 792f057a..86f0358b 100644 --- a/site/content/en/docs/administration/basics/installation.md +++ b/site/content/en/docs/administration/basics/installation.md @@ -5,7 +5,7 @@ title: 'Installation Guide' linkTitle: 'Installation Guide' weight: 1 -description: 'CVAT installation guide for different operating systems' +description: 'A CVAT installation guide for different operating systems.' --- diff --git a/site/content/en/docs/contributing/_index.md b/site/content/en/docs/contributing/_index.md index 31ee3ce8..180ebbce 100644 --- a/site/content/en/docs/contributing/_index.md +++ b/site/content/en/docs/contributing/_index.md @@ -2,7 +2,7 @@ title: 'Contributing to this project' linkTitle: 'Contributing' weight: 4 -description: 'This section contains documents for CVAT developers' +description: 'This section contains documents for CVAT developers.' hide_feedback: true ul_show: false --- diff --git a/site/content/en/docs/contributing/branching-model.md b/site/content/en/docs/contributing/branching-model.md index 0c8c5994..a72fff3d 100644 --- a/site/content/en/docs/contributing/branching-model.md +++ b/site/content/en/docs/contributing/branching-model.md @@ -2,6 +2,7 @@ title: 'Branching model' linkTitle: 'Branching model' weight: 5 +description: 'Information about the branching model that is used in the project.' --- The project uses [a successful Git branching model](https://nvie.com/posts/a-successful-git-branching-model). diff --git a/site/content/en/docs/contributing/bug-reports.md b/site/content/en/docs/contributing/bug-reports.md index e66e5f36..323321db 100644 --- a/site/content/en/docs/contributing/bug-reports.md +++ b/site/content/en/docs/contributing/bug-reports.md @@ -2,6 +2,7 @@ title: 'Bug reports' linkTitle: 'Bug reports' weight: 7 +description: 'Guidelines and an example of how to report a bug.' --- A bug is a _demonstrable problem_ that is caused by the code in the repository. diff --git a/site/content/en/docs/contributing/coding-style.md b/site/content/en/docs/contributing/coding-style.md index cb063f65..41beabf7 100644 --- a/site/content/en/docs/contributing/coding-style.md +++ b/site/content/en/docs/contributing/coding-style.md @@ -2,6 +2,7 @@ title: 'JavaScript/Typescript coding style' linkTitle: 'JavaScript/Typescript coding style' weight: 4 +description: 'Information about JavaScript/Typescript coding style that is used in CVAT development.' --- We use the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) for JavaScript code with a diff --git a/site/content/en/docs/contributing/development-environment.md b/site/content/en/docs/contributing/development-environment.md index 3fbe140c..06c49c26 100644 --- a/site/content/en/docs/contributing/development-environment.md +++ b/site/content/en/docs/contributing/development-environment.md @@ -2,6 +2,7 @@ title: 'Development environment' linkTitle: 'Development environment' weight: 2 +description: 'Installing a development environment for different operating systems.' --- - Install necessary dependencies: diff --git a/site/content/en/docs/contributing/feature-requests.md b/site/content/en/docs/contributing/feature-requests.md index 11715c75..d6b70812 100644 --- a/site/content/en/docs/contributing/feature-requests.md +++ b/site/content/en/docs/contributing/feature-requests.md @@ -2,6 +2,7 @@ title: 'Feature requests' linkTitle: 'Feature requests' weight: 8 +description: 'Information on requesting new features.' --- Feature requests are welcome. But take a moment to find out whether your idea diff --git a/site/content/en/docs/contributing/new-annotation-format.md b/site/content/en/docs/contributing/new-annotation-format.md index 0cf21485..19c641ab 100644 --- a/site/content/en/docs/contributing/new-annotation-format.md +++ b/site/content/en/docs/contributing/new-annotation-format.md @@ -2,7 +2,7 @@ title: 'How to add a new annotation format support' linkTitle: 'New annotation format support' weight: 10 -description: This section on [GitHub](https://github.com/openvinotoolkit/cvat/tree/develop/cvat/apps/dataset_manager/formats) +description: 'Instructions on adding support for new annotation formats. This section on [GitHub](https://github.com/openvinotoolkit/cvat/tree/develop/cvat/apps/dataset_manager/formats).' --- 1. Add a python script to `dataset_manager/formats` diff --git a/site/content/en/docs/contributing/pull-requests.md b/site/content/en/docs/contributing/pull-requests.md index 1d71b31f..5ec4145b 100644 --- a/site/content/en/docs/contributing/pull-requests.md +++ b/site/content/en/docs/contributing/pull-requests.md @@ -2,6 +2,7 @@ title: 'Pull requests' linkTitle: 'Pull requests' weight: 9 +description: 'Instructions on how to create a pull request.' --- Good pull requests - patches, improvements, new features - are a fantastic diff --git a/site/content/en/docs/contributing/rest-api-design.md b/site/content/en/docs/contributing/rest-api-design.md index 6f663f77..4444f7a5 100644 --- a/site/content/en/docs/contributing/rest-api-design.md +++ b/site/content/en/docs/contributing/rest-api-design.md @@ -2,6 +2,7 @@ title: 'REST API design principles' linkTitle: 'REST API design principles' weight: 100 +description: 'Information on using the REST API scheme and principles of its design.' --- ## REST API scheme @@ -42,3 +43,4 @@ Common scheme for our REST API is ` [namespace] `. - [Best practices for REST API design](https://stackoverflow.blog/2020/03/02/best-practices-for-rest-api-design/) - [Flat vs. nested resources](https://stackoverflow.com/questions/20951419/what-are-best-practices-for-rest-nested-resources) +- [REST API Design Best Practices for Sub and Nested Resources](https://www.moesif.com/blog/technical/api-design/REST-API-Design-Best-Practices-for-Sub-and-Nested-Resources/) diff --git a/site/content/en/docs/contributing/setup-additional-components.md b/site/content/en/docs/contributing/setup-additional-components.md index 6c5ef57d..fcb7b4a0 100644 --- a/site/content/en/docs/contributing/setup-additional-components.md +++ b/site/content/en/docs/contributing/setup-additional-components.md @@ -2,6 +2,7 @@ title: 'Setup additional components in development environment' linkTitle: 'Setup additional components in development environment' weight: 3 +description: 'Deploying a DL model as a serverless function and Cypress tests.' --- ## DL models as serverless functions diff --git a/site/content/en/docs/contributing/using-the-issue-tracker.md b/site/content/en/docs/contributing/using-the-issue-tracker.md index bcb619a2..ad9d7c8c 100644 --- a/site/content/en/docs/contributing/using-the-issue-tracker.md +++ b/site/content/en/docs/contributing/using-the-issue-tracker.md @@ -2,6 +2,7 @@ title: 'Using the issue tracker' linkTitle: 'Using the issue tracker' weight: 6 +description: 'Information and rules for using the issue tracker.' --- The issue tracker is the preferred channel for [bug reports](#bugs), diff --git a/site/content/en/docs/manual/advanced/3d-object-annotation-advanced.md b/site/content/en/docs/manual/advanced/3d-object-annotation-advanced.md index 763b7e49..62c17ac6 100644 --- a/site/content/en/docs/manual/advanced/3d-object-annotation-advanced.md +++ b/site/content/en/docs/manual/advanced/3d-object-annotation-advanced.md @@ -2,6 +2,7 @@ title: '3D Object annotation (advanced)' linkTitle: '3D Object annotation' weight: 18 +description: 'Overview of advanced operations available when annotating 3D objects.' --- As well as 2D-task objects, 3D-task objects support the ability to change appearance, attributes, diff --git a/site/content/en/docs/manual/advanced/ai-tools.md b/site/content/en/docs/manual/advanced/ai-tools.md index 03d3c71a..a5334c81 100644 --- a/site/content/en/docs/manual/advanced/ai-tools.md +++ b/site/content/en/docs/manual/advanced/ai-tools.md @@ -2,13 +2,14 @@ title: 'AI Tools' linkTitle: 'AI Tools' weight: 15 +description: 'Overview of semi-automatic and automatic annotation tools available in CVAT.' --- The tool is designed for semi-automatic and automatic annotation using DL models. The tool is available only if there is a corresponding model. For more details about DL models read the [Models](/docs/manual/advanced/models/) section. -### Interactors +## Interactors Interactors are used to create a polygon semi-automatically. Supported DL models are not bound to the label and can be used for any objects. @@ -17,21 +18,25 @@ For some kinds of segmentation negative points are available. Positive points are the points related to the object. Negative points should be placed outside the boundary of the object. In most cases specifying positive points alone is enough to build a polygon. +A list of available out-of-the-box interactors is placed below. -- Before you start, select the magic wand on the controls sidebar and go to the `Interactors` tab. - Then select a label for the polygon and a required DL model. +- Before you start, select the `magic wand` on the controls sidebar and go to the `Interactors` tab. + Then select a label for the polygon and a required DL model. To view help about each of the + models, you can click the `Question mark` icon. - ![](/images/image114.jpg) + ![](/images/image114_detrac.jpg) -- Click `Interact` to enter the interaction mode. Now you can place positive and/or negative points. +- Click `Interact` to enter the interaction mode. Depending on the selected model, + the method of markup will also differ. + Now you can place positive and/or negative points. The [IOG](#inside-outside-guidance) model also uses a rectangle. Left click creates a positive point and right click creates a negative point. - `Deep extreme cut` model requires a minimum of 4 points. After you set 4 positive points, - a request will be sent to the server and when the process is complete a polygon will be created. + After placing the required number of points (the number is different depending on the model), + the request will be sent to the server and when the process is complete a polygon will be created. If you are not satisfied with the result, you can set additional points or remove points. To delete a point, hover over the point you want to delete, if the point can be deleted, it will enlarge and the cursor will turn into a cross, then left-click on the point. - If you want to postpone the request and create a few more points, hold down `Ctrl` and continue, - the request will be sent after the key is released. + If you want to postpone the request and create a few more points, hold down `Ctrl` and continue (the `Block` + button on the top panel will turn blue), the request will be sent after the key is released. ![](/images/image188_detrac.jpg) @@ -39,12 +44,41 @@ In most cases specifying positive points alone is enough to build a polygon. ![](/images/image224.jpg) +- You can use the `Selected opacity` slider in the `Objects sidebar` to change the opacity of the polygon. + You can read more in the [Objects sidebar](/docs/manual/basics/objects-sidebar/#appearance) section. + - To finish interaction, click on the `Done` button on the top panel or press `N` on your keyboard. - When the object is finished, you can edit it like a polygon. You can read about editing polygons in the [Annotation with polygons](/docs/manual/advanced/annotation-with-polygons/) section. -### Detectors +### Deep extreme cut (DEXTR) + +This is an optimized version of the original model, introduced at the end of 2017. +It uses the information about extreme points of an object to get its mask. The mask then converted to a polygon. +For now this is the fastest interactor on CPU. + +![](/images/dextr_example.gif) + +### Feature backpropagating refinement scheme (f-BRS) + +The model allows to get a mask for an object using positive points +(should be left-clicked on the foreground), and negative points +(should be right-clicked on the background, if necessary). +It is recommended to run the model on GPU, if possible. + +![](/images/fbrs_example.gif) + +### Inside-Outside-Guidance + +The model uses a bounding box and inside/outside points to create a mask. +First of all, you need to create a bounding box, wrapping the object. +Then you need to use positive and negative points to say the model where is a foreground, +and where is a background. Negative points are optional. + +![](/images/iog_example.gif) + +## Detectors Detectors are used to automatically annotate one frame. Supported DL models are suitable only for certain labels. diff --git a/site/content/en/docs/manual/advanced/analytics.md b/site/content/en/docs/manual/advanced/analytics.md index 410ad9fd..40c7e1f7 100644 --- a/site/content/en/docs/manual/advanced/analytics.md +++ b/site/content/en/docs/manual/advanced/analytics.md @@ -1,7 +1,8 @@ --- -title: 'Analytics' -linkTitle: 'Analytics' +title: 'Analytics Monitoring' +linkTitle: 'Analytics Monitoring' weight: 28 +description: 'Using Analytics to monitor usage statistics.' --- If your CVAT instance was created with analytics support, you can press the `Analytics` button in the dashboard diff --git a/site/content/en/docs/manual/advanced/annotation-with-cuboids/_index.md b/site/content/en/docs/manual/advanced/annotation-with-cuboids/_index.md index 3070bb78..0c0c2169 100644 --- a/site/content/en/docs/manual/advanced/annotation-with-cuboids/_index.md +++ b/site/content/en/docs/manual/advanced/annotation-with-cuboids/_index.md @@ -2,6 +2,7 @@ title: 'Annotation with cuboids' linkTitle: 'Annotation with cuboids' weight: 17 +description: 'Guide to creating and editing cuboids.' --- It is used to annotate 3 dimensional objects such as cars, boxes, etc... diff --git a/site/content/en/docs/manual/advanced/annotation-with-points/_index.md b/site/content/en/docs/manual/advanced/annotation-with-points/_index.md index 6a5f13e0..93a29fa2 100644 --- a/site/content/en/docs/manual/advanced/annotation-with-points/_index.md +++ b/site/content/en/docs/manual/advanced/annotation-with-points/_index.md @@ -2,4 +2,5 @@ title: 'Annotation with points' linkTitle: 'Annotation with points' weight: 11 +description: 'Guide to annotating tasks using single points or shapes containing multiple points.' --- diff --git a/site/content/en/docs/manual/advanced/annotation-with-polygons/_index.md b/site/content/en/docs/manual/advanced/annotation-with-polygons/_index.md index 659eb77b..dcf9fd85 100644 --- a/site/content/en/docs/manual/advanced/annotation-with-polygons/_index.md +++ b/site/content/en/docs/manual/advanced/annotation-with-polygons/_index.md @@ -2,4 +2,5 @@ title: 'Annotation with polygons' linkTitle: 'Annotation with polygons' weight: 13 +description: 'Guide to creating and editing polygons.' --- diff --git a/site/content/en/docs/manual/advanced/annotation-with-polygons/manual-drawing.md b/site/content/en/docs/manual/advanced/annotation-with-polygons/manual-drawing.md index 887f5c29..5a5f2370 100644 --- a/site/content/en/docs/manual/advanced/annotation-with-polygons/manual-drawing.md +++ b/site/content/en/docs/manual/advanced/annotation-with-polygons/manual-drawing.md @@ -20,6 +20,8 @@ Before starting, you need to select `Polygon` on the controls sidebar and choose - When `Shift` isn't pressed, you can zoom in/out (when scrolling the mouse wheel) and move (when clicking the mouse wheel and moving the mouse), you can also delete the previous point by right-clicking on it. +- You can use the `Selected opacity` slider in the `Objects sidebar` to change the opacity of the polygon. + You can read more in the [Objects sidebar](/docs/manual/basics/objects-sidebar/#appearance) section. - Press `N` again or click the `Done` button on the top panel for completing the shape. - After creating the polygon, you can move the points or delete them by right-clicking and selecting `Delete point` or clicking with pressed `Alt` key in the context menu. diff --git a/site/content/en/docs/manual/advanced/annotation-with-polylines.md b/site/content/en/docs/manual/advanced/annotation-with-polylines.md index 35a2fbe1..5fb7735c 100644 --- a/site/content/en/docs/manual/advanced/annotation-with-polylines.md +++ b/site/content/en/docs/manual/advanced/annotation-with-polylines.md @@ -2,6 +2,7 @@ title: 'Annotation with polylines' linkTitle: 'Annotation with polylines' weight: 12 +description: 'Guide to annotating tasks using polylines.' --- It is used for road markup annotation etc. diff --git a/site/content/en/docs/manual/advanced/attribute-annotation-mode-advanced.md b/site/content/en/docs/manual/advanced/attribute-annotation-mode-advanced.md index f294003c..2100451c 100644 --- a/site/content/en/docs/manual/advanced/attribute-annotation-mode-advanced.md +++ b/site/content/en/docs/manual/advanced/attribute-annotation-mode-advanced.md @@ -2,6 +2,7 @@ title: 'Attribute annotation mode (advanced)' linkTitle: 'Attribute annotation mode' weight: 6 +description: 'Advanced operations available in attribute annotation mode.' --- Basic operations in the mode were described in section [attribute annotation mode (basics)](/docs/manual/basics/attribute-annotation-mode-basics/). diff --git a/site/content/en/docs/manual/advanced/automatic-annotation.md b/site/content/en/docs/manual/advanced/automatic-annotation.md index e9702b5d..c40bc488 100644 --- a/site/content/en/docs/manual/advanced/automatic-annotation.md +++ b/site/content/en/docs/manual/advanced/automatic-annotation.md @@ -2,6 +2,7 @@ title: 'Automatic annotation' linkTitle: 'Automatic annotation' weight: 27 +description: 'Guide to using the automatic annotation of tasks.' --- Automatic Annotation is used for creating preliminary annotations. diff --git a/site/content/en/docs/manual/advanced/cli.md b/site/content/en/docs/manual/advanced/cli.md index 2738cfce..03150336 100644 --- a/site/content/en/docs/manual/advanced/cli.md +++ b/site/content/en/docs/manual/advanced/cli.md @@ -2,7 +2,7 @@ title: 'Command line interface (CLI)' linkTitle: 'CLI' weight: 32 -description: This section on [GitHub](https://github.com/openvinotoolkit/cvat/tree/develop/utils/cli) +description: 'Guide to working with CVAT tasks in the command line interface. This section on [GitHub](https://github.com/openvinotoolkit/cvat/tree/develop/utils/cli).' --- **Description** @@ -12,23 +12,26 @@ comprehensive CVAT administration tool in the future. Overview of functionality: -- Create a new task (supports name, bug tracker, labels JSON, local/share/remote files) +- Create a new task (supports name, bug tracker, project, labels JSON, local/share/remote files) - Delete tasks (supports deleting a list of task IDs) - List all tasks (supports basic CSV or JSON output) - Download JPEG frames (supports a list of frame IDs) - Dump annotations (supports all formats via format string) +- Upload annotations for a task in the specified format (e.g. 'YOLO ZIP 1.0') +- Export and download a whole task +- Import a task **Usage** ```bash usage: cli.py [-h] [--auth USER:[PASS]] [--server-host SERVER_HOST] [--server-port SERVER_PORT] [--debug] - {create,delete,ls,frames,dump} ... + {create,delete,ls,frames,dump,upload,export,import} ... Perform common operations related to CVAT tasks. positional arguments: - {create,delete,ls,frames,dump} + {create,delete,ls,frames,dump,upload,export,import} optional arguments: -h, --help show this help message and exit @@ -45,11 +48,11 @@ optional arguments: **Examples** -- List all tasks - `cli.py ls` - Create a task `cli.py create "new task" --labels labels.json local file1.jpg file2.jpg` - Delete some tasks `cli.py delete 100 101 102` +- List all tasks + `cli.py ls` - Dump annotations `cli.py dump --format "CVAT for images 1.1" 103 output.xml` diff --git a/site/content/en/docs/manual/advanced/context-images.md b/site/content/en/docs/manual/advanced/context-images.md index 933ddddf..765e94ec 100644 --- a/site/content/en/docs/manual/advanced/context-images.md +++ b/site/content/en/docs/manual/advanced/context-images.md @@ -2,9 +2,10 @@ title: 'Context images for 2d task' linkTitle: 'Context images' weight: 23 +description: 'Adding additional contextual images to a task.' --- -When you create a task, you can provide the images with additional contextual images. +When you create a task, you can provide the images with additional contextual images. To do this, create a folder related_images and place a folder with a contextual image in it (make sure the folder has the same name as the image to which it should be tied). An example of the structure: diff --git a/site/content/en/docs/manual/advanced/downloading-annotations.md b/site/content/en/docs/manual/advanced/downloading-annotations.md index b68d1250..3f519192 100644 --- a/site/content/en/docs/manual/advanced/downloading-annotations.md +++ b/site/content/en/docs/manual/advanced/downloading-annotations.md @@ -5,18 +5,18 @@ weight: 1 --- 1. To download the latest annotations, you have to save all changes first. - click the `Save` button. There is a `Ctrl+S` shortcut to save annotations quickly. + Сlick the `Save` button. There is a `Ctrl+S` shortcut to save annotations quickly. 1. After that, сlick the `Menu` button. -1. Press the `Dump Annotation` button. +1. Press the `Export task dataset` button. ![](/images/image028.jpg) -1. Choose format dump annotation file. Dump annotation are available in several formats: +1. Choose the format for exporting the dataset. Exporting is available in several formats: - [CVAT for video](/docs/manual/advanced/xml_format/#interpolation) - is highlighted if a task has the interpolation mode. + choose if the task is created in interpolation mode. - [CVAT for images](/docs/manual/advanced/xml_format/#annotation) - is highlighted if a task has the annotation mode. + choose if a task is created in annotation mode. ![](/images/image029.jpg 'Example XML format') @@ -30,7 +30,20 @@ weight: 1 - [MOT](https://motchallenge.net/) - [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0/) - [Datumaro](https://github.com/openvinotoolkit/cvat/tree/develop/cvat/apps/dataset_manager/formats/datumaro) + - [ImageNet](http://www.image-net.org/) + - [CamVid](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) + - [WIDER Face](http://shuoyang1213.me/WIDERFACE/) + - [VGGFace2](https://github.com/ox-vgg/vgg_face2) + - [Market-1501](https://www.aitribune.com/dataset/2018051063) + - [ICDAR13/15](https://rrc.cvc.uab.es/?ch=2) + For 3D tasks, the following formats are available: - - Point Cloud Format 1.0 - - Velodyn points format 1.0 + - [Kitti Raw Format 1.0](http://www.cvlibs.net/datasets/kitti/raw_data.php) + - Sly Point Cloud Format 1.0 - Supervisely Point Cloud dataset + +1. To download images with the dataset tick the `Save images` box + +1. (Optional) To name the resulting archive, use the `Custom name` field. + + ![](/images/image225.jpg) diff --git a/site/content/en/docs/manual/advanced/filter.md b/site/content/en/docs/manual/advanced/filter.md index c1a235e7..6904c11e 100644 --- a/site/content/en/docs/manual/advanced/filter.md +++ b/site/content/en/docs/manual/advanced/filter.md @@ -2,6 +2,7 @@ title: 'Filter' linkTitle: 'Filter' weight: 20 +description: 'Guide to using the Filter feature in CVAT.' --- There are some reasons to use the feature: diff --git a/site/content/en/docs/manual/advanced/formats/_index.md b/site/content/en/docs/manual/advanced/formats/_index.md index 332a711d..89c71a97 100644 --- a/site/content/en/docs/manual/advanced/formats/_index.md +++ b/site/content/en/docs/manual/advanced/formats/_index.md @@ -2,6 +2,7 @@ title: 'Formats' linkTitle: 'Formats' weight: 31 +description: 'List of annotation formats supported by CVAT.' --- #### CVAT supported the following formats: diff --git a/site/content/en/docs/manual/advanced/opencv-tools.md b/site/content/en/docs/manual/advanced/opencv-tools.md index 76cc96e3..365afdbd 100644 --- a/site/content/en/docs/manual/advanced/opencv-tools.md +++ b/site/content/en/docs/manual/advanced/opencv-tools.md @@ -2,6 +2,7 @@ title: 'OpenCV tools' linkTitle: 'OpenCV tools' weight: 14 +description: 'Guide to using Computer Vision algorithms during annotation.' --- The tool based on [Open CV](https://opencv.org/) Computer Vision library @@ -36,10 +37,17 @@ displayed as a red square which is tied to the cursor. Increasing action threshold will affect the performance. During the drawing process you can remove the last point by clicking on it with the left mouse button. +- You can also create a boundary manually (like when + [creating a polygon](/docs/manual/advanced/annotation-with-polygons/manual-drawing/)) by temporarily disabling + the automatic line creation. To do that, switch blocking on by pressing `Ctrl`. + - In the process of drawing, you can select the number of points in the polygon using the switch. ![](/images/image224.jpg) +- You can use the `Selected opacity` slider in the `Objects sidebar` to change the opacity of the polygon. + You can read more in the [Objects sidebar](/docs/manual/basics/objects-sidebar/#appearance) section. + - Once all the points are placed, you can complete the creation of the object by clicking on the `Done` button on the top panel or press `N` on your keyboard. As a result, a polygon will be created (read more about the polygons in the [annotation with polygons](/docs/manual/advanced/annotation-with-polygons/)). diff --git a/site/content/en/docs/manual/advanced/projects.md b/site/content/en/docs/manual/advanced/projects.md index 55e80cc2..b8efb1b6 100644 --- a/site/content/en/docs/manual/advanced/projects.md +++ b/site/content/en/docs/manual/advanced/projects.md @@ -2,8 +2,11 @@ title: 'Projects' linkTitle: 'Projects' weight: 3 +description: 'Creating and exporting projects in CVAT.' --- +### Create project + At CVAT, you can create a project containing tasks of the same type. All tasks related to the project will inherit a list of labels. @@ -33,4 +36,18 @@ Here you can do the following:  Start typing an assignee's name and/or choose the right person out of the dropdown list. 1. `Tasks` — is a list of all tasks for a particular project. +It is possible to choose a subset for tasks in the project. You can use the available options +(`Train`, `Test`, `Validation`) or set your own. + You can remove the project and all related tasks through the Action menu. + +### Export project + +It is possible to download an entire project instead of exporting individual tasks. In this case, +annotations for all tasks in a project will be available in a single archive. +To export a project, do the following on the `Project` page: +- Open the `Actions` menu. +- Press the `Export project dataset` button. + +Additional information about exporting tasks can be found in the +[Downloading annotations](/docs/manual/advanced/downloading-annotations/) section. diff --git a/site/content/en/docs/manual/advanced/review.md b/site/content/en/docs/manual/advanced/review.md index ce55733c..d9eadd1e 100644 --- a/site/content/en/docs/manual/advanced/review.md +++ b/site/content/en/docs/manual/advanced/review.md @@ -2,6 +2,7 @@ title: 'Review' linkTitle: 'Review' weight: 22 +description: 'Guide to using the Review mode for task validation.' --- A special mode to check the annotation allows you to point to an object or area in the frame containing an error. diff --git a/site/content/en/docs/manual/advanced/search.md b/site/content/en/docs/manual/advanced/search.md index 0fb2f1a4..f4f2c898 100644 --- a/site/content/en/docs/manual/advanced/search.md +++ b/site/content/en/docs/manual/advanced/search.md @@ -2,6 +2,7 @@ title: 'Search' linkTitle: 'Search' weight: 2 +description: 'Overview of available search options.' --- There are several options how to use the search. diff --git a/site/content/en/docs/manual/advanced/serverless-tutorial.md b/site/content/en/docs/manual/advanced/serverless-tutorial.md index c2c35c10..38ab683d 100644 --- a/site/content/en/docs/manual/advanced/serverless-tutorial.md +++ b/site/content/en/docs/manual/advanced/serverless-tutorial.md @@ -898,6 +898,42 @@ $ docker logs nuclio-nuclio-pth.shiyinzhang.iog +If before model deployment you see that the `NODE PORT` is 0, you need to assign it manually. +Add the `port: 32001` attribute to the `function.yaml` file of each model, before you deploy the model. +Different ports should be prescribed for different models. + +```diff +triggers: +myHttpTrigger: + maxWorkers: 1 + kind: 'http' + workerAvailabilityTimeoutMilliseconds: 10000 + attributes: ++ port: 32001 + maxRequestBodySize: 33554432 # 32MB +``` + +### Installation serverless functions on Windows 10 with using the Ubuntu subsystem + +If you encounter a problem running serverless functions on Windows 10, +you can use the Ubuntu subsystem, for this do the following: + +1. Install `WSL 2` and `Docker Desktop` as described in [installation manual][cvat-installation-guide-windows-10] + +1. Install [Ubuntu 18.04 from Microsoft store][ubuntu-1804-microsoft-store]. + +1. Enable integration for Ubuntu-18.04 in the settings of `Docker Desktop` in the `Resources` `WSL integration` tab: + + ![Docker WSL integration Ubuntu 18.04](/images/docker_wsl_integration.jpg) + +1. Then you can download and install `nuctl` on Ubuntu, + using the [automatic annotation guide][cvat-auto-annotation-guide]. + +1. Install `git` and clone repository on Ubuntu, + as described in the [installation manual][cvat-installation-guide-ubuntu-1804]. + +1. After that, run the commands from this tutorial through Ubuntu. + [detectron2-github]: https://github.com/facebookresearch/detectron2 [detectron2-requirements]: https://detectron2.readthedocs.io/en/latest/tutorials/install.html [pytorch-install]: https://pytorch.org/get-started/locally/ @@ -912,13 +948,16 @@ $ docker logs nuclio-nuclio-pth.shiyinzhang.iog [retinanet-main-py]: https://github.com/openvinotoolkit/cvat/blob/b2f616859ca64687c385e636b4a25014fbb9d17c/serverless/pytorch/facebookresearch/detectron2/retinanet/nuclio/main.py [nuclio-homepage]: https://nuclio.io/ [cvat-builtin-serverless]: https://github.com/openvinotoolkit/cvat/tree/develop/serverless -[cvat-auto-annotation-guide]: https://openvinotoolkit.github.io/cvat/docs/administration/advanced/installation_automatic_annotation +[cvat-auto-annotation-guide]: /docs/administration/advanced/installation_automatic_annotation +[cvat-installation-guide-windows-10]: /docs/administration/basics/installation/#windows-10 +[cvat-installation-guide-ubuntu-1804]: /docs/administration/basics/installation/#ubuntu-1804-x86_64amd64 [mscoco-format]: https://cocodataset.org/#format-data [pascal-voc-format]: http://host.robots.ox.ac.uk/pascal/VOC/voc2012/htmldoc/index.html [faas-wiki]: https://en.wikipedia.org/wiki/Function_as_a_service -[cvat-ai-tools-user-guide]: https://openvinotoolkit.github.io/cvat/docs/manual/advanced/ai-tools/ +[cvat-ai-tools-user-guide]: /docs/manual/advanced/ai-tools/ [cvat-github]: https://github.com/openvinotoolkit/cvat [siammask-serverless]: https://github.com/openvinotoolkit/cvat/tree/develop/serverless/pytorch/foolwood/siammask/nuclio [vtest-avi]: https://github.com/opencv/opencv/blob/master/samples/data/vtest.avi?raw=true [intel-openvino-url]: https://software.intel.com/content/www/us/en/develop/tools/openvino-toolkit.html -[cvat-auto-annotation-user-guide]: https://openvinotoolkit.github.io/cvat/docs/manual/advanced/automatic-annotation/ +[cvat-auto-annotation-user-guide]: /docs/manual/advanced/automatic-annotation/ +[ubuntu-1804-microsoft-store]: https://www.microsoft.com/en-us/p/ubuntu-1804-lts/9n9tngvndl3q diff --git a/site/content/en/docs/manual/advanced/shape-grouping.md b/site/content/en/docs/manual/advanced/shape-grouping.md index 9a570760..5de6fb6b 100644 --- a/site/content/en/docs/manual/advanced/shape-grouping.md +++ b/site/content/en/docs/manual/advanced/shape-grouping.md @@ -2,6 +2,7 @@ title: 'Shape grouping' linkTitle: 'Shape grouping' weight: 21 +description: 'Grouping multiple shapes during annotation.' --- This feature allows us to group several shapes. diff --git a/site/content/en/docs/manual/advanced/shape-mode-advanced.md b/site/content/en/docs/manual/advanced/shape-mode-advanced.md index d98cefd1..05cb14b9 100644 --- a/site/content/en/docs/manual/advanced/shape-mode-advanced.md +++ b/site/content/en/docs/manual/advanced/shape-mode-advanced.md @@ -2,6 +2,7 @@ title: 'Shape mode (advanced)' linkTitle: 'Shape mode' weight: 4 +description: 'Advanced operations available during annotation in shape mode.' --- Basic operations in the mode were described in section [shape mode (basics)](/docs/manual/basics/shape-mode-basics/). diff --git a/site/content/en/docs/manual/advanced/shortcuts.md b/site/content/en/docs/manual/advanced/shortcuts.md index 1f73bf12..3470c224 100644 --- a/site/content/en/docs/manual/advanced/shortcuts.md +++ b/site/content/en/docs/manual/advanced/shortcuts.md @@ -2,6 +2,7 @@ title: 'Shortcuts' linkTitle: 'Shortcuts' weight: 24 +description: 'List of available mouse and keyboard shortcuts.' --- Many UI elements have shortcut hints. Put your pointer to a required element to see it. diff --git a/site/content/en/docs/manual/advanced/track-mode-advanced.md b/site/content/en/docs/manual/advanced/track-mode-advanced.md index dacc0772..ec603712 100644 --- a/site/content/en/docs/manual/advanced/track-mode-advanced.md +++ b/site/content/en/docs/manual/advanced/track-mode-advanced.md @@ -2,6 +2,7 @@ title: 'Track mode (advanced)' linkTitle: 'Track mode' weight: 5 +description: 'Advanced operations available during annotation in track mode.' --- Basic operations in the mode were described in section [track mode (basics)](/docs/manual/basics/track-mode-basics/). diff --git a/site/content/en/docs/manual/basics/3d-object-annotation-basics.md b/site/content/en/docs/manual/basics/3d-object-annotation-basics.md index 132a8374..41778dc6 100644 --- a/site/content/en/docs/manual/basics/3d-object-annotation-basics.md +++ b/site/content/en/docs/manual/basics/3d-object-annotation-basics.md @@ -2,12 +2,16 @@ title: '3D Object annotation (basics)' linkTitle: '3D Object annotation' weight: 13 +description: 'Overview of basic operations available when annotating 3D objects.' --- ### Navigation To move in 3D space you can use several methods: -- Move using the mouse: +- Select the `move the image` tool so you can move the camera using the mouse: + + ![](/images/image149.jpg) + - Hold down the left mouse button in the perspective window to turn the camera around the conditional point. - Hold down the right mouse button in the perspective window to move the camera inside the 3D space. - Move the mouse while holding down the wheel to zoom in/out in the perspective window. @@ -30,14 +34,14 @@ select the label of the future object and click `shape`. ![](/images/image217.jpg) -After that the cursor will be followed by a cube. In the creation process you can rotate and move the camera. -Left double-click will create an object. +After that the cursor will be followed by a cube. In the creation process you can rotate and move the camera +only using the keys. Left double-click will create an object. You can place an object only near the dots of the point cloud. ![](/images/gif026_carla_town3.gif) -To adjust the size precisely, you need to edit the cuboid on the projections. -In each projection you can: +To adjust the size precisely, you need to edit the cuboid on the projections, for this change `Сursor` on control +sidebar or press `Esc`. In each projection you can: Move the object in the projection plane - to do this, hover over the object, press the left mouse button and move the object. diff --git a/site/content/en/docs/manual/basics/attribute-annotation-mode-basics.md b/site/content/en/docs/manual/basics/attribute-annotation-mode-basics.md index 648d3ee4..18c46f0e 100644 --- a/site/content/en/docs/manual/basics/attribute-annotation-mode-basics.md +++ b/site/content/en/docs/manual/basics/attribute-annotation-mode-basics.md @@ -2,6 +2,7 @@ title: 'Attribute annotation mode (basics)' linkTitle: 'Attribute annotation mode' weight: 12 +description: 'Usage examples and basic operations available in attribute annotation mode.' --- - In this mode you can edit attributes with fast navigation between objects and frames using a keyboard. Open the drop-down list in the top panel and select Attribute annotation Mode. diff --git a/site/content/en/docs/manual/basics/basic-navigation.md b/site/content/en/docs/manual/basics/basic-navigation.md index 9f2a0373..2e8d476b 100644 --- a/site/content/en/docs/manual/basics/basic-navigation.md +++ b/site/content/en/docs/manual/basics/basic-navigation.md @@ -2,6 +2,7 @@ title: 'Basic navigation' linkTitle: 'Basic navigation' weight: 6 +description: 'Overview of basic controls.' --- 1. Use arrows below to move to the next/previous frame. Use the scroll bar slider to scroll through frames. diff --git a/site/content/en/docs/manual/basics/controls-sidebar.md b/site/content/en/docs/manual/basics/controls-sidebar.md index feaff597..0f84e20f 100644 --- a/site/content/en/docs/manual/basics/controls-sidebar.md +++ b/site/content/en/docs/manual/basics/controls-sidebar.md @@ -2,6 +2,7 @@ title: 'Controls sidebar' linkTitle: 'Controls sidebar' weight: 23 +description: 'Overview of available functions on the controls sidebar of the annotation tool.' --- ## Navigation diff --git a/site/content/en/docs/manual/basics/creating_an_annotation_task.md b/site/content/en/docs/manual/basics/creating_an_annotation_task.md index 3a66446d..ebd26bde 100644 --- a/site/content/en/docs/manual/basics/creating_an_annotation_task.md +++ b/site/content/en/docs/manual/basics/creating_an_annotation_task.md @@ -2,249 +2,224 @@ title: 'Creating an annotation task' linkTitle: 'Creating an annotation task' weight: 2 +description: 'Instructions on how to create and configure an annotation task.' --- -1. Create an annotation task pressing `Create new task` button on the tasks page or on the project page. - ![](/images/image004.jpg) +Create an annotation task pressing `Create new task` button on the tasks page or on the project page. +![](/images/image004.jpg) -1. Specify parameters of the task: +Specify parameters of the task: - ## Basic configuration +## Basic configuration - ### Name - The name of the task to be created. +### Name - ![](/images/image005.jpg) +The name of the task to be created. - ### Projects - The project that this task will be related with. +![](/images/image005.jpg) - ![](/images/image193.jpg) +### Projects - ### Labels - There are two ways of working with labels (available only if the task is not related to the project): +The project that this task will be related with. - - The `Constructor` is a simple way to add and adjust labels. To add a new label click the `Add label` button. - ![](/images/image123.jpg) +![](/images/image193.jpg) - You can set a name of the label in the `Label name` field and choose a color for each label. +### Labels - ![](/images/image124.jpg) +There are two ways of working with labels (available only if the task is not related to the project): - If necessary you can add an attribute and set its properties by clicking `Add an attribute`: +- The `Constructor` is a simple way to add and adjust labels. To add a new label click the `Add label` button. + ![](/images/image123.jpg) - ![](/images/image125.jpg) + You can set a name of the label in the `Label name` field and choose a color for each label. - The following actions are available here: + ![](/images/image124.jpg) - 1. Set the attribute’s name. - 1. Choose the way to display the attribute: - - Select — drop down list of value - - Radio — is used when it is necessary to choose just one option out of few suggested. - - Checkbox — is used when it is necessary to choose any number of options out of suggested. - - Text — is used when an attribute is entered as a text. - - Number — is used when an attribute is entered as a number. - 1. Set values for the attribute. The values could be separated by pressing `Enter`. - The entered value is displayed as a separate element which could be deleted - by pressing `Backspace` or clicking the close button (x). - If the specified way of displaying the attribute is Text or Number, - the entered value will be displayed as text by default (e.g. you can specify the text format). - 1. Checkbox `Mutable` determines if an attribute would be changed frame to frame. - 1. You can delete the attribute by clicking the close button (x). + If necessary you can add an attribute and set its properties by clicking `Add an attribute`: - Click the `Continue` button to add more labels. - If you need to cancel adding a label - press the `Cancel` button. - After all the necessary labels are added click the `Done` button. - After clicking `Done` the added labels would be displayed as separate elements of different colour. - You can edit or delete labels by clicking `Update attributes` or `Delete label`. + ![](/images/image125.jpg) - - The `Raw` is a way of working with labels for an advanced user. - Raw presents label data in _json_ format with an option of editing and copying labels as a text. - The `Done` button applies the changes and the `Reset` button cancels the changes. - ![](/images/image126.jpg) + The following actions are available here: - In `Raw` and `Constructor` mode, you can press the `Copy` button to copy the list of labels. + 1. Set the attribute’s name. + 1. Choose the way to display the attribute: + - Select — drop down list of value + - Radio — is used when it is necessary to choose just one option out of few suggested. + - Checkbox — is used when it is necessary to choose any number of options out of suggested. + - Text — is used when an attribute is entered as a text. + - Number — is used when an attribute is entered as a number. + 1. Set values for the attribute. The values could be separated by pressing `Enter`. + The entered value is displayed as a separate element which could be deleted + by pressing `Backspace` or clicking the close button (x). + If the specified way of displaying the attribute is Text or Number, + the entered value will be displayed as text by default (e.g. you can specify the text format). + 1. Checkbox `Mutable` determines if an attribute would be changed frame to frame. + 1. You can delete the attribute by clicking the close button (x). - ### Select files - Press tab `My computer` to choose some files for annotation from your PC. - If you select tab `Connected file share` you can choose files for annotation from your network. - If you select ` Remote source` , you'll see a field where you can enter a list of URLs (one URL per line). - If you upload a video or dataset with images and select `Use cache` option, you can attach a `manifest.jsonl` file. - You can find how to prepare it [here](/docs/manual/advanced/dataset_manifest/). + Click the `Continue` button to add more labels. + If you need to cancel adding a label - press the `Cancel` button. + After all the necessary labels are added click the `Done` button. + After clicking `Done` the added labels would be displayed as separate elements of different colour. + You can edit or delete labels by clicking `Update attributes` or `Delete label`. - ![](/images/image127.jpg) +- The `Raw` is a way of working with labels for an advanced user. + Raw presents label data in _json_ format with an option of editing and copying labels as a text. + The `Done` button applies the changes and the `Reset` button cancels the changes. + ![](/images/image126.jpg) - ### Data formats for a 3D task +In `Raw` and `Constructor` mode, you can press the `Copy` button to copy the list of labels. - To create a 3D task, you need to use the following directory structures: - {{< tabpane >}} - {{< tab header="Velodyne" >}} +### Select files + +Press tab `My computer` to choose some files for annotation from your PC. +If you select tab `Connected file share` you can choose files for annotation from your network. +If you select ` Remote source` , you'll see a field where you can enter a list of URLs (one URL per line). +If you upload a video or dataset with images and select `Use cache` option, you can attach a `manifest.jsonl` file. +You can find how to prepare it [here](/docs/manual/advanced/dataset_manifest/). + +![](/images/image127.jpg) + +### Data formats for a 3D task + +To create a 3D task, you need to use the following directory structures: +{{< tabpane >}} + {{< tab header="Velodyne" >}} VELODYNE FORMAT Structure: - velodyne_points/ - data/ - image_01.bin - IMAGE_00 # unknown dirname, Generally image_01.png can be under IMAGE_00, IMAGE_01, IMAGE_02, IMAGE_03, etc - data/ - image_01.png - {{< /tab >}} - {{< tab header="3D pointcloud" >}} + velodyne_points/ + data/ + image_01.bin + IMAGE_00 # unknown dirname, Generally image_01.png can be under IMAGE_00, IMAGE_01, IMAGE_02, IMAGE_03, etc + data/ + image_01.png + {{< /tab >}} + {{< tab header="3D pointcloud" >}} 3D POINTCLOUD DATA FORMAT Structure: - pointcloud/ - 00001.pcd - related_images/ - 00001_pcd/ - image_01.png # or any other image - {{< /tab >}} - {{< tab header="3D Option 1" >}} + pointcloud/ + 00001.pcd + related_images/ + 00001_pcd/ + image_01.png # or any other image + {{< /tab >}} + {{< tab header="3D Option 1" >}} 3D, DEFAULT DATAFORMAT Option 1 Structure: data/ - image.pcd - image.png - {{< /tab >}} - {{< tab header="3D Option 2" >}} + image.pcd + image.png + {{< /tab >}} + {{< tab header="3D Option 2" >}} 3D, DEFAULT DATAFORMAT Option 2 Structure: - data/ - image_1/ - image_1.pcd - context_1.png # or any other name - context_2.jpg - {{< /tab >}} - {{< /tabpane >}} - - ## Advanced configuration - - ![](/images/image128_use_cache.jpg) - - ### Use zip chunks - Force to use zip chunks as compressed data. Actual for videos only. - - ### Use cache - Defines how to work with data. Select the checkbox to switch to the "on-the-fly data processing", - which will reduce the task creation time (by preparing chunks when requests are received) - and store data in a cache of limited size with a policy of evicting less popular items. - See more [here](/docs/manual/advanced/data_on_fly/). - - ### Image Quality - Use this option to specify quality of uploaded images. - The option helps to load high resolution datasets faster. - Use the value from `5` (almost completely compressed images) to `100` (not compressed images). - - ## Overlap Size - Use this option to make overlapped segments. - The option makes tracks continuous from one segment into another. - Use it for interpolation mode. There are several options for using the parameter: - - - For an interpolation task (video sequence). - If you annotate a bounding box on two adjacent segments they will be merged into one bounding box. - If overlap equals to zero or annotation is poor on adjacent segments inside a dumped annotation file, - you will have several tracks, one for each segment, which corresponds to the object. - - For an annotation task (independent images). - If an object exists on overlapped segments, the overlap is greater than zero - and the annotation is good enough on adjacent segments, it will be automatically merged into one object. - If overlap equals to zero or annotation is poor on adjacent segments inside a dumped annotation file, - you will have several bounding boxes for the same object. - Thus, you annotate an object on the first segment. - You annotate the same object on second segment, and if you do it right, you - will have one track inside the annotations. - If annotations on different segments (on overlapped frames) - are very different, you will have two shapes for the same object. - This functionality works only for bounding boxes. - Polygons, polylines, points don't support automatic merge on overlapped segments - even the overlap parameter isn't zero and match between corresponding shapes on adjacent segments is perfect. - - ### Segment size - Use this option to divide a huge dataset into a few smaller segments. - For example, one job cannot be annotated by several labelers (it isn't supported). - Thus using "segment size" you can create several jobs for the same annotation task. - It will help you to parallel data annotation process. - - ### Start frame - Frame from which video in task begins. - - ### Stop frame - Frame on which video in task ends. - - ### Frame Step - Use this option to filter video frames. - For example, enter `25` to leave every twenty fifth frame in the video or every twenty fifth image. - - ### Chunk size - Defines a number of frames to be packed in a chunk when send from client to server. - Server defines automatically if empty. - - Recommended values: - - - 1080p or less: 36 - - 2k or less: 8 - 16 - - 4k or less: 4 - 8 - - More: 1 - 4 - - ### Dataset Repository - URL link of the repository optionally specifies the path to the repository for storage - (`default: annotation / .zip`). - The .zip and .xml file extension of annotation are supported. - Field format: `URL [PATH]` example: `https://github.com/project/repos.git [1/2/3/4/annotation.xml]` - - Supported URL formats : - - - `https://github.com/project/repos[.git]` - - `github.com/project/repos[.git]` - - `git@github.com:project/repos[.git]` - - The task will be highlighted in red after creation if annotation isn't synchronized with the repository. - - ### Use LFS - If the annotation file is large, you can create a repository with - [LFS](https://git-lfs.github.com/) support. - - ### Issue tracker - Specify full issue tracker's URL if it's necessary. - - Push `Submit` button and it will be added into the list of annotation tasks. - Then, the created task will be displayed on a tasks page: - - ![](/images/image006_detrac.jpg) - -1. The tasks page contains elements and each of them relates to a separate task. They are sorted in creation order. - Each element contains: task name, preview, progress bar, button `Open`, and menu `Actions`. - Each button is responsible for a in menu `Actions` specific function: - - - `Dump Annotation` and `Export as a dataset` — download annotations or - annotations and images in a specific format. The following formats are available: - - [CVAT for video](/docs/manual/advanced/xml_format/#interpolation) - is highlighted if a task has the interpolation mode. - - [CVAT for images](/docs/manual/advanced/xml_format/#annotation) - is highlighted if a task has the annotation mode. - - [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) - - [(VOC) Segmentation mask](http://host.robots.ox.ac.uk/pascal/VOC/) — - archive contains class and instance masks for each frame in the png - format and a text file with the value of each color. - - [YOLO](https://pjreddie.com/darknet/yolo/) - - [COCO](http://cocodataset.org/#format-data) - - [TFRecord](https://www.tensorflow.org/tutorials/load_data/tfrecord) - - [MOT](https://motchallenge.net/) - - [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0/) - - [Datumaro](https://github.com/openvinotoolkit/cvat/tree/develop/cvat/apps/dataset_manager/formats/datumaro) - - Point Cloud Format 1.0 (Available in 3d task) - - Velodyn points format 1.0 (Available in 3d task) - - `Upload annotation` is available in the same formats as in `Dump annotation`. - - [CVAT](/docs/manual/advanced/xml_format/) accepts both video and image sub-formats. - - Point Cloud Format 1.0 (Available in 3d task) - - Velodyn points format 1.0 (Available in 3d task) - - `Automatic Annotation` — automatic annotation with OpenVINO toolkit. - Presence depends on how you build CVAT instance. - - `Export task` — Export a task into a zip archive. - Read more in the [export/import a task](/docs/manual/advanced/export-import/) section. - - `Move to project` — Moving the task to the project (can be used to move a task from one project to another). - Note that attributes reset during the moving process. In case of label mismatch, - you can create or delete necessary labels in the project/task. - Some task labels can be matched with the target project labels. - - `Delete` — delete task. + data/ + image_1/ + image_1.pcd + context_1.png # or any other name + context_2.jpg + {{< /tab >}} +{{< /tabpane >}} ---- +## Advanced configuration + +![](/images/image128_use_cache.jpg) + +### Use zip chunks + +Force to use zip chunks as compressed data. Actual for videos only. + +### Use cache + +Defines how to work with data. Select the checkbox to switch to the "on-the-fly data processing", +which will reduce the task creation time (by preparing chunks when requests are received) +and store data in a cache of limited size with a policy of evicting less popular items. +See more [here](/docs/manual/advanced/data_on_fly/). + +### Image Quality + +Use this option to specify quality of uploaded images. +The option helps to load high resolution datasets faster. +Use the value from `5` (almost completely compressed images) to `100` (not compressed images). + +## Overlap Size + +Use this option to make overlapped segments. +The option makes tracks continuous from one segment into another. +Use it for interpolation mode. There are several options for using the parameter: + +- For an interpolation task (video sequence). + If you annotate a bounding box on two adjacent segments they will be merged into one bounding box. + If overlap equals to zero or annotation is poor on adjacent segments inside a dumped annotation file, + you will have several tracks, one for each segment, which corresponds to the object. +- For an annotation task (independent images). + If an object exists on overlapped segments, the overlap is greater than zero + and the annotation is good enough on adjacent segments, it will be automatically merged into one object. + If overlap equals to zero or annotation is poor on adjacent segments inside a dumped annotation file, + you will have several bounding boxes for the same object. + Thus, you annotate an object on the first segment. + You annotate the same object on second segment, and if you do it right, you + will have one track inside the annotations. + If annotations on different segments (on overlapped frames) + are very different, you will have two shapes for the same object. + This functionality works only for bounding boxes. + Polygons, polylines, points don't support automatic merge on overlapped segments + even the overlap parameter isn't zero and match between corresponding shapes on adjacent segments is perfect. + +### Segment size + +Use this option to divide a huge dataset into a few smaller segments. +For example, one job cannot be annotated by several labelers (it isn't supported). +Thus using "segment size" you can create several jobs for the same annotation task. +It will help you to parallel data annotation process. + +### Start frame + +Frame from which video in task begins. + +### Stop frame + +Frame on which video in task ends. + +### Frame Step + +Use this option to filter video frames. +For example, enter `25` to leave every twenty fifth frame in the video or every twenty fifth image. + +### Chunk size + +Defines a number of frames to be packed in a chunk when send from client to server. +Server defines automatically if empty. + +Recommended values: + +- 1080p or less: 36 +- 2k or less: 8 - 16 +- 4k or less: 4 - 8 +- More: 1 - 4 + +### Dataset Repository + +URL link of the repository optionally specifies the path to the repository for storage +(`default: annotation / .zip`). +The .zip and .xml file extension of annotation are supported. +Field format: `URL [PATH]` example: `https://github.com/project/repos.git [1/2/3/4/annotation.xml]` + +Supported URL formats : + +- `https://github.com/project/repos[.git]` +- `github.com/project/repos[.git]` +- `git@github.com:project/repos[.git]` + +After the task is created, the synchronization status is displayed on the task page. + +### Use LFS + +If the annotation file is large, you can create a repository with +[LFS](https://git-lfs.github.com/) support. + +### Issue tracker + +Specify full issue tracker's URL if it's necessary. - Push `Open` button to go to [task details](/docs/manual/basics/task-details/). +Push `Submit` button and it will be added into the list of annotation tasks. +Then, the created task will be displayed on a [tasks page](/docs/manual/basics/tasks-page/). diff --git a/site/content/en/docs/manual/basics/objects-sidebar.md b/site/content/en/docs/manual/basics/objects-sidebar.md index 2d8fcbe9..0f47ce0d 100644 --- a/site/content/en/docs/manual/basics/objects-sidebar.md +++ b/site/content/en/docs/manual/basics/objects-sidebar.md @@ -2,6 +2,7 @@ title: 'Objects sidebar' linkTitle: 'Objects sidebar' weight: 24 +description: 'Overview of available functions on the objects sidebar of the annotation tool.' --- ## Hide objects sidebar @@ -148,7 +149,8 @@ Change the opacity of every shape in the annotation. **Selected Fill Opacity** slider -Change the opacity of the selected object's fill. +Change the opacity of the selected object's fill. It is possible to change opacity while drawing an object in the case +of rectangles, polygons and cuboids. ![](/images/image089_detrac.jpg) diff --git a/site/content/en/docs/manual/basics/shape-mode-basics.md b/site/content/en/docs/manual/basics/shape-mode-basics.md index f52acded..ba447113 100644 --- a/site/content/en/docs/manual/basics/shape-mode-basics.md +++ b/site/content/en/docs/manual/basics/shape-mode-basics.md @@ -2,6 +2,7 @@ title: 'Shape mode (basics)' linkTitle: 'Shape mode' weight: 10 +description: 'Usage examples and basic operations available during annotation in shape mode.' --- Usage examples: diff --git a/site/content/en/docs/manual/basics/task-details.md b/site/content/en/docs/manual/basics/task-details.md index 2d3db9b1..527c9df1 100644 --- a/site/content/en/docs/manual/basics/task-details.md +++ b/site/content/en/docs/manual/basics/task-details.md @@ -2,6 +2,7 @@ title: 'Task details' linkTitle: 'Task details' weight: 3 +description: 'Overview of the Task details page.' --- Task details is a task page which contains a preview, a progress bar diff --git a/site/content/en/docs/manual/basics/tasks-page.md b/site/content/en/docs/manual/basics/tasks-page.md new file mode 100644 index 00000000..4aabbdf6 --- /dev/null +++ b/site/content/en/docs/manual/basics/tasks-page.md @@ -0,0 +1,31 @@ +--- +title: 'Tasks page' +linkTitle: 'Tasks page' +weight: 2.5 +description: 'Overview of the Tasks page.' +--- + +![](/images/image006_detrac.jpg) + +The tasks page contains elements and each of them relates to a separate task. They are sorted in creation order. +Each element contains: task name, preview, progress bar, button `Open`, and menu `Actions`. +Each button is responsible for a in menu `Actions` specific function: + +- `Export task dataset` — download annotations or annotations and images in a specific format. + More information is available in the [Downloading annotations](/docs/manual/advanced/downloading-annotations/) + section. +- `Upload annotation` is available in the same formats as in `Export task dataset`. The + [CVAT](/docs/manual/advanced/xml_format/) format accepts both video and image sub-formats. +- `Automatic Annotation` — automatic annotation with OpenVINO toolkit. + Presence depends on how you build CVAT instance. +- `Export task` — Export a task into a zip archive. + Read more in the [export/import a task](/docs/manual/advanced/export-import/) section. +- `Move to project` — Moving the task to the project (can be used to move a task from one project to another). + Note that attributes reset during the moving process. In case of label mismatch, + you can create or delete necessary labels in the project/task. + Some task labels can be matched with the target project labels. +- `Delete` — delete task. + +--- + +Push `Open` button to go to [task details](/docs/manual/basics/task-details/). diff --git a/site/content/en/docs/manual/basics/top-panel.md b/site/content/en/docs/manual/basics/top-panel.md index f894a73d..24573290 100644 --- a/site/content/en/docs/manual/basics/top-panel.md +++ b/site/content/en/docs/manual/basics/top-panel.md @@ -2,6 +2,7 @@ title: 'Top Panel' linkTitle: 'Top Panel' weight: 21 +description: 'Overview of controls available on the top panel of the annotation tool.' --- ![](/images/image035.jpg) @@ -51,6 +52,17 @@ Used to complete the creation of the object. This button appears only when the o --- +## Block + +![](/images/image226.jpg) + +Used to pause automatic line creation when drawing a polygon with +[OpenCV Intelligent scissors](/docs/manual/advanced/opencv-tools/#intelligent-scissors). +Also used to postpone server requests when creating an object using [AI Tools](/docs/manual/advanced/ai-tools/). +When blocking is activated, the button turns blue. + +--- + ## Player Go to the first /the latest frames. diff --git a/site/content/en/docs/manual/basics/track-mode-basics.md b/site/content/en/docs/manual/basics/track-mode-basics.md index 1a1c8780..7cb1a392 100644 --- a/site/content/en/docs/manual/basics/track-mode-basics.md +++ b/site/content/en/docs/manual/basics/track-mode-basics.md @@ -2,6 +2,7 @@ title: 'Track mode (basics)' linkTitle: 'Track mode' weight: 11 +description: 'Usage examples and basic operations available during annotation in track mode.' --- Usage examples: diff --git a/site/content/en/docs/manual/basics/types-of-shapes.md b/site/content/en/docs/manual/basics/types-of-shapes.md index 67d2a461..2f3c077a 100644 --- a/site/content/en/docs/manual/basics/types-of-shapes.md +++ b/site/content/en/docs/manual/basics/types-of-shapes.md @@ -2,6 +2,7 @@ title: 'Types of shapes' linkTitle: 'Types of shapes' weight: 5 +description: 'List of shapes available for annotation.' --- There are five shapes which you can annotate your images with: diff --git a/site/content/en/docs/manual/basics/vocabulary.md b/site/content/en/docs/manual/basics/vocabulary.md index 5a9228dc..2e9a390b 100644 --- a/site/content/en/docs/manual/basics/vocabulary.md +++ b/site/content/en/docs/manual/basics/vocabulary.md @@ -2,6 +2,7 @@ title: 'Vocabulary' linkTitle: 'Vocabulary' weight: 14 +description: 'List of terms pertaining to annotation in CVAT.' --- **Label** is a type of an annotated object (e.g. person, car, vehicle, etc.) diff --git a/site/content/en/docs/manual/basics/workspace.md b/site/content/en/docs/manual/basics/workspace.md index e331e630..de7090ca 100644 --- a/site/content/en/docs/manual/basics/workspace.md +++ b/site/content/en/docs/manual/basics/workspace.md @@ -2,6 +2,7 @@ title: 'Workspace' linkTitle: 'Workspace' weight: 22 +description: 'Overview of available functions on the workspace of the annotation tool.' --- This is the main field in which drawing and editing objects takes place. diff --git a/site/content/en/images/dextr_example.gif b/site/content/en/images/dextr_example.gif new file mode 100644 index 00000000..cb4734b9 Binary files /dev/null and b/site/content/en/images/dextr_example.gif differ diff --git a/site/content/en/images/docker_wsl_integration.jpg b/site/content/en/images/docker_wsl_integration.jpg new file mode 100644 index 00000000..593bc2ff Binary files /dev/null and b/site/content/en/images/docker_wsl_integration.jpg differ diff --git a/site/content/en/images/fbrs_example.gif b/site/content/en/images/fbrs_example.gif new file mode 100644 index 00000000..4e95c2dc Binary files /dev/null and b/site/content/en/images/fbrs_example.gif differ diff --git a/site/content/en/images/gif026_carla_town3.gif b/site/content/en/images/gif026_carla_town3.gif index 9790cb03..e13533c0 100644 Binary files a/site/content/en/images/gif026_carla_town3.gif and b/site/content/en/images/gif026_carla_town3.gif differ diff --git a/site/content/en/images/image028.jpg b/site/content/en/images/image028.jpg index b72a726c..96d25940 100644 Binary files a/site/content/en/images/image028.jpg and b/site/content/en/images/image028.jpg differ diff --git a/site/content/en/images/image114.jpg b/site/content/en/images/image114.jpg deleted file mode 100644 index ca2c01cb..00000000 Binary files a/site/content/en/images/image114.jpg and /dev/null differ diff --git a/site/content/en/images/image114_detrac.jpg b/site/content/en/images/image114_detrac.jpg new file mode 100644 index 00000000..2361b16a Binary files /dev/null and b/site/content/en/images/image114_detrac.jpg differ diff --git a/site/content/en/images/image225.jpg b/site/content/en/images/image225.jpg new file mode 100644 index 00000000..ebda01b7 Binary files /dev/null and b/site/content/en/images/image225.jpg differ diff --git a/site/content/en/images/image226.jpg b/site/content/en/images/image226.jpg new file mode 100644 index 00000000..4a5b55f9 Binary files /dev/null and b/site/content/en/images/image226.jpg differ diff --git a/site/content/en/images/iog_example.gif b/site/content/en/images/iog_example.gif new file mode 100644 index 00000000..2141122c Binary files /dev/null and b/site/content/en/images/iog_example.gif differ diff --git a/site/layouts/404.html b/site/layouts/404.html index 75f865fe..55f504f0 100644 --- a/site/layouts/404.html +++ b/site/layouts/404.html @@ -1,9 +1,9 @@ {{ define "main"}} -
-
-

404

-

Not found

-

Oops! This page doesn't exist. Try going back to our about page or documentation page.

-
-
+
+
+

404

+

Not found

+

Oops! This page doesn't exist. Try going back to our about page or documentation page.

+
+
{{ end }} diff --git a/site/layouts/partials/footer.html b/site/layouts/partials/footer.html index 61d283ba..53524702 100644 --- a/site/layouts/partials/footer.html +++ b/site/layouts/partials/footer.html @@ -26,9 +26,9 @@ - {{ if not .Site.Params.ui.footer_about_disable }} - {{ with .Site.GetPage "about" }}

{{ .Title }}

{{ end }} - {{ end }} + {{ if not .Site.Params.ui.footer_about_disable }} + {{ with .Site.GetPage "about" }}

{{ .Title }}

{{ end }} + {{ end }} diff --git a/site/layouts/partials/head.html b/site/layouts/partials/head.html new file mode 100644 index 00000000..d0b8321c --- /dev/null +++ b/site/layouts/partials/head.html @@ -0,0 +1,43 @@ + + +{{ hugo.Generator }} +{{- $outputFormat := partial "outputformat.html" . -}} + +{{ range .AlternativeOutputFormats -}} + +{{ end -}} + +{{ if and (eq (getenv "HUGO_ENV") "production") (ne $outputFormat "print") -}} + +{{ else -}} + +{{ end -}} + +{{ partialCached "favicons.html" . }} +{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{ . }} | {{ end }}{{ .Site.Title }}{{ end }} + +{{- template "_internal/opengraph.html" . -}} +{{- template "_internal/google_news.html" . -}} +{{- template "_internal/schema.html" . -}} +{{- template "_internal/twitter_cards.html" . -}} + +{{ partialCached "head-css.html" . "asdf" }} + +{{ if .Site.Params.offlineSearch }} + +{{end}} +{{ if .Site.Params.prism_syntax_highlighting }} + + +{{ end }} +{{ partial "hooks/head-end.html" . }} + +{{ if eq (getenv "HUGO_ENV") "production" }} +{{ template "_internal/google_analytics_async.html" . }} +{{ end }} \ No newline at end of file diff --git a/site/layouts/partials/navbar-version-selector.html b/site/layouts/partials/navbar-version-selector.html new file mode 100644 index 00000000..619ca172 --- /dev/null +++ b/site/layouts/partials/navbar-version-selector.html @@ -0,0 +1,11 @@ +{{ $page_rel := .Page.RelPermalink | safeURL }} +{{ $page_rel_trim := strings.TrimLeft "/" $page_rel | safeURL }} + + diff --git a/site/layouts/partials/navbar.html b/site/layouts/partials/navbar.html index 54fccf7b..d8b5d580 100644 --- a/site/layouts/partials/navbar.html +++ b/site/layouts/partials/navbar.html @@ -1,35 +1,35 @@ {{ $cover := .HasShortcode "blocks/cover" }} diff --git a/tests/cypress/integration/actions_objects2/case_10_polygon_shape_track_label_points.js b/tests/cypress/integration/actions_objects2/case_10_polygon_shape_track_label_points.js index 6672cfb8..3fc60550 100644 --- a/tests/cypress/integration/actions_objects2/case_10_polygon_shape_track_label_points.js +++ b/tests/cypress/integration/actions_objects2/case_10_polygon_shape_track_label_points.js @@ -6,7 +6,7 @@ import { taskName, labelName } from '../../support/const'; -context('Actions on polygon', () => { +context('Actions on polygon.', () => { const caseId = '10'; const newLabelName = `New label for case ${caseId}`; const createPolygonShape = { @@ -18,8 +18,6 @@ context('Actions on polygon', () => { { x: 250, y: 200 }, { x: 250, y: 250 }, ], - complete: true, - numberOfPoints: null, }; const createPolygonTrack = { reDraw: false, @@ -30,8 +28,6 @@ context('Actions on polygon', () => { { x: 350, y: 200 }, { x: 350, y: 350 }, ], - complete: true, - numberOfPoints: null, }; const createPolygonShapePoints = { reDraw: false, @@ -68,8 +64,7 @@ context('Actions on polygon', () => { { x: 650, y: 200 }, { x: 650, y: 250 }, ], - complete: true, - numberOfPoints: null, + finishWithButton: true, }; const createPolygonTrackSwitchLabel = { reDraw: false, @@ -80,8 +75,7 @@ context('Actions on polygon', () => { { x: 750, y: 200 }, { x: 750, y: 250 }, ], - complete: true, - numberOfPoints: null, + finishWithButton: true, }; before(() => { @@ -91,17 +85,58 @@ context('Actions on polygon', () => { }); describe(`Testing case "${caseId}"`, () => { - it('Draw a polygon shape, track', () => { + it('Draw a polygon shape, track.', () => { cy.createPolygon(createPolygonShape); cy.createPolygon(createPolygonTrack); }); - it('Draw a polygon shape, track with use parameter "number of points"', () => { + + it('Draw a polygon shape, track with use parameter "number of points".', () => { cy.createPolygon(createPolygonShapePoints); cy.createPolygon(createPolygonTrackPoints); }); - it('Draw a polygon shape, track with second label', () => { + + it('Draw a polygon shape, track with second label and "Done" button.', () => { cy.createPolygon(createPolygonShapeSwitchLabel); cy.createPolygon(createPolygonTrackSwitchLabel); }); + + it('Set start point.', () => { + let notFirtsPointCoords = { + x: 0, + y: 0, + }; + let firtsPointCoords = { + x: 0, + y: 0, + }; + cy.get('#cvat_canvas_shape_4') + .trigger('mousemove', {scrollBehavior: false}) + .trigger('mouseover', {scrollBehavior: false}) + .should('have.class', 'cvat_canvas_shape_activated'); + cy.get('.svg_select_points').not('.cvat_canvas_first_poly_point').first().then((notFirtsPoint) => { + notFirtsPointCoords.x = notFirtsPoint.attr('cx'); + notFirtsPointCoords.y = notFirtsPoint.attr('cy'); + }).rightclick({scrollBehavior: false}); + cy.get('.cvat-canvas-point-context-menu').contains('span', 'Set start point').click({scrollBehavior: false}); + cy.get('.cvat_canvas_first_poly_point').then((firtsPoint) => { + firtsPointCoords.x = firtsPoint.attr('cx'); + firtsPointCoords.y = firtsPoint.attr('cy'); + expect(notFirtsPointCoords).to.deep.equal(firtsPointCoords); + }); + }); + + it('Change direction.', () => { + let polyDirectionAttrDataAngle; + cy.get('#cvat_canvas_shape_4') + .trigger('mousemove', {scrollBehavior: false}) + .trigger('mouseover', {scrollBehavior: false}) + .should('have.class', 'cvat_canvas_shape_activated'); + cy.get('.cvat_canvas_poly_direction').then((polyDirection) => { + polyDirectionAttrDataAngle = polyDirection.attr('data-angle'); + }).click({scrollBehavior: false}) + cy.get('.cvat_canvas_poly_direction').then((afterChangePolyDirection) => { + expect(polyDirectionAttrDataAngle).not.equal(afterChangePolyDirection.attr('data-angle')); + }); + }); }); }); diff --git a/tests/cypress/integration/actions_objects2/case_11_polylines_shape_track_label_points.js b/tests/cypress/integration/actions_objects2/case_11_polylines_shape_track_label_points.js index 98101873..49dfe7f2 100644 --- a/tests/cypress/integration/actions_objects2/case_11_polylines_shape_track_label_points.js +++ b/tests/cypress/integration/actions_objects2/case_11_polylines_shape_track_label_points.js @@ -6,7 +6,7 @@ import { taskName, labelName } from '../../support/const'; -context('Actions on polylines', () => { +context('Actions on polylines.', () => { const caseId = '11'; const newLabelName = `New label for case ${caseId}`; const createPolylinesShape = { @@ -17,8 +17,6 @@ context('Actions on polylines', () => { { x: 250, y: 200 }, { x: 250, y: 250 }, ], - complete: true, - numberOfPoints: null, }; const createPolylinesTrack = { type: 'Track', @@ -28,8 +26,6 @@ context('Actions on polylines', () => { { x: 350, y: 200 }, { x: 350, y: 350 }, ], - complete: true, - numberOfPoints: null, }; const createPolylinesShapePoints = { type: 'Shape', @@ -63,8 +59,7 @@ context('Actions on polylines', () => { { x: 650, y: 200 }, { x: 650, y: 250 }, ], - complete: true, - numberOfPoints: null, + finishWithButton: true, }; const createPolylinesTrackSwitchLabel = { type: 'Track', @@ -74,8 +69,7 @@ context('Actions on polylines', () => { { x: 750, y: 200 }, { x: 750, y: 250 }, ], - complete: true, - numberOfPoints: null, + finishWithButton: true, }; before(() => { @@ -85,17 +79,49 @@ context('Actions on polylines', () => { }); describe(`Testing case "${caseId}"`, () => { - it('Draw a polylines shape, track', () => { + it('Draw a polylines shape, track.', () => { cy.createPolyline(createPolylinesShape); cy.createPolyline(createPolylinesTrack); }); - it('Draw a polylines shape, track with use parameter "number of points"', () => { + + it('Draw a polylines shape, track with use parameter "number of points".', () => { cy.createPolyline(createPolylinesShapePoints); cy.createPolyline(createPolylinesTrackPoints); }); - it('Draw a polylines shape, track with second label', () => { + + it('Draw a polylines shape, track with second label and "Done" button.', () => { cy.createPolyline(createPolylinesShapeSwitchLabel); cy.createPolyline(createPolylinesTrackSwitchLabel); }); + + it('Change direction.', () => { + let firtsPointCoords = { + x: 0, + y: 0, + }; + let lastPointCoords = { + x: 0, + y: 0, + }; + cy.get('#cvat_canvas_shape_4') + .trigger('mousemove', {scrollBehavior: false}) + .trigger('mouseover', {scrollBehavior: false}) + .should('have.class', 'cvat_canvas_shape_activated'); + cy.get('.svg_select_points_point').first().then((firtsPoint) => { + firtsPointCoords.x = firtsPoint.attr('cx'); + firtsPointCoords.y = firtsPoint.attr('cy'); + cy.get('.svg_select_points_point').last().then((lastPoint) => { + lastPointCoords.x = lastPoint.attr('cx'); + lastPointCoords.y = lastPoint.attr('cy'); + cy.get('.cvat_canvas_first_poly_point') + .should('have.attr', 'cx', firtsPointCoords.x) + .and('have.attr', 'cy', firtsPointCoords.y) + cy.get('.cvat_canvas_poly_direction').click({scrollBehavior: false}); + cy.get('.cvat_canvas_first_poly_point') + .should('have.attr', 'cx', lastPointCoords.x) + .and('have.attr', 'cy', lastPointCoords.y) + }); + }); + }); }); }); diff --git a/tests/cypress/integration/actions_objects2/case_12_points_shape_track_label.js b/tests/cypress/integration/actions_objects2/case_12_points_shape_track_label.js index 48fb6d6d..002a5fc0 100644 --- a/tests/cypress/integration/actions_objects2/case_12_points_shape_track_label.js +++ b/tests/cypress/integration/actions_objects2/case_12_points_shape_track_label.js @@ -6,7 +6,7 @@ import { taskName, labelName } from '../../support/const'; -context('Actions on points', () => { +context('Actions on points.', () => { const caseId = '12'; const newLabelName = `New label for case ${caseId}`; const createPointsShape = { @@ -17,8 +17,6 @@ context('Actions on points', () => { { x: 250, y: 200 }, { x: 250, y: 250 }, ], - complete: true, - numberOfPoints: null, }; const createPointsTrack = { type: 'Track', @@ -28,8 +26,6 @@ context('Actions on points', () => { { x: 350, y: 200 }, { x: 350, y: 350 }, ], - complete: true, - numberOfPoints: null, }; const createPointsShapePoints = { type: 'Shape', @@ -63,8 +59,7 @@ context('Actions on points', () => { { x: 650, y: 200 }, { x: 650, y: 250 }, ], - complete: true, - numberOfPoints: null, + finishWithButton: true, }; const createPointsTrackSwitchLabel = { type: 'Track', @@ -74,8 +69,7 @@ context('Actions on points', () => { { x: 750, y: 200 }, { x: 750, y: 250 }, ], - complete: true, - numberOfPoints: null, + finishWithButton: true, }; before(() => { @@ -85,15 +79,17 @@ context('Actions on points', () => { }); describe(`Testing case "${caseId}"`, () => { - it('Draw a points shape, track', () => { + it('Draw a points shape, track.', () => { cy.createPoint(createPointsShape); cy.createPoint(createPointsTrack); }); - it('Draw a points shape, track with use parameter "number of points"', () => { + + it('Draw a points shape, track with use parameter "number of points".', () => { cy.createPoint(createPointsShapePoints); cy.createPoint(createPointsTrackPoints); }); - it('Draw a points shape, track with second label', () => { + + it('Draw a points shape, track with second label and "Done" button.', () => { cy.createPoint(createPointsShapeSwitchLabel); cy.createPoint(createPointsTrackSwitchLabel); }); diff --git a/tests/cypress/integration/actions_objects2/case_14_appearance_features.js b/tests/cypress/integration/actions_objects2/case_14_appearance_features.js index d6b3c36c..4da966d3 100644 --- a/tests/cypress/integration/actions_objects2/case_14_appearance_features.js +++ b/tests/cypress/integration/actions_objects2/case_14_appearance_features.js @@ -78,6 +78,7 @@ context('Appearance features', () => { cy.createCuboid(createCuboidShape2Points); cy.createPoint(createPointsShape); }); + it('Set opacity level for shapes to 100. All shapes are filled.', () => { cy.get('.cvat-appearance-opacity-slider') .click('right') @@ -101,7 +102,8 @@ context('Appearance features', () => { }); }); }); - it('Set selected opacity to 0.', () => { + + it('Set "Selected opacity" to 0.', () => { cy.get('.cvat-appearance-selected-opacity-slider') .click('left') .within(() => { @@ -113,7 +115,8 @@ context('Appearance features', () => { }); }); }); - it('Activate the box, the polygon and the cuboid. Boxes are transparent during activated.', () => { + + it('Activate the rectangle, the polygon and the cuboid. Shapes are transparent during activated.', () => { for (const i of ['#cvat_canvas_shape_1', '#cvat_canvas_shape_2', '#cvat_canvas_shape_4']) { cy.get(i) .trigger('mousemove') @@ -121,10 +124,9 @@ context('Appearance features', () => { .and('have.css', 'fill-opacity', ariaValuenow); } }); - it('Activate checkbox "show projections".', () => { + + it('Activate checkbox "show projections". Activated the cuboid. Projection lines are visible.', () => { cy.get('.cvat-appearance-cuboid-projections-checkbox').click(); - }); - it('Activated the cuboid. Projection lines are visible.', () => { cy.get('#cvat_canvas_shape_4') .trigger('mousemove', { force: true }) .should('have.attr', 'projections', 'true'); @@ -132,6 +134,7 @@ context('Appearance features', () => { // Deactivate all objects cy.get('.cvat-canvas-container').click(500, 500); }); + it('Activate checkbox "outlined borders" with a red color. The borders are red on the objects.', () => { cy.get('.cvat-appearance-outlinded-borders-checkbox').click(); cy.get('.cvat-appearance-outlined-borders-button').click(); @@ -140,12 +143,14 @@ context('Appearance features', () => { cy.get(object).should('have.attr', 'stroke', `#${strokeColor}`); }); }); + it('Set "Color by" to instance. The shapes changed a color.', () => { cy.changeAppearance('Instance'); cy.get('.cvat_canvas_shape').each((object) => { cy.get(object).should('have.css', 'fill').and('not.equal', fill); }); }); + it('Set "Color by" to group. The shapes are white.', () => { cy.changeAppearance('Group'); cy.get('.cvat_canvas_shape').each((object) => { @@ -162,5 +167,84 @@ context('Appearance features', () => { cy.get('.cvat-appearance-outlinded-borders-checkbox').click(); cy.get('#cvat_canvas_shape_3').should('have.css', 'stroke', 'rgb(224, 224, 224)'); // have CSS property stroke with the value rgb(224, 224, 224) }); + + it('"Selected opacity" slider now defines opacity level of shapes being drawn.', () => { + function testDrawShapeCheckOpacity({ shape, drawingMethod, shapeType, fillOpacityBefore, fillOpacityAfter, opacityBefore, opacityAfter }) { + cy.interactControlButton(`draw-${shape}`); + cy.get(`.cvat-draw-${shape}-popover-visible`).within(() => { + if (drawingMethod) { + cy.contains('.ant-radio-wrapper', drawingMethod).click(); + } + cy.contains('button', shapeType).click(); + }); + cy.get('.cvat-canvas-container').click(100, 450); + if (fillOpacityBefore != null || fillOpacityAfter != null) { + cy.get('.cvat-appearance-selected-opacity-slider').click('left'); + cy.get('.cvat_canvas_shape_drawing').should('have.attr', 'fill-opacity', fillOpacityBefore); + cy.get('.cvat-appearance-selected-opacity-slider').click('right'); + cy.get('.cvat_canvas_shape_drawing').should('have.attr', 'fill-opacity', fillOpacityAfter); + } else if (opacityBefore != null || opacityAfter != null) { + cy.get('.cvat-appearance-selected-opacity-slider').click('left'); + cy.get('.cvat_canvas_shape_drawing').should('have.attr', 'opacity', opacityBefore); + cy.get('.cvat-appearance-selected-opacity-slider').click('right'); + cy.get('.cvat_canvas_shape_drawing').should('have.attr', 'opacity', opacityAfter); + } else { + cy.get('.cvat_canvas_shape_drawing').should('not.have.attr', 'opacity'); + cy.get('.cvat_canvas_shape_drawing').should('not.have.attr', 'fill-opacity'); + } + cy.get('body').type('{Esc}'); + } + // affect opacity level + testDrawShapeCheckOpacity({ + shape: 'rectangle', + drawingMethod: 'By 2 Points', + shapeType: 'Shape', + fillOpacityBefore: 0, + fillOpacityAfter: 1, + }); + // not affect opacity level + testDrawShapeCheckOpacity({ + shape: 'rectangle', + drawingMethod: 'By 4 Points', + shapeType: 'Shape', + opacityBefore: 0, + opacityAfter: 0, + }); + // not affect opacity level + testDrawShapeCheckOpacity({ + shape: 'polyline', + shapeType: 'Shape', + fillOpacityBefore: 0, + fillOpacityAfter: 0, + }); + // affect opacity level + testDrawShapeCheckOpacity({ + shape: 'polygon', + shapeType: 'Shape', + fillOpacityBefore: 0, + fillOpacityAfter: 1, + }); + // not affect opacity level + testDrawShapeCheckOpacity({ + shape: 'points', + shapeType: 'Shape', + opacityBefore: 0, + opacityAfter: 0, + }); + // affect opacity level + testDrawShapeCheckOpacity({ + shape: 'cuboid', + drawingMethod: 'From rectangle', + shapeType: 'Shape', + fillOpacityBefore: 0, + fillOpacityAfter: 1, + }); + // not have 'fill-opacity' or 'opacity' attributes + testDrawShapeCheckOpacity({ + shape: 'cuboid', + drawingMethod: 'By 4 Points', + shapeType: 'Shape', + }); + }); }); }); diff --git a/tests/cypress/integration/actions_projects_models/case_103_project_export.js b/tests/cypress/integration/actions_projects_models/case_103_project_export.js new file mode 100644 index 00000000..78361179 --- /dev/null +++ b/tests/cypress/integration/actions_projects_models/case_103_project_export.js @@ -0,0 +1,113 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +/// + +import { projectName, labelName } from '../../support/const_project'; + +context('Export project dataset.', { browser: '!firefox' }, () => { + const caseID = 103; + const taskName = `Task case ${caseID}`; + const attrName = `Attr for ${labelName}`; + const textDefaultValue = 'Some value for type Text'; + const imagesCount = 1; + const imageFileName = `image_${taskName.replace(/\s+/g, '_').toLowerCase()}`; + const width = 800; + const height = 800; + const posX = 10; + const posY = 10; + const color = 'white'; + const archiveName = `${imageFileName}.zip`; + const archivePath = `cypress/fixtures/${archiveName}`; + const imagesFolder = `cypress/fixtures/${imageFileName}`; + const directoryToArchive = imagesFolder; + const advancedConfigurationParams = false; + const forProject = true; + const attachToProject = false; + const multiAttrParams = false; + let projectID = ''; + + function getProjectID(projectName) { + cy.contains('.cvat-project-name', projectName) + .parents('.cvat-project-details') + .should('have.attr', 'cvat-project-id') + .then(($projectID) => { + projectID = $projectID; + }); + } + + function testCheckFile(file) { + cy.task('listFiles', 'cypress/fixtures').each((fileName) => { + if (fileName.match(file)) { + cy.readFile(`cypress/fixtures/${fileName}`).should('exist'); + } + }); + } + + before(() => { + cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount); + cy.createZipArchive(directoryToArchive, archivePath); + cy.openProject(projectName); + getProjectID(projectName); + cy.createAnnotationTask( + taskName, + labelName, + attrName, + textDefaultValue, + archiveName, + multiAttrParams, + advancedConfigurationParams, + forProject, + attachToProject, + projectName, + ); + }); + + after(() => { + cy.goToProjectsList(); + cy.deleteProject(projectName, projectID); + }); + + describe(`Testing "Case ${caseID}"`, () => { + it('Export project dataset. Annotation.', () => { + cy.goToProjectsList(); + const exportAnnotation = { + projectName: projectName, + as: 'exportAnnotations', + type: 'annotations', + dumpType: 'CVAT for images', + }; + cy.exportProject(exportAnnotation); + const regex = new RegExp(`^project_${projectName.toLowerCase()}-.*-${exportAnnotation.dumpType.toLowerCase()}.*.zip$`); + testCheckFile(regex); + }); + + it('Export project dataset. Dataset.', () => { + cy.goToProjectsList(); + const exportDataset = { + projectName: projectName, + as: 'exportDataset', + type: 'dataset', + dumpType: 'CVAT for video', + }; + cy.exportProject(exportDataset); + const regex = new RegExp(`^project_${projectName.toLowerCase()}-.*-${exportDataset.dumpType.toLowerCase()}.*.zip$`); + testCheckFile(regex); + }); + + it('Export project dataset. Annotation. Rename a archive.', () => { + cy.goToProjectsList(); + const exportAnnotationsRenameArchive = { + projectName: projectName, + as: 'exportAnnotationsRenameArchive', + type: 'annotations', + dumpType: 'CVAT for images', + archiveCustomeName: 'export_project_annotation', + }; + cy.exportProject(exportAnnotationsRenameArchive); + const regex = new RegExp(`^${exportAnnotationsRenameArchive.archiveCustomeName}.zip$`); + testCheckFile(regex); + }); + }); +}); diff --git a/tests/cypress/integration/actions_projects_models/case_104_project_export_3d.js b/tests/cypress/integration/actions_projects_models/case_104_project_export_3d.js new file mode 100644 index 00000000..2fb586b0 --- /dev/null +++ b/tests/cypress/integration/actions_projects_models/case_104_project_export_3d.js @@ -0,0 +1,104 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +/// + +import { projectName, labelName } from '../../support/const_project'; + +context('Export project dataset with 3D task.', { browser: '!firefox' }, () => { + const caseID = 104; + const task = { + name3d: `Case ${caseID}`, + label3d: labelName, + attrName3d: 'Kind', + attrValue3d: 'Oak', + archiveName: '../../cypress/integration/canvas3d_functionality/assets/test_canvas3d.zip', + advancedConfigurationParams: false, + forProject: true, + attachToProject: false, + multiAttrParams: false, + }; + let projectID = ''; + + function getProjectID(projectName) { + cy.contains('.cvat-project-name', projectName) + .parents('.cvat-project-details') + .should('have.attr', 'cvat-project-id') + .then(($projectID) => { + projectID = $projectID; + }); + } + + function testCheckFile(file) { + cy.task('listFiles', 'cypress/fixtures').each((fileName) => { + if (fileName.match(file)) { + cy.readFile(`cypress/fixtures/${fileName}`).should('exist'); + } + }); + } + + before(() => { + cy.openProject(projectName); + getProjectID(projectName); + cy.createAnnotationTask( + task.name3d, + task.label3d, + task.attrName3d, + task.attrValue3d, + task.archiveName, + task.multiAttrParams, + task.advancedConfigurationParams, + task.forProject, + task.attachToProject, + projectName, + ); + }); + + after(() => { + cy.goToProjectsList(); + cy.deleteProject(projectName, projectID); + }); + + describe(`Testing "Case ${caseID}"`, () => { + it('Export project with 3D task. Annotation.', () => { + cy.goToProjectsList(); + const exportAnnotation3d = { + projectName: projectName, + as: 'exportAnnotations3d', + type: 'annotations', + dumpType: 'Kitti Raw Format', + }; + cy.exportProject(exportAnnotation3d); + const regex = new RegExp(`^project_${projectName.toLowerCase()}-.*-${exportAnnotation3d.dumpType.toLowerCase()}.*.zip$`); + testCheckFile(regex); + }); + + it('Export project with 3D task. Dataset.', () => { + cy.goToProjectsList(); + const exportDataset3d = { + projectName: projectName, + as: 'exportDataset3d', + type: 'dataset', + dumpType: 'Sly Point Cloud Format', + }; + cy.exportProject(exportDataset3d); + const regex = new RegExp(`^project_${projectName.toLowerCase()}-.*-${exportDataset3d.dumpType.toLowerCase()}.*.zip$`); + testCheckFile(regex); + }); + + it('Export project with 3D task. Annotation. Rename a archive.', () => { + cy.goToProjectsList(); + const exportAnnotations3dRenameArchive = { + projectName: projectName, + as: 'exportAnnotations3dRenameArchive', + type: 'annotations', + dumpType: 'Kitti Raw Format', + archiveCustomeName: 'export_project_3d_annotation', + }; + cy.exportProject(exportAnnotations3dRenameArchive); + const regex = new RegExp(`^${exportAnnotations3dRenameArchive.archiveCustomeName}.zip$`); + testCheckFile(regex); + }); + }); +}); diff --git a/tests/cypress/integration/actions_projects_models/case_94_move_task_between_projects.js b/tests/cypress/integration/actions_projects_models/case_94_move_task_between_projects.js index 99c5a6bd..62c3c051 100644 --- a/tests/cypress/integration/actions_projects_models/case_94_move_task_between_projects.js +++ b/tests/cypress/integration/actions_projects_models/case_94_move_task_between_projects.js @@ -11,16 +11,16 @@ context('Move a task between projects.', () => { label: 'car', attrName: 'color', attrVaue: 'red', - multiAttrParams: false - } + multiAttrParams: false, + }; const secondProject = { name: `Second project case ${caseID}`, label: 'bicycle', attrName: 'color', attrVaue: 'yellow', - multiAttrParams: false - } + multiAttrParams: false, + }; const taskName = `Task case ${caseID}`; const imagesCount = 1; @@ -39,14 +39,24 @@ context('Move a task between projects.', () => { const attachToProject = false; const multiAttrParams = false; - function checkTask (project, expectedResult) { + function checkTask(project, expectedResult) { cy.goToProjectsList(); cy.openProject(project); cy.get('.cvat-tasks-list-item').should(expectedResult); } before(() => { - cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, firtsProject.label, imagesCount); + cy.imageGenerator( + imagesFolder, + imageFileName, + width, + height, + color, + posX, + posY, + firtsProject.label, + imagesCount, + ); cy.createZipArchive(directoryToArchive, archivePath); cy.visit('/'); cy.login(); @@ -54,8 +64,20 @@ context('Move a task between projects.', () => { beforeEach(() => { cy.goToProjectsList(); - cy.createProjects(firtsProject.name, firtsProject.label, firtsProject.attrName, firtsProject.attrVaue, firtsProject.multiAttrParams); - cy.createProjects(secondProject.name, secondProject.label, secondProject.attrName, secondProject.attrVaue, secondProject.multiAttrParams); + cy.createProjects( + firtsProject.name, + firtsProject.label, + firtsProject.attrName, + firtsProject.attrVaue, + firtsProject.multiAttrParams, + ); + cy.createProjects( + secondProject.name, + secondProject.label, + secondProject.attrName, + secondProject.attrVaue, + secondProject.multiAttrParams, + ); cy.openProject(firtsProject.name); cy.createAnnotationTask( taskName, @@ -80,8 +102,7 @@ context('Move a task between projects.', () => { }); describe(`Testing "Case ${caseID}"`, () => { - // Waiting to fix https://github.com/openvinotoolkit/cvat/issues/3281 - it.skip('Move a task between projects from a project.', () => { + it('Move a task between projects from a project.', () => { checkTask(secondProject.name, 'not.exist'); checkTask(firtsProject.name, 'exist'); cy.movingTask(taskName, secondProject.name, firtsProject.label, secondProject.label); diff --git a/tests/cypress/integration/actions_projects_models/case_95_move_task_to_project.js b/tests/cypress/integration/actions_projects_models/case_95_move_task_to_project.js index 7ac0459d..bc41e32e 100644 --- a/tests/cypress/integration/actions_projects_models/case_95_move_task_to_project.js +++ b/tests/cypress/integration/actions_projects_models/case_95_move_task_to_project.js @@ -11,14 +11,18 @@ context('Move a task to a project.', () => { label: 'Tree', attrName: 'Kind', attrValue: 'Oak', - } + nameSecond: `Case ${caseID} second`, + labelSecond: 'Car', + attrNameSecons: 'Color', + attrValueSecond: 'Red', + }; const project = { name: `Case ${caseID}`, label: 'Tree', attrName: 'Kind', - attrVaue: 'Oak' - } + attrVaue: 'Oak', + }; const imagesCount = 1; const imageFileName = `image_${task.name.replace(' ', '_').toLowerCase()}`; @@ -37,6 +41,8 @@ context('Move a task to a project.', () => { cy.login(); cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, task.name, imagesCount); cy.createZipArchive(directoryToArchive, archivePath); + cy.goToTaskList(); + cy.createAnnotationTask(task.nameSecond, task.labelSecond, task.attrNameSecons, task.attrValueSecond, archiveName); }); beforeEach(() => { @@ -58,9 +64,12 @@ context('Move a task to a project.', () => { cy.get('.cvat-tasks-list-item').should('not.exist'); cy.goToTaskList(); cy.movingTask(task.name, project.name, task.label, project.label); + // Check issue 3403 + cy.goToTaskList(); + cy.movingTask(task.nameSecond, project.name, task.labelSecond, project.label); cy.goToProjectsList(); cy.openProject(project.name); - cy.get('.cvat-tasks-list-item').should('exist'); + cy.get('.cvat-tasks-list-item').should('exist').and('have.length', 2); }); it('Move a task from task.', () => { diff --git a/tests/cypress/integration/actions_tasks/case_100_settings_default_number_of_points_in_polygon_approximation.js b/tests/cypress/integration/actions_tasks/case_100_settings_default_number_of_points_in_polygon_approximation.js new file mode 100644 index 00000000..3191502b --- /dev/null +++ b/tests/cypress/integration/actions_tasks/case_100_settings_default_number_of_points_in_polygon_approximation.js @@ -0,0 +1,57 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +/// + +import { taskName } from '../../support/const'; +import { generateString } from '../../support/utils'; + +context('Settings. Default number of points in polygon approximation.', () => { + const caseId = '100'; + + function testOpenSettingsWorkspace() { + cy.document().then((doc) => { + const settingsModal = Array.from(doc.querySelectorAll('.cvat-settings-modal')); + if (settingsModal.length === 0) { + cy.openSettings(); + cy.contains('[role="tab"]', 'Workspace').click(); + } + }); + } + + function testCheckSliderAttrValuenow(expectedValue) { + testOpenSettingsWorkspace(); + cy.get('.cvat-workspace-settings-approx-poly-threshold').find('[role="slider"]').then((slider) => { + expect(slider.attr('aria-valuenow')).to.be.equal(expectedValue); + }); + } + + before(() => { + cy.openTaskJob(taskName); + }); + + describe(`Testing case "${caseId}"`, () => { + it('Change the setting value for "Default number of points in polygon approximation".', () => { + testOpenSettingsWorkspace(); + cy.get('.cvat-workspace-settings-approx-poly-threshold') + .find('[role="slider"]') + .type(generateString(4, 'rightarrow')) + .then((slider) => { + const sliderAttrValueNow = slider.attr('aria-valuenow'); + const sliderAttrValuemin = slider.attr('aria-valuemin'); + const sliderAttrValuemax = slider.attr('aria-valuemax'); + cy.saveSettings(); + cy.closeNotification('.cvat-notification-notice-save-settings-success'); + cy.closeSettings(); + cy.reload(); + cy.closeModalUnsupportedPlatform(); // If the Firefox browser closes the modal window after reload + testCheckSliderAttrValuenow(sliderAttrValueNow); + cy.contains('strong', 'less').click(); + testCheckSliderAttrValuenow(sliderAttrValuemin); + cy.contains('strong', 'more').click(); + testCheckSliderAttrValuenow(sliderAttrValuemax); + }); + }); + }); +}); diff --git a/tests/cypress/integration/actions_tasks/case_102_create_link_shape_frame.js b/tests/cypress/integration/actions_tasks/case_102_create_link_shape_frame.js new file mode 100644 index 00000000..fe200323 --- /dev/null +++ b/tests/cypress/integration/actions_tasks/case_102_create_link_shape_frame.js @@ -0,0 +1,63 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +/// + +import { taskName, labelName } from '../../support/const'; + +context('Create a link for shape, frame.', () => { + const caseId = '102'; + const createRectangleShape2Points = { + points: 'By 2 Points', + type: 'Shape', + labelName: labelName, + firstX: 250, + firstY: 350, + secondX: 350, + secondY: 450, + }; + + before(() => { + cy.openTaskJob(taskName); + cy.createRectangle(createRectangleShape2Points); + cy.saveJob('PATCH', 200, `case${caseId}`); + }); + + describe(`Testing case "${caseId}"`, () => { + it('Create a link for a shape, for a frame.', () => { + cy.window().then(win => { + cy.stub(win, 'prompt').returns(win.prompt).as('copyToClipboardPromptShape'); + }); + cy.get('#cvat-objects-sidebar-state-item-1').find('[aria-label="more"]').trigger('mouseover'); + cy.get('#cvat_canvas_shape_1').should('have.class', 'cvat_canvas_shape_activated') + cy.get('.cvat-object-item-menu').last().should('be.visible').contains('button', 'Create object URL').click(); + cy.get('@copyToClipboardPromptShape').should('be.called'); + cy.get('@copyToClipboardPromptShape').then(prompt => { + const url = prompt.args[0][1]; + expect(url).include('frame='); + expect(url).include('type='); + expect(url).include('serverID='); + cy.visit(url); + cy.closeModalUnsupportedPlatform(); + cy.get('.cvat-canvas-container').should('be.visible'); + cy.get('#cvat_canvas_shape_1').should('be.visible'); + }); + + cy.window().then(win => { + cy.stub(win, 'prompt').returns(win.prompt).as('copyToClipboardPromptFrame'); + }); + cy.get('.cvat-player-frame-url-icon').click(); + cy.get('@copyToClipboardPromptFrame').should('be.called'); + cy.get('@copyToClipboardPromptFrame').then(prompt => { + const url = prompt.args[0][1]; + expect(url).include('frame='); + expect(url).not.include('type='); + expect(url).not.include('serverID='); + cy.visit(url); + cy.get('.cvat-canvas-container').should('be.visible'); + cy.get('#cvat_canvas_shape_1').should('be.visible'); + }); + }); + }); +}); diff --git a/tests/cypress/integration/actions_tasks/case_52_dump_upload_annotation.js b/tests/cypress/integration/actions_tasks/case_52_dump_upload_annotation.js index b46a0c55..92756549 100644 --- a/tests/cypress/integration/actions_tasks/case_52_dump_upload_annotation.js +++ b/tests/cypress/integration/actions_tasks/case_52_dump_upload_annotation.js @@ -34,8 +34,9 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => { const imagesFolder = `cypress/fixtures/${imageFileName}`; const directoryToArchive = imagesFolder; - const dumpType = 'CVAT for images'; + const exportFormat = 'CVAT for images'; let annotationArchiveName = ''; + let annotationArchiveNameCustomeName = ''; function uploadToTask(toTaskName) { cy.contains('.cvat-item-task-name', toTaskName) @@ -43,13 +44,13 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => { .find('.cvat-menu-icon') .trigger('mouseover'); cy.contains('Upload annotations').trigger('mouseover'); - cy.contains('.cvat-menu-load-submenu-item', dumpType.split(' ')[0]) + cy.contains('.cvat-menu-load-submenu-item', exportFormat.split(' ')[0]) .should('be.visible') .within(() => { cy.get('.cvat-menu-load-submenu-item-button') .click() .get('input[type=file]') - .attachFile(annotationArchiveName); + .attachFile(annotationArchiveNameCustomeName); }); } @@ -65,23 +66,39 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => { }); describe(`Testing case "${caseId}"`, () => { - it('Save job. Dump annotation. Remove annotation. Save job.', () => { + it('Save job. Dump annotation with renaming the archive.', () => { cy.saveJob('PATCH', 200, 'saveJobDump'); - cy.intercept('GET', '/api/v1/tasks/**/annotations**').as('dumpAnnotations'); - cy.interactMenu('Dump annotations'); - cy.get('.cvat-menu-dump-submenu-item').within(() => { - cy.contains(dumpType).click(); + const exportAnnotationRenameArchive = { + as: 'exportAnnotationsRenameArchive', + type: 'annotations', + format: exportFormat, + archiveCustomeName: 'task_export_annotation_custome_name' + }; + cy.exportTask(exportAnnotationRenameArchive); + const regex = new RegExp(`^${exportAnnotationRenameArchive.archiveCustomeName}.zip$`); + cy.task('listFiles', 'cypress/fixtures').each((fileName) => { + if (fileName.match(regex)) { + cy.readFile(`cypress/fixtures/${fileName}`).should('exist'); + annotationArchiveNameCustomeName = fileName; + } }); - cy.wait('@dumpAnnotations', { timeout: 5000 }).its('response.statusCode').should('equal', 202); - cy.wait('@dumpAnnotations').its('response.statusCode').should('equal', 201); + }); + + it('Save job. Dump annotation. Remove annotation. Save job.', () => { + const exportAnnotation = { + as: 'exportAnnotations', + type: 'annotations', + format: exportFormat, + }; + cy.exportTask(exportAnnotation); cy.removeAnnotations(); cy.saveJob('PUT'); cy.get('#cvat_canvas_shape_1').should('not.exist'); cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist'); - - cy.wait(2000); // Waiting for the full download. + const regex = new RegExp(`^task_${taskName.toLowerCase()}-.*-${exportAnnotation.format.toLowerCase()}.*.zip$`); cy.task('listFiles', 'cypress/fixtures').each((fileName) => { - if (fileName.includes(dumpType.toLowerCase())) { + if (fileName.match(regex)) { + cy.readFile(`cypress/fixtures/${fileName}`).should('exist'); annotationArchiveName = fileName; } }); @@ -89,7 +106,7 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => { it('Upload annotation to job.', () => { cy.interactMenu('Upload annotations'); - cy.contains('.cvat-menu-load-submenu-item', dumpType.split(' ')[0]) + cy.contains('.cvat-menu-load-submenu-item', exportFormat.split(' ')[0]) .should('be.visible') .within(() => { cy.get('.cvat-menu-load-submenu-item-button') @@ -106,6 +123,8 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => { cy.get('#cvat_canvas_shape_1').should('exist'); cy.get('#cvat-objects-sidebar-state-item-1').should('exist'); cy.removeAnnotations(); + cy.get('#cvat_canvas_shape_1').should('not.exist'); + cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist'); }); it('Upload annotation to task.', () => { diff --git a/tests/cypress/integration/actions_tasks/issue_2473_import_annotations_frames_dots_in_name.js b/tests/cypress/integration/actions_tasks/issue_2473_import_annotations_frames_dots_in_name.js index 3d43f3d3..9cb52acc 100644 --- a/tests/cypress/integration/actions_tasks/issue_2473_import_annotations_frames_dots_in_name.js +++ b/tests/cypress/integration/actions_tasks/issue_2473_import_annotations_frames_dots_in_name.js @@ -66,10 +66,15 @@ context('Import annotations for frames with dots in name.', { browser: '!firefox it('Save job. Dump annotation to YOLO format. Remove annotation. Save job.', () => { cy.saveJob('PATCH', 200, 'saveJobDump'); cy.intercept('GET', '/api/v1/tasks/**/annotations**').as('dumpAnnotations'); - cy.interactMenu('Dump annotations'); - cy.get('.cvat-menu-dump-submenu-item').within(() => { - cy.contains(dumpType).click(); - }); + cy.interactMenu('Export task dataset'); + cy.get('.cvat-modal-export-task').find('.cvat-modal-export-select').click(); + cy.get('.ant-select-dropdown') + .not('.ant-select-dropdown-hidden') + .trigger('wheel', {deltaY: 700}) + .contains('.cvat-modal-export-option-item', dumpType) + .click(); + cy.get('.cvat-modal-export-select').should('contain.text', dumpType); + cy.get('.cvat-modal-export-task').contains('button', 'OK').click(); cy.wait('@dumpAnnotations', { timeout: 5000 }).its('response.statusCode').should('equal', 202); cy.wait('@dumpAnnotations').its('response.statusCode').should('equal', 201); cy.removeAnnotations(); diff --git a/tests/cypress/integration/actions_tasks2/case_101_opencv_basic_actions.js b/tests/cypress/integration/actions_tasks2/case_101_opencv_basic_actions.js new file mode 100644 index 00000000..b2f19b2f --- /dev/null +++ b/tests/cypress/integration/actions_tasks2/case_101_opencv_basic_actions.js @@ -0,0 +1,154 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +/// + +import { taskName, labelName } from '../../support/const'; +import { generateString } from '../../support/utils'; + +context('OpenCV. Intelligent scissors. Histogram Equalization.', () => { + const caseId = '101'; + const newLabel = `Case ${caseId}` + const createOpencvShape = { + labelName: labelName, + pointsMap: [ + { x: 200, y: 200 }, + { x: 250, y: 200 }, + { x: 300, y: 250 }, + { x: 350, y: 300 }, + { x: 300, y: 350 }, + ], + }; + const createOpencvShapeSecondLabel = { + labelName: newLabel, + pointsMap: [ + { x: 300, y: 200 }, + { x: 350, y: 200 }, + { x: 400, y: 250 }, + { x: 450, y: 300 }, + { x: 400, y: 350 }, + ], + finishWithButton: true, + }; + const keyCodeN = 78; + const pointsMap = [ + { x: 300, y: 400 }, + { x: 350, y: 500 }, + { x: 400, y: 450 }, + { x: 450, y: 500 }, + { x: 400, y: 550 }, + ]; + + function openOpencvControlPopover() { + cy.get('body').focus(); + cy.get('.cvat-tools-control').trigger('mouseleave').trigger('mouseout').trigger('mouseover'); + } + + before(() => { + cy.openTask(taskName); + cy.addNewLabel(newLabel); + cy.openJob(); + }); + + describe(`Testing case "${caseId}"`, () => { + it('Load OpenCV.', () => { + openOpencvControlPopover(); + cy.get('.cvat-opencv-control-popover-visible').find('.cvat-opencv-initialization-button').click(); + // Intelligent cissors button be visible + cy.get('.cvat-opencv-drawing-tool').should('exist').and('be.visible'); + }); + + it('Create a shape with "Intelligent cissors". Create the second shape with the label change and "Done" button.', () => { + cy.opencvCreateShape(createOpencvShape); + cy.opencvCreateShape(createOpencvShapeSecondLabel); + }); + + it('Change the number of points when the shape is drawn. Cancel drawing.', () => { + openOpencvControlPopover(); + cy.get('.cvat-opencv-drawing-tool').click(); + pointsMap.forEach((element) => { + cy.get('.cvat-canvas-container').click(element.x, element.y); + }); + cy.get('.cvat_canvas_interact_intermediate_shape').then((intermediateShape) => { + // Get count of points + const intermediateShapeNumberPointsBeforeChange = intermediateShape.attr('points').split(' ').length; + // expected 7 to be above 5 + expect(intermediateShapeNumberPointsBeforeChange).to.be.gt(pointsMap.length); + // Change number of points + cy.get('.cvat-approx-poly-threshold-wrapper') + .find('[role="slider"]') + .type(generateString(4, 'rightarrow')); + cy.get('.cvat_canvas_interact_intermediate_shape').then((intermediateShape) => { + // Get count of points againe + const intermediateShapeNumberPointsAfterChange = intermediateShape.attr('points').split(' ').length; + // expected 7 to be below 10 + expect(intermediateShapeNumberPointsBeforeChange).to.be.lt(intermediateShapeNumberPointsAfterChange); + }); + }); + cy.get('.cvat-appearance-selected-opacity-slider') + .click('left') + .find('[role="slider"]') + .then((sliderSelectedOpacityLeft) => { + const sliderSelectedOpacityValuenow = sliderSelectedOpacityLeft.attr('aria-valuenow'); + cy.get('.cvat_canvas_interact_intermediate_shape') + .should('have.attr', 'fill-opacity', sliderSelectedOpacityValuenow / 100); + }); + cy.get('.cvat-appearance-selected-opacity-slider') + .click('right') + .find('[role="slider"]') + .then((sliderSelectedOpacityRight) => { + const sliderSelectedOpacityValuenow = sliderSelectedOpacityRight.attr('aria-valuenow'); + cy.get('.cvat_canvas_interact_intermediate_shape') + .should('have.attr', 'fill-opacity', sliderSelectedOpacityValuenow / 100); + }); + cy.get('body').type('{Esc}'); // Cancel drawing + cy.get('.cvat_canvas_interact_intermediate_shape').should('not.exist'); + cy.get('.cvat_canvas_shape').should('have.length', 2); + }); + + it('Check "Intelligent scissors blocking feature". Cancel drawing.', () => { + openOpencvControlPopover(); + cy.get('.cvat-opencv-drawing-tool').click(); + cy.contains('span', 'Block').click(); + cy.get('.cvat_canvas_threshold').should('not.exist'); + pointsMap.forEach((element) => { + cy.get('.cvat-canvas-container').click(element.x, element.y); + }); + cy.get('.cvat_canvas_interact_intermediate_shape').then((intermediateShape) => { + // Get count of points + const intermediateShapeNumberPoints = intermediateShape.attr('points').split(' ').length; + // The last point on the crosshair + expect(intermediateShapeNumberPoints - 1).to.be.equal(pointsMap.length) + }); + cy.get('body').type('{Ctrl}'); // Checking hotkey + cy.get('.cvat_canvas_threshold').should('exist'); + cy.get('body').type('{Esc}'); // Cancel drawing + }); + + it('Check "Histogram Equalization" feature.', () => { + openOpencvControlPopover(); + cy.get('.cvat-opencv-control-popover-visible').contains('[role="tab"]', 'Image').click(); + cy.get('.cvat-opencv-image-tool').click().should('have.class', 'cvat-opencv-image-tool-active').trigger('mouseout'); + cy.get('.cvat-notification-notice-opencv-processing-error').should('not.exist'); + cy.get('.cvat-opencv-image-tool').click().should('not.have.class', 'cvat-opencv-image-tool-active').trigger('mouseout'); + }); + + // Waiting for fix https://github.com/openvinotoolkit/cvat/issues/3474 + it.skip('Redraw the shape created with "Intelligent cissors".', () => { + cy.get('.cvat-canvas-container').click(); + cy.get('.cvat-opencv-control-popover').should('be.hidden'); + cy.get('#cvat_canvas_shape_1') + .trigger('mousemove') + .trigger('mouseover') + .should('have.class', 'cvat_canvas_shape_activated'); + cy.get('body').trigger('keydown', { keyCode: keyCodeN, shiftKey: true }).trigger('keyup'); + cy.get('.cvat-tools-control').should('have.attr', 'tabindex'); + createOpencvShape.pointsMap.forEach((el) => { + cy.get('.cvat-canvas-container') + .click(el.x + 150, el.y + 50) + }); + cy.get('body').trigger('keydown', { keyCode: keyCodeN }).trigger('keyup'); + }); + }); +}); diff --git a/tests/cypress/integration/actions_tasks2/case_23_canvas_grid_feature.js b/tests/cypress/integration/actions_tasks2/case_23_canvas_grid_feature.js index 81cd9cdc..0eb350ae 100644 --- a/tests/cypress/integration/actions_tasks2/case_23_canvas_grid_feature.js +++ b/tests/cypress/integration/actions_tasks2/case_23_canvas_grid_feature.js @@ -5,6 +5,7 @@ /// import { taskName } from '../../support/const'; +import { generateString } from '../../support/utils'; context('Canvas grid feature', () => { const caseId = '23'; @@ -12,14 +13,6 @@ context('Canvas grid feature', () => { const gridColor = 'Black'; const gridOpacity = 80; - function generateString(countPointsToMove) { - let action = ''; - for (let i = 0; i < countPointsToMove; i++) { - action += '{leftarrow}'; - } - return action; - } - before(() => { cy.openTaskJob(taskName); cy.get('.cvat-canvas-image-setups-trigger').click(); @@ -44,7 +37,7 @@ context('Canvas grid feature', () => { }); it('Set "Grid opacity" to 80%.', () => { cy.get('.cvat-image-setups-grid-opacity-input').within(() => { - cy.get('[role="slider"]').type(generateString(20)); // Moving the slider to the left up to 80. + cy.get('[role="slider"]').type(generateString(20, 'leftarrow')); // Moving the slider to the left up to 80. cy.get('[role="slider"]').should('have.attr', 'aria-valuenow', gridOpacity); }); }); diff --git a/tests/cypress/integration/actions_tasks2/case_26_canvas_brightness_contrast_saturation_feature.js b/tests/cypress/integration/actions_tasks2/case_26_canvas_brightness_contrast_saturation_feature.js index ed343710..3beb25a4 100644 --- a/tests/cypress/integration/actions_tasks2/case_26_canvas_brightness_contrast_saturation_feature.js +++ b/tests/cypress/integration/actions_tasks2/case_26_canvas_brightness_contrast_saturation_feature.js @@ -5,6 +5,7 @@ /// import { taskName } from '../../support/const'; +import { generateString } from '../../support/utils'; context('Canvas brightness/contrast/saturation feature', () => { const caseId = '26'; @@ -17,14 +18,6 @@ context('Canvas brightness/contrast/saturation feature', () => { '.cvat-image-setups-saturation', ]; - function generateStringCountAction(countAction) { - let stringAction = ''; - for (let i = 0; i < countAction; i++) { - stringAction += '{rightarrow}'; - } - return stringAction; - } - function checkStateValuesInBackground(expectedValue) { cy.get('#cvat_canvas_background') .should('have.attr', 'style') @@ -41,7 +34,7 @@ context('Canvas brightness/contrast/saturation feature', () => { describe(`Testing case "${caseId}"`, () => { it('Check apply of settings', () => { - let stringAction = generateStringCountAction(countActionMoveSlider); + let stringAction = generateString(countActionMoveSlider, 'rightarrow'); cy.get('.cvat-canvas-image-setups-content').within(() => { cy.wrap(classNameSliders).each(($el) => { cy.wrap($el) diff --git a/tests/cypress/integration/actions_tasks2/case_97_export_import_task.js b/tests/cypress/integration/actions_tasks2/case_97_export_import_task.js index a632a4ce..c558a621 100644 --- a/tests/cypress/integration/actions_tasks2/case_97_export_import_task.js +++ b/tests/cypress/integration/actions_tasks2/case_97_export_import_task.js @@ -67,9 +67,11 @@ context('Export, import an annotation task.', { browser: '!firefox' }, () => { .find('.cvat-item-open-task-actions > .cvat-menu-icon') .trigger('mouseover'); cy.intercept('GET', '/api/v1/tasks/**?action=export').as('exportTask'); - cy.get('.ant-dropdown').not('.ant-dropdown-hidden').within(() => { - cy.contains('[role="menuitem"]', 'Export Task').click().trigger('mouseout'); - }); + cy.get('.ant-dropdown') + .not('.ant-dropdown-hidden') + .within(() => { + cy.contains('[role="menuitem"]', new RegExp('^Export task$')).click().trigger('mouseout'); + }); cy.wait('@exportTask', { timeout: 5000 }).its('response.statusCode').should('equal', 202); cy.wait('@exportTask').its('response.statusCode').should('equal', 201); cy.deleteTask(taskName); @@ -82,10 +84,7 @@ context('Export, import an annotation task.', { browser: '!firefox' }, () => { it('Import the task. Check id, labels, shape.', () => { cy.intercept('POST', '/api/v1/tasks?action=import').as('importTask'); - cy.get('.cvat-import-task') - .click() - .find('input[type=file]') - .attachFile(taskBackupArchiveFullName); + cy.get('.cvat-import-task').click().find('input[type=file]').attachFile(taskBackupArchiveFullName); cy.wait('@importTask', { timeout: 5000 }).its('response.statusCode').should('equal', 202); cy.wait('@importTask').its('response.statusCode').should('equal', 201); cy.contains('Task has been imported succesfully').should('exist').and('be.visible'); diff --git a/tests/cypress/integration/actions_tasks3/case_47_export_dataset.js b/tests/cypress/integration/actions_tasks3/case_47_export_dataset.js index 97fcc1d3..059ed40d 100644 --- a/tests/cypress/integration/actions_tasks3/case_47_export_dataset.js +++ b/tests/cypress/integration/actions_tasks3/case_47_export_dataset.js @@ -6,8 +6,9 @@ import { taskName, labelName } from '../../support/const'; -context('Export as a dataset.', () => { +context('Export task dataset.', () => { const caseId = '47'; + const exportFormat = 'CVAT for images'; const rectangleShape2Points = { points: 'By 2 Points', type: 'Shape', @@ -21,18 +22,39 @@ context('Export as a dataset.', () => { before(() => { cy.openTaskJob(taskName); cy.createRectangle(rectangleShape2Points); - cy.saveJob(); + cy.saveJob('PATCH', 200, 'saveJobExportDataset'); }); describe(`Testing case "${caseId}"`, () => { - it('Go to Menu. Press "Export as a dataset" -> "CVAT for images".', () => { - cy.intercept('GET', '/api/v1/tasks/**/dataset**').as('exportDataset'); - cy.interactMenu('Export as a dataset'); - cy.get('.cvat-menu-export-submenu-item').within(() => { - cy.contains('CVAT for images').click(); + it('Export a task as dataset.', () => { + const exportDataset = { + as: 'exportDataset', + type: 'dataset', + format: exportFormat, + }; + cy.exportTask(exportDataset); + const regex = new RegExp(`^task_${taskName.toLowerCase()}-.*-${exportDataset.format.toLowerCase()}.*.zip$`); + cy.task('listFiles', 'cypress/fixtures').each((fileName) => { + if (fileName.match(regex)) { + cy.readFile(`cypress/fixtures/${fileName}`).should('exist'); + } + }); + }); + + it('Export a task as dataset with renaming the archive.', () => { + const exportDataset = { + as: 'exportDatasetRenameArchive', + type: 'dataset', + format: exportFormat, + archiveCustomeName: 'task_export_dataset_custome_name' + }; + cy.exportTask(exportDataset); + const regex = new RegExp(`^${exportDataset.archiveCustomeName}.zip$`); + cy.task('listFiles', 'cypress/fixtures').each((fileName) => { + if (fileName.match(regex)) { + cy.readFile(`cypress/fixtures/${fileName}`).should('exist'); + } }); - cy.wait('@exportDataset', { timeout: 5000 }).its('response.statusCode').should('equal', 202); - cy.wait('@exportDataset').its('response.statusCode').should('equal', 201); }); }); }); diff --git a/tests/cypress/integration/actions_tasks3/case_90_context_image.js b/tests/cypress/integration/actions_tasks3/case_90_context_image.js index 7171fc2e..02e727a4 100644 --- a/tests/cypress/integration/actions_tasks3/case_90_context_image.js +++ b/tests/cypress/integration/actions_tasks3/case_90_context_image.js @@ -21,7 +21,7 @@ context('Context images for 2D tasks.', () => { secondY: 450, }; - function previewRotate (directionRotation, expectedDeg) { + function previewRotate(directionRotation, expectedDeg) { if (directionRotation === 'right') { cy.get('[data-icon="rotate-right"]').click(); } else { @@ -30,30 +30,22 @@ context('Context images for 2D tasks.', () => { cy.get('.ant-image-preview-img').should('have.attr', 'style').and('contain', `rotate(${expectedDeg}deg)`); } - function previewScaleWheel (zoom, expectedScaleValue) { + function previewScaleWheel(zoom, expectedScaleValue) { cy.get('.ant-image-preview-img') - .trigger('wheel', {deltaY: zoom}) + .trigger('wheel', { deltaY: zoom }) .should('have.attr', 'style') .and('contain', `scale3d(${expectedScaleValue})`); } - function previewScaleButton (zoom, expectedScaleValue) { + function previewScaleButton(zoom, expectedScaleValue) { cy.get(`[data-icon="zoom-${zoom}"]`).click(); - cy.get('.ant-image-preview-img') - .should('have.attr', 'style') - .and('contain', `scale3d(${expectedScaleValue})`); + cy.get('.ant-image-preview-img').should('have.attr', 'style').and('contain', `scale3d(${expectedScaleValue})`); } before(() => { cy.visit('auth/login'); cy.login(); - cy.createAnnotationTask( - taskName, - labelName, - attrName, - textDefaultValue, - pathToArchive, - ); + cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, pathToArchive); cy.openTaskJob(taskName); }); @@ -97,20 +89,22 @@ context('Context images for 2D tasks.', () => { }); it('Preview a context image. Move.', () => { - cy.get('.ant-image-preview-img-wrapper').should('have.attr', 'style').then((translate3d) => { - cy.get('.ant-image-preview-img').trigger('mousedown', {button: 0}); - cy.get('.ant-image-preview-moving').should('exist'); - cy.get('.ant-image-preview-wrap').trigger('mousemove', 300, 300); - cy.get('.ant-image-preview-img-wrapper').should('have.attr', 'style').and('not.equal', translate3d) - cy.get('.ant-image-preview-img').trigger('mouseup'); - cy.get('.ant-image-preview-moving').should('not.exist'); - cy.get('.ant-image-preview-img-wrapper').should('have.attr', 'style').and('equal', translate3d) - }); + cy.get('.ant-image-preview-img-wrapper') + .should('have.attr', 'style') + .then((translate3d) => { + cy.get('.ant-image-preview-img').trigger('mousedown', { button: 0 }); + cy.get('.ant-image-preview-moving').should('exist'); + cy.get('.ant-image-preview-wrap').trigger('mousemove', 300, 300); + cy.get('.ant-image-preview-img-wrapper').should('have.attr', 'style').and('not.equal', translate3d); + cy.get('.ant-image-preview-img').trigger('mouseup'); + cy.get('.ant-image-preview-moving').should('not.exist'); + cy.get('.ant-image-preview-img-wrapper').should('have.attr', 'style').and('equal', translate3d); + }); }); it('Preview a context image. Cancel preview.', () => { cy.get('.ant-image-preview-wrap').type('{Esc}'); - cy.get('.ant-image-preview-wrap').should('have.attr', 'style').and('contain', 'display: none') + cy.get('.ant-image-preview-wrap').should('have.attr', 'style').and('contain', 'display: none'); }); it('Checking issue "Context image disappears after undo/redo".', () => { diff --git a/tests/cypress/integration/canvas3d_functionality/case_85_canvas3d_functionality_cuboid_сancel_drawing.js b/tests/cypress/integration/canvas3d_functionality/case_85_canvas3d_functionality_cuboid_cancel_drawing.js similarity index 84% rename from tests/cypress/integration/canvas3d_functionality/case_85_canvas3d_functionality_cuboid_сancel_drawing.js rename to tests/cypress/integration/canvas3d_functionality/case_85_canvas3d_functionality_cuboid_cancel_drawing.js index 9a3b4f00..59771df7 100644 --- a/tests/cypress/integration/canvas3d_functionality/case_85_canvas3d_functionality_cuboid_сancel_drawing.js +++ b/tests/cypress/integration/canvas3d_functionality/case_85_canvas3d_functionality_cuboid_cancel_drawing.js @@ -8,10 +8,11 @@ import { taskName, labelName } from '../../support/const_canvas3d'; context('Canvas 3D functionality. Cancel drawing.', () => { const caseId = '85'; - const screenshotsPath = 'cypress/screenshots/canvas3d_functionality/case_85_canvas3d_functionality_cuboid_сancel_drawing.js'; + const screenshotsPath = + 'cypress/screenshots/canvas3d_functionality/case_85_canvas3d_functionality_cuboid_cancel_drawing.js'; before(() => { - cy.openTask(taskName) + cy.openTask(taskName); cy.openJob(); cy.wait(1000); // Waiting for the point cloud to display }); @@ -37,7 +38,8 @@ context('Canvas 3D functionality. Cancel drawing.', () => { ); }); - it('Repeat draw.', () => { + // Temporarily disabling the test until it is fixed https://github.com/openvinotoolkit/cvat/issues/3438#issuecomment-892432089 + it.skip('Repeat draw.', () => { cy.get('body').type('n'); cy.get('.cvat-canvas3d-perspective').trigger('mousemove'); cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 450, 250).dblclick(450, 250); diff --git a/tests/cypress/integration/canvas3d_functionality/case_91_canvas3d_functionality_dump_upload_annotation_point_cloud_format.js b/tests/cypress/integration/canvas3d_functionality/case_91_canvas3d_functionality_dump_upload_annotation_point_cloud_format.js index b68e1c87..f894260e 100644 --- a/tests/cypress/integration/canvas3d_functionality/case_91_canvas3d_functionality_dump_upload_annotation_point_cloud_format.js +++ b/tests/cypress/integration/canvas3d_functionality/case_91_canvas3d_functionality_dump_upload_annotation_point_cloud_format.js @@ -13,6 +13,7 @@ context('Canvas 3D functionality. Dump/upload annotation. "Point Cloud" format', }; const dumpTypePC = 'Sly Point Cloud Format'; let annotationPCArchiveName = ''; + let annotationPCArchiveCustomeName = ''; function confirmUpdate(modalWindowClassName) { cy.get(modalWindowClassName).within(() => { @@ -20,62 +21,54 @@ context('Canvas 3D functionality. Dump/upload annotation. "Point Cloud" format', }); } - function uploadToTask(toTaskName) { - cy.contains('.cvat-item-task-name', toTaskName) - .parents('.cvat-tasks-list-item') - .find('.cvat-menu-icon') - .trigger('mouseover'); - cy.contains('Upload annotations').trigger('mouseover'); - cy.readFile('cypress/downloads/' + annotationPCArchiveName, 'binary') - .then(Cypress.Blob.binaryStringToBlob) - .then((fileContent) => { - cy.contains('.cvat-menu-load-submenu-item', dumpTypePC.split(' ')[0]) - .should('be.visible') - .within(() => { - cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({ - fileName: annotationPCArchiveName, - fileContent: fileContent, - }); - }); - }); - } - before(() => { cy.openTask(taskName); cy.openJob(); cy.wait(1000); // Waiting for the point cloud to display cy.create3DCuboid(cuboidCreationParams); + cy.saveJob('PATCH', 200, 'saveJob'); }); describe(`Testing case "${caseId}"`, () => { - it('Save a job. Dump with "Point Cloud" format.', () => { - cy.saveJob('PATCH', 200, 'saveJob'); - cy.intercept('GET', '/api/v1/tasks/**/annotations**').as('dumpAnnotations'); - cy.interactMenu('Dump annotations'); - cy.get('.cvat-menu-dump-submenu-item').then((subMenu) => { - expect(subMenu.length).to.be.equal(2); + it('Export with "Point Cloud" format.', () => { + const exportAnnotation = { + as: 'exportAnnotations', + type: 'annotations', + format: dumpTypePC, + }; + cy.exportTask(exportAnnotation); + const regex = new RegExp(`^task_${taskName.toLowerCase()}-.*-${exportAnnotation.format.toLowerCase()}.*.zip$`); + cy.task('listFiles', 'cypress/fixtures').each((fileName) => { + if (fileName.match(regex)) { + cy.readFile(`cypress/fixtures/${fileName}`).should('exist'); + annotationPCArchiveName = fileName; + } }); - cy.get('.cvat-menu-dump-submenu-item').within(() => { - cy.contains(dumpTypePC).click(); + }); + + it('Export with "Point Cloud" format. Renaming the archive', () => { + const exportAnnotationRenameArchive = { + as: 'exportAnnotationsRenameArchive', + type: 'annotations', + format: dumpTypePC, + archiveCustomeName: 'task_export_3d_annotation_custome_name_pc_format' + }; + cy.exportTask(exportAnnotationRenameArchive); + const regex = new RegExp(`^${exportAnnotationRenameArchive.archiveCustomeName}.zip$`); + cy.task('listFiles', 'cypress/fixtures').each((fileName) => { + if (fileName.match(regex)) { + cy.readFile(`cypress/fixtures/${fileName}`).should('exist'); + annotationPCArchiveCustomeName = fileName; + } }); - cy.wait('@dumpAnnotations', { timeout: 5000 }).its('response.statusCode').should('equal', 202); - cy.wait('@dumpAnnotations').its('response.statusCode').should('equal', 201); cy.removeAnnotations(); cy.saveJob('PUT'); cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist'); - - cy.wait(2000); // Waiting for the full download. - cy.task('listFiles', 'cypress/downloads').each((fileName) => { - if (fileName.includes(dumpTypePC.toLowerCase())) { - annotationPCArchiveName = fileName; - } - }); }); it('Upload "Point Cloud" format annotation to job.', () => { cy.interactMenu('Upload annotations'); - - cy.readFile('cypress/downloads/' + annotationPCArchiveName, 'binary') + cy.readFile('cypress/fixtures/' + annotationPCArchiveName, 'binary') .then(Cypress.Blob.binaryStringToBlob) .then((fileContent) => { cy.contains('.cvat-menu-load-submenu-item', dumpTypePC.split(' ')[0]) @@ -97,11 +90,28 @@ context('Canvas 3D functionality. Dump/upload annotation. "Point Cloud" format', cy.get('#cvat-objects-sidebar-state-item-1').should('exist'); cy.removeAnnotations(); cy.get('button').contains('Save').click({ force: true }); + cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist'); }); - it('Upload annotation to task.', () => { + it('Upload annotation from the archive with a custom name to task.', () => { cy.goToTaskList(); - uploadToTask(taskName); + cy.contains('.cvat-item-task-name', taskName) + .parents('.cvat-tasks-list-item') + .find('.cvat-menu-icon') + .trigger('mouseover'); + cy.contains('Upload annotations').trigger('mouseover'); + cy.readFile('cypress/fixtures/' + annotationPCArchiveCustomeName, 'binary') + .then(Cypress.Blob.binaryStringToBlob) + .then((fileContent) => { + cy.contains('.cvat-menu-load-submenu-item', dumpTypePC.split(' ')[0]) + .should('be.visible') + .within(() => { + cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({ + fileName: annotationPCArchiveCustomeName, + fileContent: fileContent, + }); + }); + }); confirmUpdate('.cvat-modal-content-load-task-annotation'); cy.contains('Annotations have been loaded').should('be.visible'); cy.get('[data-icon="close"]').click(); diff --git a/tests/cypress/integration/canvas3d_functionality/case_92_canvas3d_functionality_dump_upload_annotation_velodyne_points_format.js b/tests/cypress/integration/canvas3d_functionality/case_92_canvas3d_functionality_dump_upload_annotation_velodyne_points_format.js index d8b9676a..6a15db2d 100644 --- a/tests/cypress/integration/canvas3d_functionality/case_92_canvas3d_functionality_dump_upload_annotation_velodyne_points_format.js +++ b/tests/cypress/integration/canvas3d_functionality/case_92_canvas3d_functionality_dump_upload_annotation_velodyne_points_format.js @@ -13,6 +13,7 @@ context('Canvas 3D functionality. Dump/upload annotation. "Velodyne Points" form }; const dumpTypeVC = 'Kitti Raw Format'; let annotationVCArchiveName = ''; + let annotationVCArchiveNameCustomeName = ''; function confirmUpdate(modalWindowClassName) { cy.get(modalWindowClassName).within(() => { @@ -20,58 +21,54 @@ context('Canvas 3D functionality. Dump/upload annotation. "Velodyne Points" form }); } - function uploadToTask(toTaskName) { - cy.contains('.cvat-item-task-name', toTaskName) - .parents('.cvat-tasks-list-item') - .find('.cvat-menu-icon') - .trigger('mouseover'); - cy.contains('Upload annotations').trigger('mouseover'); - cy.readFile('cypress/downloads/' + annotationVCArchiveName, 'binary') - .then(Cypress.Blob.binaryStringToBlob) - .then((fileContent) => { - cy.contains('.cvat-menu-load-submenu-item', dumpTypeVC.split(' ')[0]) - .should('be.visible') - .within(() => { - cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({ - fileContent: fileContent, - fileName: annotationVCArchiveName, - }); - }); - }); - } - before(() => { cy.openTask(taskName); cy.openJob(); cy.wait(1000); // Waiting for the point cloud to display cy.create3DCuboid(cuboidCreationParams); + cy.saveJob('PATCH', 200, 'saveJob'); }); describe(`Testing case "${caseId}"`, () => { - it('Save a job. Dump with "Velodyne Points" format.', () => { - cy.saveJob('PATCH', 200, 'saveJob'); - cy.intercept('GET', '/api/v1/tasks/**/annotations**').as('dumpAnnotations'); - cy.interactMenu('Dump annotations'); - cy.get('.cvat-menu-dump-submenu-item').within(() => { - cy.contains(dumpTypeVC).click(); + it('Export with "Velodyne Points" format.', () => { + const exportAnnotation = { + as: 'exportAnnotations', + type: 'annotations', + format: dumpTypeVC, + }; + cy.exportTask(exportAnnotation); + const regex = new RegExp(`^task_${taskName.toLowerCase()}-.*-${exportAnnotation.format.toLowerCase()}.*.zip$`); + cy.task('listFiles', 'cypress/fixtures').each((fileName) => { + if (fileName.match(regex)) { + cy.readFile(`cypress/fixtures/${fileName}`).should('exist'); + annotationVCArchiveName = fileName; + } }); - cy.wait('@dumpAnnotations', { timeout: 5000 }).its('response.statusCode').should('equal', 202); - cy.wait('@dumpAnnotations').its('response.statusCode').should('equal', 201); - cy.removeAnnotations(); - cy.saveJob('PUT'); - cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist'); + }); - cy.wait(2000); // Waiting for the full download. - cy.task('listFiles', 'cypress/downloads').each((fileName) => { - if (fileName.includes(dumpTypeVC.toLowerCase())) { - annotationVCArchiveName = fileName; + it('Export with "Point Cloud" format. Renaming the archive', () => { + const exportAnnotationRenameArchive = { + as: 'exportAnnotationsRenameArchive', + type: 'annotations', + format: dumpTypeVC, + archiveCustomeName: 'task_export_3d_annotation_custome_name_vc_format' + }; + cy.exportTask(exportAnnotationRenameArchive); + const regex = new RegExp(`^${exportAnnotationRenameArchive.archiveCustomeName}.zip$`); + cy.task('listFiles', 'cypress/fixtures').each((fileName) => { + if (fileName.match(regex)) { + cy.readFile(`cypress/fixtures/${fileName}`).should('exist'); + annotationVCArchiveNameCustomeName = fileName; } }); + cy.removeAnnotations(); + cy.saveJob('PUT'); + cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist'); }); it('Upload "Velodyne Points" format annotation to job.', () => { cy.interactMenu('Upload annotations'); - cy.readFile('cypress/downloads/' + annotationVCArchiveName, 'binary') + cy.readFile('cypress/fixtures/' + annotationVCArchiveName, 'binary') .then(Cypress.Blob.binaryStringToBlob) .then((fileContent) => { cy.contains('.cvat-menu-load-submenu-item', dumpTypeVC.split(' ')[0]) @@ -96,7 +93,23 @@ context('Canvas 3D functionality. Dump/upload annotation. "Velodyne Points" form it('Upload annotation to task.', () => { cy.goToTaskList(); - uploadToTask(taskName); + cy.contains('.cvat-item-task-name', taskName) + .parents('.cvat-tasks-list-item') + .find('.cvat-menu-icon') + .trigger('mouseover'); + cy.contains('Upload annotations').trigger('mouseover'); + cy.readFile('cypress/fixtures/' + annotationVCArchiveNameCustomeName, 'binary') + .then(Cypress.Blob.binaryStringToBlob) + .then((fileContent) => { + cy.contains('.cvat-menu-load-submenu-item', dumpTypeVC.split(' ')[0]) + .should('be.visible') + .within(() => { + cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({ + fileName: annotationVCArchiveNameCustomeName, + fileContent: fileContent, + }); + }); + }); confirmUpdate('.cvat-modal-content-load-task-annotation'); cy.contains('Annotations have been loaded').should('be.visible'); cy.get('[data-icon="close"]').click(); diff --git a/tests/cypress/integration/canvas3d_functionality/case_93_canvas3d_functionality_export_dataset.js b/tests/cypress/integration/canvas3d_functionality/case_93_canvas3d_functionality_export_dataset.js index 7b8b52a1..65ea0add 100644 --- a/tests/cypress/integration/canvas3d_functionality/case_93_canvas3d_functionality_export_dataset.js +++ b/tests/cypress/integration/canvas3d_functionality/case_93_canvas3d_functionality_export_dataset.js @@ -16,7 +16,7 @@ context('Canvas 3D functionality. Export as a dataset.', () => { const dumpTypeVC = 'Kitti Raw Format'; before(() => { - cy.openTask(taskName) + cy.openTask(taskName); cy.openJob(); cy.wait(1000); // Waiting for the point cloud to display cy.create3DCuboid(cuboidCreationParams); @@ -25,23 +25,49 @@ context('Canvas 3D functionality. Export as a dataset.', () => { describe(`Testing case "${caseId}"`, () => { it('Export as a dataset with "Point Cloud" format.', () => { - cy.intercept('GET', '/api/v1/tasks/**/dataset**').as('exportDatasetPC'); - cy.interactMenu('Export as a dataset'); - cy.get('.cvat-menu-export-submenu-item').within(() => { - cy.contains(dumpTypePC).click(); + const exportDatasetPCFormat = { + as: 'exportDatasetPCFormat', + type: 'dataset', + format: dumpTypePC, + }; + cy.exportTask(exportDatasetPCFormat); + const regex = new RegExp(`^task_${taskName.toLowerCase()}-.*-${exportDatasetPCFormat.format.toLowerCase()}.*.zip$`); + cy.task('listFiles', 'cypress/fixtures').each((fileName) => { + if (fileName.match(regex)) { + cy.fixture(fileName).should('exist'); + } }); - cy.wait('@exportDatasetPC', { timeout: 5000 }).its('response.statusCode').should('equal', 202); - cy.wait('@exportDatasetPC').its('response.statusCode').should('equal', 201); }); it('Export as a dataset with "Velodyne Points" format.', () => { - cy.intercept('GET', '/api/v1/tasks/**/dataset**').as('exportDatasetVC'); - cy.interactMenu('Export as a dataset'); - cy.get('.cvat-menu-export-submenu-item').within(() => { - cy.contains(dumpTypeVC).click(); + const exportDatasetVCFormat = { + as: 'exportDatasetVCFormat', + type: 'dataset', + format: dumpTypeVC, + }; + cy.exportTask(exportDatasetVCFormat); + const regex = new RegExp(`^task_${taskName.toLowerCase()}-.*-${exportDatasetVCFormat.format.toLowerCase()}.*.zip$`); + cy.task('listFiles', 'cypress/fixtures').each((fileName) => { + if (fileName.match(regex)) { + cy.fixture(fileName).should('exist'); + } + }); + }); + + it('Export as a dataset with renaming the archive.', () => { + const exportDatasetVCFormatRenameArchive = { + as: 'exportDatasetVCFormatRenameArchive', + type: 'dataset', + format: dumpTypeVC, + archiveCustomeName: 'task_export_3d_dataset_custome_name_vc_format' + }; + cy.exportTask(exportDatasetVCFormatRenameArchive); + const regex = new RegExp(`^${exportDatasetVCFormatRenameArchive.archiveCustomeName}.zip$`); + cy.task('listFiles', 'cypress/fixtures').each((fileName) => { + if (fileName.match(regex)) { + cy.fixture(fileName).should('exist'); + } }); - cy.wait('@exportDatasetVC', { timeout: 5000 }).its('response.statusCode').should('equal', 202); - cy.wait('@exportDatasetVC').its('response.statusCode').should('equal', 201); cy.removeAnnotations(); cy.saveJob('PUT'); }); diff --git a/tests/cypress/integration/issues_prs2/issue_1568_cuboid_dump_annotation.js b/tests/cypress/integration/issues_prs2/issue_1568_cuboid_dump_annotation.js index bfc573b6..7c37c922 100644 --- a/tests/cypress/integration/issues_prs2/issue_1568_cuboid_dump_annotation.js +++ b/tests/cypress/integration/issues_prs2/issue_1568_cuboid_dump_annotation.js @@ -6,7 +6,7 @@ import { taskName, labelName } from '../../support/const'; -context('Dump annotation if cuboid created', () => { +context('Dump annotation if cuboid created.', () => { const issueId = '1568'; const createCuboidShape2Points = { points: 'From rectangle', @@ -17,30 +17,28 @@ context('Dump annotation if cuboid created', () => { secondX: 350, secondY: 450, }; + const exportFormat = 'Datumaro'; before(() => { cy.openTaskJob(taskName); }); describe(`Testing issue "${issueId}"`, () => { - it('Create a cuboid', () => { + it('Create a cuboid.', () => { cy.createCuboid(createCuboidShape2Points); - cy.get('#cvat-objects-sidebar-state-item-1').should('contain', '1').and('contain', 'CUBOID SHAPE'); + cy.saveJob('PATCH', 200, `dump${exportFormat}Format`); }); - it('Dump an annotation', () => { - cy.get('.cvat-annotation-header-left-group').within(() => { - cy.saveJob(); - 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('Dump an annotation.', () => { + const exportAnnotation = { + as: 'exportAnnotations', + type: 'annotations', + format: exportFormat, + }; + cy.exportTask(exportAnnotation); }); - it('Error notification is not exists', () => { - cy.wait(5000); + + it('Error notification is not exists.', () => { cy.get('.ant-notification-notice').should('not.exist'); }); }); diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 474f2358..41281b22 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -101,9 +101,9 @@ Cypress.Commands.add('changeUserActiveStatus', (authKey, accountsToChangeActiveS headers: { Authorization: `Token ${authKey}`, }, - body: { - is_active: isActive, - }, + body: { + is_active: isActive, + }, }); } }); @@ -124,7 +124,6 @@ Cypress.Commands.add('checkUserStatuses', (authKey, userName, staffStatus, super expect(superuserStatus).to.be.equal(user['is_superuser']); expect(activeStatus).to.be.equal(user['is_active']); } - }); }); }); @@ -181,9 +180,7 @@ Cypress.Commands.add( } cy.contains('button', 'Submit').click(); if (expectedResult === 'success') { - cy.get('.cvat-notification-create-task-success') - .should('exist') - .find('[data-icon="close"]').click(); + cy.get('.cvat-notification-create-task-success').should('exist').find('[data-icon="close"]').click(); } if (!forProject) { cy.goToTaskList(); @@ -304,7 +301,6 @@ Cypress.Commands.add('createPoint', (createPointParams) => { selectedValueGlobal = $labelValue.text(); }); if (createPointParams.numberOfPoints) { - createPointParams.complete = false; cy.get('.ant-input-number-input').clear().type(createPointParams.numberOfPoints); } cy.contains('button', createPointParams.type).click(); @@ -312,11 +308,15 @@ Cypress.Commands.add('createPoint', (createPointParams) => { createPointParams.pointsMap.forEach((element) => { cy.get('.cvat-canvas-container').click(element.x, element.y); }); - if (createPointParams.complete) { - const keyCodeN = 78; - cy.get('.cvat-canvas-container') - .trigger('keydown', { keyCode: keyCodeN }) - .trigger('keyup', { keyCode: keyCodeN }); + if (createPointParams.finishWithButton) { + cy.contains('span', 'Done').click(); + } else { + if (! createPointParams.numberOfPoints) { + const keyCodeN = 78; + cy.get('.cvat-canvas-container') + .trigger('keydown', { keyCode: keyCodeN }) + .trigger('keyup', { keyCode: keyCodeN }); + } } cy.checkObjectParameters(createPointParams, 'POINTS'); }); @@ -348,7 +348,6 @@ Cypress.Commands.add('createPolygon', (createPolygonParams) => { selectedValueGlobal = $labelValue.text(); }); if (createPolygonParams.numberOfPoints) { - createPolygonParams.complete = false; cy.get('.ant-input-number-input').clear().type(createPolygonParams.numberOfPoints); } cy.contains('button', createPolygonParams.type).click(); @@ -357,11 +356,15 @@ Cypress.Commands.add('createPolygon', (createPolygonParams) => { createPolygonParams.pointsMap.forEach((element) => { cy.get('.cvat-canvas-container').click(element.x, element.y); }); - if (createPolygonParams.complete) { - const keyCodeN = 78; - cy.get('.cvat-canvas-container') - .trigger('keydown', { keyCode: keyCodeN }) - .trigger('keyup', { keyCode: keyCodeN }); + if (createPolygonParams.finishWithButton) { + cy.contains('span', 'Done').click(); + } else { + if (! createPolygonParams.numberOfPoints) { + const keyCodeN = 78; + cy.get('.cvat-canvas-container') + .trigger('keydown', { keyCode: keyCodeN }) + .trigger('keyup', { keyCode: keyCodeN }); + } } cy.checkObjectParameters(createPolygonParams, 'POLYGON'); }); @@ -490,7 +493,6 @@ Cypress.Commands.add('createPolyline', (createPolylineParams) => { selectedValueGlobal = $labelValue.text(); }); if (createPolylineParams.numberOfPoints) { - createPolylineParams.complete = false; cy.get('.ant-input-number-input').clear().type(createPolylineParams.numberOfPoints); } cy.contains('button', createPolylineParams.type).click(); @@ -498,11 +500,15 @@ Cypress.Commands.add('createPolyline', (createPolylineParams) => { createPolylineParams.pointsMap.forEach((element) => { cy.get('.cvat-canvas-container').click(element.x, element.y); }); - if (createPolylineParams.complete) { - const keyCodeN = 78; - cy.get('.cvat-canvas-container') - .trigger('keydown', { keyCode: keyCodeN }) - .trigger('keyup', { keyCode: keyCodeN }); + if (createPolylineParams.finishWithButton) { + cy.contains('span', 'Done').click(); + } else { + if (! createPolylineParams.numberOfPoints) { + const keyCodeN = 78; + cy.get('.cvat-canvas-container') + .trigger('keydown', { keyCode: keyCodeN }) + .trigger('keyup', { keyCode: keyCodeN }); + } } cy.checkObjectParameters(createPolylineParams, 'POLYLINE'); }); @@ -708,3 +714,23 @@ Cypress.Commands.add('closeModalUnsupportedPlatform', () => { }); } }); + +Cypress.Commands.add('exportTask', ({ as, type, format, archiveCustomeName }) => { + cy.interactMenu('Export task dataset'); + cy.intercept('GET', `/api/v1/tasks/**/${type}**`).as(as); + cy.get('.cvat-modal-export-task').find('.cvat-modal-export-select').click(); + cy.contains('.cvat-modal-export-option-item', format).click(); + cy.get('.cvat-modal-export-task').find('.cvat-modal-export-select').should('contain.text', format); + if (type === 'dataset') { + cy.get('.cvat-modal-export-task').find('[type="checkbox"]').should('not.be.checked').check(); + } + if (archiveCustomeName) { + cy.get('.cvat-modal-export-task').find('.cvat-modal-export-filename-input').type(archiveCustomeName); + } + cy.contains('button', 'OK').click(); + cy.get('.cvat-notification-notice-export-task-start').should('be.visible'); + cy.closeNotification('.cvat-notification-notice-export-task-start'); + cy.wait(`@${as}`, { timeout: 5000 }).its('response.statusCode').should('equal', 202); + cy.wait(`@${as}`).its('response.statusCode').should('equal', 201); + cy.wait(2000) // Waiting for a full file download +}); diff --git a/tests/cypress/support/commands_opencv.js b/tests/cypress/support/commands_opencv.js new file mode 100644 index 00000000..8378c574 --- /dev/null +++ b/tests/cypress/support/commands_opencv.js @@ -0,0 +1,51 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +/// + +let selectedValueGlobal = ''; + +Cypress.Commands.add('opencvCreateShape', (opencvShapeParams) => { + if (!opencvShapeParams.reDraw) { + cy.get('body').focus(); + cy.get('.cvat-tools-control').trigger('mouseleave').trigger('mouseout').trigger('mouseover'); + cy.switchLabel(opencvShapeParams.labelName, 'opencv-control'); + cy.get('.cvat-opencv-control-popover-visible').within(() => { + cy.get('.ant-select-selection-item').then(($labelValue) => { + selectedValueGlobal = $labelValue.text(); + }); + }); + cy.get('.cvat-opencv-drawing-tool').click(); + } + opencvShapeParams.pointsMap.forEach((element) => { + cy.get('.cvat-canvas-container').click(element.x, element.y); + }); + if (opencvShapeParams.finishWithButton) { + cy.contains('span', 'Done').click(); + } else { + const keyCodeN = 78; + cy.get('.cvat-canvas-container') + .trigger('keydown', { keyCode: keyCodeN }) + .trigger('keyup', { keyCode: keyCodeN }); + } + cy.opncvCheckObjectParameters('POLYGON'); +}); + +Cypress.Commands.add('opncvCheckObjectParameters', (objectType) => { + 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('be.visible'); + cy.get(`#cvat-objects-sidebar-state-item-${maxId}`) + .should('contain', maxId) + .and('contain', objectType) + .within(() => { + cy.get('.ant-select-selection-item').should('have.text', selectedValueGlobal); + }); + }); +}); diff --git a/tests/cypress/support/commands_projects.js b/tests/cypress/support/commands_projects.js index b4ab2429..bd12a61b 100644 --- a/tests/cypress/support/commands_projects.js +++ b/tests/cypress/support/commands_projects.js @@ -29,9 +29,7 @@ Cypress.Commands.add( cy.contains('Submit').click(); }); if (expectedResult == 'success') { - cy.get('.cvat-notification-create-project-success') - .should('exist') - .find('[data-icon="close"]').click(); + cy.get('.cvat-notification-create-project-success').should('exist').find('[data-icon="close"]').click(); } else if (expectedResult == 'fail') { cy.get('.cvat-notification-create-project-success').should('not.exist'); } @@ -44,7 +42,7 @@ Cypress.Commands.add('openProject', (projectName) => { cy.get('.cvat-project-details').should('exist'); }); -Cypress.Commands.add('deleteProject', (projectName, projectID, expectedResult = 'success') => { +Cypress.Commands.add('projectActions', (projectName) => { cy.contains('.cvat-projects-project-item-title', projectName) .parents('.cvat-projects-project-item-card') .within(() => { @@ -52,6 +50,10 @@ Cypress.Commands.add('deleteProject', (projectName, projectID, expectedResult = cy.get('[type="button"]').trigger('mouseover'); }); }); +}); + +Cypress.Commands.add('deleteProject', (projectName, projectID, expectedResult = 'success') => { + cy.projectActions(projectName); cy.get('.cvat-project-actions-menu').contains('Delete').click(); cy.get('.cvat-modal-confirm-remove-project') .should('contain', `The project ${projectID} will be deleted`) @@ -61,8 +63,27 @@ Cypress.Commands.add('deleteProject', (projectName, projectID, expectedResult = if (expectedResult === 'success') { cy.get('.cvat-projects-project-item-card').should('have.css', 'opacity', '0.5'); } else if (expectedResult === 'fail') { - cy.get('.cvat-projects-project-item-card').should('not.have.attr', 'style'); + cy.get('.cvat-projects-project-item-card').should('not.have.css', 'opacity', '0.5'); + } +}); + +Cypress.Commands.add('exportProject', ({ projectName, as, type, dumpType, archiveCustomeName }) => { + cy.projectActions(projectName); + cy.intercept('GET', `/api/v1/projects/**/${type}**`).as(as); + cy.get('.cvat-project-actions-menu').contains('Export project dataset').click(); + cy.get('.cvat-modal-export-project').find('.cvat-modal-export-select').click(); + cy.contains('.cvat-modal-export-option-item', dumpType).click(); + cy.get('.cvat-modal-export-select').should('contain.text', dumpType); + if (type === 'dataset') { + cy.get('.cvat-modal-export-project').find('[type="checkbox"]').should('not.be.checked').check(); + } + if (archiveCustomeName) { + cy.get('.cvat-modal-export-project').find('.cvat-modal-export-filename-input').type(archiveCustomeName); } + cy.get('.cvat-modal-export-project').contains('button', 'OK').click(); + cy.get('.cvat-notification-notice-export-project-start').should('be.visible'); + cy.wait(`@${as}`, { timeout: 5000 }).its('response.statusCode').should('equal', 202); + cy.wait(`@${as}`).its('response.statusCode').should('equal', 201); }); Cypress.Commands.add('deleteProjectViaActions', (projectName) => { @@ -96,19 +117,21 @@ Cypress.Commands.add('closeNotification', (className) => { Cypress.Commands.add('movingTask', (taskName, projectName, labelMappingFrom, labelMappingTo, fromTask) => { if (fromTask) { cy.contains('.cvat-text-color', 'Actions').click(); - cy.get('.ant-dropdown').not('.ant-dropdown-hidden').within(() => { - cy.contains('Move to project').click(); - }); } else { cy.contains('strong', taskName).parents('.cvat-tasks-list-item').find('.cvat-menu-icon').click(); - cy.get('.ant-dropdown').not('.ant-dropdown-hidden').within(() => { - cy.contains('Move to project').click(); - }); } + cy.get('.cvat-actions-menu') + .should('be.visible') + .find('[role="menuitem"]') + .filter(':contains("Move to project")') + .last() + .click(); cy.get('.cvat-task-move-modal').find('.cvat-project-search-field').click(); - cy.get('.ant-select-dropdown').not('.ant-select-dropdown-hidden').within(() => { - cy.get(`[title="${projectName}"]`).click(); - }); + cy.get('.ant-select-dropdown') + .not('.ant-select-dropdown-hidden') + .within(() => { + cy.get(`[title="${projectName}"]`).click(); + }); if (labelMappingFrom !== labelMappingTo) { cy.get('.cvat-move-task-label-mapper-item').within(() => { cy.contains(labelMappingFrom).should('exist'); @@ -122,5 +145,5 @@ Cypress.Commands.add('movingTask', (taskName, projectName, labelMappingFrom, lab } cy.get('.cvat-task-move-modal').within(() => { cy.contains('button', 'OK').click(); - }) + }); }); diff --git a/tests/cypress/support/index.js b/tests/cypress/support/index.js index 5c8880df..c146c98e 100644 --- a/tests/cypress/support/index.js +++ b/tests/cypress/support/index.js @@ -8,6 +8,7 @@ require('./commands_review_pipeline'); require('./commands_canvas3d'); require('./commands_filters_feature'); require('./commands_models'); +require('./commands_opencv'); require('@cypress/code-coverage/support'); require('cypress-plugin-tab'); diff --git a/tests/cypress/support/utils.js b/tests/cypress/support/utils.js new file mode 100644 index 00000000..975d61e2 --- /dev/null +++ b/tests/cypress/support/utils.js @@ -0,0 +1,11 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +export function generateString(countPointsToMove, arrow) { + let action = ''; + for (let i = 0; i < countPointsToMove; i++) { + action += `{${arrow}}`; + } + return action; +} diff --git a/tests/cypress_canvas3d.json b/tests/cypress_canvas3d.json index 06a37637..d965f768 100644 --- a/tests/cypress_canvas3d.json +++ b/tests/cypress_canvas3d.json @@ -4,6 +4,7 @@ "viewportWidth": 1300, "viewportHeight": 960, "defaultCommandTimeout": 25000, + "downloadsFolder": "cypress/fixtures", "env": { "user": "admin", "email": "admin@localhost.company", diff --git a/utils/cli/core/core.py b/utils/cli/core/core.py index 3de53809..839dd9b3 100644 --- a/utils/cli/core/core.py +++ b/utils/cli/core/core.py @@ -41,6 +41,10 @@ class CLI(): data['image_quality'] = kwargs.get('image_quality') if 'frame_step' in kwargs: data['frame_filter'] = f"step={kwargs.get('frame_step')}" + if 'copy_data' in kwargs: + data['copy_data'] = kwargs.get('copy_data') + if 'use_cache' in kwargs: + data['use_cache'] = kwargs.get('use_cache') response = self.session.post(url, data=data, files=files) response.raise_for_status() diff --git a/utils/cli/core/definition.py b/utils/cli/core/definition.py index bab3cacb..fbd10808 100644 --- a/utils/cli/core/definition.py +++ b/utils/cli/core/definition.py @@ -113,7 +113,7 @@ task_create_parser.add_argument( help='string or file containing JSON labels specification' ) task_create_parser.add_argument( - '--project', + '--project_id', default=None, type=int, help='project ID if project exists' @@ -195,6 +195,19 @@ task_create_parser.add_argument( help='''set the frame step option in the advanced configuration when uploading image series or videos (default: %(default)s)''' ) +task_create_parser.add_argument( + '--copy_data', + default=False, + action='store_true', + help='''set the option to copy the data, only used when resource type is + share (default: %(default)s)''' +) +task_create_parser.add_argument( + '--use_cache', + default=True, + action='store_false', + help='''set the option to use the cache (default: %(default)s)''' +) ####################################################################### # Delete ####################################################################### diff --git a/utils/dataset_manifest/core.py b/utils/dataset_manifest/core.py index b357daf9..02d09925 100644 --- a/utils/dataset_manifest/core.py +++ b/utils/dataset_manifest/core.py @@ -223,6 +223,9 @@ class _Index: self._index = json.load(index_file, object_hook=lambda d: {int(k): v for k, v in d.items()}) + def remove(self): + os.remove(self._path) + def create(self, manifest, skip): assert os.path.exists(manifest), 'A manifest file not exists, index cannot be created' with open(manifest, 'r+') as manifest_file: @@ -265,6 +268,7 @@ class _ManifestManager(ABC): } def __init__(self, path, *args, **kwargs): self._manifest = _Manifest(path) + self._index = _Index(os.path.dirname(self._manifest.path)) def _parse_line(self, line): """ Getting a random line from the manifest file """ @@ -283,13 +287,20 @@ class _ManifestManager(ABC): return json.loads(properties) def init_index(self): - self._index = _Index(os.path.dirname(self._manifest.path)) if os.path.exists(self._index.path): self._index.load() else: self._index.create(self._manifest.path, 3 if self._manifest.TYPE == 'video' else 2) self._index.dump() + def reset_index(self): + if os.path.exists(self._index.path): + self._index.remove() + + def set_index(self): + self.reset_index() + self.init_index() + @abstractmethod def create(self, content, **kwargs): pass @@ -331,6 +342,10 @@ class _ManifestManager(ABC): def data(self): pass + @abstractmethod + def get_subset(self, subset_names): + pass + class VideoManifestManager(_ManifestManager): def __init__(self, manifest_path): super().__init__(manifest_path) @@ -394,7 +409,10 @@ class VideoManifestManager(_ManifestManager): @property def data(self): - return [self.video_name] + return (self.video_name) + + def get_subset(self, subset_names): + raise NotImplementedError() #TODO: add generic manifest structure file validation class ManifestValidator: @@ -476,4 +494,14 @@ class ImageManifestManager(_ManifestManager): @property def data(self): - return [f"{image['name']}{image['extension']}" for _, image in self] \ No newline at end of file + return (f"{image['name']}{image['extension']}" for _, image in self) + + def get_subset(self, subset_names): + return ({ + 'name': f"{image['name']}", + 'extension': f"{image['extension']}", + 'width': image['width'], + 'height': image['height'], + 'meta': image['meta'], + 'checksum': f"{image['checksum']}" + } for _, image in self if f"{image['name']}{image['extension']}" in subset_names) \ No newline at end of file