Merge branch 'release-1.2.0'

main
Nikita Manovich 5 years ago
commit 37d82f9005

@ -1,5 +1,5 @@
exclude_paths:
- '**/3rdparty/**'
- '**/engine/js/cvat-core.min.js'
- '**/engine/js/unzip_imgs.js'
- CHANGELOG.md
- '**/3rdparty/**'
- '**/engine/js/cvat-core.min.js'
- '**/engine/js/unzip_imgs.js'
- CHANGELOG.md

@ -3,13 +3,10 @@ branch = true
# relative_files = true # does not work?
source =
datumaro/datumaro/
cvat/apps/
utils/cli/
omit =
datumaro/datumaro/__main__.py
datumaro/datumaro/version.py
cvat/settings/*
*/tests/*
*/test_*

@ -0,0 +1,10 @@
.*/
3rdparty/
node_modules/
dist/
data/
datumaro/
keys/
logs/
static/
templates/

@ -1,53 +1,23 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
// Copyright (C) 2018-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
module.exports = {
"env": {
"node": false,
"browser": true,
"es6": true,
"jquery": true,
"qunit": true,
module.exports = {
env: {
node: true,
browser: true,
es6: true,
},
"parserOptions": {
"sourceType": "script",
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
},
"plugins": [
"security",
"no-unsanitized",
"no-unsafe-innerhtml",
],
"extends": [
"eslint:recommended",
"plugin:security/recommended",
"plugin:no-unsanitized/DOM",
"airbnb-base",
],
"rules": {
"no-await-in-loop": [0],
"global-require": [0],
"no-new": [0],
"class-methods-use-this": [0],
"no-restricted-properties": [0, {
"object": "Math",
"property": "pow",
}],
"no-param-reassign": [0],
"no-underscore-dangle": ["error", { "allowAfterThis": true }],
"no-restricted-syntax": [0, {"selector": "ForOfStatement"}],
"no-continue": [0],
"no-unsafe-innerhtml/no-unsafe-innerhtml": 1,
// This rule actual for user input data on the node.js environment mainly.
"security/detect-object-injection": 0,
"indent": ["warn", 4],
// recently added to airbnb
"max-classes-per-file": [0],
// it was opposite before and our code has been written according to previous rule
"arrow-parens": [0],
// object spread is a modern ECMA standard. Let's do not use it without babel
"prefer-object-spread": [0],
plugins: ['eslint-plugin-header'],
extends: ['eslint:recommended', 'prettier'],
rules: {
'header/header': [2, 'line', [{
pattern: ' {1}Copyright \\(C\\) (?:20\\d{2}-)?2020 Intel Corporation',
template: ' Copyright (C) 2020 Intel Corporation'
}, '', ' SPDX-License-Identifier: MIT']],
},
};

4
.gitignore vendored

@ -30,3 +30,7 @@ yarn-debug.log*
yarn-error.log*
.DS_Store
#Ignore Cypress tests temp files
/tests/cypress/fixtures
/tests/cypress/screenshots

@ -0,0 +1,17 @@
{
"all": true,
"compact": false,
"extension": [
".js",
".jsx",
".ts",
".tsx"
],
"exclude": [
"**/3rdparty/*",
"**/tests/*"
],
"parser-plugins": [
"typescript"
]
}

@ -0,0 +1,10 @@
.*/
3rdparty/
node_modules/
dist/
data/
datumaro/
keys/
logs/
static/
templates/

@ -0,0 +1,27 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": true,
"printWidth": 120,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false,
"overrides": [
{
"files": ["*.json", "*.yml", "*.yaml", "*.md"],
"options": {
"tabWidth": 2
}
}
]
}

@ -1,16 +1,16 @@
exports.settings = {bullet: '*', paddedTable: false}
exports.settings = { bullet: '*', paddedTable: false };
exports.plugins = [
'remark-preset-lint-recommended',
'remark-preset-lint-consistent',
['remark-preset-lint-markdown-style-guide', 'mixed'],
['remark-lint-no-dead-urls', { skipOffline: true }],
['remark-lint-maximum-line-length', 120],
['remark-lint-maximum-heading-length', 120],
['remark-lint-strong-marker', "*"],
['remark-lint-emphasis-marker', "_"],
['remark-lint-unordered-list-marker-style', "-"],
['remark-lint-ordered-list-marker-style', "."],
['remark-lint-no-file-name-irregular-characters', false],
['remark-lint-list-item-spacing', false],
]
'remark-preset-lint-recommended',
'remark-preset-lint-consistent',
['remark-preset-lint-markdown-style-guide', 'mixed'],
['remark-lint-no-dead-urls', { skipOffline: true }],
['remark-lint-maximum-line-length', 120],
['remark-lint-maximum-heading-length', 120],
['remark-lint-strong-marker', '*'],
['remark-lint-emphasis-marker', '_'],
['remark-lint-unordered-list-marker-style', '-'],
['remark-lint-ordered-list-marker-style', '.'],
['remark-lint-no-file-name-irregular-characters', false],
['remark-lint-list-item-spacing', false],
];

@ -1,20 +1,22 @@
{
"extends": "stylelint-config-standard",
"rules": {
"indentation": 4,
"value-keyword-case": null,
"selector-combinator-space-after": null,
"no-descending-specificity": null,
"at-rule-no-unknown": [true, {
"ignoreAtRules": ["extend"]
}],
"selector-type-no-unknown": [true, {
"ignoreTypes": ["first-child"]
}]
},
"ignoreFiles": [
"**/*.js",
"**/*.ts",
"**/*.py"
"extends": "stylelint-config-standard",
"rules": {
"indentation": 4,
"value-keyword-case": null,
"selector-combinator-space-after": null,
"no-descending-specificity": null,
"at-rule-no-unknown": [
true,
{
"ignoreAtRules": ["extend"]
}
],
"selector-type-no-unknown": [
true,
{
"ignoreTypes": ["first-child"]
}
]
},
"ignoreFiles": ["**/*.js", "**/*.ts", "**/*.py"]
}

@ -1,9 +1,5 @@
sudo: required
language: python
python:
- "3.5"
language: generic
dist: focal
cache:
npm: true
@ -11,9 +7,11 @@ cache:
- ~/.cache
addons:
firefox: 'latest'
chrome: stable
apt:
packages:
- libgconf-2-4
- libgconf-2-4
services:
- docker
@ -25,25 +23,34 @@ env:
DJANGO_SU_EMAIL="admin@localhost.company"
DJANGO_SU_PASSWORD="12qwaszx"
NODE_VERSION="12"
API_ABOUT_PAGE="localhost:8080/api/v1/server/about"
before_install:
- nvm install ${NODE_VERSION}
before_script:
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml build
- chmod a+rwx ${HOST_COVERAGE_DATA_DIR}
script:
# FIXME: Git package and application name conflict in PATH and try to leave only one python test execution
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'coverage run -a manage.py test cvat/apps && coverage run -a manage.py test --pattern="_test*.py" cvat/apps/dataset_manager/tests cvat/apps/engine/tests utils/cli && coverage run -a manage.py test datumaro/ && mv .coverage ${CONTAINER_COVERAGE_DATA_DIR}'
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-data && npm install && cd ../cvat-core && npm install && npm run test && coveralls-lcov -v -n ./reports/coverage/lcov.info > ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json'
# Up all containers
- if [[ $TRAVIS_EVENT_TYPE == "cron" && $TRAVIS_BRANCH == "develop" ]];
then
docker-compose -f docker-compose.yml -f ./tests/docker-compose.email.yml up -d --build;
bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done';
docker exec -it cvat bash -ic "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell";
cd ./tests && npm install && npm run cypress:run:firefox; exit $?;
fi;
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml build
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'coverage run -a manage.py test cvat/apps utils/cli && mv .coverage ${CONTAINER_COVERAGE_DATA_DIR}'
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-data && npm install && cd ../cvat-core && npm install && npm run test && mv ./reports/coverage/lcov.info ${CONTAINER_COVERAGE_DATA_DIR} && chmod a+rwx ${CONTAINER_COVERAGE_DATA_DIR}/lcov.info'
- docker-compose up -d
# Create superuser
- docker exec -it cvat bash -ic "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
# Install Cypress and run tests
- cd ./tests && npm install
- $(npm bin)/cypress run --headless --browser chrome && cd ..
# End-to-end testing
- npm install && npm run coverage
- docker-compose up -d --build
- cd ./tests && npm install && npx cypress run --headless --browser chrome
- mv ./.nyc_output ../ && cd ..
- npx nyc report --reporter=text-lcov >> ${HOST_COVERAGE_DATA_DIR}/lcov.info
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd ${CONTAINER_COVERAGE_DATA_DIR} && coveralls-lcov -v -n lcov.info > ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json'
after_success:
# https://coveralls-python.readthedocs.io/en/latest/usage/multilang.html

@ -21,6 +21,21 @@
},
"smartStep": true,
},
{
"type": "node",
"request": "launch",
"name": "ui.js: test",
"cwd": "${workspaceRoot}/tests",
"runtimeExecutable": "${workspaceRoot}/tests/node_modules/.bin/cypress",
"args": [
"run",
"--headless",
"--browser",
"chrome"
],
"outputCapture": "std",
"console": "internalConsole"
},
{
"name": "server: django",
"type": "python",

@ -1 +0,0 @@
PYTHONPATH="datumaro/:$PYTHONPATH"

@ -1,37 +1,26 @@
{
"python.pythonPath": ".env/bin/python",
"eslint.enable": true,
"eslint.validate": [
"eslint.probe": [
"javascript",
"typescript",
"typescriptreact",
"typescriptreact"
],
"eslint.onIgnoredFiles": "warn",
"eslint.workingDirectories": [
{
"directory": "./cvat-core",
"changeProcessCWD": true
"directory": "${cwd}",
},
{
"directory": "./cvat-canvas",
"changeProcessCWD": true
"pattern": "cvat-*"
},
{
"directory": "./cvat-ui",
"changeProcessCWD": true
},
{
"directory": ".",
"changeProcessCWD": true
"directory": "tests",
"!cwd": true
}
],
"python.linting.pylintEnabled": true,
"python.envFile": "${workspaceFolder}/.vscode/python.env",
"python.testing.unittestEnabled": true,
"python.testing.unittestArgs": [
"-v",
"-s",
"./datumaro",
],
"licenser.license": "Custom",
"licenser.customHeader": "Copyright (C) @YEAR@ Intel Corporation\n\nSPDX-License-Identifier: MIT",
"files.trimTrailingWhitespace": true

@ -1,11 +1,127 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.2.0] - 2020-01-08
### Fixed
- Memory consumption for the task creation process (<https://github.com/openvinotoolkit/cvat/pull/2582>)
- Frame preloading (<https://github.com/openvinotoolkit/cvat/pull/2608>)
- Project cannot be removed from the project page (<https://github.com/openvinotoolkit/cvat/pull/2626>)
## [1.2.0-beta] - 2020-12-15
### Added
- GPU support and improved documentation for auto annotation (<https://github.com/openvinotoolkit/cvat/pull/2546>)
- Manual review pipeline: issues/comments/workspace (<https://github.com/openvinotoolkit/cvat/pull/2357>)
- Basic projects implementation (<https://github.com/openvinotoolkit/cvat/pull/2255>)
- Documentation on how to mount cloud starage(AWS S3 bucket, Azure container, Google Drive) as FUSE (<https://github.com/openvinotoolkit/cvat/pull/2377>)
- Ability to work with share files without copying inside (<https://github.com/openvinotoolkit/cvat/pull/2377>)
- Tooltips in label selectors (<https://github.com/openvinotoolkit/cvat/pull/2509>)
- Page redirect after login using `next` query parameter (<https://github.com/openvinotoolkit/cvat/pull/2527>)
- [ImageNet](http://www.image-net.org) format support (<https://github.com/openvinotoolkit/cvat/pull/2376>)
- [CamVid](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) format support (<https://github.com/openvinotoolkit/cvat/pull/2559>)
### Changed
- PATCH requests from cvat-core submit only changed fields (<https://github.com/openvinotoolkit/cvat/pull/2445>)
- deploy.sh in serverless folder is seperated into deploy_cpu.sh and deploy_gpu.sh (<https://github.com/openvinotoolkit/cvat/pull/2546>)
- Bumped nuclio version to 1.5.8
- Migrated to Antd 4.9 (<https://github.com/openvinotoolkit/cvat/pull/2536>)
### Fixed
- Fixed FastRCNN inference bug for images with 4 channels i.e. png (<https://github.com/openvinotoolkit/cvat/pull/2546>)
- Django templates for email and user guide (<https://github.com/openvinotoolkit/cvat/pull/2412>)
- Saving relative paths in dummy chunks instead of absolute (<https://github.com/openvinotoolkit/cvat/pull/2424>)
- Objects with a specific label cannot be displayed if at least one tag with the label exist (<https://github.com/openvinotoolkit/cvat/pull/2435>)
- Wrong attribute can be removed in labels editor (<https://github.com/openvinotoolkit/cvat/pull/2436>)
- UI fails with the error "Cannot read property 'label' of undefined" (<https://github.com/openvinotoolkit/cvat/pull/2442>)
- Exception: "Value must be a user instance" (<https://github.com/openvinotoolkit/cvat/pull/2441>)
- Reset zoom option doesn't work in tag annotation mode (<https://github.com/openvinotoolkit/cvat/pull/2443>)
- Canvas is busy error (<https://github.com/openvinotoolkit/cvat/pull/2437>)
- Projects view layout fix (<https://github.com/openvinotoolkit/cvat/pull/2503>)
- Fixed the tasks view (infinite loading) when it is impossible to get a preview of the task (<https://github.com/openvinotoolkit/cvat/pull/2504>)
- Empty frames navigation (<https://github.com/openvinotoolkit/cvat/pull/2505>)
- TypeError: Cannot read property 'toString' of undefined (<https://github.com/openvinotoolkit/cvat/pull/2517>)
- Extra shapes are drawn after Esc, or G pressed while drawing a region in grouping (<https://github.com/openvinotoolkit/cvat/pull/2507>)
- Reset state (reviews, issues) after logout or changing a job (<https://github.com/openvinotoolkit/cvat/pull/2525>)
- TypeError: Cannot read property 'id' of undefined when updating a task (<https://github.com/openvinotoolkit/cvat/pull/2544>)
## [1.2.0-alpha] - 2020-11-09
### Added
- Ability to login into CVAT-UI with token from api/v1/auth/login (<https://github.com/openvinotoolkit/cvat/pull/2234>)
- Added layout grids toggling ('ctrl + alt + Enter')
- Added password reset functionality (<https://github.com/opencv/cvat/pull/2058>)
- Ability to work with data on the fly (https://github.com/opencv/cvat/pull/2007)
- Annotation in process outline color wheel (<https://github.com/opencv/cvat/pull/2084>)
- On the fly annotation using DL detectors (<https://github.com/opencv/cvat/pull/2102>)
- Displaying automatic annotation progress on a task view (<https://github.com/opencv/cvat/pull/2148>)
- Automatic tracking of bounding boxes using serverless functions (<https://github.com/opencv/cvat/pull/2136>)
- [Datumaro] CLI command for dataset equality comparison (<https://github.com/opencv/cvat/pull/1989>)
- [Datumaro] Merging of datasets with different labels (<https://github.com/opencv/cvat/pull/2098>)
- Add FBRS interactive segmentation serverless function (<https://github.com/openvinotoolkit/cvat/pull/2094>)
- Ability to change default behaviour of previous/next buttons of a player.
It supports regular navigation, searching a frame according to annotations
filters and searching the nearest frame without any annotations (<https://github.com/openvinotoolkit/cvat/pull/2221>)
- MacOS users notes in CONTRIBUTING.md
- Ability to prepare meta information manually (<https://github.com/openvinotoolkit/cvat/pull/2217>)
- Ability to upload prepared meta information along with a video when creating a task (<https://github.com/openvinotoolkit/cvat/pull/2217>)
- Optional chaining plugin for cvat-canvas and cvat-ui (<https://github.com/openvinotoolkit/cvat/pull/2249>)
- MOTS png mask format support (<https://github.com/openvinotoolkit/cvat/pull/2198>)
- Ability to correct upload video with a rotation record in the metadata (<https://github.com/openvinotoolkit/cvat/pull/2218>)
- User search field for assignee fields (<https://github.com/openvinotoolkit/cvat/pull/2370>)
- Support of mxf videos (<https://github.com/openvinotoolkit/cvat/pull/2514>)
### Changed
- UI models (like DEXTR) were redesigned to be more interactive (<https://github.com/opencv/cvat/pull/2054>)
- Used Ubuntu:20.04 as a base image for CVAT Dockerfile (<https://github.com/opencv/cvat/pull/2101>)
- Right colors of label tags in label mapping when a user runs automatic detection (<https://github.com/openvinotoolkit/cvat/pull/2162>)
- Nuclio became an optional component of CVAT (<https://github.com/openvinotoolkit/cvat/pull/2192>)
- A key to remove a point from a polyshape [Ctrl => Alt] (<https://github.com/openvinotoolkit/cvat/pull/2204>)
- Updated `docker-compose` file version from `2.3` to `3.3`(<https://github.com/openvinotoolkit/cvat/pull/2235>)
- Added auto inference of url schema from host in CLI, if provided (<https://github.com/openvinotoolkit/cvat/pull/2240>)
- Track frames in skips between annotation is presented in MOT and MOTS formats are marked `outside` (<https://github.com/openvinotoolkit/cvat/pull/2198>)
- UI packages installation with `npm ci` instead of `npm install` (<https://github.com/openvinotoolkit/cvat/pull/2350>)
### Removed
- Removed Z-Order flag from task creation process
### Fixed
- Fixed multiple errors which arises when polygon is of length 5 or less (<https://github.com/opencv/cvat/pull/2100>)
- Fixed task creation from PDF (<https://github.com/opencv/cvat/pull/2141>)
- Fixed CVAT format import for frame stepped tasks (<https://github.com/openvinotoolkit/cvat/pull/2151>)
- Fixed the reading problem with large PDFs (<https://github.com/openvinotoolkit/cvat/pull/2154>)
- Fixed unnecessary pyhash dependency (<https://github.com/openvinotoolkit/cvat/pull/2170>)
- Fixed Data is not getting cleared, even after deleting the Task from Django Admin App(<https://github.com/openvinotoolkit/cvat/issues/1925>)
- Fixed blinking message: "Some tasks have not been showed because they do not have any data" (<https://github.com/openvinotoolkit/cvat/pull/2200>)
- Fixed case when a task with 0 jobs is shown as "Completed" in UI (<https://github.com/openvinotoolkit/cvat/pull/2200>)
- Fixed use case when UI throws exception: Cannot read property 'objectType' of undefined #2053 (<https://github.com/openvinotoolkit/cvat/pull/2203>)
- Fixed use case when logs could be saved twice or more times #2202 (<https://github.com/openvinotoolkit/cvat/pull/2203>)
- Fixed issues from #2112 (<https://github.com/openvinotoolkit/cvat/pull/2217>)
- Git application name (renamed to dataset_repo) (<https://github.com/openvinotoolkit/cvat/pull/2243>)
- A problem in exporting of tracks, where tracks could be truncated (<https://github.com/openvinotoolkit/cvat/issues/2129>)
- Fixed CVAT startup process if the user has `umask 077` in .bashrc file (<https://github.com/openvinotoolkit/cvat/pull/2293>)
- Exception: Cannot read property "each" of undefined after drawing a single point (<https://github.com/openvinotoolkit/cvat/pull/2307>)
- Cannot read property 'label' of undefined (Fixed?) (<https://github.com/openvinotoolkit/cvat/pull/2311>)
- Excluded track frames marked `outside` in `CVAT for Images` export (<https://github.com/openvinotoolkit/cvat/pull/2345>)
- 'List of tasks' Kibana visualization (<https://github.com/openvinotoolkit/cvat/pull/2361>)
- An error on exporting not `jpg` or `png` images in TF Detection API format (<https://github.com/openvinotoolkit/datumaro/issues/35>)
## [1.1.0] - 2020-08-31
### Added
- Siammask tracker as DL serverless function (<https://github.com/opencv/cvat/pull/1988>)
- [Datumaro] Added model info and source info commands (<https://github.com/opencv/cvat/pull/1973>)
- [Datumaro] Dataset statistics (<https://github.com/opencv/cvat/pull/1668>)
@ -16,18 +132,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Notification message when users use wrong browser (<https://github.com/opencv/cvat/pull/2070>)
### Changed
- Shape coordinates are rounded to 2 digits in dumped annotations (<https://github.com/opencv/cvat/pull/1970>)
- COCO format does not produce polygon points for bbox annotations (<https://github.com/opencv/cvat/pull/1953>)
### Fixed
- Issue loading openvino models for semi-automatic and automatic annotation (<https://github.com/opencv/cvat/pull/1996>)
- Basic functions of CVAT works without activated nuclio dashboard
- Fixed a case in which exported masks could have wrong color order (<https://github.com/opencv/cvat/issues/2032>)
- Fixed error with creating task with labels with the same name (<https://github.com/opencv/cvat/pull/2031>)
- Django RQ dashboard view (<https://github.com/opencv/cvat/pull/2069>)
- Object's details menu settings (<https://github.com/opencv/cvat/pull/2084>)
## [1.1.0-beta] - 2020-08-03
### Added
- DL models as serverless functions (<https://github.com/opencv/cvat/pull/1767>)
- Source type support for tags, shapes and tracks (<https://github.com/opencv/cvat/pull/1192>)
- Source type support for CVAT Dumper/Loader (<https://github.com/opencv/cvat/pull/1192>)
@ -38,16 +159,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ability to change user password (<https://github.com/opencv/cvat/pull/1954>)
### Changed
- Smaller object details (<https://github.com/opencv/cvat/pull/1877>)
- `COCO` format does not convert bboxes to polygons on export (<https://github.com/opencv/cvat/pull/1953>)
- It is impossible to submit a DL model in OpenVINO format using UI. Now you can deploy new models on the server using serverless functions (<https://github.com/opencv/cvat/pull/1767>)
- Files and folders under share path are now alphabetically sorted
### Removed
- Removed OpenVINO and CUDA components because they are not necessary anymore (<https://github.com/opencv/cvat/pull/1767>)
- Removed the old UI code (<https://github.com/opencv/cvat/pull/1964>)
### Fixed
- Some objects aren't shown on canvas sometimes. For example after propagation on of objects is invisible (<https://github.com/opencv/cvat/pull/1834>)
- CVAT doesn't offer to restore state after an error (<https://github.com/opencv/cvat/pull/1874>)
- Cannot read property 'shapeType' of undefined because of zOrder related issues (<https://github.com/opencv/cvat/pull/1874>)
@ -68,7 +192,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Increase rate of throttling policy for unauthenticated users (<https://github.com/opencv/cvat/pull/1969>)
## [1.1.0-alpha] - 2020-06-30
### Added
- Throttling policy for unauthenticated users (<https://github.com/opencv/cvat/pull/1531>)
- Added default label color table for mask export (<https://github.com/opencv/cvat/pull/1549>)
- Added environment variables for Redis and Postgres hosts for Kubernetes deployment support (<https://github.com/opencv/cvat/pull/1641>)
@ -96,6 +222,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [Datumaro] Added image copying when exporting datasets, if possible (<https://github.com/opencv/cvat/pull/1799>)
### Changed
- Removed information about e-mail from the basic user information (<https://github.com/opencv/cvat/pull/1627>)
- Update https install manual. Makes it easier and more robust. Includes automatic renewing of lets encrypt certificates.
- Settings page move to the modal. (<https://github.com/opencv/cvat/pull/1705>)
@ -107,6 +234,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [Datumaro] Annotation-less files are not generated anymore in COCO format, unless tasks explicitly requested (<https://github.com/opencv/cvat/pull/1799>)
### Fixed
- Problem with exported frame stepped image task (<https://github.com/opencv/cvat/issues/1613>)
- Fixed dataset filter item representation for imageless dataset items (<https://github.com/opencv/cvat/pull/1593>)
- Fixed interpreter crash when trying to import `tensorflow` with no AVX instructions available (<https://github.com/opencv/cvat/pull/1567>)
@ -127,10 +255,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Error when interpolating polygons (<https://github.com/opencv/cvat/pull/1878>)
### Security
- SQL injection in Django `CVE-2020-9402` (<https://github.com/opencv/cvat/pull/1657>)
## [1.0.0] - 2020-05-29
### Added
- cvat-ui: cookie policy drawer for login page (<https://github.com/opencv/cvat/pull/1511>)
- `datumaro_project` export format (<https://github.com/opencv/cvat/pull/1352>)
- Ability to configure user agreements for the user registration form (<https://github.com/opencv/cvat/pull/1464>)
@ -139,6 +270,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ability to configure access to the analytics page based on roles (<https://github.com/opencv/cvat/pull/1592>)
### Changed
- Downloaded file name in annotations export became more informative (<https://github.com/opencv/cvat/pull/1352>)
- Added auto trimming for trailing whitespaces style enforcement (<https://github.com/opencv/cvat/pull/1352>)
- REST API: updated `GET /task/<id>/annotations`: parameters are `format`, `filename` (now optional), `action` (optional) (<https://github.com/opencv/cvat/pull/1352>)
@ -152,10 +284,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Images without annotations now also included in dataset/annotations export (<https://github.com/opencv/cvat/issues/525>)
### Removed
- `annotation` application is replaced with `dataset_manager` (<https://github.com/opencv/cvat/pull/1352>)
- `_DATUMARO_INIT_LOGLEVEL` env. variable is removed in favor of regular `--loglevel` cli parameter (<https://github.com/opencv/cvat/pull/1583>)
### Fixed
- Categories for empty projects with no sources are taken from own dataset (<https://github.com/opencv/cvat/pull/1352>)
- Added directory removal on error during `extract` command (<https://github.com/opencv/cvat/pull/1352>)
- Added debug error message on incorrect XPath (<https://github.com/opencv/cvat/pull/1352>)
@ -173,7 +307,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed COCO keypoints skeleton parsing and saving (<https://github.com/opencv/cvat/issues/1539>)
- `tf.placeholder() is not compatible with eager execution` exception for auto_segmentation (<https://github.com/opencv/cvat/pull/1562>)
- Canvas cannot be moved with move functionality on left mouse key (<https://github.com/opencv/cvat/pull/1573>)
- Deep extreme cut request is sent when draw any shape with Make AI polygon option enabled (<https://github.com/opencv/cvat/pull/1573>)
- Deep extreme cut request is sent when draw any shape with Make AI polygon option enabled (<https://github.com/opencv/cvat/pull/1573>)
- Fixed an error when exporting a task with cuboids to any format except CVAT (<https://github.com/opencv/cvat/pull/1577>)
- Synchronization with remote git repo (<https://github.com/opencv/cvat/pull/1582>)
- A problem with mask to polygons conversion when polygons are too small (<https://github.com/opencv/cvat/pull/1581>)
@ -181,19 +315,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed an issue with `z_order` having no effect on segmentations (<https://github.com/opencv/cvat/pull/1589>)
### Security
- Permission group whitelist check for analytics view (<https://github.com/opencv/cvat/pull/1608>)
## [1.0.0-beta.2] - 2020-04-30
### Added
- Re-Identification algorithm to merging bounding boxes automatically to the new UI (<https://github.com/opencv/cvat/pull/1406>)
- Methods ``import`` and ``export`` to import/export raw annotations for Job and Task in ``cvat-core`` (<https://github.com/opencv/cvat/pull/1406>)
- Versioning of client packages (``cvat-core``, ``cvat-canvas``, ``cvat-ui``). Initial versions are set to 1.0.0 (<https://github.com/opencv/cvat/pull/1448>)
- Methods `import` and `export` to import/export raw annotations for Job and Task in `cvat-core` (<https://github.com/opencv/cvat/pull/1406>)
- Versioning of client packages (`cvat-core`, `cvat-canvas`, `cvat-ui`). Initial versions are set to 1.0.0 (<https://github.com/opencv/cvat/pull/1448>)
- Cuboids feature was migrated from old UI to new one. (<https://github.com/opencv/cvat/pull/1451>)
### Removed
- Annotation convertation utils, currently supported natively via Datumaro framework (https://github.com/opencv/cvat/pull/1477)
### Fixed
- Auto annotation, TF annotation and Auto segmentation apps (https://github.com/opencv/cvat/pull/1409)
- Import works with truncated images now: "OSError:broken data stream" on corrupt images (https://github.com/opencv/cvat/pull/1430)
- Hide functionality (H) doesn't work (<https://github.com/opencv/cvat/pull/1445>)
@ -210,8 +349,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Open task button doesn't work (https://github.com/opencv/cvat/pull/1474)
## [1.0.0-beta.1] - 2020-04-15
### Added
- Special behaviour for attribute value ``__undefined__`` (invisibility, no shortcuts to be set in AAM)
- Special behaviour for attribute value `__undefined__` (invisibility, no shortcuts to be set in AAM)
- Dialog window with some helpful information about using filters
- Ability to display a bitmap in the new UI
- Button to reset colors settings (brightness, saturation, contrast) in the new UI
@ -223,13 +364,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Deep extreme cut (semi-automatic segmentation) to the new UI (https://github.com/opencv/cvat/pull/1398)
### Changed
- Increase preview size of a task till 256, 256 on the server
- Public ssh-keys are displayed in a dedicated window instead of console when create a task with a repository
- React UI is the primary UI
### Fixed
- Cleaned up memory in Auto Annotation to enable long running tasks on videos
- New shape is added when press ``esc`` when drawing instead of cancellation
- New shape is added when press `esc` when drawing instead of cancellation
- Dextr segmentation doesn't work.
- `FileNotFoundError` during dump after moving format files
- CVAT doesn't append outside shapes when merge polyshapes in old UI
@ -254,23 +397,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Uploading annotations for tasks with multiple jobs (https://github.com/opencv/cvat/pull/1396)
## [1.0.0-alpha] - 2020-03-31
### Added
- Data streaming using chunks (https://github.com/opencv/cvat/pull/1007)
- New UI: showing file names in UI (https://github.com/opencv/cvat/pull/1311)
- New UI: delete a point from context menu (https://github.com/opencv/cvat/pull/1292)
### Fixed
- Git app cannot clone a repository (https://github.com/opencv/cvat/pull/1330)
- New UI: preview position in task details (https://github.com/opencv/cvat/pull/1312)
- AWS deployment (https://github.com/opencv/cvat/pull/1316)
## [0.6.1] - 2020-03-21
### Changed
- VOC task export now does not use official label map by default, but takes one
from the source task to avoid primary-class and class part name
clashing ([#1275](https://github.com/opencv/cvat/issues/1275))
### Fixed
- File names in LabelMe format export are no longer truncated ([#1259](https://github.com/opencv/cvat/issues/1259))
- `occluded` and `z_order` annotation attributes are now correctly passed to Datumaro ([#1271](https://github.com/opencv/cvat/pull/1271))
- Annotation-less tasks now can be exported as empty datasets in COCO ([#1277](https://github.com/opencv/cvat/issues/1277))
@ -278,11 +427,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
allowed `frame_XXXXXX[.ext]` format ([#1274](https://github.com/opencv/cvat/pull/1274))
### Security
- Bump acorn from 6.3.0 to 6.4.1 in /cvat-ui ([#1270](https://github.com/opencv/cvat/pull/1270))
## [0.6.0] - 2020-03-15
### Added
- Server only support for projects. Extend REST API v1 (/api/v1/projects*)
- Server only support for projects. Extend REST API v1 (/api/v1/projects\*)
- Ability to get basic information about users without admin permissions ([#750](https://github.com/opencv/cvat/issues/750))
- Changed REST API: removed PUT and added DELETE methods for /api/v1/users/ID
- Mask-RCNN Auto Annotation Script in OpenVINO format
@ -299,6 +451,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Git repositories can be specified with IPv4 address ([#827](https://github.com/opencv/cvat/pull/827))
### Changed
- page_size parameter for all REST API methods
- React & Redux & Antd based dashboard
- Yolov3 interpretation script fix and changes to mapping.json
@ -306,6 +459,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added support for OpenVINO 2020
### Fixed
- Exception in Git plugin [#826](https://github.com/opencv/cvat/issues/826)
- Label ids in TFrecord format now start from 1 [#866](https://github.com/opencv/cvat/issues/866)
- Mask problem in COCO JSON style [#718](https://github.com/opencv/cvat/issues/718)
@ -314,15 +468,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Annotations can be filtered before dumping with Datumaro [#994](https://github.com/opencv/cvat/issues/994)
## [0.5.2] - 2019-12-15
### Fixed
- Frozen version of scikit-image==0.15 in requirements.txt because next releases don't support Python 3.5
## [0.5.1] - 2019-10-17
### Added
- Integration with Zenodo.org (DOI)
## [0.5.0] - 2019-09-12
### Added
- A converter to YOLO format
- Installation guide
- Linear interpolation for a single point
@ -341,13 +501,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added command line tool for performing common task operations (/utils/cli/)
### Changed
- Outside and keyframe buttons in the side panel for all interpolation shapes (they were only for boxes before)
- Improved error messages on the client side (#511)
### Removed
- "Flip images" has been removed. UI now contains rotation features.
### Fixed
- Incorrect width of shapes borders in some cases
- Annotation parser for tracks with a start frame less than the first segment frame
- Interpolation on the server near outside frames
@ -365,43 +528,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Creating a video task with 0 overlap
### Security
- Upgraded Django, djangorestframework, and other packages
## [0.4.2] - 2019-06-03
### Fixed
- Fixed interaction with the server share in the auto annotation plugin
## [0.4.1] - 2019-05-14
### Fixed
- JavaScript syntax incompatibility with Google Chrome versions less than 72
## [0.4.0] - 2019-05-04
### Added
- OpenVINO auto annotation: it is possible to upload a custom model and annotate images automatically.
- Ability to rotate images/video in the client part (Ctrl+R, Shift+Ctrl+R shortcuts) (#305)
- The ReID application for automatic bounding box merging has been added (#299)
- Keyboard shortcuts to switch next/previous default shape type (box, polygon etc) [Alt + <, Alt + >] (#316)
- Converter for VOC now supports interpolation tracks
- REST API (/api/v1/*, /api/docs)
- REST API (/api/v1/\*, /api/docs)
- Semi-automatic semantic segmentation with the [Deep Extreme Cut](http://www.vision.ee.ethz.ch/~cvlsegmentation/dextr/) work
### Changed
- Propagation setup has been moved from settings to bottom player panel
- Additional events like "Debug Info" or "Fit Image" have been added for analitics
- Optional using LFS for git annotation storages (#314)
### Deprecated
- "Flip images" flag in the create task dialog will be removed. Rotation functionality in client part have been added instead.
### Removed
-
### Fixed
- Django 2.1.5 (security fix, https://nvd.nist.gov/vuln/detail/CVE-2019-3498)
- Several scenarious which cause code 400 after undo/redo/save have been fixed (#315)
## [0.3.0] - 2018-12-29
### Added
- Ability to copy Object URL and Frame URL via object context menu and player context menu respectively.
- Ability to change opacity for selected shape with help "Selected Fill Opacity" slider.
- Ability to remove polyshapes points by double click.
@ -420,6 +596,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Buttons lock/hide for labels. They work for all objects with the same label on a current frame (#116)
### Changed
- Polyshape editing method has been improved. You can redraw part of shape instead of points cloning.
- Unified shortcut (Esc) for close any mode instead of different shortcuts (Alt+N, Alt+G, Alt+M etc.).
- Dump file contains information about data source (e.g. video name, archive name, ...)
@ -430,6 +607,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Drawing has become more convenience. Now it is possible to draw outside an image. Shapes will be automatically truncated after drawing process (#202)
### Fixed
- Performance bottleneck has been fixed during you create new objects (draw, copy, merge etc).
- Label UI elements aren't updated after changelabel.
- Attribute annotation mode can use invalid shape position after resize or move shapes.
@ -441,7 +619,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Text drawing outside of a frame in some cases (#202)
## [0.2.0] - 2018-09-28
### Added
- New annotation shapes: polygons, polylines, points
- Undo/redo feature
- Grid to estimate size of objects
@ -459,42 +639,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Full screen view
### Changed
- Documentation, screencasts, the primary screenshot
- Content-type for save_job request is application/json
### Fixed
- Player navigation if the browser's window is scrolled
- Filter doesn't support dash (-)
- Several memory leaks
- Inconsistent extensions between filenames in an annotation file and real filenames
## [0.1.2] - 2018-08-07
### Added
- 7z archive support when creating a task
- .vscode/launch.json file for developing with VS code
### Fixed
- #14: docker-compose down command as written in the readme does not remove volumes
- #15: all checkboxes in temporary attributes are checked when reopening job after saving the job
- #18: extend CONTRIBUTING.md
- #19: using the same attribute for label twice -> stuck
### Changed
- More strict verification for labels with attributes
## [0.1.1] - 2018-07-6
### Added
- Links on a screenshot, documentation, screencasts into README.md
- CONTRIBUTORS.md
### Fixed
- GitHub documentation
## 0.1.0 - 2018-06-29
### Added
- Initial version
## Template
```
## [Unreleased]
### Added

@ -10,78 +10,95 @@ patches and features.
## Development environment
Next steps should work on clear Ubuntu 18.04.
- Install necessary dependencies:
- Install necessary dependencies:
```sh
sudo apt-get update && sudo apt-get --no-install-recommends install -y ffmpeg build-essential curl redis-server python3-dev python3-pip python3-venv python3-tk libldap2-dev libsasl2-dev
```
Also please make sure that you have installed ffmpeg with all necessary libav* libraries and pkg-config package.
```sh
# Node and npm (you can use default versions of these packages from apt (8.*, 3.*), but we would recommend to use newer versions)
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs
# General dependencies
sudo apt-get install -y pkg-config
# Library components
sudo apt-get install -y \
libavformat-dev libavcodec-dev libavdevice-dev \
libavutil-dev libswscale-dev libswresample-dev libavfilter-dev
```
See [PyAV Dependencies installation guide](http://docs.mikeboers.com/pyav/develop/overview/installation.html#dependencies)
for details.
- Install [Visual Studio Code](https://code.visualstudio.com/docs/setup/linux#_debian-and-ubuntu-based-distributions)
for development
- Install CVAT on your local host:
```sh
git clone https://github.com/opencv/cvat
cd cvat && mkdir logs keys
python3 -m venv .env
. .env/bin/activate
pip install -U pip wheel setuptools
pip install -r cvat/requirements/development.txt
pip install -r datumaro/requirements.txt
python manage.py migrate
python manage.py collectstatic
```
Ubuntu 18.04
- Create a super user for CVAT:
```sh
$ python manage.py createsuperuser
Username (leave blank to use 'django'): ***
Email address: ***
Password: ***
Password (again): ***
```
```sh
sudo apt-get update && sudo apt-get --no-install-recommends install -y build-essential curl redis-server python3-dev python3-pip python3-venv python3-tk libldap2-dev libsasl2-dev pkg-config libavformat-dev libavcodec-dev libavdevice-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev
```
- Install npm packages for UI and start UI debug server (run the following command from CVAT root directory):
```sh
npm install && \
cd cvat-core && npm install && \
cd ../cvat-ui && npm install && npm start
```
```sh
# Node and npm (you can use default versions of these packages from apt (8.*, 3.*), but we would recommend to use newer versions)
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs
```
- Open new terminal (Ctrl + Shift + T), run Visual Studio Code from the virtual environment
```sh
cd .. && source .env/bin/activate && code
```
MacOS 10.15
```sh
brew install git python pyenv redis curl openssl node
```
- Install FFmpeg libraries (libav\*) version 4.0 or higher.
- Install [Visual Studio Code](https://code.visualstudio.com/docs/setup/linux#_debian-and-ubuntu-based-distributions)
for development
- Install CVAT on your local host:
```sh
git clone https://github.com/opencv/cvat
cd cvat && mkdir logs keys
python3 -m venv .env
. .env/bin/activate
pip install -U pip wheel setuptools
pip install -r cvat/requirements/development.txt
python manage.py migrate
python manage.py collectstatic
```
> Note for Mac users
>
> If you have any problems with installing dependencies from
> `cvat/requirements/*.txt`, you may need to reinstall your system python
> In some cases after system update it can be configured incorrectly and cannot compile some native modules
- Create a super user for CVAT:
```sh
$ python manage.py createsuperuser
Username (leave blank to use 'django'): ***
Email address: ***
Password: ***
Password (again): ***
```
- Install following VS Code extensions:
- [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
- [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
- [Stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint)
- [vscode-remark-lint](https://marketplace.visualstudio.com/items?itemName=drewbourne.vscode-remark-lint)
- [licenser](https://marketplace.visualstudio.com/items?itemName=ymotongpoo.licenser)
- [Trailing Spaces](https://marketplace.visualstudio.com/items?itemName=shardulm94.trailing-spaces)
- Install npm packages for UI and start UI debug server (run the following command from CVAT root directory):
- Reload Visual Studio Code from virtual environment
```sh
npm ci && \
cd cvat-core && npm ci && \
cd ../cvat-ui && npm ci && npm start
```
- Select `server: debug` configuration and start it (F5) to run REST server and its workers
> Note for Mac users
>
> If you faced with error
>
> `Node Sass does not yet support your current environment: OS X 64-bit with Unsupported runtime (57)`
>
> Read this article [Node Sass does not yet support your current environment](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
- Open new terminal (Ctrl + Shift + T), run Visual Studio Code from the virtual environment
```sh
cd .. && source .env/bin/activate && code
```
- Install following VS Code extensions:
- [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
- [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
- [Stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint)
- [vscode-remark-lint](https://marketplace.visualstudio.com/items?itemName=drewbourne.vscode-remark-lint)
- [licenser](https://marketplace.visualstudio.com/items?itemName=ymotongpoo.licenser)
- [Trailing Spaces](https://marketplace.visualstudio.com/items?itemName=shardulm94.trailing-spaces)
- Reload Visual Studio Code from virtual environment
- Select `server: debug` configuration and start it (F5) to run REST server and its workers
You have done! Now it is possible to insert breakpoints and debug server and client of the tool.
@ -89,30 +106,32 @@ You have done! Now it is possible to insert breakpoints and debug server and cli
You develop CVAT under WSL (Windows subsystem for Linux) following next steps.
- Install WSL using [this guide](https://docs.microsoft.com/ru-ru/windows/wsl/install-win10).
- Install WSL using [this guide](https://docs.microsoft.com/ru-ru/windows/wsl/install-win10).
- Following this guide install Ubuntu 18.04 Linux distribution for WSL.
- Following this guide install Ubuntu 18.04 Linux distribution for WSL.
- Run Ubuntu using start menu link or execute next command
```powershell
wsl -d Ubuntu-18.04
```
- Run Ubuntu using start menu link or execute next command
```powershell
wsl -d Ubuntu-18.04
```
- Run all commands from this isntallation guide in WSL Ubuntu shell.
- Run all commands from this isntallation guide in WSL Ubuntu shell.
## Setup additional components in development environment
### DL models as serverless functions
Install [nuclio platform](https://github.com/nuclio/nuclio):
- You have to install `nuctl` command line tool to build and deploy serverless
functions. Download [the latest release](
https://github.com/nuclio/nuclio/blob/development/docs/reference/nuctl/nuctl.md#download).
functions. Download [the latest release](https://github.com/nuclio/nuclio/blob/development/docs/reference/nuctl/nuctl.md#download).
- The simplest way to explore Nuclio is to run its graphical user interface (GUI)
of the Nuclio dashboard. All you need in order to run the dashboard is Docker. See
[nuclio documentation](https://github.com/nuclio/nuclio#quick-start-steps)
for more details.
of the Nuclio dashboard. All you need in order to run the dashboard is Docker. See
[nuclio documentation](https://github.com/nuclio/nuclio#quick-start-steps)
for more details.
- Create `cvat` project inside nuclio dashboard where you will deploy new
serverless functions and deploy a couple of DL models.
serverless functions and deploy a couple of DL models.
```bash
nuctl create project cvat
@ -175,7 +194,7 @@ nuctl deploy --project-name cvat \
</details>
- Display a list of running serverless functions using `nuctl` command or see them
in nuclio dashboard:
in nuclio dashboard:
```bash
nuctl get function
@ -192,7 +211,7 @@ nuctl get function
</details>
- Test your deployed DL model as a serverless function. The command below
should work on Linux and Mac OS.
should work on Linux and Mac OS.
```bash
image=$(curl https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png --output - | base64 | tr -d '\n')
@ -233,6 +252,7 @@ Server = nuclio
]
```
</details>
### Run Cypress tests
- Install Сypress as described in the [documentation](https://docs.cypress.io/guides/getting-started/installing-cypress.html).
- Run cypress tests:
@ -252,12 +272,12 @@ little exception - we prefer 4 spaces for indentation of nested blocks and state
The project uses [a successful Git branching model](https://nvie.com/posts/a-successful-git-branching-model).
Thus it has a couple of branches. Some of them are described below:
- `origin/master` to be the main branch where the source code of
HEAD always reflects a production-ready state
- `origin/master` to be the main branch where the source code of
HEAD always reflects a production-ready state
- `origin/develop` to be the main branch where the source code of
HEAD always reflects a state with the latest delivered development
changes for the next release. Some would call this the “integration branch”.
- `origin/develop` to be the main branch where the source code of
HEAD always reflects a state with the latest delivered development
changes for the next release. Some would call this the “integration branch”.
## Using the issue tracker
@ -265,13 +285,14 @@ The issue tracker is the preferred channel for [bug reports](#bugs),
[features requests](#features) and [submitting pull
requests](#pull-requests), but please respect the following restrictions:
- Please **do not** use the issue tracker for personal support requests (use
- Please **do not** use the issue tracker for personal support requests (use
[Stack Overflow](http://stackoverflow.com)).
- Please **do not** derail or troll issues. Keep the discussion on topic and
- Please **do not** derail or troll issues. Keep the discussion on topic and
respect the opinions of others.
<a name="bugs"></a>
## Bug reports
A bug is a _demonstrable problem_ that is caused by the code in the repository.
@ -280,10 +301,10 @@ Good bug reports are extremely helpful - thank you!
Guidelines for bug reports:
1. **Use the GitHub issue search** &mdash; check if the issue has already been
reported.
reported.
1. **Check if the issue has been fixed** &mdash; try to reproduce it using the
latest `develop` branch in the repository.
latest `develop` branch in the repository.
1. **Isolate the problem** &mdash; ideally create a reduced test case.
@ -310,6 +331,7 @@ Example:
> merits).
<a name="features"></a>
## Feature requests
Feature requests are welcome. But take a moment to find out whether your idea
@ -318,6 +340,7 @@ case to convince the project's developers of the merits of this feature. Please
provide as much detail and context as possible.
<a name="pull-requests"></a>
## Pull requests
Good pull requests - patches, improvements, new features - are a fantastic
@ -362,10 +385,10 @@ project:
```
1. Commit your changes in logical chunks. Please adhere to these [git commit
message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
or your code is unlikely be merged into the main project. Use Git's
[interactive rebase](https://docs.github.com/en/github/using-git/about-git-rebase)
feature to tidy up your commits before making them public.
message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
or your code is unlikely be merged into the main project. Use Git's
[interactive rebase](https://docs.github.com/en/github/using-git/about-git-rebase)
feature to tidy up your commits before making them public.
1. Locally merge (or rebase) the upstream development branch into your topic branch:

@ -1,4 +1,53 @@
FROM ubuntu:16.04
FROM ubuntu:20.04 as build-image
ARG http_proxy
ARG https_proxy
ARG no_proxy
ARG socks_proxy
ARG DJANGO_CONFIGURATION
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
apache2-dev \
build-essential \
curl \
libldap2-dev \
libsasl2-dev \
nasm \
git \
pkg-config \
python3-dev \
python3-pip \
python3-venv && \
rm -rf /var/lib/apt/lists/*
# Compile Openh264 and FFmpeg
ARG PREFIX=/opt/ffmpeg
ARG PKG_CONFIG_PATH=${PREFIX}/lib/pkgconfig
ENV FFMPEG_VERSION=4.3.1 \
OPENH264_VERSION=2.1.1
WORKDIR /tmp/openh264
RUN curl -sL https://github.com/cisco/openh264/archive/v${OPENH264_VERSION}.tar.gz --output openh264-${OPENH264_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f openh264-${OPENH264_VERSION}.tar.gz && \
make -j5 && make install PREFIX=${PREFIX} && make clean
WORKDIR /tmp/ffmpeg
RUN curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
./configure --disable-nonfree --disable-gpl --enable-libopenh264 --enable-shared --disable-static --prefix="${PREFIX}" && \
make -j5 && make install && make distclean
# Install requirements
RUN python3 -m venv /opt/venv
ENV PATH="/opt/venv/bin:${PATH}"
RUN python3 -m pip install --no-cache-dir -U pip==20.0.1 setuptools==49.6.0 wheel==0.35.1
COPY cvat/requirements/ /tmp/requirements/
RUN DATUMARO_HEADLESS=1 python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt
FROM ubuntu:20.04
ARG http_proxy
ARG https_proxy
@ -21,68 +70,26 @@ ENV DJANGO_CONFIGURATION=${DJANGO_CONFIGURATION}
# Install necessary apt packages
RUN apt-get update && \
apt-get --no-install-recommends install -yq \
software-properties-common && \
add-apt-repository ppa:mc3man/xerus-media -y && \
add-apt-repository ppa:mc3man/gstffmpeg-keep -y && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
apache2 \
apache2-dev \
apt-utils \
build-essential \
libapache2-mod-xsendfile \
supervisor \
ffmpeg \
gstreamer0.10-ffmpeg \
libavcodec-dev \
libavdevice-dev \
libavfilter-dev \
libavformat-dev \
libavutil-dev \
libswresample-dev \
libswscale-dev \
libldap2-dev \
libsasl2-dev \
pkg-config \
python3-dev \
python3-pip \
libldap-2.4-2 \
libsasl2-2 \
libpython3-dev \
tzdata \
python3-distutils \
p7zip-full \
git \
ssh \
git-lfs \
poppler-utils \
ssh \
curl && \
curl https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \
apt-get --no-install-recommends install -y git-lfs && git lfs install && \
python3 -m pip install --no-cache-dir -U pip==20.0.1 setuptools==49.6.0 wheel==0.35.1 && \
ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \
add-apt-repository --remove ppa:mc3man/gstffmpeg-keep -y && \
add-apt-repository --remove ppa:mc3man/xerus-media -y && \
rm -rf /var/lib/apt/lists/* && \
echo 'application/wasm wasm' >> /etc/mime.types
# Add a non-root user
ENV USER=${USER}
ENV HOME /home/${USER}
WORKDIR ${HOME}
RUN adduser --shell /bin/bash --disabled-password --gecos "" ${USER} && \
if [ -z ${socks_proxy} ]; then \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30\"" >> ${HOME}/.bashrc; \
else \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ProxyCommand='nc -X 5 -x ${socks_proxy} %h %p'\"" >> ${HOME}/.bashrc; \
fi
COPY components /tmp/components
# Install and initialize CVAT, copy all necessary files
COPY cvat/requirements/ /tmp/requirements/
COPY supervisord.conf mod_wsgi.conf wait-for-it.sh manage.py ${HOME}/
RUN python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt
# pycocotools package is impossible to install with its dependencies by one pip install command
RUN python3 -m pip install --no-cache-dir pycocotools==2.0.0
ARG CLAM_AV
ENV CLAM_AV=${CLAM_AV}
RUN if [ "$CLAM_AV" = "yes" ]; then \
@ -96,23 +103,52 @@ RUN if [ "$CLAM_AV" = "yes" ]; then \
rm -rf /var/lib/apt/lists/*; \
fi
COPY ssh ${HOME}/.ssh
COPY utils ${HOME}/utils
COPY cvat/ ${HOME}/cvat
COPY cvat-core/ ${HOME}/cvat-core
COPY cvat-data/ ${HOME}/cvat-data
COPY tests ${HOME}/tests
COPY datumaro/ ${HOME}/datumaro
# Add a non-root user
ENV USER=${USER}
ENV HOME /home/${USER}
RUN adduser --shell /bin/bash --disabled-password --gecos "" ${USER} && \
if [ -z ${socks_proxy} ]; then \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30\"" >> ${HOME}/.bashrc; \
else \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ProxyCommand='nc -X 5 -x ${socks_proxy} %h %p'\"" >> ${HOME}/.bashrc; \
fi
RUN python3 -m pip install --no-cache-dir -r ${HOME}/datumaro/requirements.txt
ARG INSTALL_SOURCES='no'
WORKDIR ${HOME}/sources
RUN if [ "$INSTALL_SOURCES" = "yes" ]; then \
sed -Ei 's/^# deb-src /deb-src /' /etc/apt/sources.list && \
apt-get update && \
dpkg --get-selections | while read -r line; do \
package=$(echo "$line" | awk '{print $1}'); \
mkdir "$package"; \
( \
cd "$package"; \
apt-get -q --download-only source "$package"; \
) \
done && \
rm -rf /var/lib/apt/lists/*; \
fi
COPY --from=build-image /tmp/openh264/openh264*.tar.gz /tmp/ffmpeg/ffmpeg*.tar.bz2 ${HOME}/sources/
RUN chown -R ${USER}:${USER} .
# Copy python virtual enviroment and FFmpeg binaries from build-image
COPY --from=build-image /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:${PATH}"
COPY --from=build-image /opt/ffmpeg /usr
# Install and initialize CVAT, copy all necessary files
COPY --chown=${USER} components /tmp/components
COPY --chown=${USER} ssh ${HOME}/.ssh
COPY --chown=${USER} supervisord.conf mod_wsgi.conf wait-for-it.sh manage.py ${HOME}/
COPY --chown=${USER} cvat/ ${HOME}/cvat
COPY --chown=${USER} utils/ ${HOME}/utils
COPY --chown=${USER} tests/ ${HOME}/tests
# RUN all commands below as 'django' user
USER ${USER}
WORKDIR ${HOME}
RUN mkdir data share media keys logs /tmp/supervisord
RUN python3 manage.py collectstatic
EXPOSE 8080 8443
EXPOSE 8080
ENTRYPOINT ["/usr/bin/supervisord"]

@ -3,24 +3,35 @@ FROM cvat/server
ENV DJANGO_CONFIGURATION=testing
USER root
RUN curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list && \
curl https://deb.nodesource.com/setup_12.x | bash - && \
apt-get update && \
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
gpg-agent \
gnupg2 \
apt-utils \
build-essential \
google-chrome-stable \
nodejs \
python3-dev \
ruby \
&& \
curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list && \
curl https://deb.nodesource.com/setup_12.x | bash - && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
google-chrome-stable \
nodejs \
&& \
rm -rf /var/lib/apt/lists/*;
RUN python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt && \
COPY cvat/requirements/ /tmp/requirements/
RUN DATUMARO_HEADLESS=1 python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt && \
python3 -m pip install --no-cache-dir coveralls
RUN gem install coveralls-lcov
COPY utils ${HOME}/utils
COPY cvat-core ${HOME}/cvat-core
COPY cvat-data ${HOME}/cvat-data
COPY tests ${HOME}/tests
COPY .coveragerc .
ENTRYPOINT []

@ -15,29 +15,29 @@ ENV TERM=xterm \
LANG='C.UTF-8' \
LC_ALL='C.UTF-8'
RUN apk add python3 g++ make
# Install dependencies
COPY cvat-core/package*.json /tmp/cvat-core/
COPY cvat-canvas/package*.json /tmp/cvat-canvas/
COPY cvat-ui/package*.json /tmp/cvat-ui/
COPY cvat-data/package*.json /tmp/cvat-data/
RUN npm config set loglevel info
# Install cvat-data dependencies
WORKDIR /tmp/cvat-data/
RUN npm install
RUN npm ci
# Install cvat-core dependencies
WORKDIR /tmp/cvat-core/
RUN npm install
RUN npm ci
# Install cvat-canvas dependencies
WORKDIR /tmp/cvat-canvas/
RUN npm install
RUN npm ci
# Install cvat-ui dependencies
WORKDIR /tmp/cvat-ui/
RUN npm install
RUN npm ci
# Build source code
COPY cvat-data/ /tmp/cvat-data/

@ -1,9 +1,9 @@
# Computer Vision Annotation Tool (CVAT)
[![Build Status](https://travis-ci.org/opencv/cvat.svg?branch=develop)](https://travis-ci.org/opencv/cvat)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/840351da141e4eaeac6476fd19ec0a33)](https://app.codacy.com/app/cvat/cvat?utm_source=github.com&utm_medium=referral&utm_content=opencv/cvat&utm_campaign=Badge_Grade_Dashboard)
[![Build Status](https://travis-ci.org/openvinotoolkit/cvat.svg?branch=develop)](https://travis-ci.org/openvinotoolkit/cvat)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/b9899c72f2764df0b5d26390cb872e21)](https://app.codacy.com/gh/openvinotoolkit/cvat?utm_source=github.com&utm_medium=referral&utm_content=openvinotoolkit/cvat&utm_campaign=Badge_Grade_Dashboard)
[![Gitter chat](https://badges.gitter.im/opencv-cvat/gitter.png)](https://gitter.im/opencv-cvat)
[![Coverage Status](https://coveralls.io/repos/github/opencv/cvat/badge.svg?branch=)](https://coveralls.io/github/opencv/cvat?branch=develop)
[![Coverage Status](https://coveralls.io/repos/github/openvinotoolkit/cvat/badge.svg?branch=develop)](https://coveralls.io/github/openvinotoolkit/cvat?branch=develop)
[![DOI](https://zenodo.org/badge/139156354.svg)](https://zenodo.org/badge/latestdoi/139156354)
CVAT is free, online, interactive video and image annotation
@ -19,7 +19,7 @@ annotation team. Try it online [cvat.org](https://cvat.org).
- [Installation guide](cvat/apps/documentation/installation.md)
- [User's guide](cvat/apps/documentation/user_guide.md)
- [Django REST API documentation](#rest-api)
- [Datumaro dataset framework](datumaro/README.md)
- [Datumaro dataset framework](https://github.com/openvinotoolkit/datumaro/blob/develop/README.md)
- [Command line interface](utils/cli/)
- [XML annotation format](cvat/apps/documentation/xml_format.md)
- [AWS Deployment Guide](cvat/apps/documentation/AWS-Deployment-Guide.md)
@ -32,7 +32,7 @@ annotation team. Try it online [cvat.org](https://cvat.org).
- [Annotation mode](https://youtu.be/vH_639N67HI)
- [Interpolation of bounding boxes](https://youtu.be/Hc3oudNuDsY)
- [Interpolation of polygons](https://youtu.be/K4nis9lk92s)
- [Tag_annotation_video](https://youtu.be/62bI4mF-Xfk)
- [Tag annotation video](https://youtu.be/62bI4mF-Xfk)
- [Attribute mode](https://youtu.be/iIkJsOkDzVA)
- [Segmentation mode](https://youtu.be/9Fe_GzMLo3E)
- [Tutorial for polygons](https://youtu.be/C7-r9lZbjBw)
@ -40,36 +40,41 @@ annotation team. Try it online [cvat.org](https://cvat.org).
## Supported annotation formats
Format selection is possible after clicking on the Upload annotation
and Dump annotation buttons. [Datumaro](datumaro/README.md) dataset
framework allows additional dataset transformations
via its command line tool and Python library.
| Annotation format | Import | Export |
| ------------------------------------------------------------------------------------------ | ------ | ------ |
| [CVAT for images](cvat/apps/documentation/xml_format.md#annotation) | X | X |
| [CVAT for a video](cvat/apps/documentation/xml_format.md#interpolation) | X | X |
| [Datumaro](datumaro/README.md) | | X |
| [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| Segmentation masks from [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| [YOLO](https://pjreddie.com/darknet/yolo/) | X | X |
| [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X |
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tf_records) | X | X |
| [MOT](https://motchallenge.net/) | X | X |
| [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | X | X |
Format selection is possible after clicking on the Upload annotation and Dump
annotation buttons. [Datumaro](https://github.com/openvinotoolkit/datumaro)
dataset framework allows additional dataset transformations via its command
line tool and Python library.
For more information about supported formats look at the
[documentation](cvat/apps/dataset_manager/formats/README.md#formats).
| Annotation format | Import | Export |
| ----------------------------------------------------------------------------- | ------ | ------ |
| [CVAT for images](cvat/apps/documentation/xml_format.md#annotation) | X | X |
| [CVAT for a video](cvat/apps/documentation/xml_format.md#interpolation) | X | X |
| [Datumaro](https://github.com/openvinotoolkit/datumaro) | | X |
| [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| Segmentation masks from [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| [YOLO](https://pjreddie.com/darknet/yolo/) | X | X |
| [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X |
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tf_records) | X | X |
| [MOT](https://motchallenge.net/) | X | X |
| [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | X | X |
| [ImageNet](http://www.image-net.org) | X | X |
| [CamVid](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) | X | X |
## Deep learning models for automatic labeling
| Name | Type | Framework |
| ------------------------------------------------------------------------------------------------------- | ---------- | ---------- |
| [Deep Extreme Cut](/serverless/openvino/dextr/nuclio) | interactor | OpenVINO |
| [Faster RCNN](/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | detector | TensorFlow |
| [Mask RCNN](/serverless/openvino/omz/public/mask_rcnn_inception_resnet_v2_atrous_coco/nuclio) | detector | OpenVINO |
| [YOLO v3](/serverless/openvino/omz/public/yolo-v3-tf/nuclio) | detector | OpenVINO |
| [Text detection v4](/serverless/openvino/omz/intel/text-detection-0004/nuclio) | detector | OpenVINO |
| [Semantic segmentation for ADAS](/serverless/openvino/omz/intel/semantic-segmentation-adas-0001/nuclio) | detector | OpenVINO |
| [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow |
| [Object reidentification](/serverless/openvino/omz/intel/person-reidentification-retail-300/nuclio) | reid | OpenVINO |
| Name | Type | Framework | CPU | GPU |
| ------------------------------------------------------------------------------------------------------- | ---------- | ---------- | --- | --- |
| [Deep Extreme Cut](/serverless/openvino/dextr/nuclio) | interactor | OpenVINO | X |
| [Faster RCNN](/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | detector | TensorFlow | X | X |
| [Mask RCNN](/serverless/openvino/omz/public/mask_rcnn_inception_resnet_v2_atrous_coco/nuclio) | detector | OpenVINO | X |
| [YOLO v3](/serverless/openvino/omz/public/yolo-v3-tf/nuclio) | detector | OpenVINO | X |
| [Text detection v4](/serverless/openvino/omz/intel/text-detection-0004/nuclio) | detector | OpenVINO | X |
| [Semantic segmentation for ADAS](/serverless/openvino/omz/intel/semantic-segmentation-adas-0001/nuclio) | detector | OpenVINO | X |
| [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow | X |
| [Object reidentification](/serverless/openvino/omz/intel/person-reidentification-retail-300/nuclio) | reid | OpenVINO | X |
## Online demo: [cvat.org](https://cvat.org)
@ -78,19 +83,21 @@ Try it online without local installation. Only own or assigned tasks
are visible to users.
Disabled features:
- [Analytics: management and monitoring of data annotation team](/components/analytics/README.md)
Limitations:
- No more than 10 tasks per user
- Uploaded data is limited to 500Mb
## REST API
Automatically generated Swagger documentation for Django REST API is
available on ``<cvat_origin>/api/swagger``
(default: ``localhost:8080/api/swagger``).
available on `<cvat_origin>/api/swagger`
(default: `localhost:8080/api/swagger`).
Swagger documentation is visiable on allowed hostes, Update environement variable in docker-compose.yml file with cvat hosted machine IP or domain name. Example - ``ALLOWED_HOSTS: 'localhost, 127.0.0.1'``)
Swagger documentation is visiable on allowed hostes, Update environement variable in docker-compose.yml file with cvat hosted machine IP or domain name. Example - `ALLOWED_HOSTS: 'localhost, 127.0.0.1'`)
## LICENSE
@ -104,16 +111,22 @@ contributors and other users.
However, if you have a feature request or a bug report that can reproduced,
feel free to open an issue (with steps to reproduce the bug if it's a bug
report) on [GitHub* issues](https://github.com/opencv/cvat/issues).
report) on [GitHub\* issues](https://github.com/opencv/cvat/issues).
If you are not sure or just want to browse other users common questions,
[Gitter chat](https://gitter.im/opencv-cvat) is the way to go.
Other ways to ask questions and get our support:
* [\#cvat](https://stackoverflow.com/search?q=%23cvat) tag on StackOverflow*
* [Forum on Intel Developer Zone](https://software.intel.com/en-us/forums/computer-vision)
- [\#cvat](https://stackoverflow.com/search?q=%23cvat) tag on StackOverflow\*
- [Forum on Intel Developer Zone](https://software.intel.com/en-us/forums/computer-vision)
## Links
- [Intel AI blog: New Computer Vision Tool Accelerates Annotation of Digital Images and Video](https://www.intel.ai/introducing-cvat)
- [Intel Software: Computer Vision Annotation Tool: A Universal Approach to Data Annotation](https://software.intel.com/en-us/articles/computer-vision-annotation-tool-a-universal-approach-to-data-annotation)
- [VentureBeat: Intel open-sources CVAT, a toolkit for data labeling](https://venturebeat.com/2019/03/05/intel-open-sources-cvat-a-toolkit-for-data-labeling/)
## Projects using CVAT
- [Onepanel](https://github.com/onepanelio/core) - Onepanel is an open source vision AI platform that fully integrates CVAT with scalable data processing and parallelized training pipelines.

@ -5,12 +5,14 @@
It is possible to proxy annotation logs from client to ELK. To do that run the following command below:
### Build docker image
```bash
# From project root directory
docker-compose -f docker-compose.yml -f components/analytics/docker-compose.analytics.yml build
```
### Run docker container
```bash
# From project root directory
docker-compose -f docker-compose.yml -f components/analytics/docker-compose.analytics.yml up -d
@ -19,11 +21,12 @@ docker-compose -f docker-compose.yml -f components/analytics/docker-compose.anal
At the moment it is not possible to save advanced settings. Below values should be specified manually.
## Time picker default
{
"from": "now/d",
"to": "now/d",
"display": "Today",
"section": 0
"from": "now/d",
"to": "now/d",
"display": "Today",
"section": 0
}
## Time picker quick ranges

@ -1,4 +1,4 @@
version: '2.3'
version: '3.3'
services:
cvat_elasticsearch:
container_name: cvat_elasticsearch
@ -35,9 +35,24 @@ services:
volumes: ['./components/analytics/kibana:/home/django/kibana:ro']
depends_on: ['cvat']
working_dir: '/home/django'
entrypoint: ['bash', 'wait-for-it.sh', 'elasticsearch:9200', '-t', '0', '--',
'/bin/bash', 'wait-for-it.sh', 'kibana:5601', '-t', '0', '--',
'/usr/bin/python3', 'kibana/setup.py', 'kibana/export.json']
entrypoint:
[
'bash',
'wait-for-it.sh',
'elasticsearch:9200',
'-t',
'0',
'--',
'/bin/bash',
'wait-for-it.sh',
'kibana:5601',
'-t',
'0',
'--',
'/usr/bin/python3',
'kibana/setup.py',
'kibana/export.json',
]
environment:
no_proxy: elasticsearch,kibana,${no_proxy}
@ -52,7 +67,7 @@ services:
context: ./components/analytics/logstash
args:
ELK_VERSION: 6.4.0
http_proxy: ${http_proxy}
http_proxy: ${http_proxy}
https_proxy: ${https_proxy}
depends_on: ['cvat_elasticsearch']
restart: always
@ -63,6 +78,7 @@ services:
DJANGO_LOG_SERVER_PORT: 5000
DJANGO_LOG_VIEWER_HOST: kibana
DJANGO_LOG_VIEWER_PORT: 5601
CVAT_ANALYTICS: 1
no_proxy: kibana,logstash,nuclio,${no_proxy}
volumes:

@ -1,3 +1,3 @@
http.host: 0.0.0.0
script.painless.regex.enabled: true
path.repo: ["/usr/share/elasticsearch/data/backup"]
path.repo: ['/usr/share/elasticsearch/data/backup']

@ -40,19 +40,11 @@
"_type": "search",
"_source": {
"hits": 0,
"sort": [
"@timestamp",
"desc"
],
"sort": ["@timestamp", "desc"],
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"index\":\"ec510550-c238-11e8-8e1b-758ef07f6de8\",\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"event:\\\"Send exception\\\"\"},\"filter\":[]}"
},
"columns": [
"task",
"type",
"userid",
"stack"
],
"columns": ["task", "type", "userid", "stack"],
"description": "",
"title": "Table with exceptions",
"version": 1
@ -99,7 +91,7 @@
"_id": "ec510550-c238-11e8-8e1b-758ef07f6de8",
"_type": "index-pattern",
"_source": {
"fields": "[{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@version.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"application\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"application.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"box count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"duration\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"event.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"frame count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"object count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"points count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"polygon count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"polyline count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"task\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"task.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"timestamp\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"track count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"userid\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"userid.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"working_time\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]",
"fields": "[{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@version.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"application\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"application.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"box count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"duration\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"event.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"frame count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"object count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"points count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"polygon count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"polyline count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"task\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"task\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"timestamp\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"track count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"userid\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"userid.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"working_time\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]",
"title": "cvat*",
"timeFieldName": "@timestamp",
"fieldFormatMap": "{\"duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\",\"outputFormat\":\"asSeconds\"}},\"working_time\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\",\"outputFormat\":\"asHours\"}}}"
@ -164,7 +156,7 @@
"_type": "visualization",
"_source": {
"title": "List of tasks",
"visState": "{\"title\":\"List of tasks\",\"type\":\"table\",\"params\":{\"perPage\":20,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":2,\"direction\":\"desc\"},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"sum\",\"schema\":\"metric\",\"params\":{\"field\":\"working_time\",\"customLabel\":\"Working time (h)\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"task.keyword\",\"size\":1000,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Task\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"userid.keyword\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"_key\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"User\"}}]}",
"visState": "{\"title\":\"List of tasks\",\"type\":\"table\",\"params\":{\"perPage\":20,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":2,\"direction\":\"desc\"},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"sum\",\"schema\":\"metric\",\"params\":{\"field\":\"working_time\",\"customLabel\":\"Working time (h)\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"task\",\"size\":1000,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Task\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"userid.keyword\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"_key\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"User\"}}]}",
"uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":2,\"direction\":\"desc\"}}}}",
"description": "",
"version": 1,
@ -195,4 +187,4 @@
"savedObjectVersion": 2
}
}
]
]

@ -1,5 +1,5 @@
server.host: 0.0.0.0
elasticsearch.url: http://elasticsearch:9200
elasticsearch.requestHeadersWhitelist: [ "cookie", "authorization", "x-forwarded-user" ]
kibana.defaultAppId: "discover"
elasticsearch.requestHeadersWhitelist: ['cookie', 'authorization', 'x-forwarded-user']
kibana.defaultAppId: 'discover'
server.basePath: /analytics

@ -0,0 +1,8 @@
## Serverless for Computer Vision Annotation Tool (CVAT)
### Run docker container
```bash
# From project root directory
docker-compose -f docker-compose.yml -f components/serverless/docker-compose.serverless.yml up -d
```

@ -0,0 +1,28 @@
version: '3.3'
services:
serverless:
container_name: nuclio
image: quay.io/nuclio/dashboard:1.5.8-amd64
restart: always
networks:
default:
aliases:
- nuclio
volumes:
- /tmp:/tmp
- /var/run/docker.sock:/var/run/docker.sock
environment:
http_proxy:
https_proxy:
no_proxy: 172.28.0.1,${no_proxy}
NUCLIO_CHECK_FUNCTION_CONTAINERS_HEALTHINESS: 'true'
ports:
- '8070:8070'
cvat:
environment:
CVAT_SERVERLESS: 1
no_proxy: kibana,logstash,nuclio,${no_proxy}
volumes:
cvat_events:

@ -0,0 +1 @@
webpack.config.js

@ -1,39 +1,34 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
module.exports = {
'env': {
'node': true,
'browser': true,
'es6': true,
env: {
node: true,
},
'parserOptions': {
'parser': '@typescript-eslint/parser',
'ecmaVersion': 6,
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 6,
},
'plugins': [
'@typescript-eslint',
'import',
],
'extends': [
plugins: ['@typescript-eslint', 'import'],
extends: [
'plugin:@typescript-eslint/recommended',
'airbnb-typescript/base',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
],
'rules': {
rules: {
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/indent': ['warn', 4],
'no-plusplus': 0,
'no-restricted-syntax': [
0,
{
'selector': 'ForOfStatement'
}
selector: 'ForOfStatement',
},
],
'max-len': ['error', { code: 120 }],
'no-continue': 0,
'func-names': 0,
'no-console': 0, // this rule deprecates console.log, console.warn etc. because 'it is not good in production code'
@ -41,10 +36,10 @@ module.exports = {
'import/prefer-default-export': 0, // works incorrect with interfaces
'newline-per-chained-call': 0, // makes code uglier
},
'settings': {
settings: {
'import/resolver': {
'node': {
'extensions': ['.ts', '.js', '.json'],
node: {
extensions: ['.ts', '.js', '.json'],
},
},
},

@ -1,18 +1,21 @@
# Module CVAT-CANVAS
## Description
The CVAT module written in TypeScript language.
It presents a canvas to viewing, drawing and editing of annotations.
## Versioning
If you make changes in this package, please do following:
- After not important changes (typos, backward compatible bug fixes, refactoring) do: ``npm version patch``
- After changing API (backward compatible new features) do: ``npm version minor``
- After changing API (changes that break backward compatibility) do: ``npm version major``
- After not important changes (typos, backward compatible bug fixes, refactoring) do: `npm version patch`
- After changing API (backward compatible new features) do: `npm version minor`
- After changing API (changes that break backward compatibility) do: `npm version major`
## Commands
- Building of the module from sources in the ```dist``` directory:
- Building of the module from sources in the `dist` directory:
```bash
npm run build
@ -22,6 +25,7 @@ npm run build -- --mode=development # without a minification
## Using
Canvas itself handles:
- Shape context menu (PKM)
- Image moving (mousedrag)
- Image resizing (mousewheel)
@ -51,6 +55,8 @@ Canvas itself handles:
MERGE = 'merge',
SPLIT = 'split',
GROUP = 'group',
INTERACT = 'interact',
SELECT_ROI = 'select_roi',
DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'zoom_canvas',
}
@ -70,6 +76,11 @@ Canvas itself handles:
crosshair?: boolean;
}
interface InteractionData {
shapeType: string;
minVertices?: number;
}
interface GroupData {
enabled: boolean;
resetGroup?: boolean;
@ -83,6 +94,12 @@ Canvas itself handles:
enabled: boolean;
}
interface InteractionResult {
points: number[];
shapeType: string;
button: number;
};
interface DrawnData {
shapeType: string;
points: number[];
@ -95,14 +112,15 @@ Canvas itself handles:
interface Canvas {
html(): HTMLDivElement;
setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID?: number): void;
rotate(frameAngle: number): void;
setup(frameData: any, objectStates: any[], zLayer?: number): void;
setupReviewROIs(reviewROIs: Record<number, number[]>): void;
activate(clientID: number | null, attributeID?: number): void;
rotate(rotationAngle: number): void;
focus(clientID: number, padding?: number): void;
fit(): void;
grid(stepX: number, stepY: number): void;
interact(interactionData: InteractionData): void;
draw(drawData: DrawData): void;
group(groupData: GroupData): void;
split(splitData: SplitData): void;
@ -110,7 +128,8 @@ Canvas itself handles:
select(objectState: any): void;
fitCanvas(): void;
bitmap(enabled: boolean): void;
bitmap(enable: boolean): void;
selectROI(enable: boolean): void;
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
@ -118,27 +137,33 @@ Canvas itself handles:
cancel(): void;
configure(configuration: Configuration): void;
isAbleToChangeFrame(): boolean;
readonly geometry: Geometry;
}
```
### API CSS
- All drawn objects (shapes, tracks) have an id ```cvat_canvas_shape_{objectState.clientID}```
- Drawn shapes and tracks have classes ```cvat_canvas_shape```,
```cvat_canvas_shape_activated```,
```cvat_canvas_shape_grouping```,
```cvat_canvas_shape_merging```,
```cvat_canvas_shape_drawing```,
```cvat_canvas_shape_occluded```
- Drawn texts have the class ```cvat_canvas_text```
- Tags have the class ```cvat_canvas_tag```
- Canvas image has ID ```cvat_canvas_image```
- Grid on the canvas has ID ```cvat_canvas_grid``` and ```cvat_canvas_grid_pattern```
- Crosshair during a draw has class ```cvat_canvas_crosshair```
- All drawn objects (shapes, tracks) have an id `cvat_canvas_shape_{objectState.clientID}`
- Drawn shapes and tracks have classes `cvat_canvas_shape`,
`cvat_canvas_shape_activated`,
`cvat_canvas_shape_grouping`,
`cvat_canvas_shape_merging`,
`cvat_canvas_shape_drawing`,
`cvat_canvas_shape_occluded`
- Drawn review ROIs have an id `cvat_canvas_issue_region_{issue.id}`
- Drawn review roi has the class `cvat_canvas_issue_region`
- Drawn texts have the class `cvat_canvas_text`
- Tags have the class `cvat_canvas_tag`
- Canvas image has ID `cvat_canvas_image`
- Grid on the canvas has ID `cvat_canvas_grid` and `cvat_canvas_grid_pattern`
- Crosshair during a draw has class `cvat_canvas_crosshair`
- To stick something to a specific position you can use an element with id `cvat_canvas_attachment_board`
### Events
Standard JS events are used.
```js
- canvas.setup
- canvas.activated => {state: ObjectState}
@ -146,6 +171,7 @@ Standard JS events are used.
- canvas.moved => {states: ObjectState[], x: number, y: number}
- canvas.find => {states: ObjectState[], x: number, y: number}
- canvas.drawn => {state: DrawnData}
- canvas.interacted => {shapes: InteractionResult[]}
- canvas.editstart
- canvas.edited => {state: ObjectState, points: number[]}
- canvas.splitted => {state: ObjectState}
@ -159,53 +185,62 @@ Standard JS events are used.
- canvas.zoom
- canvas.fit
- canvas.dragshape => {id: number}
- canvas.roiselected => {points: number[]}
- canvas.resizeshape => {id: number}
- canvas.contextmenu => { mouseEvent: MouseEvent, objectState: ObjectState, pointID: number }
- canvas.error => { exception: Error }
```
### WEB
```js
// Create an instance of a canvas
const canvas = new window.canvas.Canvas();
console.log('Version ', window.canvas.CanvasVersion);
console.log('Current mode is ', window.canvas.mode());
// Put canvas to a html container
htmlContainer.appendChild(canvas.html());
canvas.fitCanvas();
// Next you can use its API methods. For example:
canvas.rotate(270);
canvas.draw({
enabled: true,
shapeType: 'rectangle',
crosshair: true,
rectDrawingMethod: window.Canvas.RectDrawingMethod.CLASSIC,
});
// Create an instance of a canvas
const canvas = new window.canvas.Canvas();
console.log('Version ', window.canvas.CanvasVersion);
console.log('Current mode is ', window.canvas.mode());
// Put canvas to a html container
htmlContainer.appendChild(canvas.html());
canvas.fitCanvas();
// Next you can use its API methods. For example:
canvas.rotate(270);
canvas.draw({
enabled: true,
shapeType: 'rectangle',
crosshair: true,
rectDrawingMethod: window.Canvas.RectDrawingMethod.CLASSIC,
});
```
<!--lint disable maximum-line-length-->
## API Reaction
| | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS |
|--------------|------|-------|-------|------|-------|------|------|--------|-------------|-------------|
| setup() | + | + | + | +/- | + | +/- | +/- | +/- | + | + |
| activate() | + | - | - | - | - | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + | + | + | + | + |
| draw() | + | - | - | - | - | - | - | - | - | - |
| split() | + | - | + | - | - | - | - | - | - | - |
| group() | + | + | - | - | - | - | - | - | - | - |
| merge() | + | - | - | - | + | - | - | - | - | - |
| fitCanvas() | + | + | + | + | + | + | + | + | + | + |
| dragCanvas() | + | - | - | - | - | - | + | - | - | + |
| zoomCanvas() | + | - | - | - | - | - | - | + | + | - |
| cancel() | - | + | + | + | + | + | + | + | + | + |
| configure() | + | + | + | + | + | + | + | + | + | + |
| bitmap() | + | + | + | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + | + | + |
| | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS | INTERACT |
| ----------------- | ---- | ----- | ----- | ---- | ----- | ---- | ---- | ------ | ----------- | ----------- | -------- |
| setup() | + | + | + | +/- | + | +/- | +/- | +/- | + | + | + |
| activate() | + | - | - | - | - | - | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + | + | + | + | + | + |
| draw() | + | - | - | + | - | - | - | - | - | - | - |
| interact() | + | - | - | - | - | - | - | - | - | - | + |
| split() | + | - | + | - | - | - | - | - | - | - | - |
| group() | + | + | - | - | - | - | - | - | - | - | - |
| merge() | + | - | - | - | + | - | - | - | - | - | - |
| fitCanvas() | + | + | + | + | + | + | + | + | + | + | + |
| dragCanvas() | + | - | - | - | - | - | + | - | - | + | - |
| zoomCanvas() | + | - | - | - | - | - | - | + | + | - | - |
| cancel() | - | + | + | + | + | + | + | + | + | + | + |
| configure() | + | + | + | + | + | + | + | + | + | + | + |
| bitmap() | + | + | + | + | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + | + | + | + |
| setupReviewROIs() | + | + | + | + | + | + | + | + | + | + | + |
<!--lint enable maximum-line-length-->
You can call setup() during editing, dragging, and resizing only to update objects, not to change a frame.
You can change frame during draw only when you do not redraw an existing object

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "cvat-canvas",
"version": "2.0.2",
"version": "2.2.1",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts",
"scripts": {
@ -11,7 +11,7 @@
"license": "MIT",
"dependencies": {
"svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.3",
"svg.draw.js": "^2.0.4",
"svg.js": "2.7.1",
"svg.resize.js": "1.4.3",
"svg.select.js": "3.0.1"
@ -20,6 +20,7 @@
"@babel/cli": "^7.5.5",
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
"@babel/preset-env": "^7.5.5",
"@babel/preset-typescript": "^7.3.3",
"@types/node": "^12.6.8",
@ -32,15 +33,15 @@
"eslint-config-airbnb-typescript": "^4.0.1",
"eslint-config-typescript-recommended": "^1.4.17",
"eslint-plugin-import": "^2.18.2",
"node-sass": "^4.13.1",
"nodemon": "^1.19.1",
"node-sass": "^4.14.1",
"nodemon": "^1.19.4",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"sass-loader": "^8.0.2",
"style-loader": "^1.0.0",
"typescript": "^3.5.3",
"webpack": "^4.36.1",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.7.2"
"webpack-dev-server": "^3.11.0"
}
}

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: MIT
/* eslint-disable */
module.exports = {
parser: false,
plugins: {

@ -41,7 +41,7 @@ polyline.cvat_shape_drawing_opacity {
font-size: 1.2em;
fill: white;
cursor: default;
font-family: Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif;
font-family: Calibri, Candara, Segoe, 'Segoe UI', Optima, Arial, sans-serif;
text-shadow: 0 0 4px black;
user-select: none;
pointer-events: none;
@ -58,6 +58,23 @@ polyline.cvat_shape_drawing_opacity {
fill: darkmagenta;
}
.cvat_canvas_shape_region_selection {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: white;
stroke: white;
}
.cvat_canvas_issue_region {
display: none;
stroke-width: 0;
}
circle.cvat_canvas_issue_region {
opacity: 1 !important;
}
polyline.cvat_canvas_shape_grouping {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
@ -258,6 +275,15 @@ polyline.cvat_canvas_shape_splitting {
height: 100%;
}
#cvat_canvas_attachment_board {
position: absolute;
z-index: 4;
pointer-events: none;
width: 100%;
height: 100%;
user-select: none;
}
@keyframes loadingAnimation {
0% {
stroke-dashoffset: 1;

@ -27,10 +27,16 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
private groups: SVGGElement[];
private auxiliaryGroupID: number | null;
private auxiliaryClicks: number[];
private listeners: Record<number, Record<number, {
click: (event: MouseEvent) => void;
dblclick: (event: MouseEvent) => void;
}>>;
private listeners: Record<
number,
Record<
number,
{
click: (event: MouseEvent) => void;
dblclick: (event: MouseEvent) => void;
}
>
>;
public constructor(frameContent: SVGSVGElement) {
this.frameContent = frameContent;
@ -47,12 +53,11 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
private removeMarkers(): void {
this.groups.forEach((group: SVGGElement): void => {
const groupID = group.dataset.groupId;
Array.from(group.children)
.forEach((circle: SVGCircleElement, pointID: number): void => {
circle.removeEventListener('click', this.listeners[+groupID][pointID].click);
circle.removeEventListener('dblclick', this.listeners[+groupID][pointID].click);
circle.remove();
});
Array.from(group.children).forEach((circle: SVGCircleElement, pointID: number): void => {
circle.removeEventListener('click', this.listeners[+groupID][pointID].click);
circle.removeEventListener('dblclick', this.listeners[+groupID][pointID].click);
circle.remove();
});
group.remove();
});
@ -89,8 +94,9 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
if (this.auxiliaryGroupID !== null) {
while (this.auxiliaryClicks.length > 0) {
const resetID = this.auxiliaryClicks.pop();
this.groups[this.auxiliaryGroupID]
.children[resetID].classList.remove('cvat_canvas_autoborder_point_direction');
this.groups[this.auxiliaryGroupID].children[resetID].classList.remove(
'cvat_canvas_autoborder_point_direction',
);
}
}
@ -103,125 +109,125 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
private drawMarkers(transformedShapes: TransformedShape[]): void {
const svgNamespace = 'http://www.w3.org/2000/svg';
this.groups = transformedShapes
.map((shape: TransformedShape, groupID: number): SVGGElement => {
this.groups = transformedShapes.map(
(shape: TransformedShape, groupID: number): SVGGElement => {
const group = document.createElementNS(svgNamespace, 'g');
group.setAttribute('data-group-id', `${groupID}`);
this.listeners[groupID] = this.listeners[groupID] || {};
const circles = shape.points.split(/\s/).map((
point: string, pointID: number, points: string[],
): SVGCircleElement => {
const [x, y] = point.split(',');
const circle = document.createElementNS(svgNamespace, 'circle');
circle.classList.add('cvat_canvas_autoborder_point');
circle.setAttribute('fill', shape.color);
circle.setAttribute('stroke', 'black');
circle.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.scale}`);
circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
circle.setAttribute('r', `${consts.BASE_POINT_SIZE / this.scale}`);
const click = (event: MouseEvent): void => {
event.stopPropagation();
// another shape was clicked
if (this.auxiliaryGroupID !== null
&& this.auxiliaryGroupID !== groupID
) {
this.resetAuxiliaryShape();
}
this.auxiliaryGroupID = groupID;
// up clicked group for convenience
this.frameContent.appendChild(group);
if (this.auxiliaryClicks[1] === pointID) {
// the second point was clicked twice
this.addPointToCurrentShape(+x, +y);
this.resetAuxiliaryShape();
return;
}
// the first point can not be clicked twice
// just ignore such a click if it is
if (this.auxiliaryClicks[0] !== pointID) {
this.auxiliaryClicks.push(pointID);
} else {
return;
}
// it is the first click
if (this.auxiliaryClicks.length === 1) {
const handler = this.currentShape.remember('_paintHandler');
// draw and remove initial point just to initialize data structures
if (!handler || !handler.startPoint) {
(this.currentShape as any).draw('point', event);
(this.currentShape as any).draw('undo');
const circles = shape.points.split(/\s/).map(
(point: string, pointID: number, points: string[]): SVGCircleElement => {
const [x, y] = point.split(',');
const circle = document.createElementNS(svgNamespace, 'circle');
circle.classList.add('cvat_canvas_autoborder_point');
circle.setAttribute('fill', shape.color);
circle.setAttribute('stroke', 'black');
circle.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.scale}`);
circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
circle.setAttribute('r', `${consts.BASE_POINT_SIZE / this.scale}`);
const click = (event: MouseEvent): void => {
event.stopPropagation();
// another shape was clicked
if (this.auxiliaryGroupID !== null && this.auxiliaryGroupID !== groupID) {
this.resetAuxiliaryShape();
}
this.addPointToCurrentShape(+x, +y);
// is is the second click
} else if (this.auxiliaryClicks.length === 2) {
circle.classList.add('cvat_canvas_autoborder_point_direction');
// it is the third click
} else {
// sign defines bypass direction
const landmarks = this.auxiliaryClicks;
const sign = Math.sign(landmarks[2] - landmarks[0])
* Math.sign(landmarks[1] - landmarks[0])
* Math.sign(landmarks[2] - landmarks[1]);
// go via a polygon and get vertexes
// the first vertex has been already drawn
const way = [];
for (let i = landmarks[0] + sign; ; i += sign) {
if (i < 0) {
i = points.length - 1;
} else if (i === points.length) {
i = 0;
}
this.auxiliaryGroupID = groupID;
// up clicked group for convenience
this.frameContent.appendChild(group);
way.push(points[i]);
if (i === this.auxiliaryClicks[this.auxiliaryClicks.length - 1]) {
// put the last element twice
// specific of svg.draw.js
// way.push(points[i]);
break;
}
if (this.auxiliaryClicks[1] === pointID) {
// the second point was clicked twice
this.addPointToCurrentShape(+x, +y);
this.resetAuxiliaryShape();
return;
}
// remove the latest cursor position from drawing array
for (const wayPoint of way) {
const [_x, _y] = wayPoint.split(',')
.map((coordinate: string): number => +coordinate);
this.addPointToCurrentShape(_x, _y);
// the first point can not be clicked twice
// just ignore such a click if it is
if (this.auxiliaryClicks[0] !== pointID) {
this.auxiliaryClicks.push(pointID);
} else {
return;
}
this.resetAuxiliaryShape();
}
};
// it is the first click
if (this.auxiliaryClicks.length === 1) {
const handler = this.currentShape.remember('_paintHandler');
// draw and remove initial point just to initialize data structures
if (!handler || !handler.startPoint) {
(this.currentShape as any).draw('point', event);
(this.currentShape as any).draw('undo');
}
this.addPointToCurrentShape(+x, +y);
// is is the second click
} else if (this.auxiliaryClicks.length === 2) {
circle.classList.add('cvat_canvas_autoborder_point_direction');
// it is the third click
} else {
// sign defines bypass direction
const landmarks = this.auxiliaryClicks;
const sign =
Math.sign(landmarks[2] - landmarks[0]) *
Math.sign(landmarks[1] - landmarks[0]) *
Math.sign(landmarks[2] - landmarks[1]);
// go via a polygon and get vertexes
// the first vertex has been already drawn
const way = [];
for (let i = landmarks[0] + sign; ; i += sign) {
if (i < 0) {
i = points.length - 1;
} else if (i === points.length) {
i = 0;
}
way.push(points[i]);
if (i === this.auxiliaryClicks[this.auxiliaryClicks.length - 1]) {
// put the last element twice
// specific of svg.draw.js
// way.push(points[i]);
break;
}
}
const dblclick = (event: MouseEvent): void => {
event.stopPropagation();
};
// remove the latest cursor position from drawing array
for (const wayPoint of way) {
const [_x, _y] = wayPoint
.split(',')
.map((coordinate: string): number => +coordinate);
this.addPointToCurrentShape(_x, _y);
}
this.listeners[groupID][pointID] = {
click,
dblclick,
};
this.resetAuxiliaryShape();
}
};
circle.addEventListener('mousedown', this.listeners[groupID][pointID].click);
circle.addEventListener('dblclick', this.listeners[groupID][pointID].click);
return circle;
});
const dblclick = (event: MouseEvent): void => {
event.stopPropagation();
};
this.listeners[groupID][pointID] = {
click,
dblclick,
};
circle.addEventListener('mousedown', this.listeners[groupID][pointID].click);
circle.addEventListener('dblclick', this.listeners[groupID][pointID].click);
return circle;
},
);
group.append(...circles);
return group;
});
},
);
this.frameContent.append(...this.groups);
}
@ -231,55 +237,54 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
this.removeMarkers();
const currentClientID = this.currentShape.node.dataset.originClientId;
const shapes = Array.from(this.frameContent.getElementsByClassName('cvat_canvas_shape'))
.filter((shape: HTMLElement): boolean => +shape.getAttribute('clientID') !== this.currentID);
const transformedShapes = shapes.map((shape: HTMLElement): TransformedShape | null => {
const color = shape.getAttribute('fill');
const clientID = shape.getAttribute('clientID');
if (color === null || clientID === null) return null;
if (+clientID === +currentClientID) {
return null;
}
let points = '';
if (shape.tagName === 'polyline' || shape.tagName === 'polygon') {
points = shape.getAttribute('points');
} else if (shape.tagName === 'rect') {
const x = +shape.getAttribute('x');
const y = +shape.getAttribute('y');
const width = +shape.getAttribute('width');
const height = +shape.getAttribute('height');
if (Number.isNaN(x) || Number.isNaN(y) || Number.isNaN(x) || Number.isNaN(x)) {
const shapes = Array.from(this.frameContent.getElementsByClassName('cvat_canvas_shape')).filter(
(shape: HTMLElement): boolean => +shape.getAttribute('clientID') !== this.currentID,
);
const transformedShapes = shapes
.map((shape: HTMLElement): TransformedShape | null => {
const color = shape.getAttribute('fill');
const clientID = shape.getAttribute('clientID');
if (color === null || clientID === null) return null;
if (+clientID === +currentClientID) {
return null;
}
points = `${x},${y} ${x + width},${y} ${x + width},${y + height} ${x},${y + height}`;
} else if (shape.tagName === 'g') {
const polylineID = shape.dataset.polylineId;
const polyline = this.frameContent.getElementById(polylineID);
if (polyline && polyline.getAttribute('points')) {
points = polyline.getAttribute('points');
} else {
return null;
let points = '';
if (shape.tagName === 'polyline' || shape.tagName === 'polygon') {
points = shape.getAttribute('points');
} else if (shape.tagName === 'rect') {
const x = +shape.getAttribute('x');
const y = +shape.getAttribute('y');
const width = +shape.getAttribute('width');
const height = +shape.getAttribute('height');
if (Number.isNaN(x) || Number.isNaN(y) || Number.isNaN(x) || Number.isNaN(x)) {
return null;
}
points = `${x},${y} ${x + width},${y} ${x + width},${y + height} ${x},${y + height}`;
} else if (shape.tagName === 'g') {
const polylineID = shape.dataset.polylineId;
const polyline = this.frameContent.getElementById(polylineID);
if (polyline && polyline.getAttribute('points')) {
points = polyline.getAttribute('points');
} else {
return null;
}
}
}
return {
color,
points: points.trim(),
};
}).filter((state: TransformedShape | null): boolean => state !== null);
return {
color,
points: points.trim(),
};
})
.filter((state: TransformedShape | null): boolean => state !== null);
this.drawMarkers(transformedShapes);
}
public autoborder(
enabled: boolean,
currentShape?: SVG.Shape,
currentID?: number,
): void {
public autoborder(enabled: boolean, currentShape?: SVG.Shape, currentID?: number): void {
if (enabled && !this.enabled && currentShape) {
this.enabled = true;
this.currentShape = currentShape;

@ -8,26 +8,18 @@ import {
MergeData,
SplitData,
GroupData,
InteractionData as _InteractionData,
InteractionResult as _InteractionResult,
CanvasModel,
CanvasModelImpl,
RectDrawingMethod,
CuboidDrawingMethod,
Configuration,
Geometry,
} from './canvasModel';
import {
Master,
} from './master';
import {
CanvasController,
CanvasControllerImpl,
} from './canvasController';
import {
CanvasView,
CanvasViewImpl,
} from './canvasView';
import { Master } from './master';
import { CanvasController, CanvasControllerImpl } from './canvasController';
import { CanvasView, CanvasViewImpl } from './canvasView';
import '../scss/canvas.scss';
import pjson from '../../package.json';
@ -37,12 +29,14 @@ const CanvasVersion = pjson.version;
interface Canvas {
html(): HTMLDivElement;
setup(frameData: any, objectStates: any[], zLayer?: number): void;
setupIssueRegions(issueRegions: Record<number, number[]>): void;
activate(clientID: number | null, attributeID?: number): void;
rotate(rotationAngle: number): void;
focus(clientID: number, padding?: number): void;
fit(): void;
grid(stepX: number, stepY: number): void;
interact(interactionData: InteractionData): void;
draw(drawData: DrawData): void;
group(groupData: GroupData): void;
split(splitData: SplitData): void;
@ -51,6 +45,7 @@ interface Canvas {
fitCanvas(): void;
bitmap(enable: boolean): void;
selectRegion(enable: boolean): void;
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
@ -58,6 +53,8 @@ interface Canvas {
cancel(): void;
configure(configuration: Configuration): void;
isAbleToChangeFrame(): boolean;
readonly geometry: Geometry;
}
class CanvasImpl implements Canvas {
@ -79,17 +76,22 @@ class CanvasImpl implements Canvas {
this.model.setup(frameData, objectStates, zLayer);
}
public setupIssueRegions(issueRegions: Record<number, number[]>): void {
this.model.setupIssueRegions(issueRegions);
}
public fitCanvas(): void {
this.model.fitCanvas(
this.view.html().clientWidth,
this.view.html().clientHeight,
);
this.model.fitCanvas(this.view.html().clientWidth, this.view.html().clientHeight);
}
public bitmap(enable: boolean): void {
this.model.bitmap(enable);
}
public selectRegion(enable: boolean): void {
this.model.selectRegion(enable);
}
public dragCanvas(enable: boolean): void {
this.model.dragCanvas(enable);
}
@ -118,6 +120,10 @@ class CanvasImpl implements Canvas {
this.model.grid(stepX, stepY);
}
public interact(interactionData: InteractionData): void {
this.model.interact(interactionData);
}
public draw(drawData: DrawData): void {
this.model.draw(drawData);
}
@ -153,13 +159,15 @@ class CanvasImpl implements Canvas {
public isAbleToChangeFrame(): boolean {
return this.model.isAbleToChangeFrame();
}
public get geometry(): Geometry {
return this.model.geometry;
}
}
export type InteractionData = _InteractionData;
export type InteractionResult = _InteractionResult;
export {
CanvasImpl as Canvas,
CanvasVersion,
Configuration,
RectDrawingMethod,
CuboidDrawingMethod,
Mode as CanvasMode,
CanvasImpl as Canvas, CanvasVersion, RectDrawingMethod, CuboidDrawingMethod, Mode as CanvasMode,
};

@ -13,26 +13,33 @@ import {
SplitData,
GroupData,
Mode,
InteractionData,
Configuration,
} from './canvasModel';
export interface CanvasController {
readonly objects: any[];
readonly issueRegions: Record<number, number[]>;
readonly zLayer: number | null;
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
readonly drawData: DrawData;
readonly interactionData: InteractionData;
readonly mergeData: MergeData;
readonly splitData: SplitData;
readonly groupData: GroupData;
readonly selected: any;
readonly configuration: Configuration;
mode: Mode;
geometry: Geometry;
zoom(x: number, y: number, direction: number): void;
draw(drawData: DrawData): void;
interact(interactionData: InteractionData): void;
merge(mergeData: MergeData): void;
split(splitData: SplitData): void;
group(groupData: GroupData): void;
selectRegion(enabled: boolean): void;
enableDrag(x: number, y: number): void;
drag(x: number, y: number): void;
disableDrag(): void;
@ -84,6 +91,10 @@ export class CanvasControllerImpl implements CanvasController {
this.model.draw(drawData);
}
public interact(interactionData: InteractionData): void {
this.model.interact(interactionData);
}
public merge(mergeData: MergeData): void {
this.model.merge(mergeData);
}
@ -96,6 +107,10 @@ export class CanvasControllerImpl implements CanvasController {
this.model.group(groupData);
}
public selectRegion(enable: boolean): void {
this.model.selectRegion(enable);
}
public get geometry(): Geometry {
return this.model.geometry;
}
@ -108,6 +123,10 @@ export class CanvasControllerImpl implements CanvasController {
return this.model.zLayer;
}
public get issueRegions(): Record<number, number[]> {
return this.model.issueRegions;
}
public get objects(): any[] {
return this.model.objects;
}
@ -124,6 +143,10 @@ export class CanvasControllerImpl implements CanvasController {
return this.model.drawData;
}
public get interactionData(): InteractionData {
return this.model.interactionData;
}
public get mergeData(): MergeData {
return this.model.mergeData;
}
@ -140,6 +163,10 @@ export class CanvasControllerImpl implements CanvasController {
return this.model.selected;
}
public get configuration(): Configuration {
return this.model.configuration;
}
public set mode(value: Mode) {
this.model.mode = value;
}

@ -43,7 +43,7 @@ export interface ActiveElement {
export enum RectDrawingMethod {
CLASSIC = 'By 2 points',
EXTREME_POINTS = 'By 4 points'
EXTREME_POINTS = 'By 4 points',
}
export enum CuboidDrawingMethod {
@ -56,6 +56,7 @@ export interface Configuration {
displayAllText?: boolean;
undefinedAttrValue?: string;
showProjections?: boolean;
forceDisableEditing?: boolean;
}
export interface DrawData {
@ -69,6 +70,20 @@ export interface DrawData {
redraw?: number;
}
export interface InteractionData {
enabled: boolean;
shapeType?: string;
crosshair?: boolean;
minPosVertices?: number;
minNegVertices?: number;
}
export interface InteractionResult {
points: number[];
shapeType: string;
button: number;
}
export interface EditData {
enabled: boolean;
state: any;
@ -99,12 +114,14 @@ export enum UpdateReasons {
IMAGE_MOVED = 'image_moved',
GRID_UPDATED = 'grid_updated',
ISSUE_REGIONS_UPDATED = 'issue_regions_updated',
OBJECTS_UPDATED = 'objects_updated',
SHAPE_ACTIVATED = 'shape_activated',
SHAPE_FOCUSED = 'shape_focused',
FITTED_CANVAS = 'fitted_canvas',
INTERACT = 'interact',
DRAW = 'draw',
MERGE = 'merge',
SPLIT = 'split',
@ -112,9 +129,11 @@ export enum UpdateReasons {
SELECT = 'select',
CANCEL = 'cancel',
BITMAP = 'bitmap',
SELECT_REGION = 'select_region',
DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'zoom_canvas',
CONFIG_UPDATED = 'config_updated',
DATA_FAILED = 'data_failed',
}
export enum Mode {
@ -126,6 +145,8 @@ export enum Mode {
MERGE = 'merge',
SPLIT = 'split',
GROUP = 'group',
INTERACT = 'interact',
SELECT_REGION = 'select_region',
DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'zoom_canvas',
}
@ -133,12 +154,14 @@ export enum Mode {
export interface CanvasModel {
readonly imageBitmap: boolean;
readonly image: Image | null;
readonly issueRegions: Record<number, number[]>;
readonly objects: any[];
readonly zLayer: number | null;
readonly gridSize: Size;
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
readonly drawData: DrawData;
readonly interactionData: InteractionData;
readonly mergeData: MergeData;
readonly splitData: SplitData;
readonly groupData: GroupData;
@ -146,11 +169,13 @@ export interface CanvasModel {
readonly selected: any;
geometry: Geometry;
mode: Mode;
exception: Error | null;
zoom(x: number, y: number, direction: number): void;
move(topOffset: number, leftOffset: number): void;
setup(frameData: any, objectStates: any[], zLayer: number): void;
setupIssueRegions(issueRegions: Record<number, number[]>): void;
activate(clientID: number | null, attributeID: number | null): void;
rotate(rotationAngle: number): void;
focus(clientID: number, padding: number): void;
@ -162,9 +187,11 @@ export interface CanvasModel {
split(splitData: SplitData): void;
merge(mergeData: MergeData): void;
select(objectState: any): void;
interact(interactionData: InteractionData): void;
fitCanvas(width: number, height: number): void;
bitmap(enabled: boolean): void;
selectRegion(enabled: boolean): void;
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
@ -188,15 +215,18 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
gridSize: Size;
left: number;
objects: any[];
issueRegions: Record<number, number[]>;
scale: number;
top: number;
zLayer: number | null;
drawData: DrawData;
interactionData: InteractionData;
mergeData: MergeData;
groupData: GroupData;
splitData: SplitData;
selected: any;
mode: Mode;
exception: Error | null;
};
public constructor() {
@ -235,6 +265,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
},
left: 0,
objects: [],
issueRegions: {},
scale: 1,
top: 0,
zLayer: null,
@ -242,6 +273,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
enabled: false,
initialState: null,
},
interactionData: {
enabled: false,
},
mergeData: {
enabled: false,
},
@ -253,28 +287,29 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
},
selected: null,
mode: Mode.IDLE,
exception: null,
};
}
public zoom(x: number, y: number, direction: number): void {
const oldScale: number = this.data.scale;
const newScale: number = direction > 0 ? oldScale * 6 / 5 : oldScale * 5 / 6;
const newScale: number = direction > 0 ? (oldScale * 6) / 5 : (oldScale * 5) / 6;
this.data.scale = Math.min(Math.max(newScale, FrameZoom.MIN), FrameZoom.MAX);
const { angle } = this.data;
const mutiplier = Math.sin(angle * Math.PI / 180) + Math.cos(angle * Math.PI / 180);
const mutiplier = Math.sin((angle * Math.PI) / 180) + Math.cos((angle * Math.PI) / 180);
if ((angle / 90) % 2) {
// 90, 270, ..
this.data.top += mutiplier * ((x - this.data.imageSize.width / 2)
* (oldScale / this.data.scale - 1)) * this.data.scale;
this.data.left -= mutiplier * ((y - this.data.imageSize.height / 2)
* (oldScale / this.data.scale - 1)) * this.data.scale;
const topMultiplier = (x - this.data.imageSize.width / 2) * (oldScale / this.data.scale - 1);
const leftMultiplier = (y - this.data.imageSize.height / 2) * (oldScale / this.data.scale - 1);
this.data.top += mutiplier * topMultiplier * this.data.scale;
this.data.left -= mutiplier * leftMultiplier * this.data.scale;
} else {
this.data.left += mutiplier * ((x - this.data.imageSize.width / 2)
* (oldScale / this.data.scale - 1)) * this.data.scale;
this.data.top += mutiplier * ((y - this.data.imageSize.height / 2)
* (oldScale / this.data.scale - 1)) * this.data.scale;
const leftMultiplier = (x - this.data.imageSize.width / 2) * (oldScale / this.data.scale - 1);
const topMultiplier = (y - this.data.imageSize.height / 2) * (oldScale / this.data.scale - 1);
this.data.left += mutiplier * leftMultiplier * this.data.scale;
this.data.top += mutiplier * topMultiplier * this.data.scale;
}
this.notify(UpdateReasons.IMAGE_ZOOMED);
@ -290,10 +325,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.canvasSize.height = height;
this.data.canvasSize.width = width;
this.data.imageOffset = Math.floor(Math.max(
this.data.canvasSize.height / FrameZoom.MIN,
this.data.canvasSize.width / FrameZoom.MIN,
));
this.data.imageOffset = Math.floor(
Math.max(this.data.canvasSize.height / FrameZoom.MIN, this.data.canvasSize.width / FrameZoom.MIN),
);
this.notify(UpdateReasons.FITTED_CANVAS);
this.notify(UpdateReasons.OBJECTS_UPDATED);
@ -304,6 +338,19 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.BITMAP);
}
public selectRegion(enable: boolean): void {
if (enable && this.data.mode !== Mode.IDLE) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (!enable && this.data.mode !== Mode.SELECT_REGION) {
throw Error(`Canvas is not in the region selecting mode. Action: ${this.data.mode}`);
}
this.data.mode = enable ? Mode.SELECT_REGION : Mode.IDLE;
this.notify(UpdateReasons.SELECT_REGION);
}
public dragCanvas(enable: boolean): void {
if (enable && this.data.mode !== Mode.IDLE) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
@ -345,36 +392,42 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
this.data.imageID = frameData.number;
frameData.data(
(): void => {
frameData
.data((): void => {
this.data.image = null;
this.notify(UpdateReasons.IMAGE_CHANGED);
},
).then((data: Image): void => {
if (frameData.number !== this.data.imageID) {
// already another image
return;
}
})
.then((data: Image): void => {
if (frameData.number !== this.data.imageID) {
// already another image
return;
}
this.data.imageSize = {
height: (frameData.height as number),
width: (frameData.width as number),
};
this.data.imageSize = {
height: frameData.height as number,
width: frameData.width as number,
};
this.data.image = data;
this.notify(UpdateReasons.IMAGE_CHANGED);
this.data.zLayer = zLayer;
this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED);
}).catch((exception: any): void => {
throw exception;
});
this.data.image = data;
this.notify(UpdateReasons.IMAGE_CHANGED);
this.data.zLayer = zLayer;
this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED);
})
.catch((exception: any): void => {
this.data.exception = exception;
this.notify(UpdateReasons.DATA_FAILED);
throw exception;
});
}
public setupIssueRegions(issueRegions: Record<number, number[]>): void {
this.data.issueRegions = issueRegions;
this.notify(UpdateReasons.ISSUE_REGIONS_UPDATED);
}
public activate(clientID: number | null, attributeID: number | null): void {
if (this.data.activeElement.clientID === clientID
&& this.data.activeElement.attributeID === attributeID
) {
if (this.data.activeElement.clientID === clientID && this.data.activeElement.attributeID === attributeID) {
return;
}
@ -382,9 +435,8 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (typeof (clientID) === 'number') {
const [state] = this.objects
.filter((_state: any): boolean => _state.clientID === clientID);
if (typeof clientID === 'number') {
const [state] = this.objects.filter((_state: any): boolean => _state.clientID === clientID);
if (!state || state.objectType === 'tag') {
return;
}
@ -400,7 +452,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
public rotate(rotationAngle: number): void {
if (this.data.angle !== rotationAngle) {
this.data.angle = (360 + Math.floor((rotationAngle) / 90) * 90) % 360;
this.data.angle = (360 + Math.floor(rotationAngle / 90) * 90) % 360;
this.fit();
}
}
@ -430,13 +482,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
);
}
this.data.scale = Math.min(
Math.max(this.data.scale, FrameZoom.MIN),
FrameZoom.MAX,
);
this.data.scale = Math.min(Math.max(this.data.scale, FrameZoom.MIN), FrameZoom.MAX);
this.data.top = (this.data.canvasSize.height / 2 - this.data.imageSize.height / 2);
this.data.left = (this.data.canvasSize.width / 2 - this.data.imageSize.width / 2);
this.data.top = this.data.canvasSize.height / 2 - this.data.imageSize.height / 2;
this.data.left = this.data.canvasSize.width / 2 - this.data.imageSize.width / 2;
this.notify(UpdateReasons.IMAGE_FITTED);
}
@ -460,7 +509,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
throw new Error('Drawing has been already started');
} else if (!drawData.shapeType && !drawData.initialState) {
throw new Error('A shape type is not specified');
} else if (typeof (drawData.numberOfPoints) !== 'undefined') {
} else if (typeof drawData.numberOfPoints !== 'undefined') {
if (drawData.shapeType === 'polygon' && drawData.numberOfPoints < 3) {
throw new Error('A polygon consists of at least 3 points');
} else if (drawData.shapeType === 'polyline' && drawData.numberOfPoints < 2) {
@ -469,10 +518,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
}
if (typeof (drawData.redraw) === 'number') {
if (typeof drawData.redraw === 'number') {
const clientID = drawData.redraw;
const [state] = this.data.objects
.filter((_state: any): boolean => _state.clientID === clientID);
const [state] = this.data.objects.filter((_state: any): boolean => _state.clientID === clientID);
if (state) {
this.data.drawData = { ...drawData };
@ -490,6 +538,27 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.DRAW);
}
public interact(interactionData: InteractionData): void {
if (![Mode.IDLE, Mode.INTERACT].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (interactionData.enabled) {
if (this.data.interactionData.enabled) {
throw new Error('Interaction has been already started');
} else if (!interactionData.shapeType) {
throw new Error('A shape type was not specified');
}
}
this.data.interactionData = interactionData;
if (typeof this.data.interactionData.crosshair !== 'boolean') {
this.data.interactionData.crosshair = true;
}
this.notify(UpdateReasons.INTERACT);
}
public split(splitData: SplitData): void {
if (![Mode.IDLE, Mode.SPLIT].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
@ -548,27 +617,31 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public configure(configuration: Configuration): void {
if (typeof (configuration.displayAllText) !== 'undefined') {
if (typeof configuration.displayAllText !== 'undefined') {
this.data.configuration.displayAllText = configuration.displayAllText;
}
if (typeof (configuration.showProjections) !== 'undefined') {
if (typeof configuration.showProjections !== 'undefined') {
this.data.configuration.showProjections = configuration.showProjections;
}
if (typeof (configuration.autoborders) !== 'undefined') {
if (typeof configuration.autoborders !== 'undefined') {
this.data.configuration.autoborders = configuration.autoborders;
}
if (typeof (configuration.undefinedAttrValue) !== 'undefined') {
if (typeof configuration.undefinedAttrValue !== 'undefined') {
this.data.configuration.undefinedAttrValue = configuration.undefinedAttrValue;
}
if (typeof configuration.forceDisableEditing !== 'undefined') {
this.data.configuration.forceDisableEditing = configuration.forceDisableEditing;
}
this.notify(UpdateReasons.CONFIG_UPDATED);
}
public isAbleToChangeFrame(): boolean {
const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE].includes(this.data.mode)
|| (this.data.mode === Mode.DRAW && typeof (this.data.drawData.redraw) === 'number');
const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE, Mode.INTERACT].includes(this.data.mode)
|| (this.data.mode === Mode.DRAW && typeof this.data.drawData.redraw === 'number');
return !isUnable;
}
@ -604,10 +677,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.imageOffset = geometry.offset;
this.data.scale = geometry.scale;
this.data.imageOffset = Math.floor(Math.max(
this.data.canvasSize.height / FrameZoom.MIN,
this.data.canvasSize.width / FrameZoom.MIN,
));
this.data.imageOffset = Math.floor(
Math.max(this.data.canvasSize.height / FrameZoom.MIN, this.data.canvasSize.width / FrameZoom.MIN),
);
}
public get zLayer(): number | null {
@ -622,10 +694,13 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return this.data.image;
}
public get issueRegions(): Record<number, number[]> {
return { ...this.data.issueRegions };
}
public get objects(): any[] {
if (this.data.zLayer !== null) {
return this.data.objects
.filter((object: any): boolean => object.zOrder <= this.data.zLayer);
return this.data.objects.filter((object: any): boolean => object.zOrder <= this.data.zLayer);
}
return this.data.objects;
@ -647,6 +722,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return { ...this.data.drawData };
}
public get interactionData(): InteractionData {
return { ...this.data.interactionData };
}
public get mergeData(): MergeData {
return { ...this.data.mergeData };
}
@ -670,4 +749,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
public get mode(): Mode {
return this.data.mode;
}
public get exception(): Error {
return this.data.exception;
}
}

File diff suppressed because it is too large Load Diff

@ -2,13 +2,13 @@
//
// SPDX-License-Identifier: MIT
const BASE_STROKE_WIDTH = 1.75;
const BASE_STROKE_WIDTH = 1.25;
const BASE_GRID_WIDTH = 2;
const BASE_POINT_SIZE = 5;
const TEXT_MARGIN = 10;
const AREA_THRESHOLD = 9;
const SIZE_THRESHOLD = 3;
const POINTS_STROKE_WIDTH = 1.5;
const POINTS_STROKE_WIDTH = 1;
const POINTS_SELECTED_STROKE_WIDTH = 4;
const MIN_EDGE_LENGTH = 3;
const CUBOID_ACTIVE_EDGE_STROKE_WIDTH = 2.5;
@ -16,6 +16,7 @@ const CUBOID_UNACTIVE_EDGE_STROKE_WIDTH = 1.75;
const UNDEFINED_ATTRIBUTE_VALUE = '__undefined__';
const ARROW_PATH = 'M13.162 6.284L.682.524a.483.483 0 0 0-.574.134.477.477 0 '
+ '0 0-.012.59L4.2 6.72.096 12.192a.479.479 0 0 0 .585.724l12.48-5.76a.48.48 0 0 0 0-.872z';
const BASE_PATTERN_SIZE = 5;
export default {
BASE_STROKE_WIDTH,
@ -31,4 +32,5 @@ export default {
CUBOID_UNACTIVE_EDGE_STROKE_WIDTH,
UNDEFINED_ATTRIBUTE_VALUE,
ARROW_PATH,
BASE_PATTERN_SIZE,
};

@ -0,0 +1,76 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
export default class Crosshair {
private x: SVG.Line | null;
private y: SVG.Line | null;
private canvas: SVG.Container | null;
public constructor() {
this.x = null;
this.y = null;
this.canvas = null;
}
public show(canvas: SVG.Container, x: number, y: number, scale: number): void {
if (this.canvas && this.canvas !== canvas) {
if (this.x) this.x.remove();
if (this.y) this.y.remove();
this.x = null;
this.y = null;
}
this.canvas = canvas;
this.x = this.canvas
.line(0, y, this.canvas.node.clientWidth, y)
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * scale),
})
.addClass('cvat_canvas_crosshair');
this.y = this.canvas
.line(x, 0, x, this.canvas.node.clientHeight)
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * scale),
})
.addClass('cvat_canvas_crosshair');
}
public hide(): void {
if (this.x) {
this.x.remove();
this.x = null;
}
if (this.y) {
this.y.remove();
this.y = null;
}
this.canvas = null;
}
public move(x: number, y: number): void {
if (this.x) {
this.x.attr({ y1: y, y2: y });
}
if (this.y) {
this.y.attr({ x1: x, x2: x });
}
}
public scale(scale: number): void {
if (this.x) {
this.x.attr('stroke-width', consts.BASE_STROKE_WIDTH / (2 * scale));
}
if (this.y) {
this.y.attr('stroke-width', consts.BASE_STROKE_WIDTH / (2 * scale));
}
}
}

@ -1,12 +1,3 @@
/* eslint-disable func-names */
/* eslint-disable no-underscore-dangle */
/* eslint-disable curly */
/*
* Copyright (C) 2020 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
import consts from './consts';
export interface Point {
@ -26,9 +17,7 @@ function line(p1: Point, p2: Point): number[] {
return [a, b, c];
}
function intersection(
p1: Point, p2: Point, p3: Point, p4: Point,
): Point | null {
function intersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null {
const L1 = line(p1, p2);
const L2 = line(p3, p4);
@ -246,8 +235,20 @@ export class CuboidModel {
this.rb = new Edge([3, 5], this.points);
this.db = new Edge([7, 5], this.points);
this.edgeList = [this.fl, this.fr, this.dl, this.dr, this.ft, this.lt,
this.rt, this.dt, this.fb, this.lb, this.rb, this.db];
this.edgeList = [
this.fl,
this.fr,
this.dl,
this.dr,
this.ft,
this.lt,
this.rt,
this.dt,
this.fb,
this.lb,
this.rb,
this.db,
];
}
private initFaces(): void {
@ -324,7 +325,7 @@ function sortPointsClockwise(points: any[]): any[] {
let ang = Math.atan2(point.y - center.y, point.x - center.x);
if (!startAng) {
startAng = ang;
// ensure that all points are clockwise of the start point
// ensure that all points are clockwise of the start point
} else if (ang < startAng) {
ang += Math.PI * 2;
}
@ -347,18 +348,18 @@ function setupCuboidPoints(points: Point[]): any[] {
let p3;
let p4;
const height = Math.abs(points[0].x - points[1].x)
< Math.abs(points[1].x - points[2].x)
? Math.abs(points[1].y - points[0].y)
: Math.abs(points[1].y - points[2].y);
const height =
Math.abs(points[0].x - points[1].x) < Math.abs(points[1].x - points[2].x)
? Math.abs(points[1].y - points[0].y)
: Math.abs(points[1].y - points[2].y);
// seperate into left and right point
// we pick the first and third point because we know assume they will be on
// opposite corners
if (points[0].x < points[2].x) {
[left,, right] = points;
[left, , right] = points;
} else {
[right,, left] = points;
[right, , left] = points;
}
// get other 2 points using the given height
@ -408,7 +409,7 @@ export function cuboidFrom4Points(flattenedPoints: any[]): any[] {
points.push({ x, y });
}
const unsortedPlanePoints = points.slice(0, 3);
function rotate(array: any[], times: number): void{
function rotate(array: any[], times: number): void {
let t = times;
while (t--) {
const temp = array.shift();
@ -460,28 +461,21 @@ export function cuboidFrom4Points(flattenedPoints: any[]): any[] {
plane2.p3 = { x: plane1.p3.x + vec.x, y: plane1.p3.y + vec.y };
plane2.p4 = { x: plane1.p4.x + vec.x, y: plane1.p4.y + vec.y };
let cuboidPoints;
// right
if (Math.abs(angle) < Math.PI / 2 - 0.1) {
cuboidPoints = setupCuboidPoints(points);
// left
// left
} else if (Math.abs(angle) > Math.PI / 2 + 0.1) {
cuboidPoints = setupCuboidPoints(points);
// down
// down
} else if (angle > 0) {
cuboidPoints = [
plane1.p1, plane2.p1, plane1.p2, plane2.p2,
plane1.p3, plane2.p3, plane1.p4, plane2.p4,
];
cuboidPoints = [plane1.p1, plane2.p1, plane1.p2, plane2.p2, plane1.p3, plane2.p3, plane1.p4, plane2.p4];
cuboidPoints[0].y += 0.1;
cuboidPoints[4].y += 0.1;
// up
// up
} else {
cuboidPoints = [
plane2.p1, plane1.p1, plane2.p2, plane1.p2,
plane2.p3, plane1.p3, plane2.p4, plane1.p4,
];
cuboidPoints = [plane2.p1, plane1.p1, plane2.p2, plane1.p2, plane2.p3, plane1.p3, plane2.p4, plane1.p4];
cuboidPoints[0].y += 0.1;
cuboidPoints[4].y += 0.1;
}

@ -16,14 +16,9 @@ import {
BBox,
Box,
} from './shared';
import Crosshair from './crosshair';
import consts from './consts';
import {
DrawData,
Geometry,
RectDrawingMethod,
Configuration,
CuboidDrawingMethod,
} from './canvasModel';
import { DrawData, Geometry, RectDrawingMethod, Configuration, CuboidDrawingMethod } from './canvasModel';
import { cuboidFrom4Points } from './cuboid';
@ -44,10 +39,7 @@ export class DrawHandlerImpl implements DrawHandler {
x: number;
y: number;
};
private crosshair: {
x: SVG.Line;
y: SVG.Line;
};
private crosshair: Crosshair;
private drawData: DrawData;
private geometry: Geometry;
private autoborderHandler: AutoborderHandler;
@ -66,8 +58,9 @@ export class DrawHandlerImpl implements DrawHandler {
const frameHeight = this.geometry.image.height;
const { offset } = this.geometry;
let [xtl, ytl, xbr, ybr] = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height]
.map((coord: number): number => coord - offset);
let [xtl, ytl, xbr, ybr] = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height].map(
(coord: number): number => coord - offset,
);
xtl = Math.min(Math.max(xtl, 0), frameWidth);
xbr = Math.min(Math.max(xbr, 0), frameWidth);
@ -77,7 +70,9 @@ export class DrawHandlerImpl implements DrawHandler {
return [xtl, ytl, xbr, ybr];
}
private getFinalPolyshapeCoordinates(targetPoints: number[]): {
private getFinalPolyshapeCoordinates(
targetPoints: number[],
): {
points: number[];
box: Box;
} {
@ -108,7 +103,9 @@ export class DrawHandlerImpl implements DrawHandler {
};
}
private getFinalCuboidCoordinates(targetPoints: number[]): {
private getFinalCuboidCoordinates(
targetPoints: number[],
): {
points: number[];
box: Box;
} {
@ -135,8 +132,7 @@ export class DrawHandlerImpl implements DrawHandler {
for (let i = 0; i < points.length - 1; i += 2) {
const [x, y] = points.slice(i);
if (x >= offset && x <= offset + frameWidth
&& y >= offset && y <= offset + frameHeight) continue;
if (x >= offset && x <= offset + frameWidth && y >= offset && y <= offset + frameHeight) continue;
let xOffset = 0;
let yOffset = 0;
@ -158,9 +154,8 @@ export class DrawHandlerImpl implements DrawHandler {
if (cuboidOffsets.length === points.length / 2) {
cuboidOffsets.forEach((offsetCoords: number[]): void => {
if (Math.sqrt((offsetCoords[0] ** 2) + (offsetCoords[1] ** 2))
< minCuboidOffset.d) {
minCuboidOffset.d = Math.sqrt((offsetCoords[0] ** 2) + (offsetCoords[1] ** 2));
if (Math.sqrt(offsetCoords[0] ** 2 + offsetCoords[1] ** 2) < minCuboidOffset.d) {
minCuboidOffset.d = Math.sqrt(offsetCoords[0] ** 2 + offsetCoords[1] ** 2);
[minCuboidOffset.dx, minCuboidOffset.dy] = offsetCoords;
}
});
@ -188,22 +183,11 @@ export class DrawHandlerImpl implements DrawHandler {
private addCrosshair(): void {
const { x, y } = this.cursorPosition;
this.crosshair = {
x: this.canvas.line(0, y, this.canvas.node.clientWidth, y).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
y: this.canvas.line(x, 0, x, this.canvas.node.clientHeight).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
};
this.crosshair.show(this.canvas, x, y, this.geometry.scale);
}
private removeCrosshair(): void {
this.crosshair.x.remove();
this.crosshair.y.remove();
this.crosshair = null;
this.crosshair.hide();
}
private release(): void {
@ -228,8 +212,10 @@ export class DrawHandlerImpl implements DrawHandler {
// Or when no drawn points, but we call cancel() drawing
// We check if it is activated with remember function
if (this.drawInstance.remember('_paintHandler')) {
if (this.drawData.shapeType !== 'rectangle'
&& this.drawData.cuboidDrawingMethod !== CuboidDrawingMethod.CLASSIC) {
if (
this.drawData.shapeType !== 'rectangle' &&
this.drawData.cuboidDrawingMethod !== CuboidDrawingMethod.CLASSIC
) {
// Check for unsaved drawn shapes
this.drawInstance.draw('done');
}
@ -261,37 +247,48 @@ export class DrawHandlerImpl implements DrawHandler {
private drawBox(): void {
this.drawInstance = this.canvas.rect();
this.drawInstance.on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType, redraw: clientID } = this.drawData;
this.release();
this.drawInstance
.on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType, redraw: clientID } = this.drawData;
this.release();
if (this.canceled) return;
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone({
clientID,
shapeType,
points: [xtl, ytl, xbr, ybr],
}, Date.now() - this.startTimestamp);
}
}).on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
}).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
if (this.canceled) return;
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone(
{
clientID,
shapeType,
points: [xtl, ytl, xbr, ybr],
},
Date.now() - this.startTimestamp,
);
}
})
.on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
})
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
}
private drawBoxBy4Points(): void {
let numberOfPoints = 0;
this.drawInstance = (this.canvas as any).polygon()
.addClass('cvat_canvas_shape_drawing').attr({
this.drawInstance = (this.canvas as any)
.polygon()
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': 0,
opacity: 0,
}).on('drawstart', (): void => {
})
.on('drawstart', (): void => {
// init numberOfPoints as one on drawstart
numberOfPoints = 1;
}).on('drawpoint', (e: CustomEvent): void => {
})
.on('drawpoint', (e: CustomEvent): void => {
// increase numberOfPoints by one on drawpoint
numberOfPoints += 1;
@ -303,14 +300,18 @@ export class DrawHandlerImpl implements DrawHandler {
this.cancel();
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone({
shapeType,
clientID,
points: [xtl, ytl, xbr, ybr],
}, Date.now() - this.startTimestamp);
this.onDrawDone(
{
shapeType,
clientID,
points: [xtl, ytl, xbr, ybr],
},
Date.now() - this.startTimestamp,
);
}
}
}).on('undopoint', (): void => {
})
.on('undopoint', (): void => {
if (numberOfPoints > 0) {
numberOfPoints -= 1;
}
@ -324,12 +325,16 @@ export class DrawHandlerImpl implements DrawHandler {
const sizeDecrement = (): void => {
if (--size === 0) {
this.drawInstance.draw('done');
// we need additional settimeout because we cannot invoke draw('done')
// from event listener for drawstart event
// because of implementation of svg.js
setTimeout((): void => this.drawInstance.draw('done'));
}
};
this.drawInstance.on('drawstart', sizeDecrement);
this.drawInstance.on('drawpoint', sizeDecrement);
this.drawInstance.on('drawupdate', (): void => this.transform(this.geometry));
this.drawInstance.on('undopoint', (): number => size++);
// Add ability to cancel the latest drawn point
@ -360,10 +365,7 @@ export class DrawHandlerImpl implements DrawHandler {
} else {
this.drawInstance.draw('update', e);
const deltaTreshold = 15;
const delta = Math.sqrt(
((e.clientX - lastDrawnPoint.x) ** 2)
+ ((e.clientY - lastDrawnPoint.y) ** 2),
);
const delta = Math.sqrt((e.clientX - lastDrawnPoint.x) ** 2 + (e.clientY - lastDrawnPoint.y) ** 2);
if (delta > deltaTreshold) {
this.drawInstance.draw('point', e);
}
@ -384,50 +386,67 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance.on('drawdone', (e: CustomEvent): void => {
const targetPoints = pointsToNumberArray((e.target as SVGElement).getAttribute('points'));
const { shapeType, redraw: clientID } = this.drawData;
const { points, box } = shapeType === 'cuboid' ? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
const { points, box } =
shapeType === 'cuboid'
? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
this.release();
if (this.canceled) return;
if (shapeType === 'polygon'
&& ((box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD)
&& points.length >= 3 * 2) {
this.onDrawDone({
clientID,
shapeType,
points,
}, Date.now() - this.startTimestamp);
} else if (shapeType === 'polyline'
&& ((box.xbr - box.xtl) >= consts.SIZE_THRESHOLD
|| (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD)
&& points.length >= 2 * 2) {
this.onDrawDone({
clientID,
shapeType,
points,
}, Date.now() - this.startTimestamp);
} else if (shapeType === 'points'
&& (e.target as any).getAttribute('points') !== '0,0') {
this.onDrawDone({
clientID,
shapeType,
points,
}, Date.now() - this.startTimestamp);
if (
shapeType === 'polygon' &&
(box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD &&
points.length >= 3 * 2
) {
this.onDrawDone(
{
clientID,
shapeType,
points,
},
Date.now() - this.startTimestamp,
);
} else if (
shapeType === 'polyline' &&
(box.xbr - box.xtl >= consts.SIZE_THRESHOLD || box.ybr - box.ytl >= consts.SIZE_THRESHOLD) &&
points.length >= 2 * 2
) {
this.onDrawDone(
{
clientID,
shapeType,
points,
},
Date.now() - this.startTimestamp,
);
} else if (shapeType === 'points' && (e.target as any).getAttribute('points') !== '0,0') {
this.onDrawDone(
{
clientID,
shapeType,
points,
},
Date.now() - this.startTimestamp,
);
// TODO: think about correct constraign for cuboids
} else if (shapeType === 'cuboid'
&& points.length === 4 * 2) {
this.onDrawDone({
clientID,
shapeType,
points: cuboidFrom4Points(points),
}, Date.now() - this.startTimestamp);
} else if (shapeType === 'cuboid' && points.length === 4 * 2) {
this.onDrawDone(
{
clientID,
shapeType,
points: cuboidFrom4Points(points),
},
Date.now() - this.startTimestamp,
);
}
});
}
private drawPolygon(): void {
this.drawInstance = (this.canvas as any).polygon()
.addClass('cvat_canvas_shape_drawing').attr({
this.drawInstance = (this.canvas as any)
.polygon()
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
@ -438,8 +457,10 @@ export class DrawHandlerImpl implements DrawHandler {
}
private drawPolyline(): void {
this.drawInstance = (this.canvas as any).polyline()
.addClass('cvat_canvas_shape_drawing').attr({
this.drawInstance = (this.canvas as any)
.polyline()
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': 0,
});
@ -451,18 +472,19 @@ export class DrawHandlerImpl implements DrawHandler {
}
private drawPoints(): void {
this.drawInstance = (this.canvas as any).polygon()
.addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': 0,
opacity: 0,
});
this.drawInstance = (this.canvas as any).polygon().addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': 0,
opacity: 0,
});
this.drawPolyshape();
}
private drawCuboidBy4Points(): void {
this.drawInstance = (this.canvas as any).polyline()
.addClass('cvat_canvas_shape_drawing').attr({
this.drawInstance = (this.canvas as any)
.polyline()
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.drawPolyshape();
@ -470,25 +492,32 @@ export class DrawHandlerImpl implements DrawHandler {
private drawCuboid(): void {
this.drawInstance = this.canvas.rect();
this.drawInstance.on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType } = this.drawData;
this.release();
this.drawInstance
.on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType } = this.drawData;
this.release();
if (this.canceled) return;
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
const d = { x: (xbr - xtl) * 0.1, y: (ybr - ytl) * 0.1 };
this.onDrawDone({
shapeType,
points: cuboidFrom4Points([xtl, ybr, xbr, ybr, xbr, ytl, xbr + d.x, ytl - d.y]),
}, Date.now() - this.startTimestamp);
}
}).on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
}).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
if (this.canceled) return;
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
const d = { x: (xbr - xtl) * 0.1, y: (ybr - ytl) * 0.1 };
this.onDrawDone(
{
shapeType,
points: cuboidFrom4Points([xtl, ybr, xbr, ybr, xbr, ytl, xbr + d.x, ytl - d.y]),
},
Date.now() - this.startTimestamp,
);
}
})
.on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
})
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
}
private pastePolyshape(): void {
@ -498,22 +527,28 @@ export class DrawHandlerImpl implements DrawHandler {
.split(/[,\s]/g)
.map((coord: string): number => +coord);
const { points } = this.drawData.initialState.shapeType === 'cuboid' ? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
const { points } =
this.drawData.initialState.shapeType === 'cuboid'
? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
this.onDrawDone({
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points,
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
}, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey);
this.onDrawDone(
{
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points,
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
},
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
);
});
}
@ -534,9 +569,11 @@ export class DrawHandlerImpl implements DrawHandler {
}
private pasteBox(box: BBox): void {
this.drawInstance = (this.canvas as any).rect(box.width, box.height)
this.drawInstance = (this.canvas as any)
.rect(box.width, box.height)
.move(box.x, box.y)
.addClass('cvat_canvas_shape_drawing').attr({
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.pasteShape();
@ -548,22 +585,27 @@ export class DrawHandlerImpl implements DrawHandler {
this.release();
}
this.onDrawDone({
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points: [xtl, ytl, xbr, ybr],
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
}, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey);
this.onDrawDone(
{
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points: [xtl, ytl, xbr, ybr],
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
},
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
);
});
}
private pastePolygon(points: string): void {
this.drawInstance = (this.canvas as any).polygon(points)
.addClass('cvat_canvas_shape_drawing').attr({
this.drawInstance = (this.canvas as any)
.polygon(points)
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.pasteShape();
@ -571,8 +613,10 @@ export class DrawHandlerImpl implements DrawHandler {
}
private pastePolyline(points: string): void {
this.drawInstance = (this.canvas as any).polyline(points)
.addClass('cvat_canvas_shape_drawing').attr({
this.drawInstance = (this.canvas as any)
.polyline(points)
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.pasteShape();
@ -580,22 +624,19 @@ export class DrawHandlerImpl implements DrawHandler {
}
private pasteCuboid(points: string): void {
this.drawInstance = (this.canvas as any).cube(points).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'face-stroke': 'black',
});
this.drawInstance = (this.canvas as any)
.cube(points)
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'face-stroke': 'black',
});
this.pasteShape();
this.pastePolyshape();
}
private pastePoints(initialPoints: string): void {
function moveShape(
shape: SVG.PolyLine,
group: SVG.G,
x: number,
y: number,
scale: number,
): void {
function moveShape(shape: SVG.PolyLine, group: SVG.G, x: number, y: number, scale: number): void {
const bbox = shape.bbox();
shape.move(x - bbox.width / 2, y - bbox.height / 2);
@ -610,10 +651,9 @@ export class DrawHandlerImpl implements DrawHandler {
const { x: initialX, y: initialY } = this.cursorPosition;
this.pointsGroup = this.canvas.group();
this.drawInstance = (this.canvas as any).polyline(initialPoints)
.addClass('cvat_canvas_shape_drawing').style({
'stroke-width': 0,
});
this.drawInstance = (this.canvas as any).polyline(initialPoints).addClass('cvat_canvas_shape_drawing').style({
'stroke-width': 0,
});
let numOfPoints = initialPoints.split(' ').length;
while (numOfPoints) {
@ -626,15 +666,11 @@ export class DrawHandlerImpl implements DrawHandler {
});
}
moveShape(
this.drawInstance, this.pointsGroup, initialX, initialY, this.geometry.scale,
);
moveShape(this.drawInstance, this.pointsGroup, initialX, initialY, this.geometry.scale);
this.canvas.on('mousemove.draw', (): void => {
const { x, y } = this.cursorPosition; // was computer in another callback
moveShape(
this.drawInstance, this.pointsGroup, x, y, this.geometry.scale,
);
moveShape(this.drawInstance, this.pointsGroup, x, y, this.geometry.scale);
});
this.pastePolyshape();
@ -668,8 +704,9 @@ export class DrawHandlerImpl implements DrawHandler {
if (this.drawData.initialState) {
const { offset } = this.geometry;
if (this.drawData.shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = this.drawData.initialState.points
.map((coord: number): number => coord + offset);
const [xtl, ytl, xbr, ybr] = this.drawData.initialState.points.map(
(coord: number): number => coord + offset,
);
this.pasteBox({
x: xtl,
@ -678,8 +715,7 @@ export class DrawHandlerImpl implements DrawHandler {
height: ybr - ytl,
});
} else {
const points = this.drawData.initialState.points
.map((coord: number): number => coord + offset);
const points = this.drawData.initialState.points.map((coord: number): number => coord + offset);
const stringifiedPoints = stringifyPoints(points);
if (this.drawData.shapeType === 'polygon') {
@ -741,7 +777,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.canceled = false;
this.drawData = null;
this.geometry = null;
this.crosshair = null;
this.crosshair = new Crosshair();
this.drawInstance = null;
this.pointsGroup = null;
this.cursorPosition = {
@ -750,28 +786,20 @@ export class DrawHandlerImpl implements DrawHandler {
};
this.canvas.on('mousemove.crosshair', (e: MouseEvent): void => {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
const [x, y] = translateToSVG((this.canvas.node as any) as SVGSVGElement, [e.clientX, e.clientY]);
this.cursorPosition = { x, y };
if (this.crosshair) {
this.crosshair.x.attr({ y1: y, y2: y });
this.crosshair.y.attr({ x1: x, x2: x });
this.crosshair.move(x, y);
}
});
}
public configurate(configuration: Configuration): void {
if (typeof (configuration.autoborders) === 'boolean') {
if (typeof configuration.autoborders === 'boolean') {
this.autobordersEnabled = configuration.autoborders;
if (this.drawInstance) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(
true,
this.drawInstance,
this.drawData.redraw,
);
this.autoborderHandler.autoborder(true, this.drawInstance, this.drawData.redraw);
} else {
this.autoborderHandler.autoborder(false);
}
@ -787,12 +815,7 @@ export class DrawHandlerImpl implements DrawHandler {
}
if (this.crosshair) {
this.crosshair.x.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale),
});
this.crosshair.y.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale),
});
this.crosshair.scale(this.geometry.scale);
}
if (this.pointsGroup) {
@ -813,14 +836,8 @@ export class DrawHandlerImpl implements DrawHandler {
const paintHandler = this.drawInstance.remember('_paintHandler');
for (const point of (paintHandler as any).set.members) {
point.attr(
'stroke-width',
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
);
point.attr(
'r',
`${consts.BASE_POINT_SIZE / geometry.scale}`,
);
point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`);
point.attr('r', `${consts.BASE_POINT_SIZE / geometry.scale}`);
}
}
}

@ -47,7 +47,8 @@ export class EditHandlerImpl implements EditHandler {
if (e.button !== 0) return;
const { offset } = this.geometry;
const stringifiedPoints = `${head} ${this.editLine.node.getAttribute('points').slice(0, -2)}`;
const points = pointsToNumberArray(stringifiedPoints).slice(0, -2)
const points = pointsToNumberArray(stringifiedPoints)
.slice(0, -2)
.map((coord: number): number => coord - offset);
if (points.length >= minimumPoints * 2) {
@ -63,7 +64,7 @@ export class EditHandlerImpl implements EditHandler {
private startEdit(): void {
// get started coordinates
const [clientX, clientY] = translateFromSVG(
this.canvas.node as any as SVGSVGElement,
(this.canvas.node as any) as SVGSVGElement,
this.editedShape.attr('points').split(' ')[this.editData.pointID].split(','),
);
@ -92,10 +93,7 @@ export class EditHandlerImpl implements EditHandler {
(this.editLine as any).draw('point', e);
} else {
const deltaTreshold = 15;
const delta = Math.sqrt(
((e.clientX - lastDrawnPoint.x) ** 2)
+ ((e.clientY - lastDrawnPoint.y) ** 2),
);
const delta = Math.sqrt((e.clientX - lastDrawnPoint.x) ** 2 + (e.clientY - lastDrawnPoint.y) ** 2);
if (delta > deltaTreshold) {
(this.editLine as any).draw('point', e);
}
@ -112,17 +110,23 @@ export class EditHandlerImpl implements EditHandler {
}
const strokeColor = this.editedShape.attr('stroke');
(this.editLine as any).addClass('cvat_canvas_shape_drawing').style({
'pointer-events': 'none',
'fill-opacity': 0,
'stroke': strokeColor,
}).attr({
'data-origin-client-id': this.editData.state.clientID,
}).on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry);
lastDrawnPoint.x = e.detail.event.clientX;
lastDrawnPoint.y = e.detail.event.clientY;
}).draw(dummyEvent, { snapToGrid: 0.1 });
(this.editLine as any)
.addClass('cvat_canvas_shape_drawing')
.style({
'pointer-events': 'none',
'fill-opacity': 0,
stroke: strokeColor,
})
.attr({
'data-origin-client-id': this.editData.state.clientID,
})
.on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry);
lastDrawnPoint.x = e.detail.event.clientX;
lastDrawnPoint.y = e.detail.event.clientY;
})
.on('drawupdate', (): void => this.transform(this.geometry))
.draw(dummyEvent, { snapToGrid: 0.1 });
if (this.editData.state.shapeType === 'points') {
this.editLine.attr('stroke-width', 0);
@ -140,9 +144,7 @@ export class EditHandlerImpl implements EditHandler {
if (e.button === 0 && !e.altKey) {
(this.editLine as any).draw('point', e);
} else if (e.button === 2 && this.editLine) {
if (this.editData.state.shapeType === 'points'
|| this.editLine.attr('points').split(' ').length > 2
) {
if (this.editData.state.shapeType === 'points' || this.editLine.attr('points').split(' ').length > 2) {
(this.editLine as any).draw('undo');
}
}
@ -151,8 +153,7 @@ export class EditHandlerImpl implements EditHandler {
private selectPolygon(shape: SVG.Polygon): void {
const { offset } = this.geometry;
const points = pointsToNumberArray(shape.attr('points'))
.map((coord: number): number => coord - offset);
const points = pointsToNumberArray(shape.attr('points')).map((coord: number): number => coord - offset);
const { state } = this.editData;
this.edit({
@ -167,8 +168,7 @@ export class EditHandlerImpl implements EditHandler {
}
// Get stop point and all points
const stopPointID = Array.prototype.indexOf
.call((e.target as HTMLElement).parentElement.children, e.target);
const stopPointID = Array.prototype.indexOf.call((e.target as HTMLElement).parentElement.children, e.target);
const oldPoints = this.editedShape.attr('points').trim().split(' ');
const linePoints = this.editLine.attr('points').trim().split(' ');
@ -178,8 +178,7 @@ export class EditHandlerImpl implements EditHandler {
}
// Compute new point array
const [start, stop] = [this.editData.pointID, stopPointID]
.sort((a, b): number => +a - +b);
const [start, stop] = [this.editData.pointID, stopPointID].sort((a, b): number => +a - +b);
if (this.editData.state.shapeType !== 'polygon') {
let points = null;
@ -189,15 +188,15 @@ export class EditHandlerImpl implements EditHandler {
if (start !== this.editData.pointID) {
linePoints.reverse();
}
points = oldPoints.slice(0, start)
points = oldPoints
.slice(0, start)
.concat(linePoints)
.concat(oldPoints.slice(stop + 1));
} else {
points = oldPoints.concat(linePoints.slice(0, -1));
}
points = pointsToNumberArray(points.join(' '))
.map((coord: number): number => coord - offset);
points = pointsToNumberArray(points.join(' ')).map((coord: number): number => coord - offset);
const { state } = this.editData;
this.edit({
@ -208,25 +207,27 @@ export class EditHandlerImpl implements EditHandler {
return;
}
const cutIndexes1 = oldPoints.reduce((acc: string[], _: string, i: number) =>
i >= stop || i <= start ? [...acc, i] : acc, []);
const cutIndexes2 = oldPoints.reduce((acc: string[], _: string, i: number) =>
i <= stop && i >= start ? [...acc, i] : acc, []);
const cutIndexes1 = oldPoints.reduce(
(acc: string[], _: string, i: number) => (i >= stop || i <= start ? [...acc, i] : acc),
[],
);
const cutIndexes2 = oldPoints.reduce(
(acc: string[], _: string, i: number) => (i <= stop && i >= start ? [...acc, i] : acc),
[],
);
const curveLength = (indexes: number[]) => {
const points = indexes.map((index: number): string => oldPoints[index])
const curveLength = (indexes: number[]): number => {
const points = indexes
.map((index: number): string => oldPoints[index])
.map((point: string): string[] => point.split(','))
.map((point: string[]): number[] => [+point[0], +point[1]]);
let length = 0;
for (let i = 1; i < points.length; i++) {
length += Math.sqrt(
(points[i][0] - points[i - 1][0]) ** 2
+ (points[i][1] - points[i - 1][1]) ** 2,
);
length += Math.sqrt((points[i][0] - points[i - 1][0]) ** 2 + (points[i][1] - points[i - 1][1]) ** 2);
}
return length;
}
};
const pointsCriteria = cutIndexes1.length > cutIndexes2.length;
const lengthCriteria = curveLength(cutIndexes1) > curveLength(cutIndexes2);
@ -235,11 +236,11 @@ export class EditHandlerImpl implements EditHandler {
linePoints.reverse();
}
const firstPart = oldPoints.slice(0, start)
const firstPart = oldPoints
.slice(0, start)
.concat(linePoints)
.concat(oldPoints.slice(stop + 1));
const secondPart = oldPoints.slice(start, stop)
.concat(linePoints.slice(1).reverse());
const secondPart = oldPoints.slice(start, stop).concat(linePoints.slice(1).reverse());
if (firstPart.length < 3 || secondPart.length < 3) {
this.cancel();
@ -263,23 +264,26 @@ export class EditHandlerImpl implements EditHandler {
this.selectPolygon(this.clones[0]);
} else {
for (const points of [firstPart, secondPart]) {
this.clones.push(this.canvas.polygon(points.join(' '))
.attr('fill', this.editedShape.attr('fill'))
.attr('fill-opacity', '0.5')
.addClass('cvat_canvas_shape'));
this.clones.push(
this.canvas
.polygon(points.join(' '))
.attr('fill', this.editedShape.attr('fill'))
.attr('fill-opacity', '0.5')
.addClass('cvat_canvas_shape'),
);
}
for (const clone of this.clones) {
clone.on('click', (): void => this.selectPolygon(clone));
clone.on('mouseenter', (): void => {
clone.addClass('cvat_canvas_shape_splitting');
}).on('mouseleave', (): void => {
clone.removeClass('cvat_canvas_shape_splitting');
});
clone
.on('mouseenter', (): void => {
clone.addClass('cvat_canvas_shape_splitting');
})
.on('mouseleave', (): void => {
clone.removeClass('cvat_canvas_shape_splitting');
});
}
}
return;
}
private setupPoints(enabled: boolean): void {
@ -289,7 +293,7 @@ export class EditHandlerImpl implements EditHandler {
if (enabled) {
(this.editedShape as any).selectize(true, {
deepSelect: true,
pointSize: 2 * consts.BASE_POINT_SIZE / self.geometry.scale,
pointSize: (2 * consts.BASE_POINT_SIZE) / self.geometry.scale,
rotationPoint: false,
pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested
@ -355,9 +359,7 @@ export class EditHandlerImpl implements EditHandler {
}
private initEditing(): void {
this.editedShape = this.canvas
.select(`#cvat_canvas_shape_${this.editData.state.clientID}`)
.first().clone();
this.editedShape = this.canvas.select(`#cvat_canvas_shape_${this.editData.state.clientID}`).first().clone();
this.setupPoints(true);
this.startEdit();
// draw points for this with selected and start editing till another point is clicked
@ -407,15 +409,11 @@ export class EditHandlerImpl implements EditHandler {
}
public configurate(configuration: Configuration): void {
if (typeof (configuration.autoborders) === 'boolean') {
if (typeof configuration.autoborders === 'boolean') {
this.autobordersEnabled = configuration.autoborders;
if (this.editLine) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(
true,
this.editLine,
this.editData.state.clientID,
);
this.autoborderHandler.autoborder(true, this.editLine, this.editData.state.clientID);
} else {
this.autoborderHandler.autoborder(false);
}
@ -443,14 +441,8 @@ export class EditHandlerImpl implements EditHandler {
const paintHandler = this.editLine.remember('_paintHandler');
for (const point of (paintHandler as any).set.members) {
point.attr(
'stroke-width',
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
);
point.attr(
'r',
`${consts.BASE_POINT_SIZE / geometry.scale}`,
);
point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`);
point.attr('r', `${consts.BASE_POINT_SIZE / geometry.scale}`);
}
}
}

@ -5,10 +5,7 @@
import * as SVG from 'svg.js';
import { GroupData } from './canvasModel';
import {
translateToSVG,
} from './shared';
import { translateToSVG } from './shared';
export interface GroupHandler {
group(groupData: GroupData): void;
@ -35,16 +32,15 @@ export class GroupHandlerImpl implements GroupHandler {
private statesToBeGroupped: any[];
private highlightedShapes: Record<number, SVG.Shape>;
private getSelectionBox(event: MouseEvent): {
xtl: number;
ytl: number;
xbr: number;
ybr: number;
} {
const point = translateToSVG(
(this.canvas.node as any as SVGSVGElement),
[event.clientX, event.clientY],
);
private getSelectionBox(
event: MouseEvent,
): {
xtl: number;
ytl: number;
xbr: number;
ybr: number;
} {
const point = translateToSVG((this.canvas.node as any) as SVGSVGElement, [event.clientX, event.clientY]);
const stopSelectionPoint = {
x: point[0],
y: point[1],
@ -60,10 +56,7 @@ export class GroupHandlerImpl implements GroupHandler {
private onSelectStart(event: MouseEvent): void {
if (!this.selectionRect) {
const point = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[event.clientX, event.clientY],
);
const point = translateToSVG((this.canvas.node as any) as SVGSVGElement, [event.clientX, event.clientY]);
this.startSelectionPoint = {
x: point[0],
y: point[1],
@ -102,12 +95,16 @@ export class GroupHandlerImpl implements GroupHandler {
// TODO: Doesn't work properly for groups
const bbox = shape.bbox();
const clientID = shape.attr('clientID');
if (bbox.x > box.xtl && bbox.y > box.ytl
if (
bbox.x > box.xtl
&& bbox.y > box.ytl
&& bbox.x + bbox.width < box.xbr
&& bbox.y + bbox.height < box.ybr
&& !(clientID in this.highlightedShapes)) {
const objectState = this.getStates()
.filter((state: any): boolean => state.clientID === clientID)[0];
&& !(clientID in this.highlightedShapes)
) {
const objectState = this.getStates().filter(
(state: any): boolean => state.clientID === clientID,
)[0];
if (objectState) {
this.statesToBeGroupped.push(objectState);
@ -127,7 +124,6 @@ export class GroupHandlerImpl implements GroupHandler {
this.resetSelectedObjects();
this.initialized = false;
this.selectionRect = null;
this.startSelectionPoint = {
x: null,
y: null,
@ -216,6 +212,10 @@ export class GroupHandlerImpl implements GroupHandler {
}
this.statesToBeGroupped = [];
this.highlightedShapes = {};
if (this.selectionRect) {
this.selectionRect.remove();
this.selectionRect = null;
}
}
public cancel(): void {

@ -0,0 +1,268 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
import Crosshair from './crosshair';
import { translateToSVG } from './shared';
import { InteractionData, InteractionResult, Geometry } from './canvasModel';
export interface InteractionHandler {
transform(geometry: Geometry): void;
interact(interactData: InteractionData): void;
cancel(): void;
}
export class InteractionHandlerImpl implements InteractionHandler {
private onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void;
private geometry: Geometry;
private canvas: SVG.Container;
private interactionData: InteractionData;
private cursorPosition: { x: number; y: number };
private shapesWereUpdated: boolean;
private interactionShapes: SVG.Shape[];
private currentInteractionShape: SVG.Shape | null;
private crosshair: Crosshair;
private prepareResult(): InteractionResult[] {
return this.interactionShapes.map(
(shape: SVG.Shape): InteractionResult => {
if (shape.type === 'circle') {
const points = [(shape as SVG.Circle).cx(), (shape as SVG.Circle).cy()];
return {
points: points.map((coord: number): number => coord - this.geometry.offset),
shapeType: 'points',
button: shape.attr('stroke') === 'green' ? 0 : 2,
};
}
const bbox = ((shape.node as any) as SVGRectElement).getBBox();
const points = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height];
return {
points: points.map((coord: number): number => coord - this.geometry.offset),
shapeType: 'rectangle',
button: 0,
};
},
);
}
private shouldRaiseEvent(ctrlKey: boolean): boolean {
const { interactionData, interactionShapes, shapesWereUpdated } = this;
const { minPosVertices, minNegVertices, enabled } = interactionData;
const positiveShapes = interactionShapes.filter(
(shape: SVG.Shape): boolean => (shape as any).attr('stroke') === 'green',
);
const negativeShapes = interactionShapes.filter(
(shape: SVG.Shape): boolean => (shape as any).attr('stroke') !== 'green',
);
if (interactionData.shapeType === 'rectangle') {
return enabled && !ctrlKey && !!interactionShapes.length;
}
const minimumVerticesAchieved =
(typeof minPosVertices === 'undefined' || minPosVertices <= positiveShapes.length) &&
(typeof minNegVertices === 'undefined' || minPosVertices <= negativeShapes.length);
return enabled && !ctrlKey && minimumVerticesAchieved && shapesWereUpdated;
}
private addCrosshair(): void {
const { x, y } = this.cursorPosition;
this.crosshair.show(this.canvas, x, y, this.geometry.scale);
}
private removeCrosshair(): void {
this.crosshair.hide();
}
private interactPoints(): void {
const eventListener = (e: MouseEvent): void => {
if ((e.button === 0 || e.button === 2) && !e.altKey) {
e.preventDefault();
const [cx, cy] = translateToSVG((this.canvas.node as any) as SVGSVGElement, [e.clientX, e.clientY]);
this.currentInteractionShape = this.canvas
.circle((consts.BASE_POINT_SIZE * 2) / this.geometry.scale)
.center(cx, cy)
.fill('white')
.stroke(e.button === 0 ? 'green' : 'red')
.addClass('cvat_interaction_point')
.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
});
this.interactionShapes.push(this.currentInteractionShape);
this.shapesWereUpdated = true;
if (this.shouldRaiseEvent(e.ctrlKey)) {
this.onInteraction(this.prepareResult(), true, false);
}
const self = this.currentInteractionShape;
self.on('mouseenter', (): void => {
self.attr({
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale,
});
self.on('mousedown', (_e: MouseEvent): void => {
_e.preventDefault();
_e.stopPropagation();
self.remove();
this.interactionShapes = this.interactionShapes.filter(
(shape: SVG.Shape): boolean => shape !== self,
);
this.shapesWereUpdated = true;
if (this.shouldRaiseEvent(_e.ctrlKey)) {
this.onInteraction(this.prepareResult(), true, false);
}
});
});
self.on('mouseleave', (): void => {
self.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
});
self.off('mousedown');
});
}
};
// clear this listener in relese()
this.canvas.on('mousedown.interaction', eventListener);
}
private interactRectangle(): void {
let initialized = false;
const eventListener = (e: MouseEvent): void => {
if (e.button === 0 && !e.altKey) {
if (!initialized) {
(this.currentInteractionShape as any).draw(e, { snapToGrid: 0.1 });
initialized = true;
} else {
(this.currentInteractionShape as any).draw(e);
}
}
};
this.currentInteractionShape = this.canvas.rect();
this.canvas.on('mousedown.interaction', eventListener);
this.currentInteractionShape
.on('drawstop', (): void => {
this.interactionShapes.push(this.currentInteractionShape);
this.shapesWereUpdated = true;
this.canvas.off('mousedown.interaction', eventListener);
this.interact({ enabled: false });
})
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
}
private initInteraction(): void {
if (this.interactionData.crosshair) {
this.addCrosshair();
}
}
private startInteraction(): void {
if (this.interactionData.shapeType === 'rectangle') {
this.interactRectangle();
} else if (this.interactionData.shapeType === 'points') {
this.interactPoints();
} else {
throw new Error('Interactor implementation supports only rectangle and points');
}
}
private release(): void {
if (this.crosshair) {
this.removeCrosshair();
}
this.canvas.off('mousedown.interaction');
this.interactionShapes.forEach((shape: SVG.Shape): SVG.Shape => shape.remove());
this.interactionShapes = [];
if (this.currentInteractionShape) {
this.currentInteractionShape.remove();
this.currentInteractionShape = null;
}
}
public constructor(
onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void,
canvas: SVG.Container,
geometry: Geometry,
) {
this.onInteraction = (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean): void => {
this.shapesWereUpdated = false;
onInteraction(shapes, shapesUpdated, isDone);
};
this.canvas = canvas;
this.geometry = geometry;
this.shapesWereUpdated = false;
this.interactionShapes = [];
this.interactionData = { enabled: false };
this.currentInteractionShape = null;
this.crosshair = new Crosshair();
this.cursorPosition = {
x: 0,
y: 0,
};
this.canvas.on('mousemove.interaction', (e: MouseEvent): void => {
const [x, y] = translateToSVG((this.canvas.node as any) as SVGSVGElement, [e.clientX, e.clientY]);
this.cursorPosition = { x, y };
if (this.crosshair) {
this.crosshair.move(x, y);
}
});
document.body.addEventListener('keyup', (e: KeyboardEvent): void => {
if (e.keyCode === 17 && this.shouldRaiseEvent(false)) {
// 17 is ctrl
this.onInteraction(this.prepareResult(), true, false);
}
});
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.crosshair) {
this.crosshair.scale(this.geometry.scale);
}
const shapesToBeScaled = this.currentInteractionShape
? [...this.interactionShapes, this.currentInteractionShape]
: [...this.interactionShapes];
for (const shape of shapesToBeScaled) {
if (shape.type === 'circle') {
(shape as SVG.Circle).radius(consts.BASE_POINT_SIZE / this.geometry.scale);
shape.attr('stroke-width', consts.POINTS_STROKE_WIDTH / this.geometry.scale);
} else {
shape.attr('stroke-width', consts.BASE_STROKE_WIDTH / this.geometry.scale);
}
}
}
public interact(interactionData: InteractionData): void {
if (interactionData.enabled) {
this.interactionData = interactionData;
this.initInteraction();
this.startInteraction();
} else {
this.onInteraction(this.prepareResult(), this.shouldRaiseEvent(false), true);
this.release();
this.interactionData = interactionData;
}
}
public cancel(): void {
this.release();
this.onInteraction(null);
}
}

@ -12,7 +12,6 @@ export interface MergeHandler {
repeatSelection(): void;
}
export class MergeHandlerImpl implements MergeHandler {
// callback is used to notify about merging end
private onMergeDone: (objects: any[] | null, duration?: number) => void;
@ -40,8 +39,10 @@ export class MergeHandlerImpl implements MergeHandler {
}
private checkConstraints(state: any): boolean {
return !this.constraints || (state.label.id === this.constraints.labelID
&& state.shapeType === this.constraints.shapeType);
return (
!this.constraints ||
(state.label.id === this.constraints.labelID && state.shapeType === this.constraints.shapeType)
);
}
private release(): void {
@ -118,8 +119,7 @@ export class MergeHandlerImpl implements MergeHandler {
}
} else {
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first();
if (shape && this.checkConstraints(objectState)
&& !stateFrames.includes(objectState.frame)) {
if (shape && this.checkConstraints(objectState) && !stateFrames.includes(objectState.frame)) {
this.statesToBeMerged.push(objectState);
this.highlightedShapes[objectState.clientID] = shape;
shape.addClass('cvat_canvas_shape_merging');

@ -0,0 +1,133 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
import { translateToSVG } from './shared';
import { Geometry } from './canvasModel';
export interface RegionSelector {
select(enabled: boolean): void;
cancel(): void;
transform(geometry: Geometry): void;
}
export class RegionSelectorImpl implements RegionSelector {
private onRegionSelected: (points?: number[]) => void;
private geometry: Geometry;
private canvas: SVG.Container;
private selectionRect: SVG.Rect | null;
private startSelectionPoint: {
x: number;
y: number;
};
private getSelectionBox(event: MouseEvent): { xtl: number; ytl: number; xbr: number; ybr: number } {
const point = translateToSVG((this.canvas.node as any) as SVGSVGElement, [event.clientX, event.clientY]);
const stopSelectionPoint = {
x: point[0],
y: point[1],
};
return {
xtl: Math.min(this.startSelectionPoint.x, stopSelectionPoint.x),
ytl: Math.min(this.startSelectionPoint.y, stopSelectionPoint.y),
xbr: Math.max(this.startSelectionPoint.x, stopSelectionPoint.x),
ybr: Math.max(this.startSelectionPoint.y, stopSelectionPoint.y),
};
}
private onMouseMove = (event: MouseEvent): void => {
if (this.selectionRect) {
const box = this.getSelectionBox(event);
this.selectionRect.attr({
x: box.xtl,
y: box.ytl,
width: box.xbr - box.xtl,
height: box.ybr - box.ytl,
});
}
};
private onMouseDown = (event: MouseEvent): void => {
if (!this.selectionRect && !event.altKey) {
const point = translateToSVG((this.canvas.node as any) as SVGSVGElement, [event.clientX, event.clientY]);
this.startSelectionPoint = {
x: point[0],
y: point[1],
};
this.selectionRect = this.canvas
.rect()
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
})
.addClass('cvat_canvas_shape_region_selection');
this.selectionRect.attr({ ...this.startSelectionPoint });
}
};
private onMouseUp = (): void => {
const { offset } = this.geometry;
if (this.selectionRect) {
const {
w, h, x, y, x2, y2,
} = this.selectionRect.bbox();
this.selectionRect.remove();
this.selectionRect = null;
if (w === 0 && h === 0) {
this.onRegionSelected([x - offset, y - offset]);
} else {
this.onRegionSelected([x - offset, y - offset, x2 - offset, y2 - offset]);
}
}
};
private startSelection(): void {
this.canvas.node.addEventListener('mousemove', this.onMouseMove);
this.canvas.node.addEventListener('mousedown', this.onMouseDown);
this.canvas.node.addEventListener('mouseup', this.onMouseUp);
}
private stopSelection(): void {
this.canvas.node.removeEventListener('mousemove', this.onMouseMove);
this.canvas.node.removeEventListener('mousedown', this.onMouseDown);
this.canvas.node.removeEventListener('mouseup', this.onMouseUp);
}
private release(): void {
this.stopSelection();
}
public constructor(onRegionSelected: (points?: number[]) => void, canvas: SVG.Container, geometry: Geometry) {
this.onRegionSelected = onRegionSelected;
this.geometry = geometry;
this.canvas = canvas;
this.selectionRect = null;
}
public select(enabled: boolean): void {
if (enabled) {
this.startSelection();
} else {
this.release();
}
}
public cancel(): void {
this.release();
this.onRegionSelected();
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.selectionRect) {
this.selectionRect.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
});
}
}
}

@ -83,22 +83,25 @@ export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
return output;
}
export function displayShapeSize(
shapesContainer: SVG.Container,
textContainer: SVG.Container,
): ShapeSizeElement {
export function displayShapeSize(shapesContainer: SVG.Container, textContainer: SVG.Container): ShapeSizeElement {
const shapeSize: ShapeSizeElement = {
sizeElement: textContainer.text('').font({
weight: 'bolder',
}).fill('white').addClass('cvat_canvas_text'),
update(shape: SVG.Shape): void{
sizeElement: textContainer
.text('')
.font({
weight: 'bolder',
})
.fill('white')
.addClass('cvat_canvas_text'),
update(shape: SVG.Shape): void {
const bbox = shape.bbox();
const text = `${bbox.width.toFixed(1)}x${bbox.height.toFixed(1)}`;
const [x, y]: number[] = translateToSVG(
textContainer.node as any as SVGSVGElement,
translateFromSVG((shapesContainer.node as any as SVGSVGElement), [bbox.x, bbox.y]),
(textContainer.node as any) as SVGSVGElement,
translateFromSVG((shapesContainer.node as any) as SVGSVGElement, [bbox.x, bbox.y]),
);
this.sizeElement.clear().plain(text)
this.sizeElement
.clear()
.plain(text)
.move(x + consts.TEXT_MARGIN, y + consts.TEXT_MARGIN);
},
rm(): void {
@ -120,7 +123,9 @@ export function pointsToNumberArray(points: string | Point[]): number[] {
}, []);
}
return points.trim().split(/[,\s]+/g)
return points
.trim()
.split(/[,\s]+/g)
.map((coord: string): number => +coord);
}
@ -138,14 +143,19 @@ export function parsePoints(source: string | number[]): Point[] {
}, []);
}
return source.trim().split(/\s/).map((point: string): Point => {
const [x, y] = point.split(',').map((coord: string): number => +coord);
return { x, y };
});
return source
.trim()
.split(/\s/)
.map(
(point: string): Point => {
const [x, y] = point.split(',').map((coord: string): number => +coord);
return { x, y };
},
);
}
export function stringifyPoints(points: (Point | number)[]): string {
if (typeof (points[0]) === 'number') {
if (typeof points[0] === 'number') {
return points.reduce((acc: string, val: number, idx: number): string => {
if (idx % 2) {
return `${acc},${val}`;
@ -166,5 +176,5 @@ export function scalarProduct(a: Vector2D, b: Vector2D): number {
}
export function vectorLength(vector: Vector2D): number {
return Math.sqrt((vector.i ** 2) + (vector.j ** 2));
return Math.sqrt(vector.i ** 2 + vector.j ** 2);
}

@ -85,12 +85,16 @@ export class SplitHandlerImpl implements SplitHandler {
this.highlightedShape = shape;
this.highlightedShape.addClass('cvat_canvas_shape_splitting');
this.canvas.node.append(this.highlightedShape.node);
this.highlightedShape.on('click.split', (): void => {
this.splitDone = true;
this.onSplitDone(state);
}, {
once: true,
});
this.highlightedShape.on(
'click.split',
(): void => {
this.splitDone = true;
this.onSplitDone(state);
},
{
once: true,
},
);
}
}
}

File diff suppressed because it is too large Load Diff

@ -5,14 +5,9 @@
import * as SVG from 'svg.js';
import consts from './consts';
import {
translateToSVG,
} from './shared';
import {
Geometry,
} from './canvasModel';
import { translateToSVG } from './shared';
import { Geometry } from './canvasModel';
export interface ZoomHandler {
zoom(): void;
@ -35,10 +30,7 @@ export class ZoomHandlerImpl implements ZoomHandler {
private onSelectStart(event: MouseEvent): void {
if (!this.selectionRect && event.which === 1) {
const point = translateToSVG(
(this.canvas.node as any as SVGSVGElement),
[event.clientX, event.clientY],
);
const point = translateToSVG((this.canvas.node as any) as SVGSVGElement, [event.clientX, event.clientY]);
this.startSelectionPoint = {
x: point[0],
y: point[1],
@ -52,16 +44,15 @@ export class ZoomHandlerImpl implements ZoomHandler {
}
}
private getSelectionBox(event: MouseEvent): {
private getSelectionBox(
event: MouseEvent,
): {
x: number;
y: number;
width: number;
height: number;
} {
const point = translateToSVG(
(this.canvas.node as any as SVGSVGElement),
[event.clientX, event.clientY],
);
const point = translateToSVG((this.canvas.node as any) as SVGSVGElement, [event.clientX, event.clientY]);
const stopSelectionPoint = {
x: point[0],
y: point[1],

@ -1,21 +1,19 @@
{
"compilerOptions": {
"baseUrl": ".",
"emitDeclarationOnly": true,
"module": "es6",
"target": "es6",
"noImplicitAny": true,
"preserveConstEnums": true,
"declaration": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"moduleResolution": "node",
"declarationDir": "dist/declaration",
"paths": {
"cvat-canvas.node": ["dist/cvat-canvas.node"]
}
},
"include": [
"src/typescript/*.ts"
]
"compilerOptions": {
"baseUrl": ".",
"emitDeclarationOnly": true,
"module": "es6",
"target": "es6",
"noImplicitAny": true,
"preserveConstEnums": true,
"declaration": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"moduleResolution": "node",
"declarationDir": "dist/declaration",
"paths": {
"cvat-canvas.node": ["dist/cvat-canvas.node"]
}
},
"include": ["src/typescript/*.ts"]
}

@ -1,11 +1,12 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/* eslint-disable */
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');
const DtsBundleWebpack = require('dts-bundle-webpack')
// eslint-disable-next-line @typescript-eslint/no-var-requires
const DtsBundleWebpack = require('dts-bundle-webpack');
const nodeConfig = {
target: 'node',
@ -22,30 +23,38 @@ const nodeConfig = {
extensions: ['.ts', '.js', '.json'],
},
module: {
rules: [{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: ['@babel/plugin-proposal-class-properties'],
presets: [
['@babel/preset-env'],
['@babel/typescript'],
],
sourceType: 'unambiguous',
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining',
],
presets: [['@babel/preset-env'], ['@babel/typescript']],
sourceType: 'unambiguous',
},
},
},
}, {
test: /\.(css|scss)$/,
exclude: /node_modules/,
use: ['style-loader', {
loader: 'css-loader',
options: {
importLoaders: 2,
},
}, 'postcss-loader', 'sass-loader']
}],
{
test: /\.(css|scss)$/,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'sass-loader',
],
},
],
},
plugins: [
new DtsBundleWebpack({
@ -53,7 +62,7 @@ const nodeConfig = {
main: 'dist/declaration/src/typescript/canvas.d.ts',
out: '../cvat-canvas.node.d.ts',
}),
]
],
};
const webConfig = {
@ -79,32 +88,43 @@ const webConfig = {
extensions: ['.ts', '.js', '.json'],
},
module: {
rules: [{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: ['@babel/plugin-proposal-class-properties'],
presets: [
['@babel/preset-env', {
targets: '> 2.5%', // https://github.com/browserslist/browserslist
}],
['@babel/typescript'],
],
sourceType: 'unambiguous',
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: ['@babel/plugin-proposal-class-properties'],
presets: [
[
'@babel/preset-env',
{
targets: '> 2.5%', // https://github.com/browserslist/browserslist
},
],
['@babel/typescript'],
],
sourceType: 'unambiguous',
},
},
},
}, {
test: /\.scss$/,
exclude: /node_modules/,
use: ['style-loader', {
loader: 'css-loader',
options: {
importLoaders: 2,
},
}, 'postcss-loader', 'sass-loader']
}],
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'sass-loader',
],
},
],
},
plugins: [
new DtsBundleWebpack({
@ -112,7 +132,7 @@ const webConfig = {
main: 'dist/declaration/src/typescript/canvas.d.ts',
out: '../cvat-canvas.d.ts',
}),
]
],
};
module.exports = [webConfig, nodeConfig]
module.exports = [webConfig, nodeConfig];

@ -0,0 +1 @@
webpack.config.js

@ -1,55 +1,47 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
// Copyright (C) 2018-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
module.exports = {
"env": {
"node": false,
"browser": true,
"es6": true,
"jquery": true,
"qunit": true,
module.exports = {
env: {
node: true,
browser: true,
es6: true,
'jest/globals': true,
},
"parserOptions": {
"parser": "babel-eslint",
"sourceType": "module",
"ecmaVersion": 2018,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module',
ecmaVersion: 2018,
},
"plugins": [
"security",
"no-unsanitized",
"no-unsafe-innerhtml",
],
"extends": [
"eslint:recommended",
"plugin:security/recommended",
"plugin:no-unsanitized/DOM",
"airbnb-base",
],
"rules": {
"no-await-in-loop": [0],
"global-require": [0],
"no-new": [0],
"class-methods-use-this": [0],
"no-restricted-properties": [0, {
"object": "Math",
"property": "pow",
}],
"no-plusplus": [0],
"no-param-reassign": [0],
"no-underscore-dangle": ["error", { "allowAfterThis": true }],
"no-restricted-syntax": [0, {"selector": "ForOfStatement"}],
"no-continue": [0],
"no-unsafe-innerhtml/no-unsafe-innerhtml": 1,
plugins: ['security', 'jest', 'no-unsafe-innerhtml', 'no-unsanitized'],
extends: ['eslint:recommended', 'plugin:security/recommended', 'plugin:no-unsanitized/DOM', 'airbnb-base'],
rules: {
'no-await-in-loop': [0],
'global-require': [0],
'no-new': [0],
'class-methods-use-this': [0],
'no-restricted-properties': [
0,
{
object: 'Math',
property: 'pow',
},
],
'no-plusplus': [0],
'no-param-reassign': [0],
'no-underscore-dangle': ['error', { allowAfterThis: true }],
'no-restricted-syntax': [0, { selector: 'ForOfStatement' }],
'no-continue': [0],
'no-unsafe-innerhtml/no-unsafe-innerhtml': 1,
// This rule actual for user input data on the node.js environment mainly.
"security/detect-object-injection": 0,
"indent": ["warn", 4],
"no-useless-constructor": 0,
"func-names": [0],
"valid-typeof": [0],
"no-console": [0], // this rule deprecates console.log, console.warn etc. because "it is not good in production code"
"max-classes-per-file": [0],
'security/detect-object-injection': 0,
indent: ['warn', 4],
'no-useless-constructor': 0,
'func-names': [0],
'valid-typeof': [0],
'no-console': [0],
'max-classes-per-file': [0],
'max-len': ['warn', { code: 120 }],
},
};

@ -1,40 +1,47 @@
# Module CVAT-CORE
## Description
This CVAT module is a client-side JavaScipt library to management of objects, frames, logs, etc.
It contains the core logic of the Computer Vision Annotation Tool.
## Versioning
If you make changes in this package, please do following:
- After not important changes (typos, backward compatible bug fixes, refactoring) do: ``npm version patch``
- After changing API (backward compatible new features) do: ``npm version minor``
- After changing API (changes that break backward compatibility) do: ``npm version major``
- After not important changes (typos, backward compatible bug fixes, refactoring) do: `npm version patch`
- After changing API (backward compatible new features) do: `npm version minor`
- After changing API (changes that break backward compatibility) do: `npm version major`
### Commands
- Dependencies installation
```bash
npm install
npm ci
```
- Building the module from sources in the ```dist``` directory:
- Building the module from sources in the `dist` directory:
```bash
npm run build
npm run build -- --mode=development # without a minification
```
- Building the documentation in the ```docs``` directory:
- Building the documentation in the `docs` directory:
```bash
npm run-script docs
```
- Running of tests:
```bash
npm run-script test
```
- Updating of a module version:
```bash
npm version patch # updated after minor fixes
npm version minor # updated after major changes which don't affect API compatibility with previous versions
@ -42,5 +49,6 @@ npm version major # updated after major changes which affect API compatibility
```
Visual studio code configurations:
- cvat.js debug starts debugging with entrypoint api.js
- cvat.js test builds library and runs entrypoint tests.js

@ -1,32 +1,15 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
const { defaults } = require('jest-config');
module.exports = {
coverageDirectory: 'reports/coverage',
coverageReporters: ['lcov'],
moduleFileExtensions: [
...defaults.moduleFileExtensions,
'ts',
'tsx',
],
reporters: [
'default',
['jest-junit', { outputDirectory: 'reports/junit' }],
],
testMatch: [
'**/tests/**/*.js',
],
testPathIgnorePatterns: [
'/node_modules/',
'/tests/mocks/*',
],
moduleFileExtensions: [...defaults.moduleFileExtensions, 'ts', 'tsx'],
reporters: ['default', ['jest-junit', { outputDirectory: 'reports/junit' }]],
testMatch: ['**/tests/**/*.js'],
testPathIgnorePatterns: ['/node_modules/', '/tests/mocks/*'],
automock: false,
};

@ -1,8 +1,6 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
module.exports = {
plugins: [],

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.5.0",
"version": "3.10.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {
@ -25,6 +25,7 @@
"eslint-plugin-no-unsafe-innerhtml": "^1.0.16",
"eslint-plugin-no-unsanitized": "^3.0.2",
"eslint-plugin-security": "^1.4.0",
"eslint-plugin-jest": "^24.1.0",
"jest": "^24.8.0",
"jest-junit": "^6.4.0",
"jsdoc": "^3.6.4",
@ -32,16 +33,17 @@
"webpack-cli": "^3.3.2"
},
"dependencies": {
"axios": "^0.18.0",
"axios": "^0.21.1",
"browser-or-node": "^1.2.1",
"cvat-data": "../cvat-data",
"detect-browser": "^5.0.0",
"detect-browser": "^5.2.0",
"error-stack-parser": "^2.0.2",
"form-data": "^2.5.0",
"jest-config": "^24.8.0",
"jest-config": "^26.6.3",
"js-cookie": "^2.2.0",
"jsonpath": "^1.0.2",
"platform": "^1.3.5",
"quickhull": "^1.0.3",
"store": "^2.0.12",
"worker-loader": "^2.0.0"
}

@ -1,14 +1,13 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
/**
* Class representing an annotation loader
* @memberof module:API.cvat.classes
* @hideconstructor
*/
* Class representing an annotation loader
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Loader {
constructor(initialData) {
const data = {
@ -21,42 +20,42 @@
Object.defineProperties(this, {
name: {
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.name,
},
format: {
/**
* @name format
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
* @name format
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.format,
},
version: {
/**
* @name version
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
* @name version
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.version,
},
enabled: {
/**
* @name enabled
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
* @name enabled
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.enabled,
},
});
@ -64,10 +63,10 @@
}
/**
* Class representing an annotation dumper
* @memberof module:API.cvat.classes
* @hideconstructor
*/
* Class representing an annotation dumper
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Dumper {
constructor(initialData) {
const data = {
@ -80,42 +79,42 @@
Object.defineProperties(this, {
name: {
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.name,
},
format: {
/**
* @name format
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
* @name format
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.format,
},
version: {
/**
* @name version
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
* @name version
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.version,
},
enabled: {
/**
* @name enabled
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
* @name enabled
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.enabled,
},
});
@ -123,10 +122,10 @@
}
/**
* Class representing an annotation format
* @memberof module:API.cvat.classes
* @hideconstructor
*/
* Class representing an annotation format
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class AnnotationFormats {
constructor(initialData) {
const data = {
@ -138,22 +137,22 @@
Object.defineProperties(this, {
loaders: {
/**
* @name loaders
* @type {module:API.cvat.classes.Loader[]}
* @memberof module:API.cvat.classes.AnnotationFormats
* @readonly
* @instance
*/
* @name loaders
* @type {module:API.cvat.classes.Loader[]}
* @memberof module:API.cvat.classes.AnnotationFormats
* @readonly
* @instance
*/
get: () => [...data.importers],
},
dumpers: {
/**
* @name dumpers
* @type {module:API.cvat.classes.Dumper[]}
* @memberof module:API.cvat.classes.AnnotationFormats
* @readonly
* @instance
*/
* @name dumpers
* @type {module:API.cvat.classes.Dumper[]}
* @memberof module:API.cvat.classes.AnnotationFormats
* @readonly
* @instance
*/
get: () => [...data.exporters],
},
});

@ -1,11 +1,6 @@
/*
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const {
@ -28,17 +23,10 @@
const { checkObjectType } = require('./common');
const Statistics = require('./statistics');
const { Label } = require('./labels');
const {
DataError,
ArgumentError,
ScriptingError,
} = require('./exceptions');
const { DataError, ArgumentError, ScriptingError } = require('./exceptions');
const {
HistoryActions,
ObjectShape,
ObjectType,
colors,
HistoryActions, ObjectShape, ObjectType, colors,
} = require('./enums');
const ObjectState = require('./object-state');
@ -64,15 +52,12 @@
shapeModel = new CuboidShape(shapeData, clientID, color, injection);
break;
default:
throw new DataError(
`An unexpected type of shape "${type}"`,
);
throw new DataError(`An unexpected type of shape "${type}"`);
}
return shapeModel;
}
function trackFactory(trackData, clientID, injection) {
if (trackData.shapes.length) {
const { type } = trackData.shapes[0];
@ -96,9 +81,7 @@
trackModel = new CuboidTrack(trackData, clientID, color, injection);
break;
default:
throw new DataError(
`An unexpected type of track "${type}"`,
);
throw new DataError(`An unexpected type of track "${type}"`);
}
return trackModel;
@ -185,18 +168,20 @@
export() {
const data = {
tracks: this.tracks.filter((track) => !track.removed)
.map((track) => track.toJSON()),
tracks: this.tracks.filter((track) => !track.removed).map((track) => track.toJSON()),
shapes: Object.values(this.shapes)
.reduce((accumulator, value) => {
accumulator.push(...value);
return accumulator;
}, []).filter((shape) => !shape.removed)
}, [])
.filter((shape) => !shape.removed)
.map((shape) => shape.toJSON()),
tags: Object.values(this.tags).reduce((accumulator, value) => {
accumulator.push(...value);
return accumulator;
}, []).filter((tag) => !tag.removed)
tags: Object.values(this.tags)
.reduce((accumulator, value) => {
accumulator.push(...value);
return accumulator;
}, [])
.filter((tag) => !tag.removed)
.map((tag) => tag.toJSON()),
};
@ -252,7 +237,7 @@
const objectsForMerge = objectStates.map((state) => {
checkObjectType('object state', state, null, ObjectState);
const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') {
if (typeof object === 'undefined') {
throw new ArgumentError(
'The object has not been saved yet. Call ObjectState.put([state]) before you can merge it',
);
@ -263,15 +248,11 @@
const keyframes = {}; // frame: position
const { label, shapeType } = objectStates[0];
if (!(label.id in this.labels)) {
throw new ArgumentError(
`Unknown label for the task: ${label.id}`,
);
throw new ArgumentError(`Unknown label for the task: ${label.id}`);
}
if (!Object.values(ObjectShape).includes(shapeType)) {
throw new ArgumentError(
`Got unknown shapeType "${shapeType}"`,
);
throw new ArgumentError(`Got unknown shapeType "${shapeType}"`);
}
const labelAttributes = label.attributes.reduce((accumulator, attribute) => {
@ -290,18 +271,14 @@
}
if (state.shapeType !== shapeType) {
throw new ArgumentError(
`All shapes are expected to be ${shapeType}, but got ${state.shapeType}`,
);
throw new ArgumentError(`All shapes are expected to be ${shapeType}, but got ${state.shapeType}`);
}
// If this object is shape, get it position and save as a keyframe
if (object instanceof Shape) {
// Frame already saved and it is not outside
if (object.frame in keyframes && !keyframes[object.frame].outside) {
throw new ArgumentError(
'Expected only one visible shape per frame',
);
throw new ArgumentError('Expected only one visible shape per frame');
}
keyframes[object.frame] = {
@ -325,9 +302,8 @@
// Push outside shape after each annotation shape
// Any not outside shape rewrites it
if (!((object.frame + 1) in keyframes) && object.frame + 1 <= this.stopFrame) {
keyframes[object.frame + 1] = JSON
.parse(JSON.stringify(keyframes[object.frame]));
if (!(object.frame + 1 in keyframes) && object.frame + 1 <= this.stopFrame) {
keyframes[object.frame + 1] = JSON.parse(JSON.stringify(keyframes[object.frame]));
keyframes[object.frame + 1].outside = true;
keyframes[object.frame + 1].frame++;
}
@ -344,17 +320,14 @@
continue;
}
throw new ArgumentError(
'Expected only one visible shape per frame',
);
throw new ArgumentError('Expected only one visible shape per frame');
}
// We do not save an attribute if it has the same value
// We save only updates
let updatedAttributes = false;
for (const attrID in shape.attributes) {
if (!(attrID in attributes)
|| attributes[attrID] !== shape.attributes[attrID]) {
if (!(attrID in attributes) || attributes[attrID] !== shape.attributes[attrID]) {
updatedAttributes = true;
attributes[attrID] = shape.attributes[attrID];
}
@ -367,15 +340,16 @@
occluded: shape.occluded,
outside: shape.outside,
zOrder: shape.zOrder,
attributes: updatedAttributes ? Object.keys(attributes)
.reduce((accumulator, attrID) => {
attributes: updatedAttributes
? Object.keys(attributes).reduce((accumulator, attrID) => {
accumulator.push({
spec_id: +attrID,
value: attributes[attrID],
});
return accumulator;
}, []) : [],
}, [])
: [],
};
}
} else {
@ -399,22 +373,24 @@
const clientID = ++this.count;
const track = {
frame: Math.min.apply(null, Object.keys(keyframes).map((frame) => +frame)),
frame: Math.min.apply(
null,
Object.keys(keyframes).map((frame) => +frame),
),
shapes: Object.values(keyframes),
group: 0,
source: objectStates[0].source,
label_id: label.id,
attributes: Object.keys(objectStates[0].attributes)
.reduce((accumulator, attrID) => {
if (!labelAttributes[attrID].mutable) {
accumulator.push({
spec_id: +attrID,
value: objectStates[0].attributes[attrID],
});
}
attributes: Object.keys(objectStates[0].attributes).reduce((accumulator, attrID) => {
if (!labelAttributes[attrID].mutable) {
accumulator.push({
spec_id: +attrID,
value: objectStates[0].attributes[attrID],
});
}
return accumulator;
}, []),
return accumulator;
}, []),
};
const trackModel = trackFactory(track, clientID, this.injection);
@ -426,20 +402,23 @@
object.removed = true;
}
this.history.do(HistoryActions.MERGED_OBJECTS, () => {
trackModel.removed = true;
for (const object of objectsForMerge) {
object.removed = false;
}
}, () => {
trackModel.removed = false;
for (const object of objectsForMerge) {
object.removed = true;
}
}, [
...objectsForMerge
.map((object) => object.clientID), trackModel.clientID,
], objectStates[0].frame);
this.history.do(
HistoryActions.MERGED_OBJECTS,
() => {
trackModel.removed = true;
for (const object of objectsForMerge) {
object.removed = false;
}
},
() => {
trackModel.removed = false;
for (const object of objectsForMerge) {
object.removed = true;
}
},
[...objectsForMerge.map((object) => object.clientID), trackModel.clientID],
objectStates[0].frame,
);
}
split(objectState, frame) {
@ -447,10 +426,8 @@
checkObjectType('frame', frame, 'integer', null);
const object = this.objects[objectState.clientID];
if (typeof (object) === 'undefined') {
throw new ArgumentError(
'The object has not been saved yet. Call annotations.put([state]) before',
);
if (typeof object === 'undefined') {
throw new ArgumentError('The object has not been saved yet. Call annotations.put([state]) before');
}
if (objectState.objectType !== ObjectType.TRACK) {
@ -474,17 +451,16 @@
occluded: objectState.occluded,
outside: objectState.outside,
zOrder: objectState.zOrder,
attributes: Object.keys(objectState.attributes)
.reduce((accumulator, attrID) => {
if (!labelAttributes[attrID].mutable) {
accumulator.push({
spec_id: +attrID,
value: objectState.attributes[attrID],
});
}
attributes: Object.keys(objectState.attributes).reduce((accumulator, attrID) => {
if (!labelAttributes[attrID].mutable) {
accumulator.push({
spec_id: +attrID,
value: objectState.attributes[attrID],
});
}
return accumulator;
}, []),
return accumulator;
}, []),
frame,
};
@ -526,15 +502,21 @@
// Remove source object
object.removed = true;
this.history.do(HistoryActions.SPLITTED_TRACK, () => {
object.removed = false;
prevTrack.removed = true;
nextTrack.removed = true;
}, () => {
object.removed = true;
prevTrack.removed = false;
nextTrack.removed = false;
}, [object.clientID, prevTrack.clientID, nextTrack.clientID], frame);
this.history.do(
HistoryActions.SPLITTED_TRACK,
() => {
object.removed = false;
prevTrack.removed = true;
nextTrack.removed = true;
},
() => {
object.removed = true;
prevTrack.removed = false;
nextTrack.removed = false;
},
[object.clientID, prevTrack.clientID, nextTrack.clientID],
frame,
);
}
group(objectStates, reset) {
@ -543,10 +525,8 @@
const objectsForGroup = objectStates.map((state) => {
checkObjectType('object state', state, null, ObjectState);
const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') {
throw new ArgumentError(
'The object has not been saved yet. Call annotations.put([state]) before',
);
if (typeof object === 'undefined') {
throw new ArgumentError('The object has not been saved yet. Call annotations.put([state]) before');
}
return object;
});
@ -558,15 +538,21 @@
}
const redoGroups = objectsForGroup.map((object) => object.group);
this.history.do(HistoryActions.GROUPED_OBJECTS, () => {
objectsForGroup.forEach((object, idx) => {
object.group = undoGroups[idx];
});
}, () => {
objectsForGroup.forEach((object, idx) => {
object.group = redoGroups[idx];
});
}, objectsForGroup.map((object) => object.clientID), objectStates[0].frame);
this.history.do(
HistoryActions.GROUPED_OBJECTS,
() => {
objectsForGroup.forEach((object, idx) => {
object.group = undoGroups[idx];
});
},
() => {
objectsForGroup.forEach((object, idx) => {
object.group = redoGroups[idx];
});
},
objectsForGroup.map((object) => object.clientID),
objectStates[0].frame,
);
return groupIdx;
}
@ -629,9 +615,7 @@
} else if (object instanceof Tag) {
objectType = 'tag';
} else {
throw new ScriptingError(
`Unexpected object type: "${objectType}"`,
);
throw new ScriptingError(`Unexpected object type: "${objectType}"`);
}
const label = object.label.name;
@ -645,7 +629,8 @@
if (objectType === 'track') {
const keyframes = Object.keys(object.shapes)
.sort((a, b) => +a - +b).map((el) => +el);
.sort((a, b) => +a - +b)
.map((el) => +el);
let prevKeyframe = keyframes[0];
let visible = false;
@ -680,7 +665,7 @@
for (const label of Object.keys(labels)) {
for (const key of Object.keys(labels[label])) {
if (typeof (labels[label][key]) === 'object') {
if (typeof labels[label][key] === 'object') {
for (const objectType of Object.keys(labels[label][key])) {
total[key][objectType] += labels[label][key][objectType];
}
@ -723,8 +708,7 @@
checkObjectType('state attributes', state.attributes, null, Object);
checkObjectType('state label', state.label, null, Label);
const attributes = Object.keys(state.attributes)
.reduce(convertAttributes.bind(state), []);
const attributes = Object.keys(state.attributes).reduce(convertAttributes.bind(state), []);
const labelAttributes = state.label.attributes.reduce((accumulator, attribute) => {
accumulator[attribute.id] = attribute;
return accumulator;
@ -749,8 +733,7 @@
if (!Object.values(ObjectShape).includes(state.shapeType)) {
throw new ArgumentError(
'Object shape must be one of: '
+ `${JSON.stringify(Object.values(ObjectShape))}`,
`Object shape must be one of: ${JSON.stringify(Object.values(ObjectShape))}`,
);
}
@ -768,27 +751,26 @@
});
} else if (state.objectType === 'track') {
constructed.tracks.push({
attributes: attributes
.filter((attr) => !labelAttributes[attr.spec_id].mutable),
attributes: attributes.filter((attr) => !labelAttributes[attr.spec_id].mutable),
frame: state.frame,
group: 0,
source: state.source,
label_id: state.label.id,
shapes: [{
attributes: attributes
.filter((attr) => labelAttributes[attr.spec_id].mutable),
frame: state.frame,
occluded: state.occluded || false,
outside: false,
points: [...state.points],
type: state.shapeType,
z_order: state.zOrder,
}],
shapes: [
{
attributes: attributes.filter((attr) => labelAttributes[attr.spec_id].mutable),
frame: state.frame,
occluded: state.occluded || false,
outside: false,
points: [...state.points],
type: state.shapeType,
z_order: state.zOrder,
},
],
});
} else {
throw new ArgumentError(
'Object type must be one of: '
+ `${JSON.stringify(Object.values(ObjectType))}`,
`Object type must be one of: ${JSON.stringify(Object.values(ObjectType))}`,
);
}
}
@ -796,20 +778,24 @@
// Add constructed objects to a collection
const imported = this.import(constructed);
const importedArray = imported.tags
.concat(imported.tracks)
.concat(imported.shapes);
const importedArray = imported.tags.concat(imported.tracks).concat(imported.shapes);
if (objectStates.length) {
this.history.do(HistoryActions.CREATED_OBJECTS, () => {
importedArray.forEach((object) => {
object.removed = true;
});
}, () => {
importedArray.forEach((object) => {
object.removed = false;
});
}, importedArray.map((object) => object.clientID), objectStates[0].frame);
this.history.do(
HistoryActions.CREATED_OBJECTS,
() => {
importedArray.forEach((object) => {
object.removed = true;
});
},
() => {
importedArray.forEach((object) => {
object.removed = false;
});
},
importedArray.map((object) => object.clientID),
objectStates[0].frame,
);
}
return importedArray.map((value) => value.clientID);
@ -829,14 +815,11 @@
}
const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') {
throw new ArgumentError(
'The object has not been saved yet. Call annotations.put([state]) before',
);
if (typeof object === 'undefined') {
throw new ArgumentError('The object has not been saved yet. Call annotations.put([state]) before');
}
const distance = object.constructor.distance(state.points, x, y);
if (distance !== null && (minimumDistance === null
|| distance < minimumDistance)) {
if (distance !== null && (minimumDistance === null || distance < minimumDistance)) {
minimumDistance = distance;
minimumState = state;
}
@ -848,14 +831,47 @@
};
}
searchEmpty(frameFrom, frameTo) {
const sign = Math.sign(frameTo - frameFrom);
const predicate = sign > 0 ? (frame) => frame <= frameTo : (frame) => frame >= frameTo;
const update = sign > 0 ? (frame) => frame + 1 : (frame) => frame - 1;
for (let frame = frameFrom; predicate(frame); frame = update(frame)) {
if (frame in this.shapes && this.shapes[frame].some((shape) => !shape.removed)) {
continue;
}
if (frame in this.tags && this.tags[frame].some((tag) => !tag.removed)) {
continue;
}
const filteredTracks = this.tracks.filter((track) => !track.removed);
let found = false;
for (const track of filteredTracks) {
const keyframes = track.boundedKeyframes(frame);
const { prev, first } = keyframes;
const last = prev === null ? first : prev;
const lastShape = track.shapes[last];
const isKeyfame = frame in track.shapes;
if (first <= frame && (!lastShape.outside || isKeyfame)) {
found = true;
break;
}
}
if (found) continue;
return frame;
}
return null;
}
search(filters, frameFrom, frameTo) {
const [groups, query] = this.annotationsFilter.toJSONQuery(filters);
const sign = Math.sign(frameTo - frameFrom);
const flattenedQuery = groups.flat(Number.MAX_SAFE_INTEGER);
const containsDifficultProperties = flattenedQuery
.some((fragment) => fragment
.match(/^width/) || fragment.match(/^height/));
const containsDifficultProperties = flattenedQuery.some(
(fragment) => fragment.match(/^width/) || fragment.match(/^height/),
);
const deepSearch = (deepSearchFrom, deepSearchTo) => {
// deepSearchFrom is expected to be a frame that doesn't satisfy a filter
@ -878,12 +894,8 @@
};
const keyframesMemory = {};
const predicate = sign > 0
? (frame) => frame <= frameTo
: (frame) => frame >= frameTo;
const update = sign > 0
? (frame) => frame + 1
: (frame) => frame - 1;
const predicate = sign > 0 ? (frame) => frame <= frameTo : (frame) => frame >= frameTo;
const update = sign > 0 ? (frame) => frame + 1 : (frame) => frame - 1;
for (let frame = frameFrom; predicate(frame); frame = update(frame)) {
// First prepare all data for the frame
// Consider all shapes, tags, and not outside tracks that have keyframe here
@ -897,15 +909,9 @@
.map((tag) => tag.get(frame)),
);
const tracks = Object.values(this.tracks)
.filter((track) => (
frame in track.shapes
|| frame === frameFrom
|| frame === frameTo
)).filter((track) => !track.removed);
statesData.push(
...tracks.map((track) => track.get(frame))
.filter((state) => !state.outside),
);
.filter((track) => frame in track.shapes || frame === frameFrom || frame === frameTo)
.filter((track) => !track.removed);
statesData.push(...tracks.map((track) => track.get(frame)).filter((state) => !state.outside));
// Nothing to filtering, go to the next iteration
if (!statesData.length) {
@ -926,12 +932,8 @@
for (const track of tracks) {
const trackIsSatisfy = filtered.includes(track.clientID);
if (!trackIsSatisfy) {
keyframesMemory[track.clientID] = [
filtered.includes(track.clientID),
frame,
];
} else if (keyframesMemory[track.clientID]
&& keyframesMemory[track.clientID][0] === false) {
keyframesMemory[track.clientID] = [filtered.includes(track.clientID), frame];
} else if (keyframesMemory[track.clientID] && keyframesMemory[track.clientID][0] === false) {
withDeepSearch = true;
}
}
@ -939,9 +941,7 @@
if (withDeepSearch) {
const reducer = sign > 0 ? Math.min : Math.max;
const deepSearchFrom = reducer(
...Object.values(keyframesMemory).map((value) => value[1]),
);
const deepSearchFrom = reducer(...Object.values(keyframesMemory).map((value) => value[1]));
return deepSearch(deepSearchFrom, frame);
}

@ -1,20 +1,11 @@
/*
* Copyright (C) 2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
const jsonpath = require('jsonpath');
const {
AttributeType,
ObjectType,
} = require('./enums');
const { AttributeType, ObjectType } = require('./enums');
const { ArgumentError } = require('./exceptions');
class AnnotationsFilter {
constructor() {
// eslint-disable-next-line security/detect-unsafe-regex
@ -47,8 +38,7 @@ class AnnotationsFilter {
if (operators.includes(expression[i])) {
if (!nestedCounter) {
const subexpression = expression
.substr(start + 1, i - start - 1).trim();
const subexpression = expression.substr(start + 1, i - start - 1).trim();
splitted.push(subexpression);
splitted.push(expression[i]);
start = i;
@ -56,18 +46,14 @@ class AnnotationsFilter {
}
}
const subexpression = expression
.substr(start + 1).trim();
const subexpression = expression.substr(start + 1).trim();
splitted.push(subexpression);
splitted.forEach((internalExpression) => {
if (internalExpression === '|' || internalExpression === '&') {
container.push(internalExpression);
} else {
this._groupByBrackets(
container,
internalExpression,
);
this._groupByBrackets(container, internalExpression);
}
});
}
@ -103,12 +89,8 @@ class AnnotationsFilter {
endBracket = i;
const subcontainer = [];
const subexpression = expression
.substr(startBracket + 1, endBracket - 1 - startBracket);
this._splitWithOperator(
subcontainer,
subexpression,
);
const subexpression = expression.substr(startBracket + 1, endBracket - 1 - startBracket);
this._splitWithOperator(subcontainer, subexpression);
container.push(subcontainer);
@ -136,7 +118,7 @@ class AnnotationsFilter {
for (const group of groups) {
if (Array.isArray(group)) {
expression += `(${this._join(group)})`;
} else if (typeof (group) === 'string') {
} else if (typeof group === 'string') {
// it can be operator or expression
if (group === '|' || group === '&') {
expression += group;
@ -158,11 +140,10 @@ class AnnotationsFilter {
_convertObjects(statesData) {
const objects = statesData.map((state) => {
const labelAttributes = state.label.attributes
.reduce((acc, attr) => {
acc[attr.id] = attr;
return acc;
}, {});
const labelAttributes = state.label.attributes.reduce((acc, attr) => {
acc[attr.id] = attr;
return acc;
}, {});
let xtl = Number.MAX_SAFE_INTEGER;
let xbr = Number.MIN_SAFE_INTEGER;
@ -172,10 +153,12 @@ class AnnotationsFilter {
if (state.objectType !== ObjectType.TAG) {
state.points.forEach((coord, idx) => {
if (idx % 2) { // y
if (idx % 2) {
// y
ytl = Math.min(ytl, coord);
ybr = Math.max(ybr, coord);
} else { // x
} else {
// x
xtl = Math.min(xtl, coord);
xbr = Math.max(xbr, coord);
}
@ -216,7 +199,7 @@ class AnnotationsFilter {
toJSONQuery(filters) {
try {
if (!Array.isArray(filters) || filters.some((value) => typeof (value) !== 'string')) {
if (!Array.isArray(filters) || filters.some((value) => typeof value !== 'string')) {
throw Error('Argument must be an array of strings');
}
@ -225,7 +208,10 @@ class AnnotationsFilter {
}
const groups = [];
const expression = filters.map((filter) => `(${filter})`).join('|').replace(/\\"/g, '`');
const expression = filters
.map((filter) => `(${filter})`)
.join('|')
.replace(/\\"/g, '`');
this._splitWithOperator(groups, expression);
return [groups, `$.objects[?(${this._join(groups)})].clientID`];
} catch (error) {

@ -1,15 +1,19 @@
/*
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
const MAX_HISTORY_LENGTH = 128;
class AnnotationHistory {
constructor() {
this.frozen = false;
this.clear();
}
freeze(frozen) {
this.frozen = frozen;
}
get() {
return {
undo: this._undo.map((undo) => [undo.action, undo.frame]),
@ -18,6 +22,7 @@ class AnnotationHistory {
}
do(action, undo, redo, clientIDs, frame) {
if (this.frozen) return;
const actionItem = {
clientIDs,
action,

File diff suppressed because it is too large Load Diff

@ -1,16 +1,11 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const serverProxy = require('./server-proxy');
const { Task } = require('./session');
const { ScriptingError } = ('./exceptions');
const { ScriptingError } = './exceptions';
class AnnotationsSaver {
constructor(version, collection, session) {
@ -53,12 +48,7 @@
}
async _request(data, action) {
const result = await serverProxy.annotations.updateAnnotations(
this.sessionType,
this.id,
data,
action,
);
const result = await serverProxy.annotations.updateAnnotations(this.sessionType, this.id, data, action);
return result;
}
@ -102,27 +92,37 @@
},
};
const keys = ['id', 'label_id', 'group', 'frame',
'occluded', 'z_order', 'points', 'type', 'shapes',
'attributes', 'value', 'spec_id', 'source', 'outside'];
const keys = [
'id',
'label_id',
'group',
'frame',
'occluded',
'z_order',
'points',
'type',
'shapes',
'attributes',
'value',
'spec_id',
'source',
'outside',
];
// Find created and updated objects
for (const type of Object.keys(exported)) {
for (const object of exported[type]) {
if (object.id in this.initialObjects[type]) {
const exportedHash = JSON.stringify(object, keys);
const initialHash = JSON.stringify(
this.initialObjects[type][object.id], keys,
);
const initialHash = JSON.stringify(this.initialObjects[type][object.id], keys);
if (exportedHash !== initialHash) {
splitted.updated[type].push(object);
}
} else if (typeof (object.id) === 'undefined') {
} else if (typeof object.id === 'undefined') {
splitted.created[type].push(object);
} else {
throw new ScriptingError(
`Id of object is defined "${object.id}"`
+ 'but it absents in initial state',
`Id of object is defined "${object.id}" but it absents in initial state`,
);
}
}
@ -144,21 +144,17 @@
}
}
return splitted;
}
_updateCreatedObjects(saved, indexes) {
const savedLength = saved.tracks.length
+ saved.shapes.length + saved.tags.length;
const savedLength = saved.tracks.length + saved.shapes.length + saved.tags.length;
const indexesLength = indexes.tracks.length
+ indexes.shapes.length + indexes.tags.length;
const indexesLength = indexes.tracks.length + indexes.shapes.length + indexes.tags.length;
if (indexesLength !== savedLength) {
throw new ScriptingError(
'Number of indexes is differed by number of saved objects'
+ `${indexesLength} vs ${savedLength}`,
`Number of indexes is differed by number of saved objects ${indexesLength} vs ${savedLength}`,
);
}
@ -180,7 +176,9 @@
};
// Remove them from the request body
exported.tracks.concat(exported.shapes).concat(exported.tags)
exported.tracks
.concat(exported.shapes)
.concat(exported.tags)
.map((value) => {
delete value.clientID;
return value;
@ -214,11 +212,7 @@
}
}
} else {
const {
created,
updated,
deleted,
} = this._split(exported);
const { created, updated, deleted } = this._split(exported);
onUpdate('Created objects are being saved on the server');
const indexes = this._receiveIndexes(created);

@ -1,11 +1,6 @@
/*
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const serverProxy = require('./server-proxy');
@ -14,15 +9,8 @@
const AnnotationsHistory = require('./annotations-history');
const { checkObjectType } = require('./common');
const { Task } = require('./session');
const {
Loader,
Dumper,
} = require('./annotation-formats.js');
const {
ScriptingError,
DataError,
ArgumentError,
} = require('./exceptions');
const { Loader, Dumper } = require('./annotation-formats');
const { ScriptingError, DataError, ArgumentError } = require('./exceptions');
const jobCache = new WeakMap();
const taskCache = new WeakMap();
@ -36,9 +24,7 @@
return jobCache;
}
throw new ScriptingError(
`Unknown session type was received ${sessionType}`,
);
throw new ScriptingError(`Unknown session type was received ${sessionType}`);
}
async function getAnnotationsFromServer(session) {
@ -46,8 +32,7 @@
const cache = getCache(sessionType);
if (!cache.has(session)) {
const rawAnnotations = await serverProxy.annotations
.getAnnotations(sessionType, session.id);
const rawAnnotations = await serverProxy.annotations.getAnnotations(sessionType, session.id);
// Get meta information about frames
const startFrame = sessionType === 'job' ? session.startFrame : 0;
@ -122,6 +107,19 @@
);
}
function searchEmptyFrame(session, frameFrom, frameTo) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.searchEmpty(frameFrom, frameTo);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function mergeAnnotations(session, objectStates) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
@ -229,28 +227,22 @@
async function uploadAnnotations(session, file, loader) {
const sessionType = session instanceof Task ? 'task' : 'job';
if (!(loader instanceof Loader)) {
throw new ArgumentError(
'A loader must be instance of Loader class',
);
throw new ArgumentError('A loader must be instance of Loader class');
}
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',
);
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);
result = await serverProxy.annotations.dumpAnnotations(session.task.id, name, dumper.name);
} else {
result = await serverProxy.annotations
.dumpAnnotations(session.id, name, dumper.name);
result = await serverProxy.annotations.dumpAnnotations(session.id, name, dumper.name);
}
return result;
@ -284,19 +276,14 @@
async function exportDataset(session, format) {
if (!(format instanceof String || typeof format === 'string')) {
throw new ArgumentError(
'Format must be a 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',
);
throw new ArgumentError('A dataset can only be created from a task');
}
let result = null;
result = await serverProxy.tasks
.exportDataset(session.id, format);
result = await serverProxy.tasks.exportDataset(session.id, format);
return result;
}
@ -327,6 +314,19 @@
);
}
function freezeHistory(session, frozen) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.freeze(frozen);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function clearActions(session) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
@ -360,6 +360,7 @@
hasUnsavedChanges,
mergeAnnotations,
searchAnnotations,
searchEmptyFrame,
splitAnnotations,
groupAnnotations,
clearAnnotations,
@ -372,6 +373,7 @@
exportDataset,
undoActions,
redoActions,
freezeHistory,
clearActions,
getActions,
closeSession,

@ -1,52 +1,22 @@
/*
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* eslint prefer-arrow-callback: [ "error", { "allowNamedFunctions": true } ] */
/* global
require:false
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const lambdaManager = require('./lambda-manager');
const {
isBoolean,
isInteger,
isEnum,
isString,
checkFilter,
isBoolean, isInteger, isEnum, isString, checkFilter,
} = require('./common');
const { TaskStatus, TaskMode } = require('./enums');
const User = require('./user');
const { AnnotationFormats } = require('./annotation-formats.js');
const { AnnotationFormats } = require('./annotation-formats');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
function attachUsers(task, users) {
if (task.assignee !== null) {
[task.assignee] = users.filter((user) => user.id === task.assignee);
}
for (const segment of task.segments) {
for (const job of segment.jobs) {
if (job.assignee !== null) {
[job.assignee] = users.filter((user) => user.id === job.assignee);
}
}
}
if (task.owner !== null) {
[task.owner] = users.filter((user) => user.id === task.owner);
}
return task;
}
const { Project } = require('./project');
function implementAPI(cvat) {
cvat.plugins.list.implementation = PluginRegistry.list;
@ -79,10 +49,24 @@
return result;
};
cvat.server.register.implementation = async (username, firstName, lastName,
email, password1, password2, userConfirmations) => {
const user = await serverProxy.server.register(username, firstName,
lastName, email, password1, password2, userConfirmations);
cvat.server.register.implementation = async (
username,
firstName,
lastName,
email,
password1,
password2,
userConfirmations,
) => {
const user = await serverProxy.server.register(
username,
firstName,
lastName,
email,
password1,
password2,
userConfirmations,
);
return new User(user);
};
@ -99,6 +83,14 @@
await serverProxy.server.changePassword(oldPassword, newPassword1, newPassword2);
};
cvat.server.requestPasswordReset.implementation = async (email) => {
await serverProxy.server.requestPasswordReset(email);
};
cvat.server.resetPassword.implementation = async (newPassword1, newPassword2, uid, token) => {
await serverProxy.server.resetPassword(newPassword1, newPassword2, uid, token);
};
cvat.server.authorized.implementation = async () => {
const result = await serverProxy.server.authorized();
return result;
@ -109,17 +101,31 @@
return result;
};
cvat.server.installedApps.implementation = async () => {
const result = await serverProxy.server.installedApps();
return result;
};
cvat.users.get.implementation = async (filter) => {
checkFilter(filter, {
id: isInteger,
self: isBoolean,
search: isString,
limit: isInteger,
});
let users = null;
if ('self' in filter && filter.self) {
users = await serverProxy.users.getSelf();
users = await serverProxy.users.self();
users = [users];
} else {
users = await serverProxy.users.getUsers();
const searchParams = {};
for (const key in filter) {
if (filter[key] && key !== 'self') {
searchParams[key] = filter[key];
}
}
users = await serverProxy.users.get(new URLSearchParams(searchParams).toString());
}
users = users.map((user) => new User(user));
@ -132,44 +138,37 @@
jobID: isInteger,
});
if (('taskID' in filter) && ('jobID' in filter)) {
throw new ArgumentError(
'Only one of fields "taskID" and "jobID" allowed simultaneously',
);
if ('taskID' in filter && 'jobID' in filter) {
throw new ArgumentError('Only one of fields "taskID" and "jobID" allowed simultaneously');
}
if (!Object.keys(filter).length) {
throw new ArgumentError(
'Job filter must not be empty',
);
throw new ArgumentError('Job filter must not be empty');
}
let tasks = null;
let tasks = [];
if ('taskID' in filter) {
tasks = await serverProxy.tasks.getTasks(`id=${filter.taskID}`);
} else {
const job = await serverProxy.jobs.getJob(filter.jobID);
if (typeof (job.task_id) !== 'undefined') {
const job = await serverProxy.jobs.get(filter.jobID);
if (typeof job.task_id !== 'undefined') {
tasks = await serverProxy.tasks.getTasks(`id=${job.task_id}`);
}
}
// If task was found by its id, then create task instance and get Job instance from it
if (tasks !== null && tasks.length) {
const users = (await serverProxy.users.getUsers())
.map((userData) => new User(userData));
const task = new Task(attachUsers(tasks[0], users));
return filter.jobID ? task.jobs
.filter((job) => job.id === filter.jobID) : task.jobs;
if (tasks.length) {
const task = new Task(tasks[0]);
return filter.jobID ? task.jobs.filter((job) => job.id === filter.jobID) : task.jobs;
}
return [];
return tasks;
};
cvat.tasks.get.implementation = async (filter) => {
checkFilter(filter, {
page: isInteger,
projectId: isInteger,
name: isString,
id: isInteger,
owner: isString,
@ -181,40 +180,79 @@
if ('search' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError(
'Do not use the filter field "search" with others',
);
throw new ArgumentError('Do not use the filter field "search" with others');
}
}
if ('id' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError(
'Do not use the filter field "id" with others',
);
throw new ArgumentError('Do not use the filter field "id" with others');
}
}
if (
'projectId' in filter
&& (('page' in filter && Object.keys(filter).length > 2) || Object.keys(filter).length > 2)
) {
throw new ArgumentError('Do not use the filter field "projectId" with other');
}
const searchParams = new URLSearchParams();
for (const field of ['name', 'owner', 'assignee', 'search', 'status', 'mode', 'id', 'page']) {
for (const field of ['name', 'owner', 'assignee', 'search', 'status', 'mode', 'id', 'page', 'projectId']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]);
}
}
const users = (await serverProxy.users.getUsers())
.map((userData) => new User(userData));
const tasksData = await serverProxy.tasks.getTasks(searchParams.toString());
const tasks = tasksData
.map((task) => attachUsers(task, users))
.map((task) => new Task(task));
const tasks = tasksData.map((task) => new Task(task));
tasks.count = tasksData.count;
return tasks;
};
cvat.projects.get.implementation = async (filter) => {
checkFilter(filter, {
id: isInteger,
page: isInteger,
name: isString,
assignee: isString,
owner: isString,
search: isString,
status: isEnum.bind(TaskStatus),
});
if ('search' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError('Do not use the filter field "search" with others');
}
}
if ('id' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError('Do not use the filter field "id" with others');
}
}
const searchParams = new URLSearchParams();
for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]);
}
}
const projectsData = await serverProxy.projects.get(searchParams.toString());
// prettier-ignore
const projects = projectsData.map((project) => new Project(project));
projects.count = projectsData.count;
return projects;
};
cvat.projects.searchNames.implementation = async (search, limit) => serverProxy.projects.searchNames(search, limit);
return cvat;
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,153 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
const User = require('./user');
const { ArgumentError } = require('./exceptions');
const { negativeIDGenerator } = require('./common');
/**
* Class representing a single comment
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Comment {
constructor(initialData) {
const data = {
id: undefined,
message: undefined,
created_date: undefined,
updated_date: undefined,
removed: false,
author: undefined,
};
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property];
}
}
if (data.author && !(data.author instanceof User)) data.author = new User(data.author);
if (typeof id === 'undefined') {
data.id = negativeIDGenerator();
}
if (typeof data.created_date === 'undefined') {
data.created_date = new Date().toISOString();
}
Object.defineProperties(
this,
Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Comment
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* @name message
* @type {string}
* @memberof module:API.cvat.classes.Comment
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
message: {
get: () => data.message,
set: (value) => {
if (!value.trim().length) {
throw new ArgumentError('Value must not be empty');
}
data.message = value;
},
},
/**
* @name createdDate
* @type {string}
* @memberof module:API.cvat.classes.Comment
* @readonly
* @instance
*/
createdDate: {
get: () => data.created_date,
},
/**
* @name updatedDate
* @type {string}
* @memberof module:API.cvat.classes.Comment
* @readonly
* @instance
*/
updatedDate: {
get: () => data.updated_date,
},
/**
* Instance of a user who has created the comment
* @name author
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Comment
* @readonly
* @instance
*/
author: {
get: () => data.author,
},
/**
* @name removed
* @type {boolean}
* @memberof module:API.cvat.classes.Comment
* @instance
*/
removed: {
get: () => data.removed,
set: (value) => {
if (typeof value !== 'boolean') {
throw new ArgumentError('Value must be a boolean value');
}
data.removed = value;
},
},
__internal: {
get: () => data,
},
}),
);
}
serialize() {
const data = {
message: this.message,
};
if (this.id > 0) {
data.id = this.id;
}
if (this.createdDate) {
data.created_date = this.createdDate;
}
if (this.updatedDate) {
data.updated_date = this.updatedDate;
}
if (this.author) {
data.author = this.author.serialize();
}
return data;
}
toJSON() {
const data = this.serialize();
const { author, ...updated } = data;
return {
...updated,
author_id: author ? author.id : undefined,
};
}
}
module.exports = Comment;

@ -1,21 +1,16 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const { ArgumentError } = require('./exceptions');
function isBoolean(value) {
return typeof (value) === 'boolean';
return typeof value === 'boolean';
}
function isInteger(value) {
return typeof (value) === 'number' && Number.isInteger(value);
return typeof value === 'number' && Number.isInteger(value);
}
// Called with specific Enum context
@ -32,20 +27,16 @@
}
function isString(value) {
return typeof (value) === 'string';
return typeof value === 'string';
}
function checkFilter(filter, fields) {
for (const prop in filter) {
if (Object.prototype.hasOwnProperty.call(filter, prop)) {
if (!(prop in fields)) {
throw new ArgumentError(
`Unsupported filter property has been recieved: "${prop}"`,
);
throw new ArgumentError(`Unsupported filter property has been recieved: "${prop}"`);
} else if (!fields[prop](filter[prop])) {
throw new ArgumentError(
`Received filter property "${prop}" is not satisfied for checker`,
);
throw new ArgumentError(`Received filter property "${prop}" is not satisfied for checker`);
}
}
}
@ -53,15 +44,13 @@
function checkObjectType(name, value, type, instance) {
if (type) {
if (typeof (value) !== type) {
if (typeof value !== type) {
// specific case for integers which aren't native type in JS
if (type === 'integer' && Number.isInteger(value)) {
return true;
}
throw new ArgumentError(
`"${name}" is expected to be "${type}", but "${typeof (value)}" has been got.`,
);
throw new ArgumentError(`"${name}" is expected to be "${type}", but "${typeof value}" has been got.`);
}
} else if (instance) {
if (!(value instanceof instance)) {
@ -72,15 +61,20 @@
);
}
throw new ArgumentError(
`"${name}" is expected to be ${instance.name}, but "undefined" has been got.`,
);
throw new ArgumentError(`"${name}" is expected to be ${instance.name}, but "undefined" has been got.`);
}
}
return true;
}
function negativeIDGenerator() {
const value = negativeIDGenerator.start;
negativeIDGenerator.start -= 1;
return value;
}
negativeIDGenerator.start = -1;
module.exports = {
isBoolean,
isInteger,
@ -88,5 +82,6 @@
isString,
checkFilter,
checkObjectType,
negativeIDGenerator,
};
})();

@ -1,7 +1,6 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
module.exports = {
backendAPI: 'http://localhost:7000/api/v1',

@ -1,11 +1,6 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
const Axios = require('axios');
@ -13,7 +8,6 @@ Axios.defaults.withCredentials = true;
Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN';
Axios.defaults.xsrfCookieName = 'csrftoken';
onmessage = (e) => {
Axios.get(e.data.url, e.data.config)
.then((response) => {
@ -26,7 +20,9 @@ onmessage = (e) => {
.catch((error) => {
postMessage({
id: e.data.id,
error,
error: error,
status: error.response.status,
responseData: error.response.data,
isSuccess: false,
});
});

@ -1,33 +1,32 @@
/*
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
/**
* Share files types
* @enum {string}
* @name ShareFileType
* @memberof module:API.cvat.enums
* @property {string} DIR 'DIR'
* @property {string} REG 'REG'
* @readonly
*/
* Share files types
* @enum {string}
* @name ShareFileType
* @memberof module:API.cvat.enums
* @property {string} DIR 'DIR'
* @property {string} REG 'REG'
* @readonly
*/
const ShareFileType = Object.freeze({
DIR: 'DIR',
REG: 'REG',
});
/**
* Task statuses
* @enum {string}
* @name TaskStatus
* @memberof module:API.cvat.enums
* @property {string} ANNOTATION 'annotation'
* @property {string} VALIDATION 'validation'
* @property {string} COMPLETED 'completed'
* @readonly
*/
* Task statuses
* @enum {string}
* @name TaskStatus
* @memberof module:API.cvat.enums
* @property {string} ANNOTATION 'annotation'
* @property {string} VALIDATION 'validation'
* @property {string} COMPLETED 'completed'
* @readonly
*/
const TaskStatus = Object.freeze({
ANNOTATION: 'annotation',
VALIDATION: 'validation',
@ -35,18 +34,34 @@
});
/**
* List of RQ statuses
* @enum {string}
* @name RQStatus
* @memberof module:API.cvat.enums
* @property {string} QUEUED 'queued'
* @property {string} STARTED 'started'
* @property {string} FINISHED 'finished'
* @property {string} FAILED 'failed'
* @property {string} UNKNOWN 'unknown'
* @readonly
*/
const RQStatus = Object.freeze({
* Review statuses
* @enum {string}
* @name ReviewStatus
* @memberof module:API.cvat.enums
* @property {string} ACCEPTED 'accepted'
* @property {string} REJECTED 'rejected'
* @property {string} REVIEW_FURTHER 'review_further'
* @readonly
*/
const ReviewStatus = Object.freeze({
ACCEPTED: 'accepted',
REJECTED: 'rejected',
REVIEW_FURTHER: 'review_further',
});
/**
* List of RQ statuses
* @enum {string}
* @name RQStatus
* @memberof module:API.cvat.enums
* @property {string} QUEUED 'queued'
* @property {string} STARTED 'started'
* @property {string} FINISHED 'finished'
* @property {string} FAILED 'failed'
* @property {string} UNKNOWN 'unknown'
* @readonly
*/
const RQStatus = Object.freeze({
QUEUED: 'queued',
STARTED: 'started',
FINISHED: 'finished',
@ -55,31 +70,31 @@
});
/**
* Task modes
* @enum {string}
* @name TaskMode
* @memberof module:API.cvat.enums
* @property {string} ANNOTATION 'annotation'
* @property {string} INTERPOLATION 'interpolation'
* @readonly
*/
* Task modes
* @enum {string}
* @name TaskMode
* @memberof module:API.cvat.enums
* @property {string} ANNOTATION 'annotation'
* @property {string} INTERPOLATION 'interpolation'
* @readonly
*/
const TaskMode = Object.freeze({
ANNOTATION: 'annotation',
INTERPOLATION: 'interpolation',
});
/**
* Attribute types
* @enum {string}
* @name AttributeType
* @memberof module:API.cvat.enums
* @property {string} CHECKBOX 'checkbox'
* @property {string} SELECT 'select'
* @property {string} RADIO 'radio'
* @property {string} NUMBER 'number'
* @property {string} TEXT 'text'
* @readonly
*/
* Attribute types
* @enum {string}
* @name AttributeType
* @memberof module:API.cvat.enums
* @property {string} CHECKBOX 'checkbox'
* @property {string} SELECT 'select'
* @property {string} RADIO 'radio'
* @property {string} NUMBER 'number'
* @property {string} TEXT 'text'
* @readonly
*/
const AttributeType = Object.freeze({
CHECKBOX: 'checkbox',
RADIO: 'radio',
@ -89,15 +104,15 @@
});
/**
* Object types
* @enum {string}
* @name ObjectType
* @memberof module:API.cvat.enums
* @property {string} TAG 'tag'
* @property {string} SHAPE 'shape'
* @property {string} TRACK 'track'
* @readonly
*/
* Object types
* @enum {string}
* @name ObjectType
* @memberof module:API.cvat.enums
* @property {string} TAG 'tag'
* @property {string} SHAPE 'shape'
* @property {string} TRACK 'track'
* @readonly
*/
const ObjectType = Object.freeze({
TAG: 'tag',
SHAPE: 'shape',
@ -105,17 +120,17 @@
});
/**
* Object shapes
* @enum {string}
* @name ObjectShape
* @memberof module:API.cvat.enums
* @property {string} RECTANGLE 'rectangle'
* @property {string} POLYGON 'polygon'
* @property {string} POLYLINE 'polyline'
* @property {string} POINTS 'points'
* @property {string} CUBOID 'cuboid'
* @readonly
*/
* Object shapes
* @enum {string}
* @name ObjectShape
* @memberof module:API.cvat.enums
* @property {string} RECTANGLE 'rectangle'
* @property {string} POLYGON 'polygon'
* @property {string} POLYLINE 'polyline'
* @property {string} POINTS 'points'
* @property {string} CUBOID 'cuboid'
* @readonly
*/
const ObjectShape = Object.freeze({
RECTANGLE: 'rectangle',
POLYGON: 'polygon',
@ -125,17 +140,17 @@
});
/**
* Annotation type
* @enum {string}
* @name Source
* @memberof module:API.cvat.enums
* @property {string} MANUAL 'manual'
* @property {string} AUTO 'auto'
* @readonly
*/
* Annotation type
* @enum {string}
* @name Source
* @memberof module:API.cvat.enums
* @property {string} MANUAL 'manual'
* @property {string} AUTO 'auto'
* @readonly
*/
const Source = Object.freeze({
MANUAL:'manual',
AUTO:'auto',
MANUAL: 'manual',
AUTO: 'auto',
});
/**
@ -211,27 +226,27 @@
});
/**
* Types of actions with annotations
* @enum {string}
* @name HistoryActions
* @memberof module:API.cvat.enums
* @property {string} CHANGED_LABEL Changed label
* @property {string} CHANGED_ATTRIBUTES Changed attributes
* @property {string} CHANGED_POINTS Changed points
* @property {string} CHANGED_OUTSIDE Changed outside
* @property {string} CHANGED_OCCLUDED Changed occluded
* @property {string} CHANGED_ZORDER Changed z-order
* @property {string} CHANGED_LOCK Changed lock
* @property {string} CHANGED_COLOR Changed color
* @property {string} CHANGED_HIDDEN Changed hidden
* @property {string} CHANGED_SOURCE Changed source
* @property {string} MERGED_OBJECTS Merged objects
* @property {string} SPLITTED_TRACK Splitted track
* @property {string} GROUPED_OBJECTS Grouped objects
* @property {string} CREATED_OBJECTS Created objects
* @property {string} REMOVED_OBJECT Removed object
* @readonly
*/
* Types of actions with annotations
* @enum {string}
* @name HistoryActions
* @memberof module:API.cvat.enums
* @property {string} CHANGED_LABEL Changed label
* @property {string} CHANGED_ATTRIBUTES Changed attributes
* @property {string} CHANGED_POINTS Changed points
* @property {string} CHANGED_OUTSIDE Changed outside
* @property {string} CHANGED_OCCLUDED Changed occluded
* @property {string} CHANGED_ZORDER Changed z-order
* @property {string} CHANGED_LOCK Changed lock
* @property {string} CHANGED_COLOR Changed color
* @property {string} CHANGED_HIDDEN Changed hidden
* @property {string} CHANGED_SOURCE Changed source
* @property {string} MERGED_OBJECTS Merged objects
* @property {string} SPLITTED_TRACK Splitted track
* @property {string} GROUPED_OBJECTS Grouped objects
* @property {string} CREATED_OBJECTS Created objects
* @property {string} REMOVED_OBJECT Removed object
* @readonly
*/
const HistoryActions = Object.freeze({
CHANGED_LABEL: 'Changed label',
CHANGED_ATTRIBUTES: 'Changed attributes',
@ -265,23 +280,49 @@
};
/**
* Array of hex colors
* @name colors
* @memberof module:API.cvat.enums
* @type {string[]}
* @readonly
*/
* Array of hex colors
* @name colors
* @memberof module:API.cvat.enums
* @type {string[]}
* @readonly
*/
const colors = [
'#33ddff', '#fa3253', '#34d1b7', '#ff007c', '#ff6037', '#ddff33',
'#24b353', '#b83df5', '#66ff66', '#32b7fa', '#ffcc33', '#83e070',
'#fafa37', '#5986b3', '#8c78f0', '#ff6a4d', '#f078f0', '#2a7dd1',
'#b25050', '#cc3366', '#cc9933', '#aaf0d1', '#ff00cc', '#3df53d',
'#fa32b7', '#fa7dbb', '#ff355e', '#f59331', '#3d3df5', '#733380',
'#33ddff',
'#fa3253',
'#34d1b7',
'#ff007c',
'#ff6037',
'#ddff33',
'#24b353',
'#b83df5',
'#66ff66',
'#32b7fa',
'#ffcc33',
'#83e070',
'#fafa37',
'#5986b3',
'#8c78f0',
'#ff6a4d',
'#f078f0',
'#2a7dd1',
'#b25050',
'#cc3366',
'#cc9933',
'#aaf0d1',
'#ff00cc',
'#3df53d',
'#fa32b7',
'#fa7dbb',
'#ff355e',
'#f59331',
'#3d3df5',
'#733380',
];
module.exports = {
ShareFileType,
TaskStatus,
ReviewStatus,
TaskMode,
AttributeType,
ObjectType,

@ -1,11 +1,6 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const Platform = require('platform');
@ -13,15 +8,15 @@
const config = require('./config');
/**
* Base exception class
* @memberof module:API.cvat.exceptions
* @extends Error
* @ignore
*/
* Base exception class
* @memberof module:API.cvat.exceptions
* @extends Error
* @ignore
*/
class Exception extends Error {
/**
* @param {string} message - Exception message
*/
* @param {string} message - Exception message
*/
constructor(message) {
super(message);
@ -32,126 +27,125 @@
const filename = `${info.fileName}`;
const line = info.lineNumber;
const column = info.columnNumber;
const {
jobID,
taskID,
clientID,
} = config;
const { jobID, taskID, clientID } = config;
const projID = undefined; // wasn't implemented
Object.defineProperties(this, Object.freeze({
system: {
/**
* @name system
* @type {string}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => system,
},
client: {
/**
* @name client
* @type {string}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => client,
},
time: {
/**
* @name time
* @type {string}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => time,
},
jobID: {
/**
* @name jobID
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => jobID,
},
taskID: {
/**
* @name taskID
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => taskID,
},
projID: {
/**
* @name projID
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => projID,
},
clientID: {
/**
* @name clientID
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => clientID,
},
filename: {
/**
* @name filename
* @type {string}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => filename,
},
line: {
/**
* @name line
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => line,
},
column: {
/**
* @name column
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => column,
},
}));
Object.defineProperties(
this,
Object.freeze({
system: {
/**
* @name system
* @type {string}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => system,
},
client: {
/**
* @name client
* @type {string}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => client,
},
time: {
/**
* @name time
* @type {string}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => time,
},
jobID: {
/**
* @name jobID
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => jobID,
},
taskID: {
/**
* @name taskID
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => taskID,
},
projID: {
/**
* @name projID
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => projID,
},
clientID: {
/**
* @name clientID
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => clientID,
},
filename: {
/**
* @name filename
* @type {string}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => filename,
},
line: {
/**
* @name line
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => line,
},
column: {
/**
* @name column
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => column,
},
}),
);
}
/**
* Save an exception on a server
* @name save
* @method
* @memberof Exception
* @instance
* @async
*/
* Save an exception on a server
* @name save
* @method
* @memberof Exception
* @instance
* @async
*/
async save() {
const exceptionObject = {
system: this.system,
@ -178,86 +172,89 @@
}
/**
* Exceptions are referred with arguments data
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
* Exceptions are referred with arguments data
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
class ArgumentError extends Exception {
/**
* @param {string} message - Exception message
*/
* @param {string} message - Exception message
*/
constructor(message) {
super(message);
}
}
/**
* Unexpected problems with data which are not connected with a user input
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
* Unexpected problems with data which are not connected with a user input
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
class DataError extends Exception {
/**
* @param {string} message - Exception message
*/
/**
* @param {string} message - Exception message
*/
constructor(message) {
super(message);
}
}
/**
* Unexpected situations in code
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
* Unexpected situations in code
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
class ScriptingError extends Exception {
/**
* @param {string} message - Exception message
*/
* @param {string} message - Exception message
*/
constructor(message) {
super(message);
}
}
/**
* Plugin-referred exceptions
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
* Plugin-referred exceptions
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
class PluginError extends Exception {
/**
* @param {string} message - Exception message
*/
* @param {string} message - Exception message
*/
constructor(message) {
super(message);
}
}
/**
* Exceptions in interaction with a server
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
* Exceptions in interaction with a server
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
class ServerError extends Exception {
/**
* @param {string} message - Exception message
* @param {(string|integer)} code - Response code
*/
* @param {string} message - Exception message
* @param {(string|integer)} code - Response code
*/
constructor(message, code) {
super(message);
Object.defineProperties(this, Object.freeze({
/**
* @name code
* @type {(string|integer)}
* @memberof module:API.cvat.exceptions.ServerError
* @readonly
* @instance
*/
code: {
get: () => code,
},
}));
Object.defineProperties(
this,
Object.freeze({
/**
* @name code
* @type {(string|integer)}
* @memberof module:API.cvat.exceptions.ServerError
* @readonly
* @instance
*/
code: {
get: () => code,
},
}),
);
}
}

@ -1,12 +1,6 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
global:false
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const cvatData = require('cvat-data');
@ -19,100 +13,95 @@
const frameDataCache = {};
/**
* Class provides meta information about specific frame and frame itself
* @memberof module:API.cvat.classes
* @hideconstructor
*/
* Class provides meta information about specific frame and frame itself
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class FrameData {
constructor({
width,
height,
name,
taskID,
frameNumber,
startFrame,
stopFrame,
decodeForward,
width, height, name, taskID, frameNumber, startFrame, stopFrame, decodeForward,
}) {
Object.defineProperties(this, Object.freeze({
/**
* @name filename
* @type {string}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
filename: {
value: name,
writable: false,
},
/**
* @name width
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
width: {
value: width,
writable: false,
},
/**
* @name height
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
height: {
value: height,
writable: false,
},
tid: {
value: taskID,
writable: false,
},
/**
* @name number
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
number: {
value: frameNumber,
writable: false,
},
startFrame: {
value: startFrame,
writable: false,
},
stopFrame: {
value: stopFrame,
writable: false,
},
decodeForward: {
value: decodeForward,
writable: false,
},
}));
Object.defineProperties(
this,
Object.freeze({
/**
* @name filename
* @type {string}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
filename: {
value: name,
writable: false,
},
/**
* @name width
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
width: {
value: width,
writable: false,
},
/**
* @name height
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
height: {
value: height,
writable: false,
},
tid: {
value: taskID,
writable: false,
},
/**
* @name number
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
number: {
value: frameNumber,
writable: false,
},
startFrame: {
value: startFrame,
writable: false,
},
stopFrame: {
value: stopFrame,
writable: false,
},
decodeForward: {
value: decodeForward,
writable: false,
},
}),
);
}
/**
* Method returns URL encoded image which can be placed in the img tag
* @method data
* @returns {string}
* @memberof module:API.cvat.classes.FrameData
* @instance
* @async
* @param {function} [onServerRequest = () => {}]
* callback which will be called if data absences local
* @throws {module:API.cvat.exception.ServerError}
* @throws {module:API.cvat.exception.PluginError}
*/
* Method returns URL encoded image which can be placed in the img tag
* @method data
* @returns {string}
* @memberof module:API.cvat.classes.FrameData
* @instance
* @async
* @param {function} [onServerRequest = () => {}]
* callback which will be called if data absences local
* @throws {module:API.cvat.exception.ServerError}
* @throws {module:API.cvat.exception.PluginError}
*/
async data(onServerRequest = () => {}) {
const result = await PluginRegistry
.apiWrapper.call(this, FrameData.prototype.data, onServerRequest);
const result = await PluginRegistry.apiWrapper.call(this, FrameData.prototype.data, onServerRequest);
return result;
}
}
@ -136,15 +125,14 @@
const { provider } = frameDataCache[this.tid];
const { chunkSize } = frameDataCache[this.tid];
const start = parseInt(this.number / chunkSize, 10) * chunkSize;
const stop = Math.min(
this.stopFrame,
(parseInt(this.number / chunkSize, 10) + 1) * chunkSize - 1,
);
const stop = Math.min(this.stopFrame, (parseInt(this.number / chunkSize, 10) + 1) * chunkSize - 1);
const chunkNumber = Math.floor(this.number / chunkSize);
const onDecodeAll = async (frameNumber) => {
if (frameDataCache[this.tid].activeChunkRequest
&& chunkNumber === frameDataCache[this.tid].activeChunkRequest.chunkNumber) {
if (
frameDataCache[this.tid].activeChunkRequest
&& chunkNumber === frameDataCache[this.tid].activeChunkRequest.chunkNumber
) {
const callbackArray = frameDataCache[this.tid].activeChunkRequest.callbacks;
for (let i = callbackArray.length - 1; i >= 0; --i) {
if (callbackArray[i].frameNumber === frameNumber) {
@ -160,8 +148,10 @@
};
const rejectRequestAll = () => {
if (frameDataCache[this.tid].activeChunkRequest
&& chunkNumber === frameDataCache[this.tid].activeChunkRequest.chunkNumber) {
if (
frameDataCache[this.tid].activeChunkRequest
&& chunkNumber === frameDataCache[this.tid].activeChunkRequest.chunkNumber
) {
for (const r of frameDataCache[this.tid].activeChunkRequest.callbacks) {
r.reject(r.frameNumber);
}
@ -172,147 +162,160 @@
const makeActiveRequest = () => {
const taskDataCache = frameDataCache[this.tid];
const activeChunk = taskDataCache.activeChunkRequest;
activeChunk.request = serverProxy.frames.getData(this.tid,
activeChunk.chunkNumber).then((chunk) => {
frameDataCache[this.tid].activeChunkRequest.completed = true;
if (!taskDataCache.nextChunkRequest) {
provider.requestDecodeBlock(chunk,
taskDataCache.activeChunkRequest.start,
taskDataCache.activeChunkRequest.stop,
taskDataCache.activeChunkRequest.onDecodeAll,
taskDataCache.activeChunkRequest.rejectRequestAll);
}
}).catch((exception) => {
if (exception instanceof Exception) {
reject(exception);
} else {
reject(new Exception(exception.message));
}
}).finally(() => {
if (taskDataCache.nextChunkRequest) {
if (taskDataCache.activeChunkRequest) {
for (const r of taskDataCache.activeChunkRequest.callbacks) {
r.reject(r.frameNumber);
activeChunk.request = serverProxy.frames
.getData(this.tid, activeChunk.chunkNumber)
.then((chunk) => {
frameDataCache[this.tid].activeChunkRequest.completed = true;
if (!taskDataCache.nextChunkRequest) {
provider.requestDecodeBlock(
chunk,
taskDataCache.activeChunkRequest.start,
taskDataCache.activeChunkRequest.stop,
taskDataCache.activeChunkRequest.onDecodeAll,
taskDataCache.activeChunkRequest.rejectRequestAll,
);
}
})
.catch((exception) => {
if (exception instanceof Exception) {
reject(exception);
} else {
reject(new Exception(exception.message));
}
})
.finally(() => {
if (taskDataCache.nextChunkRequest) {
if (taskDataCache.activeChunkRequest) {
for (const r of taskDataCache.activeChunkRequest.callbacks) {
r.reject(r.frameNumber);
}
}
taskDataCache.activeChunkRequest = taskDataCache.nextChunkRequest;
taskDataCache.nextChunkRequest = null;
makeActiveRequest();
}
taskDataCache.activeChunkRequest = taskDataCache.nextChunkRequest;
taskDataCache.nextChunkRequest = null;
makeActiveRequest();
}
});
});
};
if (isNode) {
resolve('Dummy data');
} else if (isBrowser) {
provider.frame(this.number).then((frame) => {
if (frame === null) {
onServerRequest();
const activeRequest = frameDataCache[this.tid].activeChunkRequest;
if (!provider.isChunkCached(start, stop)) {
if (!activeRequest
|| (activeRequest
&& activeRequest.completed
&& activeRequest.chunkNumber !== chunkNumber)) {
if (activeRequest && activeRequest.rejectRequestAll) {
activeRequest.rejectRequestAll();
}
frameDataCache[this.tid].activeChunkRequest = {
request: null,
chunkNumber,
start,
stop,
onDecodeAll,
rejectRequestAll,
completed: false,
callbacks: [{
provider
.frame(this.number)
.then((frame) => {
if (frame === null) {
onServerRequest();
const activeRequest = frameDataCache[this.tid].activeChunkRequest;
if (!provider.isChunkCached(start, stop)) {
if (
!activeRequest
|| (activeRequest
&& activeRequest.completed
&& activeRequest.chunkNumber !== chunkNumber)
) {
if (activeRequest && activeRequest.rejectRequestAll) {
activeRequest.rejectRequestAll();
}
frameDataCache[this.tid].activeChunkRequest = {
request: null,
chunkNumber,
start,
stop,
onDecodeAll,
rejectRequestAll,
completed: false,
callbacks: [
{
resolve: resolveWrapper,
reject,
frameNumber: this.number,
},
],
};
makeActiveRequest();
} else if (activeRequest.chunkNumber === chunkNumber) {
if (!activeRequest.onDecodeAll && !activeRequest.rejectRequestAll) {
activeRequest.onDecodeAll = onDecodeAll;
activeRequest.rejectRequestAll = rejectRequestAll;
}
activeRequest.callbacks.push({
resolve: resolveWrapper,
reject,
frameNumber: this.number,
}],
};
makeActiveRequest();
} else if (activeRequest.chunkNumber === chunkNumber) {
if (!activeRequest.onDecodeAll
&& !activeRequest.rejectRequestAll) {
activeRequest.onDecodeAll = onDecodeAll;
activeRequest.rejectRequestAll = rejectRequestAll;
});
} else {
if (frameDataCache[this.tid].nextChunkRequest) {
const { callbacks } = frameDataCache[this.tid].nextChunkRequest;
for (const r of callbacks) {
r.reject(r.frameNumber);
}
}
frameDataCache[this.tid].nextChunkRequest = {
request: null,
chunkNumber,
start,
stop,
onDecodeAll,
rejectRequestAll,
completed: false,
callbacks: [
{
resolve: resolveWrapper,
reject,
frameNumber: this.number,
},
],
};
}
} else {
activeRequest.callbacks.push({
resolve: resolveWrapper,
reject,
frameNumber: this.number,
});
} else {
if (frameDataCache[this.tid].nextChunkRequest) {
const { callbacks } = frameDataCache[this.tid].nextChunkRequest;
for (const r of callbacks) {
r.reject(r.frameNumber);
}
}
frameDataCache[this.tid].nextChunkRequest = {
request: null,
chunkNumber,
start,
stop,
onDecodeAll,
rejectRequestAll,
completed: false,
callbacks: [{
resolve: resolveWrapper,
reject,
frameNumber: this.number,
}],
};
provider.requestDecodeBlock(null, start, stop, onDecodeAll, rejectRequestAll);
}
} else {
activeRequest.callbacks.push({
resolve: resolveWrapper,
reject,
frameNumber: this.number,
});
provider.requestDecodeBlock(null, start, stop,
onDecodeAll, rejectRequestAll);
}
} else {
if (this.number % chunkSize > chunkSize / 4
&& provider.decodedBlocksCacheSize > 1
&& this.decodeForward
&& !provider.isNextChunkExists(this.number)) {
const nextChunkNumber = Math.floor(this.number / chunkSize) + 1;
if (nextChunkNumber * chunkSize < this.stopFrame) {
provider.setReadyToLoading(nextChunkNumber);
const nextStart = nextChunkNumber * chunkSize;
const nextStop = (nextChunkNumber + 1) * chunkSize - 1;
if (!provider.isChunkCached(nextStart, nextStop)) {
if (!frameDataCache[this.tid].activeChunkRequest) {
frameDataCache[this.tid].activeChunkRequest = {
request: null,
chunkNumber: nextChunkNumber,
start: nextStart,
stop: nextStop,
onDecodeAll: null,
rejectRequestAll: null,
completed: false,
callbacks: [],
};
makeActiveRequest();
if (
this.number % chunkSize > chunkSize / 4
&& provider.decodedBlocksCacheSize > 1
&& this.decodeForward
&& !provider.isNextChunkExists(this.number)
) {
const nextChunkNumber = Math.floor(this.number / chunkSize) + 1;
if (nextChunkNumber * chunkSize < this.stopFrame) {
provider.setReadyToLoading(nextChunkNumber);
const nextStart = nextChunkNumber * chunkSize;
const nextStop = (nextChunkNumber + 1) * chunkSize - 1;
if (!provider.isChunkCached(nextStart, nextStop)) {
if (!frameDataCache[this.tid].activeChunkRequest) {
frameDataCache[this.tid].activeChunkRequest = {
request: null,
chunkNumber: nextChunkNumber,
start: nextStart,
stop: nextStop,
onDecodeAll: null,
rejectRequestAll: null,
completed: false,
callbacks: [],
};
makeActiveRequest();
}
} else {
provider.requestDecodeBlock(null, nextStart, nextStop, null, null);
}
} else {
provider.requestDecodeBlock(null, nextStart, nextStop,
null, null);
}
}
resolveWrapper(frame);
}
resolveWrapper(frame);
}
}).catch((exception) => {
if (exception instanceof Exception) {
reject(exception);
} else {
reject(new Exception(exception.message));
}
});
})
.catch((exception) => {
if (exception instanceof Exception) {
reject(exception);
} else {
reject(new Exception(exception.message));
}
});
}
});
};
@ -324,16 +327,12 @@
[size] = meta.frames;
} else if (mode === 'annotation') {
if (frame >= meta.size) {
throw new ArgumentError(
`Meta information about frame ${frame} can't be received from the server`,
);
throw new ArgumentError(`Meta information about frame ${frame} can't be received from the server`);
} else {
size = meta.frames[frame];
}
} else {
throw new DataError(
`Invalid mode is specified ${mode}`,
);
throw new DataError(`Invalid mode is specified ${mode}`);
}
return size;
}
@ -377,23 +376,28 @@
decodeForward: false,
});
frameData.data().then(() => {
if (!(chunkIdx in this._requestedChunks)
|| !this._requestedChunks[chunkIdx].requestedFrames.has(requestedFrame)) {
reject(chunkIdx);
} else {
this._requestedChunks[chunkIdx].requestedFrames.delete(requestedFrame);
this._requestedChunks[chunkIdx].buffer[requestedFrame] = frameData;
if (this._requestedChunks[chunkIdx].requestedFrames.size === 0) {
const bufferedframes = Object.keys(
this._requestedChunks[chunkIdx].buffer,
).map((f) => +f);
this._requestedChunks[chunkIdx].resolve(new Set(bufferedframes));
frameData
.data()
.then(() => {
if (
!(chunkIdx in this._requestedChunks)
|| !this._requestedChunks[chunkIdx].requestedFrames.has(requestedFrame)
) {
reject(chunkIdx);
} else {
this._requestedChunks[chunkIdx].requestedFrames.delete(requestedFrame);
this._requestedChunks[chunkIdx].buffer[requestedFrame] = frameData;
if (this._requestedChunks[chunkIdx].requestedFrames.size === 0) {
const bufferedframes = Object.keys(this._requestedChunks[chunkIdx].buffer).map(
(f) => +f,
);
this._requestedChunks[chunkIdx].resolve(new Set(bufferedframes));
}
}
}
}).catch(() => {
reject(chunkIdx);
});
})
.catch(() => {
reject(chunkIdx);
});
}
});
}
@ -455,7 +459,7 @@
await this.fillBuffer(start, step, count);
this._activeFillBufferRequest = false;
} catch (error) {
if (typeof (error) === 'number' && error in this._requestedChunks) {
if (typeof error === 'number' && error in this._requestedChunks) {
this._activeFillBufferRequest = false;
}
throw error;
@ -465,8 +469,7 @@
async require(frameNumber, taskID, fillBuffer, frameStep) {
for (const frame in this._buffer) {
if (frame < frameNumber
|| frame >= frameNumber + this._size * frameStep) {
if (frame < frameNumber || frame >= frameNumber + this._size * frameStep) {
delete this._buffer[frame];
}
}
@ -486,9 +489,12 @@
frame = this._buffer[frameNumber];
delete this._buffer[frameNumber];
const cachedFrames = this.cachedFrames();
if (fillBuffer && !this._activeFillBufferRequest
if (
fillBuffer
&& !this._activeFillBufferRequest
&& this._size > this._chunkSize
&& cachedFrames.length < (this._size * 3) / 4) {
&& cachedFrames.length < (this._size * 3) / 4
) {
const maxFrame = cachedFrames ? Math.max(...cachedFrames) : frameNumber;
if (maxFrame < this._stopFrame) {
this.makeFillRequest(maxFrame + 1, frameStep).catch((e) => {
@ -512,8 +518,10 @@
clear() {
for (const chunkIdx in this._requestedChunks) {
if (Object.prototype.hasOwnProperty.call(this._requestedChunks, chunkIdx)
&& this._requestedChunks[chunkIdx].reject) {
if (
Object.prototype.hasOwnProperty.call(this._requestedChunks, chunkIdx)
&& this._requestedChunks[chunkIdx].reject
) {
this._requestedChunks[chunkIdx].reject('not needed');
}
}
@ -530,38 +538,39 @@
async function getPreview(taskID) {
return new Promise((resolve, reject) => {
// Just go to server and get preview (no any cache)
serverProxy.frames.getPreview(taskID).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);
});
serverProxy.frames
.getPreview(taskID)
.then((result) => {
if (isNode) {
// eslint-disable-next-line no-undef
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);
});
});
}
async function getFrame(taskID, chunkSize, chunkType, mode, frame,
startFrame, stopFrame, isPlaying, step) {
async function getFrame(taskID, chunkSize, chunkType, mode, frame, startFrame, stopFrame, isPlaying, step) {
if (!(taskID in frameDataCache)) {
const blockType = chunkType === 'video' ? cvatData.BlockType.MP4VIDEO
: cvatData.BlockType.ARCHIVE;
const blockType = chunkType === 'video' ? cvatData.BlockType.MP4VIDEO : cvatData.BlockType.ARCHIVE;
const meta = await serverProxy.frames.getMeta(taskID);
const mean = meta.frames.reduce((a, b) => a + b.width * b.height, 0)
/ meta.frames.length;
const stdDev = Math.sqrt(meta.frames.map(
(x) => Math.pow(x.width * x.height - mean, 2),
).reduce((a, b) => a + b) / meta.frames.length);
const mean = meta.frames.reduce((a, b) => a + b.width * b.height, 0) / meta.frames.length;
const stdDev = Math.sqrt(
meta.frames.map((x) => Math.pow(x.width * x.height - mean, 2)).reduce((a, b) => a + b)
/ meta.frames.length,
);
// limit of decoded frames cache by 2GB
const decodedBlocksCacheSize = Math.floor(2147483648 / (mean + stdDev) / 4 / chunkSize)
|| 1;
const decodedBlocksCacheSize = Math.floor(2147483648 / (mean + stdDev) / 4 / chunkSize) || 1;
frameDataCache[taskID] = {
meta,
@ -570,8 +579,11 @@
startFrame,
stopFrame,
provider: new cvatData.FrameProvider(
blockType, chunkSize, Math.max(decodedBlocksCacheSize, 9),
decodedBlocksCacheSize, 1,
blockType,
chunkSize,
Math.max(decodedBlocksCacheSize, 9),
decodedBlocksCacheSize,
1,
),
frameBuffer: new FrameBuffer(
Math.min(180, decodedBlocksCacheSize * chunkSize),

@ -0,0 +1,335 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
const quickhull = require('quickhull');
const PluginRegistry = require('./plugins');
const Comment = require('./comment');
const User = require('./user');
const { ArgumentError } = require('./exceptions');
const { negativeIDGenerator } = require('./common');
const serverProxy = require('./server-proxy');
/**
* Class representing a single issue
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Issue {
constructor(initialData) {
const data = {
id: undefined,
position: undefined,
comment_set: [],
frame: undefined,
created_date: undefined,
resolved_date: undefined,
owner: undefined,
resolver: undefined,
removed: false,
};
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property];
}
}
if (data.owner && !(data.owner instanceof User)) data.owner = new User(data.owner);
if (data.resolver && !(data.resolver instanceof User)) data.resolver = new User(data.resolver);
if (data.comment_set) {
data.comment_set = data.comment_set.map((comment) => new Comment(comment));
}
if (typeof data.id === 'undefined') {
data.id = negativeIDGenerator();
}
if (typeof data.created_date === 'undefined') {
data.created_date = new Date().toISOString();
}
Object.defineProperties(
this,
Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* Region of interests of the issue
* @name position
* @type {number[]}
* @memberof module:API.cvat.classes.Issue
* @instance
* @readonly
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
position: {
get: () => data.position,
set: (value) => {
if (Array.isArray(value) || value.some((coord) => typeof coord !== 'number')) {
throw new ArgumentError(`Array of numbers is expected. Got ${value}`);
}
data.position = value;
},
},
/**
* List of comments attached to the issue
* @name comments
* @type {module:API.cvat.classes.Comment[]}
* @memberof module:API.cvat.classes.Issue
* @instance
* @readonly
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
comments: {
get: () => data.comment_set.filter((comment) => !comment.removed),
},
/**
* @name frame
* @type {integer}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
*/
frame: {
get: () => data.frame,
},
/**
* @name createdDate
* @type {string}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
*/
createdDate: {
get: () => data.created_date,
},
/**
* @name resolvedDate
* @type {string}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
*/
resolvedDate: {
get: () => data.resolved_date,
},
/**
* An instance of a user who has raised the issue
* @name owner
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
*/
owner: {
get: () => data.owner,
},
/**
* An instance of a user who has resolved the issue
* @name resolver
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
*/
resolver: {
get: () => data.resolver,
},
/**
* @name removed
* @type {boolean}
* @memberof module:API.cvat.classes.Comment
* @instance
*/
removed: {
get: () => data.removed,
set: (value) => {
if (typeof value !== 'boolean') {
throw new ArgumentError('Value must be a boolean value');
}
data.removed = value;
},
},
__internal: {
get: () => data,
},
}),
);
}
static hull(coordinates) {
if (coordinates.length > 4) {
const points = coordinates.reduce((acc, coord, index, arr) => {
if (index % 2) acc.push({ x: arr[index - 1], y: coord });
return acc;
}, []);
return quickhull(points)
.map((point) => [point.x, point.y])
.flat();
}
return coordinates;
}
/**
* @typedef {Object} CommentData
* @property {number} [author] an ID of a user who has created the comment
* @property {string} message a comment message
* @global
*/
/**
* Method appends a comment to the issue
* For a new issue it saves comment locally, for a saved issue it saves comment on the server
* @method comment
* @memberof module:API.cvat.classes.Issue
* @param {CommentData} data
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async comment(data) {
const result = await PluginRegistry.apiWrapper.call(this, Issue.prototype.comment, data);
return result;
}
/**
* The method resolves the issue
* New issues are resolved locally, server-saved issues are resolved on the server
* @method resolve
* @memberof module:API.cvat.classes.Issue
* @param {module:API.cvat.classes.User} user
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async resolve(user) {
const result = await PluginRegistry.apiWrapper.call(this, Issue.prototype.resolve, user);
return result;
}
/**
* The method resolves the issue
* New issues are reopened locally, server-saved issues are reopened on the server
* @method reopen
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async reopen() {
const result = await PluginRegistry.apiWrapper.call(this, Issue.prototype.reopen);
return result;
}
serialize() {
const { comments } = this;
const data = {
position: this.position,
frame: this.frame,
comment_set: comments.map((comment) => comment.serialize()),
};
if (this.id > 0) {
data.id = this.id;
}
if (this.createdDate) {
data.created_date = this.createdDate;
}
if (this.resolvedDate) {
data.resolved_date = this.resolvedDate;
}
if (this.owner) {
data.owner = this.owner.toJSON();
}
if (this.resolver) {
data.resolver = this.resolver.toJSON();
}
return data;
}
toJSON() {
const data = this.serialize();
const { owner, resolver, ...updated } = data;
return {
...updated,
comment_set: this.comments.map((comment) => comment.toJSON()),
owner_id: owner ? owner.id : undefined,
resolver_id: resolver ? resolver.id : undefined,
};
}
}
Issue.prototype.comment.implementation = async function (data) {
if (typeof data !== 'object' || data === null) {
throw new ArgumentError(`The argument "data" must be a not null object. Got ${data}`);
}
if (typeof data.message !== 'string' || data.message.length < 1) {
throw new ArgumentError(`Comment message must be a not empty string. Got ${data.message}`);
}
if (!(data.author instanceof User)) {
throw new ArgumentError(`Author of the comment must a User instance. Got ${data.author}`);
}
const comment = new Comment(data);
const { id } = this;
if (id >= 0) {
const jsonified = comment.toJSON();
jsonified.issue = id;
const response = await serverProxy.comments.create(jsonified);
const savedComment = new Comment(response);
this.__internal.comment_set.push(savedComment);
} else {
this.__internal.comment_set.push(comment);
}
};
Issue.prototype.resolve.implementation = async function (user) {
if (!(user instanceof User)) {
throw new ArgumentError(`The argument "user" must be an instance of a User class. Got ${typeof user}`);
}
const { id } = this;
if (id >= 0) {
const response = await serverProxy.issues.update(id, { resolver_id: user.id });
this.__internal.resolved_date = response.resolved_date;
this.__internal.resolver = new User(response.resolver);
} else {
this.__internal.resolved_date = new Date().toISOString();
this.__internal.resolver = user;
}
};
Issue.prototype.reopen.implementation = async function () {
const { id } = this;
if (id >= 0) {
const response = await serverProxy.issues.update(id, { resolver_id: null });
this.__internal.resolved_date = response.resolved_date;
this.__internal.resolver = response.resolver;
} else {
this.__internal.resolved_date = null;
this.__internal.resolver = null;
}
};
module.exports = Issue;

@ -1,23 +1,16 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const {
AttributeType,
} = require('./enums');
const { AttributeType } = require('./enums');
const { ArgumentError } = require('./exceptions');
/**
* Class representing an attribute
* @memberof module:API.cvat.classes
* @hideconstructor
*/
* Class representing an attribute
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Attribute {
constructor(initialData) {
const data = {
@ -42,73 +35,74 @@
}
if (!Object.values(AttributeType).includes(data.input_type)) {
throw new ArgumentError(
`Got invalid attribute type ${data.input_type}`,
);
throw new ArgumentError(`Got invalid attribute type ${data.input_type}`);
}
Object.defineProperties(this, Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* @name defaultValue
* @type {(string|integer|boolean)}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
defaultValue: {
get: () => data.default_value,
},
/**
* @name inputType
* @type {module:API.cvat.enums.AttributeType}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
inputType: {
get: () => data.input_type,
},
/**
* @name mutable
* @type {boolean}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
mutable: {
get: () => data.mutable,
},
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
name: {
get: () => data.name,
},
/**
* @name values
* @type {(string[]|integer[]|boolean[])}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
values: {
get: () => [...data.values],
},
}));
Object.defineProperties(
this,
Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* @name defaultValue
* @type {(string|integer|boolean)}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
defaultValue: {
get: () => data.default_value,
},
/**
* @name inputType
* @type {module:API.cvat.enums.AttributeType}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
inputType: {
get: () => data.input_type,
},
/**
* @name mutable
* @type {boolean}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
mutable: {
get: () => data.mutable,
},
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
name: {
get: () => data.name,
},
/**
* @name values
* @type {(string[]|integer[]|boolean[])}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
values: {
get: () => [...data.values],
},
}),
);
}
toJSON() {
@ -120,7 +114,7 @@
values: this.values,
};
if (typeof (this.id) !== 'undefined') {
if (typeof this.id !== 'undefined') {
object.id = this.id;
}
@ -129,10 +123,10 @@
}
/**
* Class representing a label
* @memberof module:API.cvat.classes
* @hideconstructor
*/
* Class representing a label
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Label {
constructor(initialData) {
const data = {
@ -151,62 +145,67 @@
data.attributes = [];
if (Object.prototype.hasOwnProperty.call(initialData, 'attributes')
&& Array.isArray(initialData.attributes)) {
if (
Object.prototype.hasOwnProperty.call(initialData, 'attributes')
&& Array.isArray(initialData.attributes)
) {
for (const attrData of initialData.attributes) {
data.attributes.push(new Attribute(attrData));
}
}
Object.defineProperties(this, Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
name: {
get: () => data.name,
},
/**
* @name color
* @type {string}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
color: {
get: () => data.color,
set: (color) => {
if (typeof color === 'string' && color.match(/^#[0-9a-f]{6}$|^$/)) {
data.color = color;
} else {
throw new ArgumentError('Trying to set wrong color format');
}
Object.defineProperties(
this,
Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
name: {
get: () => data.name,
},
/**
* @name color
* @type {string}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
color: {
get: () => data.color,
set: (color) => {
if (typeof color === 'string' && color.match(/^#[0-9a-f]{6}$|^$/)) {
data.color = color;
} else {
throw new ArgumentError('Trying to set wrong color format');
}
},
},
/**
* @name attributes
* @type {module:API.cvat.classes.Attribute[]}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
attributes: {
get: () => [...data.attributes],
},
},
/**
* @name attributes
* @type {module:API.cvat.classes.Attribute[]}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
attributes: {
get: () => [...data.attributes],
},
}));
}),
);
}
toJSON() {
@ -216,7 +215,7 @@
color: this.color,
};
if (typeof (this.id) !== 'undefined') {
if (typeof this.id !== 'undefined') {
object.id = this.id;
}

@ -1,11 +1,6 @@
/*
* Copyright (C) 2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions');
@ -28,14 +23,12 @@ class LambdaManager {
const models = [];
for (const model of result) {
models.push(new MLModel({
id: model.id,
name: model.name,
description: model.description,
framework: model.framework,
labels: [...model.labels],
type: model.kind,
}));
models.push(
new MLModel({
...model,
type: model.kind,
}),
);
}
this.cachedList = models;
@ -45,20 +38,18 @@ class LambdaManager {
async run(task, model, args) {
if (!(task instanceof Task)) {
throw new ArgumentError(
`Argument task is expected to be an instance of Task class, but got ${typeof (task)}`,
`Argument task is expected to be an instance of Task class, but got ${typeof task}`,
);
}
if (!(model instanceof MLModel)) {
throw new ArgumentError(
`Argument model is expected to be an instance of MLModel class, but got ${typeof (model)}`,
`Argument model is expected to be an instance of MLModel class, but got ${typeof model}`,
);
}
if (args && typeof (args) !== 'object') {
throw new ArgumentError(
`Argument args is expected to be an object, but got ${typeof (model)}`,
);
if (args && typeof args !== 'object') {
throw new ArgumentError(`Argument args is expected to be an object, but got ${typeof model}`);
}
const body = args;
@ -82,7 +73,7 @@ class LambdaManager {
}
async cancel(requestID) {
if (typeof (requestID) !== 'string') {
if (typeof requestID !== 'string') {
throw new ArgumentError(`Request id argument is required to be a string. But got ${requestID}`);
}
@ -112,7 +103,11 @@ class LambdaManager {
delete this.listening[requestID];
}
} catch (error) {
onUpdate(RQStatus.UNKNOWN, 0, `Could not get a status of the request ${requestID}. ${error.toString()}`);
onUpdate(
RQStatus.UNKNOWN,
0,
`Could not get a status of the request ${requestID}. ${error.toString()}`,
);
}
};

@ -1,21 +1,17 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/* global
require:false
*/
const { detect } = require('detect-browser');
const PluginRegistry = require('./plugins');
const { ArgumentError } = require('./exceptions');
const { LogType } = require('./enums');
/**
* Class representing a single log
* @memberof module:API.cvat.classes
* @hideconstructor
*/
* Class representing a single log
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Log {
constructor(logType, payload) {
this.onCloseCallback = null;
@ -30,7 +26,7 @@ class Log {
}
validatePayload() {
if (typeof (this.payload) !== 'object') {
if (typeof this.payload !== 'object') {
throw new ArgumentError('Payload must be an object');
}
@ -63,22 +59,21 @@ class Log {
}
/**
* Method saves a durable log in a storage <br>
* Note then you can call close() multiple times <br>
* Log duration will be computed based on the latest call <br>
* All payloads will be shallowly combined (all top level properties will exist)
* @method close
* @memberof module:API.cvat.classes.Log
* @param {object} [payload] part of payload can be added when close a log
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
* Method saves a durable log in a storage <br>
* Note then you can call close() multiple times <br>
* Log duration will be computed based on the latest call <br>
* All payloads will be shallowly combined (all top level properties will exist)
* @method close
* @memberof module:API.cvat.classes.Log
* @param {object} [payload] part of payload can be added when close a log
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async close(payload = {}) {
const result = await PluginRegistry
.apiWrapper.call(this, Log.prototype.close, payload);
const result = await PluginRegistry.apiWrapper.call(this, Log.prototype.close, payload);
return result;
}
}
@ -96,8 +91,7 @@ class LogWithCount extends Log {
validatePayload() {
Log.prototype.validatePayload.call(this);
if (!Number.isInteger(this.payload.count) || this.payload.count < 1) {
const message = `The field "count" is required for "${this.type}" log`
+ 'It must be a positive integer';
const message = `The field "count" is required for "${this.type}" log. It must be a positive integer`;
throw new ArgumentError(message);
}
}
@ -148,12 +142,14 @@ class LogWithWorkingTime extends Log {
validatePayload() {
Log.prototype.validatePayload.call(this);
if (!('working_time' in this.payload)
|| !typeof (this.payload.working_time) === 'number'
if (
!('working_time' in this.payload)
|| !typeof this.payload.working_time === 'number'
|| this.payload.working_time < 0
) {
const message = `The field "working_time" is required for ${this.type} log. `
+ 'It must be a number not less than 0';
const message = `
The field "working_time" is required for ${this.type} log. It must be a number not less than 0
`;
throw new ArgumentError(message);
}
}
@ -163,40 +159,35 @@ class LogWithExceptionInfo extends Log {
validatePayload() {
Log.prototype.validatePayload.call(this);
if (typeof (this.payload.message) !== 'string') {
const message = `The field "message" is required for ${this.type} log. `
+ 'It must be a string';
if (typeof this.payload.message !== 'string') {
const message = `The field "message" is required for ${this.type} log. It must be a string`;
throw new ArgumentError(message);
}
if (typeof (this.payload.filename) !== 'string') {
const message = `The field "filename" is required for ${this.type} log. `
+ 'It must be a string';
if (typeof this.payload.filename !== 'string') {
const message = `The field "filename" is required for ${this.type} log. It must be a string`;
throw new ArgumentError(message);
}
if (typeof (this.payload.line) !== 'number') {
const message = `The field "line" is required for ${this.type} log. `
+ 'It must be a number';
if (typeof this.payload.line !== 'number') {
const message = `The field "line" is required for ${this.type} log. It must be a number`;
throw new ArgumentError(message);
}
if (typeof (this.payload.column) !== 'number') {
const message = `The field "column" is required for ${this.type} log. `
+ 'It must be a number';
if (typeof this.payload.column !== 'number') {
const message = `The field "column" is required for ${this.type} log. It must be a number`;
throw new ArgumentError(message);
}
if (typeof (this.payload.stack) !== 'string') {
const message = `The field "stack" is required for ${this.type} log. `
+ 'It must be a string';
if (typeof this.payload.stack !== 'string') {
const message = `The field "stack" is required for ${this.type} log. It must be a string`;
throw new ArgumentError(message);
}
}
dump() {
let body = super.dump();
const payload = body.payload;
const { payload } = body;
const client = detect();
body = {
...body,
@ -222,8 +213,11 @@ class LogWithExceptionInfo extends Log {
function logFactory(logType, payload) {
const logsWithCount = [
LogType.deleteObject, LogType.mergeObjects, LogType.copyObject,
LogType.undoAction, LogType.redoAction,
LogType.deleteObject,
LogType.mergeObjects,
LogType.copyObject,
LogType.undoAction,
LogType.redoAction,
];
if (logsWithCount.includes(logType)) {

@ -1,11 +1,7 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/* global
require:false
*/
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const logFactory = require('./log');
@ -14,6 +10,12 @@ const { LogType } = require('./enums');
const WORKING_TIME_THRESHOLD = 100000; // ms, 1.66 min
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
class LoggerStorage {
constructor() {
this.clientID = Date.now().toString().substr(-6);
@ -22,6 +24,7 @@ class LoggerStorage {
this.collection = [];
this.ignoreRules = {}; // by event
this.isActiveChecker = null;
this.saving = false;
this.ignoreRules[LogType.zoomImage] = {
lastLog: null,
@ -34,8 +37,10 @@ class LoggerStorage {
this.ignoreRules[LogType.changeAttribute] = {
lastLog: null,
ignore(previousLog, currentPayload) {
return currentPayload.object_id === previousLog.payload.object_id
&& currentPayload.id === previousLog.payload.id;
return (
currentPayload.object_id === previousLog.payload.object_id
&& currentPayload.id === previousLog.payload.id
);
},
};
}
@ -50,32 +55,28 @@ class LoggerStorage {
}
async configure(isActiveChecker, activityHelper) {
const result = await PluginRegistry
.apiWrapper.call(
this, LoggerStorage.prototype.configure,
isActiveChecker, activityHelper,
);
const result = await PluginRegistry.apiWrapper.call(
this,
LoggerStorage.prototype.configure,
isActiveChecker,
activityHelper,
);
return result;
}
async log(logType, payload = {}, wait = false) {
const result = await PluginRegistry
.apiWrapper.call(this, LoggerStorage.prototype.log, logType, payload, wait);
const result = await PluginRegistry.apiWrapper.call(this, LoggerStorage.prototype.log, logType, payload, wait);
return result;
}
async save() {
const result = await PluginRegistry
.apiWrapper.call(this, LoggerStorage.prototype.save);
const result = await PluginRegistry.apiWrapper.call(this, LoggerStorage.prototype.save);
return result;
}
}
LoggerStorage.prototype.configure.implementation = function (
isActiveChecker,
userActivityCallback,
) {
if (typeof (isActiveChecker) !== 'function') {
LoggerStorage.prototype.configure.implementation = function (isActiveChecker, userActivityCallback) {
if (typeof isActiveChecker !== 'function') {
throw new ArgumentError('isActiveChecker argument must be callable');
}
@ -88,11 +89,11 @@ LoggerStorage.prototype.configure.implementation = function (
};
LoggerStorage.prototype.log.implementation = function (logType, payload, wait) {
if (typeof (payload) !== 'object') {
if (typeof payload !== 'object') {
throw new ArgumentError('Payload must be an object');
}
if (typeof (wait) !== 'boolean') {
if (typeof wait !== 'boolean') {
throw new ArgumentError('Payload must be an object');
}
@ -146,6 +147,10 @@ LoggerStorage.prototype.log.implementation = function (logType, payload, wait) {
};
LoggerStorage.prototype.save.implementation = async function () {
while (this.saving) {
await sleep(100);
}
const collectionToSend = [...this.collection];
const lastLog = this.collection[this.collection.length - 1];
@ -164,14 +169,18 @@ LoggerStorage.prototype.save.implementation = async function () {
const userActivityLog = logFactory(LogType.sendUserActivity, logPayload);
collectionToSend.push(userActivityLog);
await serverProxy.logs.save(collectionToSend.map((log) => log.dump()));
for (const rule of Object.values(this.ignoreRules)) {
rule.lastLog = null;
try {
this.saving = true;
await serverProxy.logs.save(collectionToSend.map((log) => log.dump()));
for (const rule of Object.values(this.ignoreRules)) {
rule.lastLog = null;
}
this.collection = [];
this.workingTime = 0;
this.lastLogTime = Date.now();
} finally {
this.saving = false;
}
this.collection = [];
this.workingTime = 0;
this.lastLogTime = Date.now();
};
module.exports = new LoggerStorage();

@ -1,12 +1,11 @@
/*
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/**
* Class representing a machine learning model
* @memberof module:API.cvat.classes
*/
* Class representing a machine learning model
* @memberof module:API.cvat.classes
*/
class MLModel {
constructor(data) {
this._id = data.id;
@ -15,12 +14,17 @@ class MLModel {
this._framework = data.framework;
this._description = data.description;
this._type = data.type;
this._params = {
canvas: {
minPosVertices: data.min_pos_points,
},
};
}
/**
* @returns {string}
* @readonly
*/
*/
get id() {
return this._id;
}
@ -28,7 +32,7 @@ class MLModel {
/**
* @returns {string}
* @readonly
*/
*/
get name() {
return this._name;
}
@ -36,7 +40,7 @@ class MLModel {
/**
* @returns {string[]}
* @readonly
*/
*/
get labels() {
if (Array.isArray(this._labels)) {
return [...this._labels];
@ -48,7 +52,7 @@ class MLModel {
/**
* @returns {string}
* @readonly
*/
*/
get framework() {
return this._framework;
}
@ -56,7 +60,7 @@ class MLModel {
/**
* @returns {string}
* @readonly
*/
*/
get description() {
return this._description;
}
@ -64,10 +68,20 @@ class MLModel {
/**
* @returns {module:API.cvat.enums.ModelType}
* @readonly
*/
*/
get type() {
return this._type;
}
/**
* @returns {object}
* @readonly
*/
get params() {
return {
canvas: { ...this._params.canvas },
};
}
}
module.exports = MLModel;

@ -1,31 +1,26 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
const { Source } = require('./enums');
/* global
require:false
*/
(() => {
const PluginRegistry = require('./plugins');
const { ArgumentError } = require('./exceptions');
/**
* Class representing a state of an object on a specific frame
* @memberof module:API.cvat.classes
*/
* Class representing a state of an object on a specific frame
* @memberof module:API.cvat.classes
*/
class ObjectState {
/**
* @param {Object} serialized - is an dictionary which contains
* initial information about an ObjectState;
* </br> Necessary fields: objectType, shapeType, frame, updated, group
* </br> Optional fields: keyframes, clientID, serverID
* </br> Optional fields which can be set later: points, zOrder, outside,
* occluded, hidden, attributes, lock, label, color, keyframe, source
*/
* @param {Object} serialized - is an dictionary which contains
* initial information about an ObjectState;
* </br> Necessary fields: objectType, shapeType, frame, updated, group
* </br> Optional fields: keyframes, clientID, serverID
* </br> Optional fields which can be set later: points, zOrder, outside,
* occluded, hidden, attributes, lock, label, color, keyframe, source
*/
constructor(serialized) {
const data = {
label: null,
@ -77,282 +72,289 @@ const { Source } = require('./enums');
writable: false,
});
Object.defineProperties(this, Object.freeze({
// Internal property. We don't need document it.
updateFlags: {
get: () => data.updateFlags,
},
frame: {
/**
* @name frame
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.frame,
},
objectType: {
/**
* @name objectType
* @type {module:API.cvat.enums.ObjectType}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.objectType,
},
shapeType: {
/**
* @name shapeType
* @type {module:API.cvat.enums.ObjectShape}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.shapeType,
},
source: {
/**
* @name source
* @type {module:API.cvat.enums.Source}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.source,
},
clientID: {
/**
* @name clientID
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.clientID,
},
serverID: {
/**
* @name serverID
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.serverID,
},
label: {
/**
* @name shape
* @type {module:API.cvat.classes.Label}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.label,
set: (labelInstance) => {
data.updateFlags.label = true;
data.label = labelInstance;
Object.defineProperties(
this,
Object.freeze({
// Internal property. We don't need document it.
updateFlags: {
get: () => data.updateFlags,
},
},
color: {
/**
* @name color
* @type {string}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.color,
set: (color) => {
data.updateFlags.color = true;
data.color = color;
frame: {
/**
* @name frame
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.frame,
},
},
hidden: {
/**
* @name hidden
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.hidden,
set: (hidden) => {
data.updateFlags.hidden = true;
data.hidden = hidden;
objectType: {
/**
* @name objectType
* @type {module:API.cvat.enums.ObjectType}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.objectType,
},
},
points: {
/**
* @name points
* @type {number[]}
* @memberof module:API.cvat.classes.ObjectState
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
*/
get: () => data.points,
set: (points) => {
if (Array.isArray(points)) {
data.updateFlags.points = true;
data.points = [...points];
} else {
throw new ArgumentError(
'Points are expected to be an array '
+ `but got ${typeof (points) === 'object'
? points.constructor.name : typeof (points)}`,
);
}
shapeType: {
/**
* @name shapeType
* @type {module:API.cvat.enums.ObjectShape}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.shapeType,
},
},
group: {
/**
* Object with short group info { color, id }
* @name group
* @type {object}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @readonly
*/
get: () => data.group,
},
zOrder: {
/**
* @name zOrder
* @type {integer | null}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.zOrder,
set: (zOrder) => {
data.updateFlags.zOrder = true;
data.zOrder = zOrder;
source: {
/**
* @name source
* @type {module:API.cvat.enums.Source}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.source,
},
},
outside: {
/**
* @name outside
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.outside,
set: (outside) => {
data.updateFlags.outside = true;
data.outside = outside;
clientID: {
/**
* @name clientID
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.clientID,
},
},
keyframe: {
/**
* @name keyframe
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.keyframe,
set: (keyframe) => {
data.updateFlags.keyframe = true;
data.keyframe = keyframe;
serverID: {
/**
* @name serverID
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.serverID,
},
},
keyframes: {
/**
* Object of keyframes { first, prev, next, last }
* @name keyframes
* @type {object | null}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => {
if (typeof (data.keyframes) === 'object') {
return { ...data.keyframes };
}
label: {
/**
* @name shape
* @type {module:API.cvat.classes.Label}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.label,
set: (labelInstance) => {
data.updateFlags.label = true;
data.label = labelInstance;
},
},
color: {
/**
* @name color
* @type {string}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.color,
set: (color) => {
data.updateFlags.color = true;
data.color = color;
},
},
hidden: {
/**
* @name hidden
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.hidden,
set: (hidden) => {
data.updateFlags.hidden = true;
data.hidden = hidden;
},
},
points: {
/**
* @name points
* @type {number[]}
* @memberof module:API.cvat.classes.ObjectState
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
*/
get: () => data.points,
set: (points) => {
if (Array.isArray(points)) {
data.updateFlags.points = true;
data.points = [...points];
} else {
throw new ArgumentError(
'Points are expected to be an array '
+ `but got ${
typeof points === 'object' ? points.constructor.name : typeof points
}`,
);
}
},
},
group: {
/**
* Object with short group info { color, id }
* @name group
* @type {object}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @readonly
*/
get: () => data.group,
},
zOrder: {
/**
* @name zOrder
* @type {integer | null}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.zOrder,
set: (zOrder) => {
data.updateFlags.zOrder = true;
data.zOrder = zOrder;
},
},
outside: {
/**
* @name outside
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.outside,
set: (outside) => {
data.updateFlags.outside = true;
data.outside = outside;
},
},
keyframe: {
/**
* @name keyframe
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.keyframe,
set: (keyframe) => {
data.updateFlags.keyframe = true;
data.keyframe = keyframe;
},
},
keyframes: {
/**
* Object of keyframes { first, prev, next, last }
* @name keyframes
* @type {object | null}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => {
if (typeof data.keyframes === 'object') {
return { ...data.keyframes };
}
return null;
return null;
},
},
},
occluded: {
/**
* @name occluded
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.occluded,
set: (occluded) => {
data.updateFlags.occluded = true;
data.occluded = occluded;
occluded: {
/**
* @name occluded
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.occluded,
set: (occluded) => {
data.updateFlags.occluded = true;
data.occluded = occluded;
},
},
},
lock: {
/**
* @name lock
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.lock,
set: (lock) => {
data.updateFlags.lock = true;
data.lock = lock;
lock: {
/**
* @name lock
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.lock,
set: (lock) => {
data.updateFlags.lock = true;
data.lock = lock;
},
},
},
pinned: {
/**
* @name pinned
* @type {boolean | null}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => {
if (typeof (data.pinned) === 'boolean') {
return data.pinned;
}
pinned: {
/**
* @name pinned
* @type {boolean | null}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => {
if (typeof data.pinned === 'boolean') {
return data.pinned;
}
return null;
return null;
},
set: (pinned) => {
data.updateFlags.pinned = true;
data.pinned = pinned;
},
},
set: (pinned) => {
data.updateFlags.pinned = true;
data.pinned = pinned;
updated: {
/**
* Timestamp of the latest updated of the object
* @name updated
* @type {number}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @readonly
*/
get: () => data.updated,
},
},
updated: {
/**
* Timestamp of the latest updated of the object
* @name updated
* @type {number}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @readonly
*/
get: () => data.updated,
},
attributes: {
/**
* Object is id:value pairs where "id" is an integer
* attribute identifier and "value" is an attribute value
* @name attributes
* @type {Object}
* @memberof module:API.cvat.classes.ObjectState
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
*/
get: () => data.attributes,
set: (attributes) => {
if (typeof (attributes) !== 'object') {
throw new ArgumentError(
'Attributes are expected to be an object '
+ `but got ${typeof (attributes) === 'object'
? attributes.constructor.name : typeof (attributes)}`,
);
}
attributes: {
/**
* Object is id:value pairs where "id" is an integer
* attribute identifier and "value" is an attribute value
* @name attributes
* @type {Object}
* @memberof module:API.cvat.classes.ObjectState
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
*/
get: () => data.attributes,
set: (attributes) => {
if (typeof attributes !== 'object') {
throw new ArgumentError(
'Attributes are expected to be an object '
+ `but got ${
typeof attributes === 'object'
? attributes.constructor.name
: typeof attributes
}`,
);
}
for (const attrID of Object.keys(attributes)) {
data.updateFlags.attributes = true;
data.attributes[attrID] = attributes[attrID];
}
for (const attrID of Object.keys(attributes)) {
data.updateFlags.attributes = true;
data.attributes[attrID] = attributes[attrID];
}
},
},
},
}));
}),
);
this.label = serialized.label;
this.lock = serialized.lock;
@ -360,31 +362,31 @@ const { Source } = require('./enums');
if ([Source.MANUAL, Source.AUTO].includes(serialized.source)) {
data.source = serialized.source;
}
if (typeof (serialized.zOrder) === 'number') {
if (typeof serialized.zOrder === 'number') {
this.zOrder = serialized.zOrder;
}
if (typeof (serialized.occluded) === 'boolean') {
if (typeof serialized.occluded === 'boolean') {
this.occluded = serialized.occluded;
}
if (typeof (serialized.outside) === 'boolean') {
if (typeof serialized.outside === 'boolean') {
this.outside = serialized.outside;
}
if (typeof (serialized.keyframe) === 'boolean') {
if (typeof serialized.keyframe === 'boolean') {
this.keyframe = serialized.keyframe;
}
if (typeof (serialized.pinned) === 'boolean') {
if (typeof serialized.pinned === 'boolean') {
this.pinned = serialized.pinned;
}
if (typeof (serialized.hidden) === 'boolean') {
if (typeof serialized.hidden === 'boolean') {
this.hidden = serialized.hidden;
}
if (typeof (serialized.color) === 'string') {
if (typeof serialized.color === 'string') {
this.color = serialized.color;
}
if (Array.isArray(serialized.points)) {
this.points = serialized.points;
}
if (typeof (serialized.attributes) === 'object') {
if (typeof serialized.attributes === 'object') {
this.attributes = serialized.attributes;
}
@ -392,38 +394,36 @@ const { Source } = require('./enums');
}
/**
* Method saves/updates an object state in a collection
* @method save
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @returns {module:API.cvat.classes.ObjectState} updated state of an object
*/
* Method saves/updates an object state in a collection
* @method save
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @returns {module:API.cvat.classes.ObjectState} updated state of an object
*/
async save() {
const result = await PluginRegistry
.apiWrapper.call(this, ObjectState.prototype.save);
const result = await PluginRegistry.apiWrapper.call(this, ObjectState.prototype.save);
return result;
}
/**
* Method deletes an object from a collection
* @method delete
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @param {integer} frame current frame number
* @param {boolean} [force=false] delete object even if it is locked
* @async
* @returns {boolean} true if object has been deleted
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
* Method deletes an object from a collection
* @method delete
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @param {integer} frame current frame number
* @param {boolean} [force=false] delete object even if it is locked
* @async
* @returns {boolean} true if object has been deleted
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async delete(frame, force = false) {
const result = await PluginRegistry
.apiWrapper.call(this, ObjectState.prototype.delete, frame, force);
const result = await PluginRegistry.apiWrapper.call(this, ObjectState.prototype.delete, frame, force);
return result;
}
}

@ -1,11 +1,6 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const { PluginError } = require('./exceptions');
@ -16,8 +11,7 @@
// I have to optimize the wrapper
const pluginList = await PluginRegistry.list();
for (const plugin of pluginList) {
const pluginDecorators = plugin.functions
.filter((obj) => obj.callback === wrappedFunc)[0];
const pluginDecorators = plugin.functions.filter((obj) => obj.callback === wrappedFunc)[0];
if (pluginDecorators && pluginDecorators.enter) {
try {
await pluginDecorators.enter.call(this, plugin, ...args);
@ -34,8 +28,7 @@
let result = await wrappedFunc.implementation.call(this, ...args);
for (const plugin of pluginList) {
const pluginDecorators = plugin.functions
.filter((obj) => obj.callback === wrappedFunc)[0];
const pluginDecorators = plugin.functions.filter((obj) => obj.callback === wrappedFunc)[0];
if (pluginDecorators && pluginDecorators.leave) {
try {
result = await pluginDecorators.leave.call(this, plugin, result, ...args);
@ -56,15 +49,15 @@
static async register(plug) {
const functions = [];
if (typeof (plug) !== 'object') {
throw new PluginError(`Plugin should be an object, but got "${typeof (plug)}"`);
if (typeof plug !== 'object') {
throw new PluginError(`Plugin should be an object, but got "${typeof plug}"`);
}
if (!('name' in plug) || typeof (plug.name) !== 'string') {
if (!('name' in plug) || typeof plug.name !== 'string') {
throw new PluginError('Plugin must contain a "name" field and it must be a string');
}
if (!('description' in plug) || typeof (plug.description) !== 'string') {
if (!('description' in plug) || typeof plug.description !== 'string') {
throw new PluginError('Plugin must contain a "description" field and it must be a string');
}
@ -72,17 +65,19 @@
throw new PluginError('Plugin must not contain a "functions" field');
}
(function traverse(plugin, api) {
function traverse(plugin, api) {
const decorator = {};
for (const key in plugin) {
if (Object.prototype.hasOwnProperty.call(plugin, key)) {
if (typeof (plugin[key]) === 'object') {
if (typeof plugin[key] === 'object') {
if (Object.prototype.hasOwnProperty.call(api, key)) {
traverse(plugin[key], api[key]);
}
} else if (['enter', 'leave'].includes(key)
&& typeof (api) === 'function'
&& typeof (plugin[key] === 'function')) {
} else if (
['enter', 'leave'].includes(key)
&& typeof api === 'function'
&& typeof (plugin[key] === 'function')
) {
decorator.callback = api;
decorator[key] = plugin[key];
}
@ -92,9 +87,9 @@
if (Object.keys(decorator).length) {
functions.push(decorator);
}
}(plug, {
cvat: this,
}));
}
traverse(plug, { cvat: this });
Object.defineProperty(plug, 'functions', {
value: functions,

@ -0,0 +1,265 @@
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const { Label } = require('./labels');
const User = require('./user');
/**
* Class representing a project
* @memberof module:API.cvat.classes
*/
class Project {
/**
* In a fact you need use the constructor only if you want to create a project
* @param {object} initialData - Object which is used for initalization
* <br> It can contain keys:
* <br> <li style="margin-left: 10px;"> name
* <br> <li style="margin-left: 10px;"> labels
*/
constructor(initialData) {
const data = {
id: undefined,
name: undefined,
status: undefined,
assignee: undefined,
owner: undefined,
bug_tracker: undefined,
created_date: undefined,
updated_date: undefined,
};
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property];
}
}
data.labels = [];
data.tasks = [];
if (Array.isArray(initialData.labels)) {
for (const label of initialData.labels) {
const classInstance = new Label(label);
data.labels.push(classInstance);
}
}
if (Array.isArray(initialData.tasks)) {
for (const task of initialData.tasks) {
const taskInstance = new Task(task);
data.tasks.push(taskInstance);
}
}
Object.defineProperties(
this,
Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Project
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
name: {
get: () => data.name,
set: (value) => {
if (!value.trim().length) {
throw new ArgumentError('Value must not be empty');
}
data.name = value;
},
},
/**
* @name status
* @type {module:API.cvat.enums.TaskStatus}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
status: {
get: () => data.status,
},
/**
* Instance of a user who was assigned for the project
* @name assignee
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
assignee: {
get: () => data.assignee,
set: (assignee) => {
if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError('Value must be a user instance');
}
data.assignee = assignee;
},
},
/**
* Instance of a user who has created the project
* @name owner
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
owner: {
get: () => data.owner,
},
/**
* @name bugTracker
* @type {string}
* @memberof module:API.cvat.classes.Project
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
bugTracker: {
get: () => data.bug_tracker,
set: (tracker) => {
data.bug_tracker = tracker;
},
},
/**
* @name createdDate
* @type {string}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
*/
createdDate: {
get: () => data.created_date,
},
/**
* @name updatedDate
* @type {string}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
*/
updatedDate: {
get: () => data.updated_date,
},
/**
* After project has been created value can be appended only.
* @name labels
* @type {module:API.cvat.classes.Label[]}
* @memberof module:API.cvat.classes.Project
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
labels: {
get: () => [...data.labels],
set: (labels) => {
if (!Array.isArray(labels)) {
throw new ArgumentError('Value must be an array of Labels');
}
if (!Array.isArray(labels) || labels.some((label) => !(label instanceof Label))) {
throw new ArgumentError(
`Each array value must be an instance of Label. ${typeof label} was found`,
);
}
data.labels = [...labels];
},
},
/**
* Tasks linked with the project
* @name tasks
* @type {module:API.cvat.classes.Task[]}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
tasks: {
get: () => [...data.tasks],
},
}),
);
}
/**
* Method updates data of a created project or creates new project from scratch
* @method save
* @returns {module:API.cvat.classes.Project}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async save() {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.save);
return result;
}
/**
* Method deletes a task from a server
* @method delete
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async delete() {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.delete);
return result;
}
}
module.exports = {
Project,
};
Project.prototype.save.implementation = async function () {
if (typeof this.id !== 'undefined') {
const projectData = {
name: this.name,
assignee_id: this.assignee ? this.assignee.id : null,
bug_tracker: this.bugTracker,
labels: [...this.labels.map((el) => el.toJSON())],
};
await serverProxy.projects.save(this.id, projectData);
return this;
}
const projectSpec = {
name: this.name,
labels: [...this.labels.map((el) => el.toJSON())],
};
if (this.bugTracker) {
projectSpec.bug_tracker = this.bugTracker;
}
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;
};
})();

@ -0,0 +1,397 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
const store = require('store');
const PluginRegistry = require('./plugins');
const Issue = require('./issue');
const User = require('./user');
const { ArgumentError, DataError } = require('./exceptions');
const { ReviewStatus } = require('./enums');
const { negativeIDGenerator } = require('./common');
const serverProxy = require('./server-proxy');
/**
* Class representing a single review
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Review {
constructor(initialData) {
const data = {
id: undefined,
job: undefined,
issue_set: [],
estimated_quality: undefined,
status: undefined,
reviewer: undefined,
assignee: undefined,
reviewed_frames: undefined,
reviewed_states: undefined,
};
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property];
}
}
if (data.reviewer && !(data.reviewer instanceof User)) data.reviewer = new User(data.reviewer);
if (data.assignee && !(data.assignee instanceof User)) data.assignee = new User(data.assignee);
data.reviewed_frames = Array.isArray(data.reviewed_frames) ? new Set(data.reviewed_frames) : new Set();
data.reviewed_states = Array.isArray(data.reviewed_states) ? new Set(data.reviewed_states) : new Set();
if (data.issue_set) {
data.issue_set = data.issue_set.map((issue) => new Issue(issue));
}
if (typeof data.id === 'undefined') {
data.id = negativeIDGenerator();
}
Object.defineProperties(
this,
Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* An identifier of a job the review is attached to
* @name job
* @type {integer}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
job: {
get: () => data.job,
},
/**
* List of attached issues
* @name issues
* @type {number[]}
* @memberof module:API.cvat.classes.Review
* @instance
* @readonly
*/
issues: {
get: () => data.issue_set.filter((issue) => !issue.removed),
},
/**
* Estimated quality of the review
* @name estimatedQuality
* @type {number}
* @memberof module:API.cvat.classes.Review
* @instance
* @readonly
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
estimatedQuality: {
get: () => data.estimated_quality,
set: (value) => {
if (typeof value !== 'number' || value < 0 || value > 5) {
throw new ArgumentError(`Value must be a number in range [0, 5]. Got ${value}`);
}
data.estimated_quality = value;
},
},
/**
* @name status
* @type {module:API.cvat.enums.ReviewStatus}
* @memberof module:API.cvat.classes.Review
* @instance
* @readonly
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
status: {
get: () => data.status,
set: (status) => {
const type = ReviewStatus;
let valueInEnum = false;
for (const value in type) {
if (type[value] === status) {
valueInEnum = true;
break;
}
}
if (!valueInEnum) {
throw new ArgumentError(
'Value must be a value from the enumeration cvat.enums.ReviewStatus',
);
}
data.status = status;
},
},
/**
* An instance of a user who has done the review
* @name reviewer
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
reviewer: {
get: () => data.reviewer,
set: (reviewer) => {
if (!(reviewer instanceof User)) {
throw new ArgumentError(`Reviewer must be an instance of the User class. Got ${reviewer}`);
}
data.reviewer = reviewer;
},
},
/**
* An instance of a user who was assigned for annotation before the review
* @name assignee
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
assignee: {
get: () => data.assignee,
},
/**
* A set of frames that have been visited during review
* @name reviewedFrames
* @type {number[]}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
reviewedFrames: {
get: () => Array.from(data.reviewed_frames),
},
/**
* A set of reviewed states (server IDs combined with frames)
* @name reviewedFrames
* @type {string[]}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
reviewedStates: {
get: () => Array.from(data.reviewed_states),
},
__internal: {
get: () => data,
},
}),
);
}
/**
* Method appends a frame to a set of reviewed frames
* Reviewed frames are saved only in local storage
* @method reviewFrame
* @memberof module:API.cvat.classes.Review
* @param {number} frame
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async reviewFrame(frame) {
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.reviewFrame, frame);
return result;
}
/**
* Method appends a frame to a set of reviewed frames
* Reviewed states are saved only in local storage. They are used to automatic annotations quality assessment
* @method reviewStates
* @memberof module:API.cvat.classes.Review
* @param {string[]} stateIDs
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async reviewStates(stateIDs) {
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.reviewStates, stateIDs);
return result;
}
/**
* @typedef {Object} IssueData
* @property {number} frame
* @property {number[]} position
* @property {number} owner
* @property {CommentData[]} comment_set
* @global
*/
/**
* Method adds a new issue to the review
* @method openIssue
* @memberof module:API.cvat.classes.Review
* @param {IssueData} data
* @returns {module:API.cvat.classes.Issue}
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async openIssue(data) {
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.openIssue, data);
return result;
}
/**
* Method submits local review to the server
* @method submit
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.DataError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async submit() {
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.submit);
return result;
}
serialize() {
const { issues, reviewedFrames, reviewedStates } = this;
const data = {
job: this.job,
issue_set: issues.map((issue) => issue.serialize()),
reviewed_frames: Array.from(reviewedFrames),
reviewed_states: Array.from(reviewedStates),
};
if (this.id > 0) {
data.id = this.id;
}
if (typeof this.estimatedQuality !== 'undefined') {
data.estimated_quality = this.estimatedQuality;
}
if (typeof this.status !== 'undefined') {
data.status = this.status;
}
if (this.reviewer) {
data.reviewer = this.reviewer.toJSON();
}
if (this.assignee) {
data.reviewer = this.assignee.toJSON();
}
return data;
}
toJSON() {
const data = this.serialize();
const {
reviewer,
assignee,
reviewed_frames: reviewedFrames,
reviewed_states: reviewedStates,
...updated
} = data;
return {
...updated,
issue_set: this.issues.map((issue) => issue.toJSON()),
reviewer_id: reviewer ? reviewer.id : undefined,
assignee_id: assignee ? assignee.id : undefined,
};
}
async toLocalStorage() {
const data = this.serialize();
store.set(`job-${this.job}-review`, JSON.stringify(data));
}
}
Review.prototype.reviewFrame.implementation = function (frame) {
if (!Number.isInteger(frame)) {
throw new ArgumentError(`The argument "frame" is expected to be an integer. Got ${frame}`);
}
this.__internal.reviewed_frames.add(frame);
};
Review.prototype.reviewStates.implementation = function (stateIDs) {
if (!Array.isArray(stateIDs) || stateIDs.some((stateID) => typeof stateID !== 'string')) {
throw new ArgumentError(`The argument "stateIDs" is expected to be an array of string. Got ${stateIDs}`);
}
stateIDs.forEach((stateID) => this.__internal.reviewed_states.add(stateID));
};
Review.prototype.openIssue.implementation = async function (data) {
if (typeof data !== 'object' || data === null) {
throw new ArgumentError(`The argument "data" must be a not null object. Got ${data}`);
}
if (typeof data.frame !== 'number') {
throw new ArgumentError(`Issue frame must be a number. Got ${data.frame}`);
}
if (!(data.owner instanceof User)) {
throw new ArgumentError(`Issue owner must be a User instance. Got ${data.owner}`);
}
if (!Array.isArray(data.position) || data.position.some((coord) => typeof coord !== 'number')) {
throw new ArgumentError(`Issue position must be an array of numbers. Got ${data.position}`);
}
if (!Array.isArray(data.comment_set)) {
throw new ArgumentError(`Issue comment set must be an array. Got ${data.comment_set}`);
}
const copied = {
frame: data.frame,
position: Issue.hull(data.position),
owner: data.owner,
comment_set: [],
};
const issue = new Issue(copied);
for (const comment of data.comment_set) {
await issue.comment.implementation.call(issue, comment);
}
this.__internal.issue_set.push(issue);
return issue;
};
Review.prototype.submit.implementation = async function () {
if (typeof this.estimatedQuality === 'undefined') {
throw new DataError('Estimated quality is expected to be a number. Got "undefined"');
}
if (typeof this.status === 'undefined') {
throw new DataError('Review status is expected to be a string. Got "undefined"');
}
if (this.id < 0) {
const data = this.toJSON();
const response = await serverProxy.jobs.reviews.create(data);
store.remove(`job-${this.job}-review`);
this.__internal.id = response.id;
this.__internal.issue_set = response.issue_set.map((issue) => new Issue(issue));
this.__internal.estimated_quality = response.estimated_quality;
this.__internal.status = response.status;
if (response.reviewer) this.__internal.reviewer = new User(response.reviewer);
if (response.assignee) this.__internal.assignee = new User(response.assignee);
}
};
module.exports = Review;

@ -1,17 +1,10 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const FormData = require('form-data');
const {
ServerError,
} = require('./exceptions');
const { ServerError } = require('./exceptions');
const store = require('store');
const config = require('./config');
const DownloadWorker = require('./download.worker');
@ -38,7 +31,13 @@
if (e.data.isSuccess) {
requests[e.data.id].resolve(e.data.responseData);
} else {
requests[e.data.id].reject(e.data.error);
requests[e.data.id].reject({
error: e.data.error,
response: {
status: e.data.status,
data: e.data.responseData,
},
});
}
delete requests[e.data.id];
@ -71,12 +70,15 @@
});
}
Object.defineProperties(this, Object.freeze({
get: {
value: get,
writable: false,
},
}));
Object.defineProperties(
this,
Object.freeze({
get: {
value: get,
writable: false,
},
}),
);
}
}
@ -154,7 +156,6 @@
return response.data;
}
async function userAgreements() {
const { backendAPI } = config;
let response = null;
@ -169,15 +170,7 @@
return response.data;
}
async function register(
username,
firstName,
lastName,
email,
password1,
password2,
confirmations,
) {
async function register(username, firstName, lastName, email, password1, password2, confirmations) {
let response = null;
try {
const data = JSON.stringify({
@ -203,20 +196,19 @@
}
async function login(username, password) {
const authenticationData = ([
const authenticationData = [
`${encodeURIComponent('username')}=${encodeURIComponent(username)}`,
`${encodeURIComponent('password')}=${encodeURIComponent(password)}`,
]).join('&').replace(/%20/g, '+');
]
.join('&')
.replace(/%20/g, '+');
Axios.defaults.headers.common.Authorization = '';
let authenticationResponse = null;
try {
authenticationResponse = await Axios.post(
`${config.backendAPI}/auth/login`,
authenticationData, {
proxy: config.proxy,
},
);
authenticationResponse = await Axios.post(`${config.backendAPI}/auth/login`, authenticationData, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
@ -251,7 +243,7 @@
const data = JSON.stringify({
old_password: oldPassword,
new_password1: newPassword1,
new_password2:newPassword2,
new_password2: newPassword2,
});
await Axios.post(`${config.backendAPI}/auth/password/change`, data, {
proxy: config.proxy,
@ -264,9 +256,44 @@
}
}
async function requestPasswordReset(email) {
try {
const data = JSON.stringify({
email,
});
await Axios.post(`${config.backendAPI}/auth/password/reset`, data, {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}
async function resetPassword(newPassword1, newPassword2, uid, _token) {
try {
const data = JSON.stringify({
new_password1: newPassword1,
new_password2: newPassword2,
uid,
token: _token,
});
await Axios.post(`${config.backendAPI}/auth/password/reset/confirm`, data, {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}
async function authorized() {
try {
await module.exports.users.getSelf();
await module.exports.users.self();
} catch (serverError) {
if (serverError.code === 401) {
return false;
@ -280,10 +307,88 @@
async function serverRequest(url, data) {
try {
return (await Axios({
url,
...data,
})).data;
return (
await Axios({
url,
...data,
})
).data;
} catch (errorData) {
throw generateError(errorData);
}
}
async function searchProjectNames(search, limit) {
const { backendAPI, proxy } = config;
let response = null;
try {
response = await Axios.get(
`${backendAPI}/projects?names_only=true&page=1&page_size=${limit}&search=${search}`,
{
proxy,
},
);
} catch (errorData) {
throw generateError(errorData);
}
response.data.results.count = response.data.count;
return response.data.results;
}
async function getProjects(filter = '') {
const { backendAPI, proxy } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/projects?page_size=12&${filter}`, {
proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
response.data.results.count = response.data.count;
return response.data.results;
}
async function saveProject(id, projectData) {
const { backendAPI } = config;
try {
await Axios.patch(`${backendAPI}/projects/${id}`, JSON.stringify(projectData), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}
async function deleteProject(id) {
const { backendAPI } = config;
try {
await Axios.delete(`${backendAPI}/projects/${id}`);
} catch (errorData) {
throw generateError(errorData);
}
}
async function createProject(projectSpec) {
const { backendAPI } = config;
try {
const response = await Axios.post(`${backendAPI}/projects`, JSON.stringify(projectSpec), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
@ -324,7 +429,12 @@
const { backendAPI } = config;
try {
await Axios.delete(`${backendAPI}/tasks/${id}`);
await Axios.delete(`${backendAPI}/tasks/${id}`, {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
@ -337,10 +447,9 @@
return new Promise((resolve, reject) => {
async function request() {
try {
const response = await Axios
.get(`${url}`, {
proxy: config.proxy,
});
const response = await Axios.get(`${url}`, {
proxy: config.proxy,
});
if (response.status === 202) {
setTimeout(request, 3000);
} else {
@ -374,21 +483,22 @@
} else if (response.data.state === 'Failed') {
// If request has been successful, but task hasn't been created
// Then passed data is wrong and we can pass code 400
const message = 'Could not create the task on the server. '
+ `${response.data.message}.`;
const message = `
Could not create the task on the server. ${response.data.message}.
`;
reject(new ServerError(message, 400));
} else {
// If server has another status, it is unexpected
// Therefore it is server error and we can pass code 500
reject(new ServerError(
`Unknown task state has been received: ${response.data.state}`,
500,
));
reject(
new ServerError(
`Unknown task state has been received: ${response.data.state}`,
500,
),
);
}
} catch (errorData) {
reject(
generateError(errorData),
);
reject(generateError(errorData));
}
}
@ -421,7 +531,7 @@
throw generateError(errorData);
}
onUpdate('The data is being uploaded to the server..');
onUpdate('The data are being uploaded to the server..');
try {
await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, taskData, {
proxy: config.proxy,
@ -462,6 +572,90 @@
return response.data;
}
async function getJobReviews(jobID) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/jobs/${jobID}/reviews`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function createReview(data) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.post(`${backendAPI}/reviews`, JSON.stringify(data), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function getJobIssues(jobID) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/jobs/${jobID}/issues`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function createComment(data) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.post(`${backendAPI}/comments`, JSON.stringify(data), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function updateIssue(issueID, data) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.patch(`${backendAPI}/issues/${issueID}`, JSON.stringify(data), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function saveJob(id, jobData) {
const { backendAPI } = config;
@ -477,20 +671,14 @@
}
}
async function getUsers(id = null) {
async function getUsers(filter = 'page_size=all') {
const { backendAPI } = config;
let response = null;
try {
if (id === null) {
response = await Axios.get(`${backendAPI}/users?page_size=all`, {
proxy: config.proxy,
});
} else {
response = await Axios.get(`${backendAPI}/users/${id}`, {
proxy: config.proxy,
});
}
response = await Axios.get(`${backendAPI}/users?${filter}`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
@ -524,10 +712,7 @@
});
} catch (errorData) {
const code = errorData.response ? errorData.response.status : errorData.code;
throw new ServerError(
`Could not get preview frame for the task ${tid} from the server`,
code,
);
throw new ServerError(`Could not get preview frame for the task ${tid} from the server`, code);
}
return response.data;
@ -546,7 +731,14 @@
},
);
} catch (errorData) {
throw generateError(errorData);
throw generateError({
...errorData,
message: '',
response: {
...errorData.response,
data: String.fromCharCode.apply(null, new Uint8Array(errorData.response.data)),
},
});
}
return response;
@ -621,10 +813,13 @@
return new Promise((resolve, reject) => {
async function request() {
try {
const response = await Axios
.put(`${backendAPI}/${session}s/${id}/annotations?format=${format}`, annotationData, {
const response = await Axios.put(
`${backendAPI}/${session}s/${id}/annotations?format=${format}`,
annotationData,
{
proxy: config.proxy,
});
},
);
if (response.status === 202) {
annotationData = new FormData();
setTimeout(request, 3000);
@ -655,17 +850,19 @@
async function request() {
Axios.get(`${url}`, {
proxy: config.proxy,
}).then((response) => {
if (response.status === 202) {
setTimeout(request, 3000);
} else {
query = `${query}&action=download`;
url = `${baseURL}?${query}`;
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);
@ -704,13 +901,12 @@
const { backendAPI } = config;
try {
const response = await Axios.post(`${backendAPI}/lambda/requests`,
JSON.stringify(body), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
const response = await Axios.post(`${backendAPI}/lambda/requests`, JSON.stringify(body), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
} catch (errorData) {
@ -722,13 +918,12 @@
const { backendAPI } = config;
try {
const response = await Axios.post(`${backendAPI}/lambda/functions/${funId}`,
JSON.stringify(body), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
const response = await Axios.post(`${backendAPI}/lambda/functions/${funId}`, JSON.stringify(body), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
} catch (errorData) {
@ -767,99 +962,145 @@
const { backendAPI } = config;
try {
await Axios.delete(
`${backendAPI}/lambda/requests/${requestId}`, {
method: 'DELETE',
},
);
await Axios.delete(`${backendAPI}/lambda/requests/${requestId}`, {
method: 'DELETE',
});
} catch (errorData) {
throw generateError(errorData);
}
}
async function installedApps() {
const { backendAPI } = config;
try {
const response = await Axios.get(`${backendAPI}/server/plugins`, {
proxy: config.proxy,
});
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
}
Object.defineProperties(this, Object.freeze({
server: {
value: Object.freeze({
about,
share,
formats,
exception,
login,
logout,
changePassword,
authorized,
register,
request: serverRequest,
userAgreements,
}),
writable: false,
},
tasks: {
value: Object.freeze({
getTasks,
saveTask,
createTask,
deleteTask,
exportDataset,
}),
writable: false,
},
jobs: {
value: Object.freeze({
getJob,
saveJob,
}),
writable: false,
},
users: {
value: Object.freeze({
getUsers,
getSelf,
}),
writable: false,
},
frames: {
value: Object.freeze({
getData,
getMeta,
getPreview,
}),
writable: false,
},
annotations: {
value: Object.freeze({
updateAnnotations,
getAnnotations,
dumpAnnotations,
uploadAnnotations,
}),
writable: false,
},
logs: {
value: Object.freeze({
save: saveLogs,
}),
writable: false,
},
lambda: {
value: Object.freeze({
list: getLambdaFunctions,
status: getRequestStatus,
requests: getLambdaRequests,
run: runLambdaRequest,
call: callLambdaFunction,
cancel: cancelLambdaRequest,
}),
writable: false,
},
}));
Object.defineProperties(
this,
Object.freeze({
server: {
value: Object.freeze({
about,
share,
formats,
exception,
login,
logout,
changePassword,
requestPasswordReset,
resetPassword,
authorized,
register,
request: serverRequest,
userAgreements,
installedApps,
}),
writable: false,
},
projects: {
value: Object.freeze({
get: getProjects,
searchNames: searchProjectNames,
save: saveProject,
create: createProject,
delete: deleteProject,
}),
writable: false,
},
tasks: {
value: Object.freeze({
getTasks,
saveTask,
createTask,
deleteTask,
exportDataset,
}),
writable: false,
},
jobs: {
value: Object.freeze({
get: getJob,
save: saveJob,
issues: getJobIssues,
reviews: {
get: getJobReviews,
create: createReview,
},
}),
writable: false,
},
users: {
value: Object.freeze({
get: getUsers,
self: getSelf,
}),
writable: false,
},
frames: {
value: Object.freeze({
getData,
getMeta,
getPreview,
}),
writable: false,
},
annotations: {
value: Object.freeze({
updateAnnotations,
getAnnotations,
dumpAnnotations,
uploadAnnotations,
}),
writable: false,
},
logs: {
value: Object.freeze({
save: saveLogs,
}),
writable: false,
},
lambda: {
value: Object.freeze({
list: getLambdaFunctions,
status: getRequestStatus,
requests: getLambdaRequests,
run: runLambdaRequest,
call: callLambdaFunction,
cancel: cancelLambdaRequest,
}),
writable: false,
},
issues: {
value: Object.freeze({
update: updateIssue,
}),
writable: false,
},
comments: {
value: Object.freeze({
create: createComment,
}),
writable: false,
},
}),
);
}
}

File diff suppressed because it is too large Load Diff

@ -1,97 +1,98 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
/**
* Class representing collection statistics
* @memberof module:API.cvat.classes
* @hideconstructor
*/
* Class representing collection statistics
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Statistics {
constructor(label, total) {
Object.defineProperties(this, Object.freeze({
/**
* Statistics by labels with a structure:
* @example
* {
* label: {
* boxes: {
* tracks: 10,
* shapes: 11,
* },
* polygons: {
* tracks: 13,
* shapes: 14,
* },
* polylines: {
* tracks: 16,
* shapes: 17,
* },
* points: {
* tracks: 19,
* shapes: 20,
* },
* cuboids: {
* tracks: 21,
* shapes: 22,
* },
* tags: 66,
* manually: 186,
* interpolated: 500,
* total: 608,
* }
* }
* @name label
* @type {Object}
* @memberof module:API.cvat.classes.Statistics
* @readonly
* @instance
*/
label: {
get: () => JSON.parse(JSON.stringify(label)),
},
/**
* Total statistics (covers all labels) with a structure:
* @example
* {
* boxes: {
* tracks: 10,
* shapes: 11,
* },
* polygons: {
* tracks: 13,
* shapes: 14,
* },
* polylines: {
* tracks: 16,
* shapes: 17,
* },
* points: {
* tracks: 19,
* shapes: 20,
* },
* cuboids: {
* tracks: 21,
* shapes: 22,
* },
* tags: 66,
* manually: 186,
* interpolated: 500,
* total: 608,
* }
* @name total
* @type {Object}
* @memberof module:API.cvat.classes.Statistics
* @readonly
* @instance
*/
total: {
get: () => JSON.parse(JSON.stringify(total)),
},
}));
Object.defineProperties(
this,
Object.freeze({
/**
* Statistics by labels with a structure:
* @example
* {
* label: {
* boxes: {
* tracks: 10,
* shapes: 11,
* },
* polygons: {
* tracks: 13,
* shapes: 14,
* },
* polylines: {
* tracks: 16,
* shapes: 17,
* },
* points: {
* tracks: 19,
* shapes: 20,
* },
* cuboids: {
* tracks: 21,
* shapes: 22,
* },
* tags: 66,
* manually: 186,
* interpolated: 500,
* total: 608,
* }
* }
* @name label
* @type {Object}
* @memberof module:API.cvat.classes.Statistics
* @readonly
* @instance
*/
label: {
get: () => JSON.parse(JSON.stringify(label)),
},
/**
* Total statistics (covers all labels) with a structure:
* @example
* {
* boxes: {
* tracks: 10,
* shapes: 11,
* },
* polygons: {
* tracks: 13,
* shapes: 14,
* },
* polylines: {
* tracks: 16,
* shapes: 17,
* },
* points: {
* tracks: 19,
* shapes: 20,
* },
* cuboids: {
* tracks: 21,
* shapes: 22,
* },
* tags: 66,
* manually: 186,
* interpolated: 500,
* total: 608,
* }
* @name total
* @type {Object}
* @memberof module:API.cvat.classes.Statistics
* @readonly
* @instance
*/
total: {
get: () => JSON.parse(JSON.stringify(total)),
},
}),
);
}
}

@ -1,14 +1,13 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
/**
* Class representing a user
* @memberof module:API.cvat.classes
* @hideconstructor
*/
* Class representing a user
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class User {
constructor(initialData) {
const data = {
@ -27,134 +26,157 @@
};
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property)
&& property in initialData) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property];
}
}
Object.defineProperties(this, Object.freeze({
id: {
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.id,
},
username: {
/**
* @name username
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.username,
},
email: {
/**
* @name email
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.email,
},
firstName: {
/**
* @name firstName
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.first_name,
},
lastName: {
/**
* @name lastName
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.last_name,
},
groups: {
/**
* @name groups
* @type {string[]}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => JSON.parse(JSON.stringify(data.groups)),
},
lastLogin: {
/**
* @name lastLogin
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.last_login,
},
dateJoined: {
/**
* @name dateJoined
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.date_joined,
},
isStaff: {
/**
* @name isStaff
* @type {boolean}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.is_staff,
},
isSuperuser: {
/**
* @name isSuperuser
* @type {boolean}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.is_superuser,
},
isActive: {
/**
* @name isActive
* @type {boolean}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.is_active,
},
isVerified: {
/**
* @name isVerified
* @type {boolean}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => !data.email_verification_required,
},
}));
Object.defineProperties(
this,
Object.freeze({
id: {
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.id,
},
username: {
/**
* @name username
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.username,
},
email: {
/**
* @name email
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.email,
},
firstName: {
/**
* @name firstName
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.first_name,
},
lastName: {
/**
* @name lastName
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.last_name,
},
groups: {
/**
* @name groups
* @type {string[]}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => JSON.parse(JSON.stringify(data.groups)),
},
lastLogin: {
/**
* @name lastLogin
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.last_login,
},
dateJoined: {
/**
* @name dateJoined
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.date_joined,
},
isStaff: {
/**
* @name isStaff
* @type {boolean}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.is_staff,
},
isSuperuser: {
/**
* @name isSuperuser
* @type {boolean}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.is_superuser,
},
isActive: {
/**
* @name isActive
* @type {boolean}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.is_active,
},
isVerified: {
/**
* @name isVerified
* @type {boolean}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => !data.email_verification_required,
},
}),
);
}
serialize() {
return {
id: this.id,
username: this.username,
email: this.email,
first_name: this.firstName,
last_name: this.lastName,
groups: this.groups,
last_login: this.lastLogin,
date_joined: this.dateJoined,
is_staff: this.isStaff,
is_superuser: this.isSuperuser,
is_active: this.isActive,
email_verification_required: this.isVerified,
};
}
toJSON() {
return this.serialize();
}
}

@ -1,13 +1,6 @@
/*
* Copyright (C) 2018-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
@ -48,30 +41,25 @@ describe('Feature: get annotations', () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
// Out of task
expect(task.annotations.get(500))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.get(500)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
// Out of task
expect(task.annotations.get(-1))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.get(-1)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get annotations for frame out of job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 101 }))[0];
// Out of segment
expect(job.annotations.get(500))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(job.annotations.get(500)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
// Out of segment
expect(job.annotations.get(-1))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(job.annotations.get(-1)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
// TODO: Test filter (hasn't been implemented yet)
});
describe('Feature: put annotations', () => {
test('put a shape to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
@ -173,8 +161,7 @@ describe('Feature: put annotations', () => {
zOrder: 0,
});
expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('put shape with bad attributes to a task', async () => {
@ -191,8 +178,7 @@ describe('Feature: put annotations', () => {
zOrder: 0,
});
expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('put shape with bad zOrder to a task', async () => {
@ -209,8 +195,7 @@ describe('Feature: put annotations', () => {
zOrder: 'bad value',
});
expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
const state1 = new window.cvat.classes.ObjectState({
frame: 1,
@ -223,8 +208,7 @@ describe('Feature: put annotations', () => {
zOrder: NaN,
});
expect(task.annotations.put([state1]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.put([state1])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('put shape without points and with invalud points to a task', async () => {
@ -240,16 +224,13 @@ describe('Feature: put annotations', () => {
zOrder: 0,
});
await expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.DataError);
await expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.DataError);
delete state.points;
await expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.DataError);
await expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.DataError);
state.points = ['150,50 250,30'];
expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('put shape without type to a task', async () => {
@ -264,8 +245,7 @@ describe('Feature: put annotations', () => {
zOrder: 0,
});
expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('put shape without label and with bad label to a task', async () => {
@ -280,16 +260,13 @@ describe('Feature: put annotations', () => {
zOrder: 0,
});
await expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.label = 'bad label';
await expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.label = {};
await expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('put shape with bad frame to a task', async () => {
@ -305,8 +282,7 @@ describe('Feature: put annotations', () => {
zOrder: 0,
});
expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
@ -436,8 +412,7 @@ describe('Feature: save annotations', () => {
// have been sent to a server
const oldImplementation = serverProxy.annotations.updateAnnotations;
serverProxy.annotations.updateAnnotations = async (session, id, data, action) => {
const result = await oldImplementation
.call(serverProxy.annotations, session, id, data, action);
const result = await oldImplementation.call(serverProxy.annotations, session, id, data, action);
if (action === 'delete') {
okay = okay || (action === 'delete' && !!(data.shapes.length || data.tracks.length));
}
@ -459,10 +434,12 @@ describe('Feature: merge annotations', () => {
const annotations1 = await task.annotations.get(1);
const states = [annotations0[0], annotations1[0]];
await task.annotations.merge(states);
const merged0 = (await task.annotations.get(0))
.filter((state) => state.objectType === window.cvat.enums.ObjectType.TRACK);
const merged1 = (await task.annotations.get(1))
.filter((state) => state.objectType === window.cvat.enums.ObjectType.TRACK);
const merged0 = (await task.annotations.get(0)).filter(
(state) => state.objectType === window.cvat.enums.ObjectType.TRACK,
);
const merged1 = (await task.annotations.get(1)).filter(
(state) => state.objectType === window.cvat.enums.ObjectType.TRACK,
);
expect(merged0).toHaveLength(1);
expect(merged1).toHaveLength(1);
@ -476,10 +453,12 @@ describe('Feature: merge annotations', () => {
const annotations1 = await job.annotations.get(1);
const states = [annotations0[0], annotations1[0]];
await job.annotations.merge(states);
const merged0 = (await job.annotations.get(0))
.filter((state) => state.objectType === window.cvat.enums.ObjectType.TRACK);
const merged1 = (await job.annotations.get(1))
.filter((state) => state.objectType === window.cvat.enums.ObjectType.TRACK);
const merged0 = (await job.annotations.get(0)).filter(
(state) => state.objectType === window.cvat.enums.ObjectType.TRACK,
);
const merged1 = (await job.annotations.get(1)).filter(
(state) => state.objectType === window.cvat.enums.ObjectType.TRACK,
);
expect(merged0).toHaveLength(1);
expect(merged1).toHaveLength(1);
@ -492,8 +471,7 @@ describe('Feature: merge annotations', () => {
const annotations0 = await task.annotations.get(0);
const states = [annotations0[0], {}];
expect(task.annotations.merge(states))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.merge(states)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('trying to merge object state which is not saved in a collection', async () => {
@ -510,8 +488,7 @@ describe('Feature: merge annotations', () => {
});
const states = [annotations0[0], state];
expect(task.annotations.merge(states))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.merge(states)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('trying to merge with bad label', async () => {
@ -525,19 +502,18 @@ describe('Feature: merge annotations', () => {
attributes: [],
});
expect(task.annotations.merge(states))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.merge(states)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('trying to merge with different shape types', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations0 = await task.annotations.get(0);
const annotations1 = (await task.annotations.get(1))
.filter((state) => state.shapeType === window.cvat.enums.ObjectShape.POLYGON);
const annotations1 = (await task.annotations.get(1)).filter(
(state) => state.shapeType === window.cvat.enums.ObjectShape.POLYGON,
);
const states = [annotations0[0], annotations1[0]];
expect(task.annotations.merge(states))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.merge(states)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('trying to merge with different labels', async () => {
@ -551,8 +527,7 @@ describe('Feature: merge annotations', () => {
attributes: [],
});
expect(task.annotations.merge(states))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.merge(states)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
@ -587,8 +562,9 @@ describe('Feature: split annotations', () => {
const annotations5 = await task.annotations.get(5);
expect(annotations4[0].clientID).toBe(annotations5[0].clientID);
expect(task.annotations.split(annotations5[0], 'bad frame'))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.split(annotations5[0], 'bad frame')).rejects.toThrow(
window.cvat.exceptions.ArgumentError,
);
});
});
@ -597,7 +573,7 @@ describe('Feature: group annotations', () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
let annotations = await task.annotations.get(0);
const groupID = await task.annotations.group(annotations);
expect(typeof (groupID)).toBe('number');
expect(typeof groupID).toBe('number');
annotations = await task.annotations.get(0);
for (const state of annotations) {
expect(state.group.id).toBe(groupID);
@ -608,7 +584,7 @@ describe('Feature: group annotations', () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
let annotations = await job.annotations.get(0);
const groupID = await job.annotations.group(annotations);
expect(typeof (groupID)).toBe('number');
expect(typeof groupID).toBe('number');
annotations = await job.annotations.get(0);
for (const state of annotations) {
expect(state.group.id).toBe(groupID);
@ -629,15 +605,13 @@ describe('Feature: group annotations', () => {
zOrder: 0,
});
expect(task.annotations.group([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.group([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('trying to group not object state', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
expect(task.annotations.group(annotations.concat({})))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.group(annotations.concat({}))).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
@ -689,8 +663,7 @@ describe('Feature: clear annotations', () => {
test('clear annotations with bad reload parameter', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
await task.annotations.clear(true);
expect(task.annotations.clear('reload'))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.clear('reload')).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
@ -752,18 +725,16 @@ describe('Feature: select object', () => {
test('trying to select from not object states', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
expect(task.annotations.select(annotations.concat({}), 500, 500))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.select(annotations.concat({}), 500, 500)).rejects.toThrow(
window.cvat.exceptions.ArgumentError,
);
});
test('trying to select with invalid coordinates', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
expect(task.annotations.select(annotations, null, null))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.select(annotations, null, null))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.select(annotations, '5', '10'))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.select(annotations, null, null)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.select(annotations, null, null)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.select(annotations, '5', '10')).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});

@ -1,13 +1,6 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
@ -35,22 +28,18 @@ describe('Feature: get frame meta', () => {
test('pass frame number out of a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
expect(task.frames.get(100))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.frames.get(-1))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.frames.get(100)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.frames.get(-1)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('pass bad frame number', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
expect(task.frames.get('5'))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.frames.get('5')).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('do not pass any frame number', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
expect(task.frames.get())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.frames.get()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
@ -59,14 +48,14 @@ describe('Feature: get frame data', () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const frame = await task.frames.get(0);
const frameData = await frame.data();
expect(typeof (frameData)).toBe('string');
expect(typeof frameData).toBe('string');
});
test('get frame data for a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const frame = await job.frames.get(0);
const frameData = await frame.data();
expect(typeof (frameData)).toBe('string');
expect(typeof frameData).toBe('string');
});
});
@ -74,12 +63,12 @@ describe('Feature: get frame preview', () => {
test('get frame preview for a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const frame = await task.frames.preview();
expect(typeof (frame)).toBe('string');
expect(typeof frame).toBe('string');
});
test('get frame preview for a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const frame = await job.frames.preview();
expect(typeof (frame)).toBe('string');
expect(typeof frame).toBe('string');
});
});

@ -1,13 +1,6 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
@ -20,7 +13,6 @@ window.cvat = require('../../src/api');
const { Job } = require('../../src/session');
// Test cases
describe('Feature: get a list of jobs', () => {
test('get jobs by a task id', async () => {
@ -45,7 +37,6 @@ describe('Feature: get a list of jobs', () => {
expect(result).toHaveLength(0);
});
test('get jobs by a job id', async () => {
const result = await window.cvat.jobs.get({
jobID: 1,
@ -64,32 +55,39 @@ describe('Feature: get a list of jobs', () => {
});
test('get jobs by invalid filter with both taskID and jobID', async () => {
expect(window.cvat.jobs.get({
taskID: 1,
jobID: 1,
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(
window.cvat.jobs.get({
taskID: 1,
jobID: 1,
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get jobs by invalid job id', async () => {
expect(window.cvat.jobs.get({
jobID: '1',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(
window.cvat.jobs.get({
jobID: '1',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get jobs by invalid task id', async () => {
expect(window.cvat.jobs.get({
taskID: '1',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(
window.cvat.jobs.get({
taskID: '1',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get jobs by unknown filter', async () => {
expect(window.cvat.jobs.get({
unknown: 50,
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(
window.cvat.jobs.get({
unknown: 50,
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: save job', () => {
test('save status of a job', async () => {
let result = await window.cvat.jobs.get({

@ -1,13 +1,6 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
@ -168,30 +161,25 @@ describe('Feature: save object from its state', () => {
const state = annotations[0];
state.occluded = 'false';
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
const oldPoints = state.points;
state.occluded = false;
state.points = ['100', '50', '100', {}];
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.points = oldPoints;
state.lock = 'true';
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
const oldLabel = state.label;
state.lock = false;
state.label = 1;
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.label = oldLabel;
state.attributes = { 1: {}, 2: false, 3: () => {} };
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('save bad values for a track', async () => {
@ -200,40 +188,33 @@ describe('Feature: save object from its state', () => {
const state = annotations[0];
state.occluded = 'false';
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
const oldPoints = state.points;
state.occluded = false;
state.points = ['100', '50', '100', {}];
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.points = oldPoints;
state.lock = 'true';
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
const oldLabel = state.label;
state.lock = false;
state.label = 1;
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.label = oldLabel;
state.outside = 5;
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.outside = false;
state.keyframe = '10';
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.keyframe = true;
state.attributes = { 1: {}, 2: false, 3: () => {} };
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('trying to change locked shape', async () => {

@ -1,13 +1,6 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {

@ -0,0 +1,170 @@
// Copyright (C) 2019-2020 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 { Task } = require('../../src/session');
const { Project } = require('../../src/project');
describe('Feature: get projects', () => {
test('get all projects', async () => {
const result = await window.cvat.projects.get();
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(2);
for (const el of result) {
expect(el).toBeInstanceOf(Project);
}
});
test('get project by id', async () => {
const result = await window.cvat.projects.get({
id: 2,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(Project);
expect(result[0].id).toBe(2);
expect(result[0].tasks).toHaveLength(1);
expect(result[0].tasks[0]).toBeInstanceOf(Task);
});
test('get a project by an unknown id', async () => {
const result = await window.cvat.projects.get({
id: 1,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(0);
});
test('get a project by an invalid id', async () => {
expect(
window.cvat.projects.get({
id: '1',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get projects by filters', async () => {
const result = await window.cvat.projects.get({
status: 'completed',
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(Project);
expect(result[0].id).toBe(2);
expect(result[0].status).toBe('completed');
});
test('get projects by invalid filters', async () => {
expect(
window.cvat.projects.get({
unknown: '5',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: save a project', () => {
test('save some changed fields in a project', async () => {
let result = await window.cvat.tasks.get({
id: 2,
});
result[0].bugTracker = 'newBugTracker';
result[0].name = 'New Project Name';
result[0].save();
result = await window.cvat.tasks.get({
id: 2,
});
expect(result[0].bugTracker).toBe('newBugTracker');
expect(result[0].name).toBe('New Project Name');
});
test('save some new labels in a project', async () => {
let result = await window.cvat.projects.get({
id: 6,
});
const labelsLength = result[0].labels.length;
const newLabel = new window.cvat.classes.Label({
name: "My boss's car",
attributes: [
{
default_value: 'false',
input_type: 'checkbox',
mutable: true,
name: 'parked',
values: ['false'],
},
],
});
result[0].labels = [...result[0].labels, newLabel];
result[0].save();
result = await window.cvat.projects.get({
id: 6,
});
expect(result[0].labels).toHaveLength(labelsLength + 1);
const appendedLabel = result[0].labels.filter((el) => el.name === "My boss's car");
expect(appendedLabel).toHaveLength(1);
expect(appendedLabel[0].attributes).toHaveLength(1);
expect(appendedLabel[0].attributes[0].name).toBe('parked');
expect(appendedLabel[0].attributes[0].defaultValue).toBe('false');
expect(appendedLabel[0].attributes[0].mutable).toBe(true);
expect(appendedLabel[0].attributes[0].inputType).toBe('checkbox');
});
test('save new project without an id', async () => {
const project = new window.cvat.classes.Project({
name: 'New Empty Project',
labels: [
{
name: 'car',
attributes: [
{
default_value: 'false',
input_type: 'checkbox',
mutable: true,
name: 'parked',
values: ['false'],
},
],
},
],
bug_tracker: 'bug tracker value',
});
const result = await project.save();
expect(typeof result.id).toBe('number');
});
});
describe('Feature: delete a project', () => {
test('delete a project', async () => {
let result = await window.cvat.projects.get({
id: 6,
});
await result[0].delete();
result = await window.cvat.projects.get({
id: 6,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(0);
});
});

@ -1,13 +1,6 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
@ -17,11 +10,7 @@ jest.mock('../../src/server-proxy', () => {
// Initialize api
window.cvat = require('../../src/api');
const {
AnnotationFormats,
Loader,
Dumper,
} = require('../../src/annotation-formats');
const { AnnotationFormats, Loader, Dumper } = require('../../src/annotation-formats');
// Test cases
describe('Feature: get info about cvat', () => {
@ -34,7 +23,6 @@ describe('Feature: get info about cvat', () => {
});
});
describe('Feature: get share storage info', () => {
test('get files in a root of a share storage', async () => {
const result = await window.cvat.server.share();
@ -49,9 +37,7 @@ describe('Feature: get share storage info', () => {
});
test('get files in a some unknown dir of a share storage', async () => {
expect(window.cvat.server.share(
'Unknown Directory',
)).rejects.toThrow(window.cvat.exceptions.ServerError);
expect(window.cvat.server.share('Unknown Directory')).rejects.toThrow(window.cvat.exceptions.ServerError);
});
});
@ -84,4 +70,4 @@ describe('Feature: get annotation dumpers', () => {
expect(dumper).toBeInstanceOf(Dumper);
}
});
});
});

@ -1,13 +1,6 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
@ -20,7 +13,6 @@ window.cvat = require('../../src/api');
const { Task } = require('../../src/session');
// Test cases
describe('Feature: get a list of tasks', () => {
test('get all tasks', async () => {
@ -51,9 +43,11 @@ describe('Feature: get a list of tasks', () => {
});
test('get a task by an invalid id', async () => {
expect(window.cvat.tasks.get({
id: '50',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(
window.cvat.tasks.get({
id: '50',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get tasks by filters', async () => {
@ -69,9 +63,11 @@ describe('Feature: get a list of tasks', () => {
});
test('get tasks by invalid filters', async () => {
expect(window.cvat.tasks.get({
unknown: '5',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(
window.cvat.tasks.get({
unknown: '5',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get task by name, status and mode', async () => {
@ -98,7 +94,6 @@ describe('Feature: save a task', () => {
});
result[0].bugTracker = 'newBugTracker';
result[0].zOrder = true;
result[0].name = 'New Task Name';
result[0].save();
@ -108,7 +103,6 @@ describe('Feature: save a task', () => {
});
expect(result[0].bugTracker).toBe('newBugTracker');
expect(result[0].zOrder).toBe(true);
expect(result[0].name).toBe('New Task Name');
});
@ -119,14 +113,16 @@ describe('Feature: save a task', () => {
const labelsLength = result[0].labels.length;
const newLabel = new window.cvat.classes.Label({
name: 'My boss\'s car',
attributes: [{
default_value: 'false',
input_type: 'checkbox',
mutable: true,
name: 'parked',
values: ['false'],
}],
name: "My boss's car",
attributes: [
{
default_value: 'false',
input_type: 'checkbox',
mutable: true,
name: 'parked',
values: ['false'],
},
],
});
result[0].labels = [...result[0].labels, newLabel];
@ -137,7 +133,7 @@ describe('Feature: save a task', () => {
});
expect(result[0].labels).toHaveLength(labelsLength + 1);
const appendedLabel = result[0].labels.filter((el) => el.name === 'My boss\'s car');
const appendedLabel = result[0].labels.filter((el) => el.name === "My boss's car");
expect(appendedLabel).toHaveLength(1);
expect(appendedLabel[0].attributes).toHaveLength(1);
expect(appendedLabel[0].attributes[0].name).toBe('parked');
@ -149,23 +145,39 @@ describe('Feature: save a task', () => {
test('save new task without an id', async () => {
const task = new window.cvat.classes.Task({
name: 'New Task',
labels: [{
name: 'My boss\'s car',
attributes: [{
default_value: 'false',
input_type: 'checkbox',
mutable: true,
name: 'parked',
values: ['false'],
}],
}],
labels: [
{
name: "My boss's car",
attributes: [
{
default_value: 'false',
input_type: 'checkbox',
mutable: true,
name: 'parked',
values: ['false'],
},
],
},
],
bug_tracker: 'bug tracker value',
image_quality: 50,
});
const result = await task.save();
expect(typeof result.id).toBe('number');
});
test('save new task in project', async () => {
const task = new window.cvat.classes.Task({
name: 'New Task',
project_id: 2,
bug_tracker: 'bug tracker value',
image_quality: 50,
z_order: true,
});
const result = await task.save();
expect(typeof (result.id)).toBe('number');
expect(result.projectId).toBe(2);
});
});

@ -1,13 +1,6 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
@ -41,14 +34,18 @@ describe('Feature: get a list of users', () => {
});
test('get users with unknown filter key', async () => {
expect(window.cvat.users.get({
unknown: '50',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(
window.cvat.users.get({
unknown: '50',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get users with invalid filter key', async () => {
expect(window.cvat.users.get({
self: 1,
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(
window.cvat.users.get({
self: 1,
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save