Merge branch 'release-1.5.0'

main
Nikita Manovich 5 years ago
commit 56c9626a3a

@ -8,3 +8,4 @@ keys/
logs/ logs/
static/ static/
templates/ templates/
*/webpack.config.js

@ -0,0 +1,43 @@
name: Github pages
on:
push:
branches:
- develop
jobs:
deploy:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
fetch-depth: 0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: '0.83.1'
extended: true
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: '14.x'
- name: Install npm packages
working-directory: ./site
run: |
npm ci
- name: Build docs
run: |
pip install gitpython packaging
python site/build_docs.py
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
force_orphan: true

@ -59,7 +59,7 @@ jobs:
cache-from: type=local,src=/tmp/cvat_cache_server cache-from: type=local,src=/tmp/cvat_cache_server
tags: openvino/cvat_server:latest tags: openvino/cvat_server:latest
load: true load: true
- name: Runing unit tests - name: Running unit tests
env: env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }} HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
CONTAINER_COVERAGE_DATA_DIR: "/coverage_data" CONTAINER_COVERAGE_DATA_DIR: "/coverage_data"
@ -80,7 +80,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
specs: ['actions_tasks', 'actions_tasks2', 'actions_tasks3', 'actions_objects', 'actions_objects2', 'actions_users', 'actions_projects', 'canvas3d_functionality', 'issues_prs', 'issues_prs2'] specs: ['actions_tasks', 'actions_tasks2', 'actions_tasks3', 'actions_objects', 'actions_objects2', 'actions_users', 'actions_projects_models', 'canvas3d_functionality', 'canvas3d_functionality_2', 'issues_prs', 'issues_prs2']
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Getting SHA from the default branch - name: Getting SHA from the default branch
@ -151,7 +151,7 @@ jobs:
run: | run: |
npm ci npm ci
npm run coverage npm run coverage
docker-compose -f docker-compose.yml -f docker-compose.dev.yml build cvat_ui docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml build cvat_ui
- name: Running e2e tests - name: Running e2e tests
env: env:
DJANGO_SU_NAME: 'admin' DJANGO_SU_NAME: 'admin'
@ -159,16 +159,24 @@ jobs:
DJANGO_SU_PASSWORD: '12qwaszx' DJANGO_SU_PASSWORD: '12qwaszx'
API_ABOUT_PAGE: "localhost:8080/api/v1/server/about" API_ABOUT_PAGE: "localhost:8080/api/v1/server/about"
run: | run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml up -d
/bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done' /bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done'
docker exec -i cvat /bin/bash -c "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" docker exec -i cvat /bin/bash -c "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" docker exec -i cvat /bin/bash -c "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" docker exec -i cvat /bin/bash -c "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 cd ./tests
npm ci npm ci
if [[ ${{ github.ref }} == 'refs/heads/develop' ]]; then if [[ ${{ github.ref }} == 'refs/heads/develop' ]]; then
npx cypress run --headless --browser chrome --spec 'cypress/integration/${{ matrix.specs }}/**/*.js' if [ ${{ matrix.specs }} == 'canvas3d_functionality' ] || [ ${{ matrix.specs }} == 'canvas3d_functionality_2' ]; then
npx cypress run --browser chrome --config-file cypress_canvas3d.json --spec 'cypress/integration/${{ matrix.specs }}/**/*.js'
else
npx cypress run --headless --browser chrome --spec 'cypress/integration/${{ matrix.specs }}/**/*.js'
fi
mv ./.nyc_output/out.json ./.nyc_output/out_${{ matrix.specs }}.json mv ./.nyc_output/out.json ./.nyc_output/out_${{ matrix.specs }}.json
else else
npx cypress run --headless --browser chrome --env coverage=false --spec 'cypress/integration/${{ matrix.specs }}/**/*.js' if [ ${{ matrix.specs }} == 'canvas3d_functionality' ] || [ ${{ matrix.specs }} == 'canvas3d_functionality_2' ]; then
npx cypress run --browser chrome --env coverage=false --config-file cypress_canvas3d.json --spec 'cypress/integration/${{ matrix.specs }}/**/*.js'
else
npx cypress run --headless --browser chrome --env coverage=false --spec 'cypress/integration/${{ matrix.specs }}/**/*.js'
fi
fi fi
- name: Creating a log file from "cvat" container logs - name: Creating a log file from "cvat" container logs
if: failure() if: failure()
@ -199,7 +207,7 @@ jobs:
needs: [Unit_testing, E2E_testing] needs: [Unit_testing, E2E_testing]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Geting SHA from the default branch - name: Getting SHA from the default branch
id: get-sha id: get-sha
run: | run: |
URL_get_default_branch="https://api.github.com/repos/${{ github.repository }}" URL_get_default_branch="https://api.github.com/repos/${{ github.repository }}"

@ -4,36 +4,40 @@ on:
types: [published] types: [published]
jobs: jobs:
build_and_push_to_registry: Unit_testing:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- name: Build images
run: |
CLAM_AV=yes INSTALL_SOURCES=yes docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml build
- name: Run unit tests - name: Run unit tests
env: env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }} HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
CONTAINER_COVERAGE_DATA_DIR: '/coverage_data' CONTAINER_COVERAGE_DATA_DIR: '/coverage_data'
DJANGO_SU_NAME: 'admin'
DJANGO_SU_EMAIL: 'admin@localhost.company'
DJANGO_SU_PASSWORD: '12qwaszx'
run: | run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'coverage run -a manage.py test cvat/apps utils/cli' docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'coverage run -a manage.py test cvat/apps utils/cli'
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-data && npm ci && cd ../cvat-core && npm ci && npm run test' docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-data && npm ci && cd ../cvat-core && npm ci && npm run test'
docker-compose up -d
docker exec -i cvat /bin/bash -c "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"
E2E_testing:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- name: Run end-to-end tests - name: Run end-to-end tests
env:
DJANGO_SU_NAME: 'admin'
DJANGO_SU_EMAIL: 'admin@localhost.company'
DJANGO_SU_PASSWORD: '12qwaszx'
API_ABOUT_PAGE: "localhost:8080/api/v1/server/about"
run: | run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml build
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml up -d
/bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done'
docker exec -i cvat /bin/bash -c "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 cd ./tests
npm ci npm ci
npm run cypress:run:chrome npm run cypress:run:chrome
npm run cypress:run:chrome:canvas3d
- name: Uploading cypress screenshots as an artifact - name: Uploading cypress screenshots as an artifact
if: failure() if: failure()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
@ -41,6 +45,14 @@ jobs:
name: cypress_screenshots name: cypress_screenshots
path: ${{ github.workspace }}/tests/cypress/screenshots path: ${{ github.workspace }}/tests/cypress/screenshots
Push_to_registry:
runs-on: ubuntu-latest
needs: [Unit_testing, E2E_testing]
steps:
- uses: actions/checkout@v2
- name: Build images
run: |
CLAM_AV=yes INSTALL_SOURCES=yes docker-compose -f docker-compose.yml -f docker-compose.dev.yml build
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v1 uses: docker/login-action@v1
with: with:

@ -11,31 +11,16 @@ jobs:
- name: Run checks - name: Run checks
run: | run: |
URL="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" npm ci
PR_FILES=$(curl -s -X GET -G $URL | jq -r '.[] | select(.status != "removed") | .filename') mkdir -p remark_report
for files in $PR_FILES; do
extension="${files##*.}"
if [[ $extension == 'md' ]]; then
changed_files_remark+=" ${files}"
fi
done
if [[ ! -z ${changed_files_remark} ]]; then echo "Remark version: "`npx remark --version`
npm ci npx remark --quiet --report json --no-stdout . 2> ./remark_report/remark_report.json
npm install remark-cli@9.0.0 vfile-reporter-json@2.0.2 get_report=`cat ./remark_report/remark_report.json | jq -r '.[] | select(.messages | length > 0)'`
mkdir -p remark_report if [[ ! -z ${get_report} ]]; then
pip install json2html
echo "Remark version: "`npx remark --version` python ./tests/json_to_html.py ./remark_report/remark_report.json
echo "The files will be checked: "`echo ${changed_files_remark}` exit 1
npx remark --quiet --report json --no-stdout ${changed_files_remark} 2> ./remark_report/remark_report.json
get_report=`cat ./remark_report/remark_report.json | jq -r '.[] | select(.messages | length > 0)'`
if [[ ! -z ${get_report} ]]; then
pip install json2html
python ./tests/json_to_html.py ./remark_report/remark_report.json
exit 1
fi
else
echo "No files with the \"md\" extension found"
fi fi
- name: Upload artifacts - name: Upload artifacts

@ -18,7 +18,7 @@ jobs:
DJANGO_SU_PASSWORD: "12qwaszx" DJANGO_SU_PASSWORD: "12qwaszx"
API_ABOUT_PAGE: "localhost:8080/api/v1/server/about" API_ABOUT_PAGE: "localhost:8080/api/v1/server/about"
run: | run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f ./tests/docker-compose.email.yml up -d --build docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f ./tests/docker-compose.email.yml -f components/serverless/docker-compose.serverless.yml up -d --build
/bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done' /bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done'
docker exec -i cvat /bin/bash -c "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" docker exec -i cvat /bin/bash -c "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"
- name: End-to-end testing - name: End-to-end testing

6
.gitignore vendored

@ -41,3 +41,9 @@ yarn-error.log*
/helm-chart/values.*.yaml /helm-chart/values.*.yaml
/helm-chart/*.values.yaml /helm-chart/*.values.yaml
/helm-chart/charts/* /helm-chart/charts/*
#Ignore website temp files
/site/public/
/site/resources/
/site/node_modules/
/site/tech-doc-hugo

3
.gitmodules vendored

@ -0,0 +1,3 @@
[submodule "site/themes/docsy"]
path = site/themes/docsy
url = https://github.com/google/docsy

@ -9,7 +9,9 @@
], ],
"exclude": [ "exclude": [
"**/3rdparty/*", "**/3rdparty/*",
"**/tests/*" "**/tests/*",
"cvat-ui/src/actions/boundaries-actions.ts",
"cvat-ui/src/utils/platform-checker.ts"
], ],
"parser-plugins": [ "parser-plugins": [
"typescript" "typescript"

@ -1,6 +1,8 @@
exports.settings = { bullet: '*', paddedTable: false }; exports.settings = { bullet: '*', paddedTable: false };
exports.plugins = [ exports.plugins = [
'remark-frontmatter',
'remark-gfm',
'remark-preset-lint-recommended', 'remark-preset-lint-recommended',
'remark-preset-lint-consistent', 'remark-preset-lint-consistent',
['remark-lint-list-item-indent', 'space'], ['remark-lint-list-item-indent', 'space'],

@ -7,7 +7,7 @@
{ {
"type": "chrome", "type": "chrome",
"request": "launch", "request": "launch",
"preLaunchTask": "ui.js: server", "preLaunchTask": "npm: start - cvat-ui",
"name": "ui.js: debug", "name": "ui.js: debug",
"url": "http://localhost:3000", "url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/cvat-ui", "webRoot": "${workspaceFolder}/cvat-ui",
@ -45,7 +45,7 @@
"python": "${command:python.interpreterPath}", "python": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py", "program": "${workspaceRoot}/manage.py",
"env": { "env": {
"CVAT_SERVERLESS": "1", "CVAT_SERVERLESS": "1"
}, },
"args": [ "args": [
"runserver", "runserver",
@ -207,7 +207,6 @@
{ {
"name": "server: debug", "name": "server: debug",
"configurations": [ "configurations": [
"server: chrome",
"server: django", "server: django",
"server: RQ - default", "server: RQ - default",
"server: RQ - low", "server: RQ - low",

@ -1,5 +1,4 @@
{ {
"eslint.enable": true,
"eslint.probe": [ "eslint.probe": [
"javascript", "javascript",
"typescript", "typescript",
@ -24,5 +23,14 @@
"python.linting.pycodestyleEnabled": false, "python.linting.pycodestyleEnabled": false,
"licenser.license": "Custom", "licenser.license": "Custom",
"licenser.customHeader": "Copyright (C) @YEAR@ Intel Corporation\n\nSPDX-License-Identifier: MIT", "licenser.customHeader": "Copyright (C) @YEAR@ Intel Corporation\n\nSPDX-License-Identifier: MIT",
"files.trimTrailingWhitespace": true "files.trimTrailingWhitespace": true,
"python.pythonPath": ".env/bin/python",
"sqltools.connections": [
{
"previewLimit": 50,
"driver": "SQLite",
"name": "cvat",
"database": "${workspaceFolder:cvat}/db.sqlite3"
}
]
} }

30
.vscode/tasks.json vendored

@ -4,11 +4,35 @@
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"label": "ui.js: server",
"type": "npm", "type": "npm",
"script": "start", "script": "start",
"path": "cvat-ui/", "path": "cvat-ui/",
"problemMatcher": [] "label": "npm: start - cvat-ui",
"detail": "webpack-dev-server --env.API_URL=http://localhost:7000 --config ./webpack.config.js --mode=development",
"promptOnClose": true,
"isBackground": true,
"problemMatcher": {
"owner": "webpack",
"severity": "error",
"fileLocation": "absolute",
"pattern": [
{
"regexp": "ERROR in (.*)",
"file": 1
},
{
"regexp": "\\((\\d+),(\\d+)\\):(.*)",
"line": 1,
"column": 2,
"message": 3
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "webpack-dev-server",
"endsPattern": "Compiled"
}
}
} }
] ]
} }

@ -5,7 +5,52 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.4.0] - 2021-05-18 ## \[1.5.0] - 2021-08-02
### Added
- Support of context images for 2D image tasks (<https://github.com/openvinotoolkit/cvat/pull/3122>)
- Support of cloud storage without copying data into CVAT: server part (<https://github.com/openvinotoolkit/cvat/pull/2620>)
- Filter `is_active` for user list (<https://github.com/openvinotoolkit/cvat/pull/3235>)
- Ability to export/import tasks (<https://github.com/openvinotoolkit/cvat/pull/3056>)
- Add a tutorial for semi-automatic/automatic annotation (<https://github.com/openvinotoolkit/cvat/pull/3124>)
- Explicit "Done" button when drawing any polyshapes (<https://github.com/openvinotoolkit/cvat/pull/3417>)
- Histogram equalization with OpenCV javascript (<https://github.com/openvinotoolkit/cvat/pull/3447>)
- Client-side polyshapes approximation when using semi-automatic interactors & scissors (<https://github.com/openvinotoolkit/cvat/pull/3450>)
### Changed
- Updated manifest format, added meta with related images (<https://github.com/openvinotoolkit/cvat/pull/3122>)
- Update of COCO format documentation (<https://github.com/openvinotoolkit/cvat/pull/3197>)
- Updated Webpack Dev Server config to add proxy (<https://github.com/openvinotoolkit/cvat/pull/3368>)
- Update to Django 3.1.12 (<https://github.com/openvinotoolkit/cvat/pull/3378>)
- Updated visibility for removable points in AI tools (<https://github.com/openvinotoolkit/cvat/pull/3417>)
- Updated UI handling for IOG serverless function (<https://github.com/openvinotoolkit/cvat/pull/3417>)
- Changed Nginx proxy to Traefik in `docker-compose.yml` (<https://github.com/openvinotoolkit/cvat/pull/3409>)
- Simplify the process of deploying CVAT with HTTPS (<https://github.com/openvinotoolkit/cvat/pull/3409>)
### Fixed
- Project page requests took a long time and did many DB queries (<https://github.com/openvinotoolkit/cvat/pull/3223>)
- Fixed Python 3.6 support (<https://github.com/openvinotoolkit/cvat/pull/3258>)
- Incorrect attribute import in tracks (<https://github.com/openvinotoolkit/cvat/pull/3229>)
- Issue "is not a constructor" when create object, save, undo, save, redo save (<https://github.com/openvinotoolkit/cvat/pull/3292>)
- Fix CLI create an infinite loop if git repository responds with failure (<https://github.com/openvinotoolkit/cvat/pull/3267>)
- Bug with sidebar & fullscreen (<https://github.com/openvinotoolkit/cvat/pull/3289>)
- 504 Gateway Time-out on `data/meta` requests (<https://github.com/openvinotoolkit/cvat/pull/3269>)
- TypeError: Cannot read property 'clientX' of undefined when draw cuboids with hotkeys (<https://github.com/openvinotoolkit/cvat/pull/3308>)
- Duplication of the cuboids when redraw them (<https://github.com/openvinotoolkit/cvat/pull/3308>)
- Some code issues in Deep Extreme Cut handler code (<https://github.com/openvinotoolkit/cvat/pull/3325>)
- UI fails when inactive user is assigned to a task/job (<https://github.com/openvinotoolkit/cvat/pull/3343>)
- Calculate precise progress of decoding a video file (<https://github.com/openvinotoolkit/cvat/pull/3381>)
- Falsely successful `cvat_ui` image build in case of OOM error that leads to the default nginx welcome page
(<https://github.com/openvinotoolkit/cvat/pull/3379>)
- Fixed issue when save filtered object in AAM (<https://github.com/openvinotoolkit/cvat/pull/3401>)
- Context image disappears after undo/redo (<https://github.com/openvinotoolkit/cvat/pull/3416>)
- Using combined data sources (directory and image) when create a task (<https://github.com/openvinotoolkit/cvat/pull/3424>)
- Creating task with labels in project (<https://github.com/openvinotoolkit/cvat/pull/3454>)
## \[1.4.0] - 2021-05-18
### Added ### Added
@ -13,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Hotkeys to switch a label of existing object or to change default label (for objects created with N) (<https://github.com/openvinotoolkit/cvat/pull/3070>) - Hotkeys to switch a label of existing object or to change default label (for objects created with N) (<https://github.com/openvinotoolkit/cvat/pull/3070>)
- A script to convert some kinds of DICOM files to regular images (<https://github.com/openvinotoolkit/cvat/pull/3095>) - A script to convert some kinds of DICOM files to regular images (<https://github.com/openvinotoolkit/cvat/pull/3095>)
- Helm chart prototype (<https://github.com/openvinotoolkit/cvat/pull/3102>) - Helm chart prototype (<https://github.com/openvinotoolkit/cvat/pull/3102>)
- Initial implementation of moving tasks between projects (<https://github.com/openvinotoolkit/cvat/pull/3164>)
### Changed ### Changed
@ -31,7 +77,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Manifest: migration (<https://github.com/openvinotoolkit/cvat/pull/3146>) - Manifest: migration (<https://github.com/openvinotoolkit/cvat/pull/3146>)
- Fixed cropping polygon in some corner cases (<https://github.com/openvinotoolkit/cvat/pull/3184>) - Fixed cropping polygon in some corner cases (<https://github.com/openvinotoolkit/cvat/pull/3184>)
## [1.3.0] - 3/31/2021 ## \[1.3.0] - 3/31/2021
### Added ### Added
@ -56,7 +102,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(<https://github.com/openvinotoolkit/cvat/pull/2891>) (<https://github.com/openvinotoolkit/cvat/pull/2891>)
- [Market-1501](https://www.aitribune.com/dataset/2018051063) format support (<https://github.com/openvinotoolkit/cvat/pull/2869>) - [Market-1501](https://www.aitribune.com/dataset/2018051063) format support (<https://github.com/openvinotoolkit/cvat/pull/2869>)
- Ability of upload manifest for dataset with images (<https://github.com/openvinotoolkit/cvat/pull/2763>) - Ability of upload manifest for dataset with images (<https://github.com/openvinotoolkit/cvat/pull/2763>)
- Annotations filters UI using react-awesome-query-builder (https://github.com/openvinotoolkit/cvat/issues/1418) - Annotations filters UI using react-awesome-query-builder (<https://github.com/openvinotoolkit/cvat/issues/1418>)
- Storing settings in local storage to keep them between browser sessions (<https://github.com/openvinotoolkit/cvat/pull/3017>) - Storing settings in local storage to keep them between browser sessions (<https://github.com/openvinotoolkit/cvat/pull/3017>)
- [ICDAR](https://rrc.cvc.uab.es/?ch=2) format support (<https://github.com/openvinotoolkit/cvat/pull/2866>) - [ICDAR](https://rrc.cvc.uab.es/?ch=2) format support (<https://github.com/openvinotoolkit/cvat/pull/2866>)
- Added switcher to maintain polygon crop behavior (<https://github.com/openvinotoolkit/cvat/pull/3021> - Added switcher to maintain polygon crop behavior (<https://github.com/openvinotoolkit/cvat/pull/3021>
@ -85,7 +131,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed filters select overflow (<https://github.com/openvinotoolkit/cvat/pull/2614>) - Fixed filters select overflow (<https://github.com/openvinotoolkit/cvat/pull/2614>)
- Fixed tasks in project auto annotation (<https://github.com/openvinotoolkit/cvat/pull/2725>) - Fixed tasks in project auto annotation (<https://github.com/openvinotoolkit/cvat/pull/2725>)
- Cuboids are missed in annotations statistics (<https://github.com/openvinotoolkit/cvat/pull/2704>) - Cuboids are missed in annotations statistics (<https://github.com/openvinotoolkit/cvat/pull/2704>)
- The list of files attached to the task is not displayed (<https://github.com/openvinotoolkit/cvat/pul - The list of files attached to the task is not displayed (<https://github.com/openvinotoolkit/cvat/pull/2706>)
- A couple of css-related issues (top bar disappear, wrong arrow position on collapse elements) (<https://github.com/openvinotoolkit/cvat/pull/2736>) - A couple of css-related issues (top bar disappear, wrong arrow position on collapse elements) (<https://github.com/openvinotoolkit/cvat/pull/2736>)
- Issue with point region doesn't work in Firefox (<https://github.com/openvinotoolkit/cvat/pull/2727>) - Issue with point region doesn't work in Firefox (<https://github.com/openvinotoolkit/cvat/pull/2727>)
- Fixed cuboid perspective change (<https://github.com/openvinotoolkit/cvat/pull/2733>) - Fixed cuboid perspective change (<https://github.com/openvinotoolkit/cvat/pull/2733>)
@ -104,7 +150,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updating label attributes when label contains number attributes (<https://github.com/openvinotoolkit/cvat/pull/2969>) - Updating label attributes when label contains number attributes (<https://github.com/openvinotoolkit/cvat/pull/2969>)
- Crop a polygon if its points are outside the bounds of the image (<https://github.com/openvinotoolkit/cvat/pull/3025>) - Crop a polygon if its points are outside the bounds of the image (<https://github.com/openvinotoolkit/cvat/pull/3025>)
## [1.2.0] - 2021-01-08 ## \[1.2.0] - 2021-01-08
### Fixed ### Fixed
@ -112,7 +158,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Frame preloading (<https://github.com/openvinotoolkit/cvat/pull/2608>) - Frame preloading (<https://github.com/openvinotoolkit/cvat/pull/2608>)
- Project cannot be removed from the project page (<https://github.com/openvinotoolkit/cvat/pull/2626>) - Project cannot be removed from the project page (<https://github.com/openvinotoolkit/cvat/pull/2626>)
## [1.2.0-beta] - 2020-12-15 ## \[1.2.0-beta] - 2020-12-15
### Added ### Added
@ -129,7 +175,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- PATCH requests from cvat-core submit only changed fields (<https://github.com/openvinotoolkit/cvat/pull/2445>) - 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>) - deploy.sh in serverless folder is separated into deploy_cpu.sh and deploy_gpu.sh (<https://github.com/openvinotoolkit/cvat/pull/2546>)
- Bumped nuclio version to 1.5.8 - Bumped nuclio version to 1.5.8
- Migrated to Antd 4.9 (<https://github.com/openvinotoolkit/cvat/pull/2536>) - Migrated to Antd 4.9 (<https://github.com/openvinotoolkit/cvat/pull/2536>)
@ -152,20 +198,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Reset state (reviews, issues) after logout or changing a job (<https://github.com/openvinotoolkit/cvat/pull/2525>) - 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>) - 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 ## \[1.2.0-alpha] - 2020-11-09
### Added ### Added
- Ability to login into CVAT-UI with token from api/v1/auth/login (<https://github.com/openvinotoolkit/cvat/pull/2234>) - 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 layout grids toggling ('ctrl + alt + Enter')
- Added password reset functionality (<https://github.com/opencv/cvat/pull/2058>) - 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) - 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>) - 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>) - 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>) - 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>) - 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] 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>) - \[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>) - 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. - Ability to change default behaviour of previous/next buttons of a player.
It supports regular navigation, searching a frame according to annotations It supports regular navigation, searching a frame according to annotations
@ -185,7 +231,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Used Ubuntu:20.04 as a base image for CVAT Dockerfile (<https://github.com/opencv/cvat/pull/2101>) - 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>) - 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>) - 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>) - 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>) - 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>) - 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>) - Track frames in skips between annotation is presented in MOT and MOTS formats are marked `outside` (<https://github.com/openvinotoolkit/cvat/pull/2198>)
@ -217,15 +263,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 'List of tasks' Kibana visualization (<https://github.com/openvinotoolkit/cvat/pull/2361>) - '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>) - 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 ## \[1.1.0] - 2020-08-31
### Added ### Added
- Siammask tracker as DL serverless function (<https://github.com/opencv/cvat/pull/1988>) - 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] Added model info and source info commands (<https://github.com/opencv/cvat/pull/1973>)
- [Datumaro] Dataset statistics (<https://github.com/opencv/cvat/pull/1668>) - \[Datumaro] Dataset statistics (<https://github.com/opencv/cvat/pull/1668>)
- Ability to change label color in tasks and predefined labels (<https://github.com/opencv/cvat/pull/2014>) - Ability to change label color in tasks and predefined labels (<https://github.com/opencv/cvat/pull/2014>)
- [Datumaro] Multi-dataset merge (https://github.com/opencv/cvat/pull/1695) - \[Datumaro] Multi-dataset merge (<https://github.com/opencv/cvat/pull/1695>)
- Ability to configure email verification for new users (<https://github.com/opencv/cvat/pull/1929>) - Ability to configure email verification for new users (<https://github.com/opencv/cvat/pull/1929>)
- Link to django admin page from UI (<https://github.com/opencv/cvat/pull/2068>) - Link to django admin page from UI (<https://github.com/opencv/cvat/pull/2068>)
- Notification message when users use wrong browser (<https://github.com/opencv/cvat/pull/2070>) - Notification message when users use wrong browser (<https://github.com/opencv/cvat/pull/2070>)
@ -244,7 +290,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Django RQ dashboard view (<https://github.com/opencv/cvat/pull/2069>) - Django RQ dashboard view (<https://github.com/opencv/cvat/pull/2069>)
- Object's details menu settings (<https://github.com/opencv/cvat/pull/2084>) - Object's details menu settings (<https://github.com/opencv/cvat/pull/2084>)
## [1.1.0-beta] - 2020-08-03 ## \[1.1.0-beta] - 2020-08-03
### Added ### Added
@ -252,7 +298,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Source type support for tags, shapes and tracks (<https://github.com/opencv/cvat/pull/1192>) - 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>) - Source type support for CVAT Dumper/Loader (<https://github.com/opencv/cvat/pull/1192>)
- Intelligent polygon editing (<https://github.com/opencv/cvat/pull/1921>) - Intelligent polygon editing (<https://github.com/opencv/cvat/pull/1921>)
- Support creating multiple jobs for each task through python cli (https://github.com/opencv/cvat/pull/1950) - Support creating multiple jobs for each task through python cli (<https://github.com/opencv/cvat/pull/1950>)
- python cli over https (<https://github.com/opencv/cvat/pull/1942>) - python cli over https (<https://github.com/opencv/cvat/pull/1942>)
- Error message when plugins weren't able to initialize instead of infinite loading (<https://github.com/opencv/cvat/pull/1966>) - Error message when plugins weren't able to initialize instead of infinite loading (<https://github.com/opencv/cvat/pull/1966>)
- Ability to change user password (<https://github.com/opencv/cvat/pull/1954>) - Ability to change user password (<https://github.com/opencv/cvat/pull/1954>)
@ -279,11 +325,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Cannot read property 'pinned' of undefined because of zOrder related issues (<https://github.com/opencv/cvat/pull/1874>) - Cannot read property 'pinned' of undefined because of zOrder related issues (<https://github.com/opencv/cvat/pull/1874>)
- Do not iterate over hidden objects in aam (which are invisible because of zOrder) (<https://github.com/opencv/cvat/pull/1874>) - Do not iterate over hidden objects in aam (which are invisible because of zOrder) (<https://github.com/opencv/cvat/pull/1874>)
- Cursor position is reset after changing a text field (<https://github.com/opencv/cvat/pull/1874>) - Cursor position is reset after changing a text field (<https://github.com/opencv/cvat/pull/1874>)
- Hidden points and cuboids can be selected to be groupped (<https://github.com/opencv/cvat/pull/1874>) - Hidden points and cuboids can be selected to be grouped (<https://github.com/opencv/cvat/pull/1874>)
- `outside` annotations should not be in exported images (<https://github.com/opencv/cvat/issues/1620>) - `outside` annotations should not be in exported images (<https://github.com/opencv/cvat/issues/1620>)
- `CVAT for video format` import error with interpolation (<https://github.com/opencv/cvat/issues/1893>) - `CVAT for video format` import error with interpolation (<https://github.com/opencv/cvat/issues/1893>)
- `Image compression` definition mismatch (<https://github.com/opencv/cvat/issues/1900>) - `Image compression` definition mismatch (<https://github.com/opencv/cvat/issues/1900>)
- Points are dublicated during polygon interpolation sometimes (<https://github.com/opencv/cvat/pull/1892>) - Points are duplicated during polygon interpolation sometimes (<https://github.com/opencv/cvat/pull/1892>)
- When redraw a shape with activated autobordering, previous points are visible (<https://github.com/opencv/cvat/pull/1892>) - When redraw a shape with activated autobordering, previous points are visible (<https://github.com/opencv/cvat/pull/1892>)
- No mapping between side object element and context menu in some attributes (<https://github.com/opencv/cvat/pull/1923>) - No mapping between side object element and context menu in some attributes (<https://github.com/opencv/cvat/pull/1923>)
- Interpolated shapes exported as `keyframe = True` (<https://github.com/opencv/cvat/pull/1937>) - Interpolated shapes exported as `keyframe = True` (<https://github.com/opencv/cvat/pull/1937>)
@ -292,7 +338,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Clearing frame cache when close a task (<https://github.com/opencv/cvat/pull/1966>) - Clearing frame cache when close a task (<https://github.com/opencv/cvat/pull/1966>)
- Increase rate of throttling policy for unauthenticated users (<https://github.com/opencv/cvat/pull/1969>) - Increase rate of throttling policy for unauthenticated users (<https://github.com/opencv/cvat/pull/1969>)
## [1.1.0-alpha] - 2020-06-30 ## \[1.1.0-alpha] - 2020-06-30
### Added ### Added
@ -307,9 +353,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- ClamAV antivirus integration (<https://github.com/opencv/cvat/pull/1712>) - ClamAV antivirus integration (<https://github.com/opencv/cvat/pull/1712>)
- Added canvas background color selector (<https://github.com/opencv/cvat/pull/1705>) - Added canvas background color selector (<https://github.com/opencv/cvat/pull/1705>)
- SCSS files linting with Stylelint tool (<https://github.com/opencv/cvat/pull/1766>) - SCSS files linting with Stylelint tool (<https://github.com/opencv/cvat/pull/1766>)
- Supported import and export or single boxes in MOT format (https://github.com/opencv/cvat/pull/1764) - Supported import and export or single boxes in MOT format (<https://github.com/opencv/cvat/pull/1764>)
- [Datumaro] Added `stats` command, which shows some dataset statistics - \[Datumaro] Added `stats` command, which shows some dataset statistics
like image mean and std (https://github.com/opencv/cvat/pull/1734) like image mean and std (<https://github.com/opencv/cvat/pull/1734>)
- Add option to upload annotations upon task creation on CLI - Add option to upload annotations upon task creation on CLI
- Polygon and polylines interpolation (<https://github.com/opencv/cvat/pull/1571>) - Polygon and polylines interpolation (<https://github.com/opencv/cvat/pull/1571>)
- Ability to redraw shape from scratch (Shift + N) for an activated shape (<https://github.com/opencv/cvat/pull/1571>) - Ability to redraw shape from scratch (Shift + N) for an activated shape (<https://github.com/opencv/cvat/pull/1571>)
@ -319,9 +365,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added new tag annotation workspace (<https://github.com/opencv/cvat/pull/1570>) - Added new tag annotation workspace (<https://github.com/opencv/cvat/pull/1570>)
- Appearance block in attribute annotation mode (<https://github.com/opencv/cvat/pull/1820>) - Appearance block in attribute annotation mode (<https://github.com/opencv/cvat/pull/1820>)
- Keyframe navigations and some switchers in attribute annotation mode (<https://github.com/opencv/cvat/pull/1820>) - Keyframe navigations and some switchers in attribute annotation mode (<https://github.com/opencv/cvat/pull/1820>)
- [Datumaro] Added `convert` command to convert datasets directly (<https://github.com/opencv/cvat/pull/1837>) - \[Datumaro] Added `convert` command to convert datasets directly (<https://github.com/opencv/cvat/pull/1837>)
- [Datumaro] Added an option to specify image extension when exporting datasets (<https://github.com/opencv/cvat/pull/1799>) - \[Datumaro] Added an option to specify image extension when exporting datasets (<https://github.com/opencv/cvat/pull/1799>)
- [Datumaro] Added image copying when exporting datasets, if possible (<https://github.com/opencv/cvat/pull/1799>) - \[Datumaro] Added image copying when exporting datasets, if possible (<https://github.com/opencv/cvat/pull/1799>)
### Changed ### Changed
@ -331,10 +377,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Settings page move to the modal. (<https://github.com/opencv/cvat/pull/1705>) - Settings page move to the modal. (<https://github.com/opencv/cvat/pull/1705>)
- Implemented import and export of annotations with relative image paths (<https://github.com/opencv/cvat/pull/1463>) - Implemented import and export of annotations with relative image paths (<https://github.com/opencv/cvat/pull/1463>)
- Using only single click to start editing or remove a point (<https://github.com/opencv/cvat/pull/1571>) - Using only single click to start editing or remove a point (<https://github.com/opencv/cvat/pull/1571>)
- Added support for attributes in VOC XML format (https://github.com/opencv/cvat/pull/1792) - Added support for attributes in VOC XML format (<https://github.com/opencv/cvat/pull/1792>)
- Added annotation attributes in COCO format (https://github.com/opencv/cvat/pull/1782) - Added annotation attributes in COCO format (<https://github.com/opencv/cvat/pull/1782>)
- Colorized object items in the side panel (<https://github.com/opencv/cvat/pull/1753>) - Colorized object items in the side panel (<https://github.com/opencv/cvat/pull/1753>)
- [Datumaro] Annotation-less files are not generated anymore in COCO format, unless tasks explicitly requested (<https://github.com/opencv/cvat/pull/1799>) - \[Datumaro] Annotation-less files are not generated anymore in COCO format, unless tasks explicitly requested (<https://github.com/opencv/cvat/pull/1799>)
### Fixed ### Fixed
@ -361,7 +407,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- SQL injection in Django `CVE-2020-9402` (<https://github.com/opencv/cvat/pull/1657>) - SQL injection in Django `CVE-2020-9402` (<https://github.com/opencv/cvat/pull/1657>)
## [1.0.0] - 2020-05-29 ## \[1.0.0] - 2020-05-29
### Added ### Added
@ -398,7 +444,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added directory removal on error during `extract` command (<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>) - Added debug error message on incorrect XPath (<https://github.com/opencv/cvat/pull/1352>)
- Exporting frame stepped task - Exporting frame stepped task
(<https://github.com/opencv/cvat/issues/1294, https://github.com/opencv/cvat/issues/1334>) (<https://github.com/opencv/cvat/issues/1294>, <https://github.com/opencv/cvat/issues/1334>)
- Fixed broken command line interface for `cvat` export format in Datumaro (<https://github.com/opencv/cvat/issues/1494>) - Fixed broken command line interface for `cvat` export format in Datumaro (<https://github.com/opencv/cvat/issues/1494>)
- Updated Rest API document, Swagger document serving instruction issue (<https://github.com/opencv/cvat/issues/1495>) - Updated Rest API document, Swagger document serving instruction issue (<https://github.com/opencv/cvat/issues/1495>)
- Fixed cuboid occluded view (<https://github.com/opencv/cvat/pull/1500>) - Fixed cuboid occluded view (<https://github.com/opencv/cvat/pull/1500>)
@ -423,7 +469,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Permission group whitelist check for analytics view (<https://github.com/opencv/cvat/pull/1608>) - Permission group whitelist check for analytics view (<https://github.com/opencv/cvat/pull/1608>)
## [1.0.0-beta.2] - 2020-04-30 ## \[1.0.0-beta.2] - 2020-04-30
### Added ### Added
@ -434,14 +480,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed ### Removed
- Annotation convertation utils, currently supported natively via Datumaro framework - Annotation conversion utils, currently supported natively via Datumaro framework
(https://github.com/opencv/cvat/pull/1477) (<https://github.com/opencv/cvat/pull/1477>)
### Fixed ### Fixed
- Auto annotation, TF annotation and Auto segmentation apps (https://github.com/opencv/cvat/pull/1409) - 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 - Import works with truncated images now: "OSError:broken data stream" on corrupt images
(https://github.com/opencv/cvat/pull/1430) (<https://github.com/opencv/cvat/pull/1430>)
- Hide functionality (H) doesn't work (<https://github.com/opencv/cvat/pull/1445>) - Hide functionality (H) doesn't work (<https://github.com/opencv/cvat/pull/1445>)
- The highlighted attribute doesn't correspond to the chosen attribute in AAM (<https://github.com/opencv/cvat/pull/1445>) - The highlighted attribute doesn't correspond to the chosen attribute in AAM (<https://github.com/opencv/cvat/pull/1445>)
- Inconvinient image shaking while drawing a polygon (hold Alt key during drawing/editing/grouping to drag an image) (<https://github.com/opencv/cvat/pull/1445>) - Inconvinient image shaking while drawing a polygon (hold Alt key during drawing/editing/grouping to drag an image) (<https://github.com/opencv/cvat/pull/1445>)
@ -449,13 +495,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Block of text information doesn't disappear after deactivating for locked shapes (<https://github.com/opencv/cvat/pull/1445>) - Block of text information doesn't disappear after deactivating for locked shapes (<https://github.com/opencv/cvat/pull/1445>)
- Annotation uploading fails in annotation view (<https://github.com/opencv/cvat/pull/1445>) - Annotation uploading fails in annotation view (<https://github.com/opencv/cvat/pull/1445>)
- UI freezes after canceling pasting with escape (<https://github.com/opencv/cvat/pull/1445>) - UI freezes after canceling pasting with escape (<https://github.com/opencv/cvat/pull/1445>)
- Duplicating keypoints in COCO export (https://github.com/opencv/cvat/pull/1435) - Duplicating keypoints in COCO export (<https://github.com/opencv/cvat/pull/1435>)
- CVAT new UI: add arrows on a mouse cursor (<https://github.com/opencv/cvat/pull/1391>) - CVAT new UI: add arrows on a mouse cursor (<https://github.com/opencv/cvat/pull/1391>)
- Delete point bug (in new UI) (<https://github.com/opencv/cvat/pull/1440>) - Delete point bug (in new UI) (<https://github.com/opencv/cvat/pull/1440>)
- Fix apache startup after PC restart (https://github.com/opencv/cvat/pull/1467) - Fix apache startup after PC restart (<https://github.com/opencv/cvat/pull/1467>)
- Open task button doesn't work (https://github.com/opencv/cvat/pull/1474) - Open task button doesn't work (<https://github.com/opencv/cvat/pull/1474>)
## [1.0.0-beta.1] - 2020-04-15 ## \[1.0.0-beta.1] - 2020-04-15
### Added ### Added
@ -464,12 +510,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ability to display a bitmap in the new UI - Ability to display a bitmap in the new UI
- Button to reset colors settings (brightness, saturation, contrast) in the new UI - Button to reset colors settings (brightness, saturation, contrast) in the new UI
- Option to display shape text always - Option to display shape text always
- Dedicated message with clarifications when share is unmounted (https://github.com/opencv/cvat/pull/1373) - Dedicated message with clarifications when share is unmounted (<https://github.com/opencv/cvat/pull/1373>)
- Ability to create one tracked point (https://github.com/opencv/cvat/pull/1383) - Ability to create one tracked point (<https://github.com/opencv/cvat/pull/1383>)
- Ability to draw/edit polygons and polylines with automatic bordering feature - Ability to draw/edit polygons and polylines with automatic bordering feature
(https://github.com/opencv/cvat/pull/1394) (<https://github.com/opencv/cvat/pull/1394>)
- Tutorial: instructions for CVAT over HTTPS - Tutorial: instructions for CVAT over HTTPS
- Deep extreme cut (semi-automatic segmentation) to the new UI (https://github.com/opencv/cvat/pull/1398) - Deep extreme cut (semi-automatic segmentation) to the new UI (<https://github.com/opencv/cvat/pull/1398>)
### Changed ### Changed
@ -489,38 +535,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Hidden points (or outsided) are visible after changing a frame - Hidden points (or outsided) are visible after changing a frame
- Merge is allowed for points, but clicks on points conflict with frame dragging logic - Merge is allowed for points, but clicks on points conflict with frame dragging logic
- Removed objects are visible for search - Removed objects are visible for search
- Add missed task_id and job_id fields into exception logs for the new UI (https://github.com/opencv/cvat/pull/1372) - Add missed task_id and job_id fields into exception logs for the new UI (<https://github.com/opencv/cvat/pull/1372>)
- UI fails when annotations saving occurs during drag/resize/edit (https://github.com/opencv/cvat/pull/1383) - UI fails when annotations saving occurs during drag/resize/edit (<https://github.com/opencv/cvat/pull/1383>)
- Multiple savings when hold Ctrl+S (a lot of the same copies of events were sent with the same working time) - Multiple savings when hold Ctrl+S (a lot of the same copies of events were sent with the same working time)
(https://github.com/opencv/cvat/pull/1383) (<https://github.com/opencv/cvat/pull/1383>)
- UI doesn't have any reaction when git repos synchronization failed (https://github.com/opencv/cvat/pull/1383) - UI doesn't have any reaction when git repos synchronization failed (<https://github.com/opencv/cvat/pull/1383>)
- Bug when annotations cannot be saved after (delete - save - undo - save) (https://github.com/opencv/cvat/pull/1383) - Bug when annotations cannot be saved after (delete - save - undo - save) (<https://github.com/opencv/cvat/pull/1383>)
- VOC format exports Upper case labels correctly in lower case (https://github.com/opencv/cvat/pull/1379) - VOC format exports Upper case labels correctly in lower case (<https://github.com/opencv/cvat/pull/1379>)
- Fixed polygon exporting bug in COCO dataset (https://github.com/opencv/cvat/issues/1387) - Fixed polygon exporting bug in COCO dataset (<https://github.com/opencv/cvat/issues/1387>)
- Task creation from remote files (https://github.com/opencv/cvat/pull/1392) - Task creation from remote files (<https://github.com/opencv/cvat/pull/1392>)
- Job cannot be opened in some cases when the previous job was failed during opening - Job cannot be opened in some cases when the previous job was failed during opening
(https://github.com/opencv/cvat/issues/1403) (<https://github.com/opencv/cvat/issues/1403>)
- Deactivated shape is still highlighted on the canvas (https://github.com/opencv/cvat/issues/1403) - Deactivated shape is still highlighted on the canvas (<https://github.com/opencv/cvat/issues/1403>)
- AttributeError: 'tuple' object has no attribute 'read' in ReID algorithm (https://github.com/opencv/cvat/issues/1403) - AttributeError: 'tuple' object has no attribute 'read' in ReID algorithm (<https://github.com/opencv/cvat/issues/1403>)
- Wrong semi-automatic segmentation near edges of an image (https://github.com/opencv/cvat/issues/1403) - Wrong semi-automatic segmentation near edges of an image (<https://github.com/opencv/cvat/issues/1403>)
- Git repos paths (https://github.com/opencv/cvat/pull/1400) - Git repos paths (<https://github.com/opencv/cvat/pull/1400>)
- Uploading annotations for tasks with multiple jobs (https://github.com/opencv/cvat/pull/1396) - Uploading annotations for tasks with multiple jobs (<https://github.com/opencv/cvat/pull/1396>)
## [1.0.0-alpha] - 2020-03-31 ## \[1.0.0-alpha] - 2020-03-31
### Added ### Added
- Data streaming using chunks (https://github.com/opencv/cvat/pull/1007) - 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: 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) - New UI: delete a point from context menu (<https://github.com/opencv/cvat/pull/1292>)
### Fixed ### Fixed
- Git app cannot clone a repository (https://github.com/opencv/cvat/pull/1330) - 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) - New UI: preview position in task details (<https://github.com/opencv/cvat/pull/1312>)
- AWS deployment (https://github.com/opencv/cvat/pull/1316) - AWS deployment (<https://github.com/opencv/cvat/pull/1316>)
## [0.6.1] - 2020-03-21 ## \[0.6.1] - 2020-03-21
### Changed ### Changed
@ -540,7 +586,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bump acorn from 6.3.0 to 6.4.1 in /cvat-ui ([#1270](https://github.com/opencv/cvat/pull/1270)) - 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 ## \[0.6.0] - 2020-03-15
### Added ### Added
@ -578,19 +624,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Output labels for VOC format can be specified with Datumaro [#942](https://github.com/opencv/cvat/issues/942) - Output labels for VOC format can be specified with Datumaro [#942](https://github.com/opencv/cvat/issues/942)
- Annotations can be filtered before dumping with Datumaro [#994](https://github.com/opencv/cvat/issues/994) - Annotations can be filtered before dumping with Datumaro [#994](https://github.com/opencv/cvat/issues/994)
## [0.5.2] - 2019-12-15 ## \[0.5.2] - 2019-12-15
### Fixed ### Fixed
- Frozen version of scikit-image==0.15 in requirements.txt because next releases don't support Python 3.5 - 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 ## \[0.5.1] - 2019-10-17
### Added ### Added
- Integration with Zenodo.org (DOI) - Integration with Zenodo.org (DOI)
## [0.5.0] - 2019-09-12 ## \[0.5.0] - 2019-09-12
### Added ### Added
@ -608,7 +654,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added in a command line model manager tester - Added in a command line model manager tester
- Ability to dump/load annotations in several formats from UI (CVAT, Pascal VOC, YOLO, MS COCO, png mask, TFRecord) - Ability to dump/load annotations in several formats from UI (CVAT, Pascal VOC, YOLO, MS COCO, png mask, TFRecord)
- Auth for REST API (api/v1/auth/): login, logout, register, ... - Auth for REST API (api/v1/auth/): login, logout, register, ...
- Preview for the new CVAT UI (dashboard only) is available: http://localhost:9080/ - Preview for the new CVAT UI (dashboard only) is available: <http://localhost:9080/>
- Added command line tool for performing common task operations (/utils/cli/) - Added command line tool for performing common task operations (/utils/cli/)
### Changed ### Changed
@ -642,26 +688,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Upgraded Django, djangorestframework, and other packages - Upgraded Django, djangorestframework, and other packages
## [0.4.2] - 2019-06-03 ## \[0.4.2] - 2019-06-03
### Fixed ### Fixed
- Fixed interaction with the server share in the auto annotation plugin - Fixed interaction with the server share in the auto annotation plugin
## [0.4.1] - 2019-05-14 ## \[0.4.1] - 2019-05-14
### Fixed ### Fixed
- JavaScript syntax incompatibility with Google Chrome versions less than 72 - JavaScript syntax incompatibility with Google Chrome versions less than 72
## [0.4.0] - 2019-05-04 ## \[0.4.0] - 2019-05-04
### Added ### Added
- OpenVINO auto annotation: it is possible to upload a custom model and annotate images automatically. - 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) - 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) - 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) - Keyboard shortcuts to switch next/previous default shape type (box, polygon etc) (Alt + <, Alt + >) (#316)
- Converter for VOC now supports interpolation tracks - 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 - Semi-automatic semantic segmentation with the [Deep Extreme Cut](http://www.vision.ee.ethz.ch/~cvlsegmentation/dextr/) work
@ -679,10 +725,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Django 2.1.5 (security fix, https://nvd.nist.gov/vuln/detail/CVE-2019-3498) - Django 2.1.5 (security fix, [CVE-2019-3498](https://nvd.nist.gov/vuln/detail/CVE-2019-3498))
- Several scenarious which cause code 400 after undo/redo/save have been fixed (#315) - Several scenarious which cause code 400 after undo/redo/save have been fixed (#315)
## [0.3.0] - 2018-12-29 ## \[0.3.0] - 2018-12-29
### Added ### Added
@ -709,7 +755,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Polyshape editing method has been improved. You can redraw part of shape instead of points cloning. - 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.). - 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, ...) - Dump file contains information about data source (e.g. video name, archive name, ...)
- Update requests library due to https://nvd.nist.gov/vuln/detail/CVE-2018-18074 - Update requests library due to [CVE-2018-18074](https://nvd.nist.gov/vuln/detail/CVE-2018-18074)
- Per task/job permissions to create/access/change/delete tasks and annotations - Per task/job permissions to create/access/change/delete tasks and annotations
- Documentation was improved - Documentation was improved
- Timeout for creating tasks was increased (from 1h to 4h) (#136) - Timeout for creating tasks was increased (from 1h to 4h) (#136)
@ -728,7 +774,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Dashboard loading has been accelerated (#156) - Dashboard loading has been accelerated (#156)
- Text drawing outside of a frame in some cases (#202) - Text drawing outside of a frame in some cases (#202)
## [0.2.0] - 2018-09-28 ## \[0.2.0] - 2018-09-28
### Added ### Added
@ -760,7 +806,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Several memory leaks - Several memory leaks
- Inconsistent extensions between filenames in an annotation file and real filenames - Inconsistent extensions between filenames in an annotation file and real filenames
## [0.1.2] - 2018-08-07 ## \[0.1.2] - 2018-08-07
### Added ### Added
@ -778,7 +824,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- More strict verification for labels with attributes - More strict verification for labels with attributes
## [0.1.1] - 2018-07-6 ## \[0.1.1] - 2018-07-6
### Added ### Added
@ -789,7 +835,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- GitHub documentation - GitHub documentation
## 0.1.0 - 2018-06-29 ## \[0.1.0] - 2018-06-29
### Added ### Added

@ -134,7 +134,7 @@ RUN if [ "$INSTALL_SOURCES" = "yes" ]; then \
fi fi
COPY --from=build-image /tmp/openh264/openh264*.tar.gz /tmp/ffmpeg/ffmpeg*.tar.gz ${HOME}/sources/ COPY --from=build-image /tmp/openh264/openh264*.tar.gz /tmp/ffmpeg/ffmpeg*.tar.gz ${HOME}/sources/
# Copy python virtual enviroment and FFmpeg binaries from build-image # Copy python virtual environment and FFmpeg binaries from build-image
COPY --from=build-image /opt/venv /opt/venv COPY --from=build-image /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:${PATH}" ENV PATH="/opt/venv/bin:${PATH}"
COPY --from=build-image /opt/ffmpeg /usr COPY --from=build-image /opt/ffmpeg /usr

@ -1,4 +1,4 @@
FROM node:lts-alpine AS cvat-ui FROM node:lts-buster AS cvat-ui
ARG http_proxy ARG http_proxy
ARG https_proxy ARG https_proxy
@ -15,8 +15,6 @@ ENV TERM=xterm \
LANG='C.UTF-8' \ LANG='C.UTF-8' \
LC_ALL='C.UTF-8' LC_ALL='C.UTF-8'
RUN apk add python3 g++ make
# Install dependencies # Install dependencies
COPY cvat-core/package*.json /tmp/cvat-core/ COPY cvat-core/package*.json /tmp/cvat-core/
COPY cvat-canvas/package*.json /tmp/cvat-canvas/ COPY cvat-canvas/package*.json /tmp/cvat-canvas/

@ -13,18 +13,19 @@ annotate million of objects with different properties. Many UI
and UX decisions are based on feedbacks from professional data and UX decisions are based on feedbacks from professional data
annotation team. Try it online [cvat.org](https://cvat.org). annotation team. Try it online [cvat.org](https://cvat.org).
![CVAT screenshot](cvat/apps/documentation/static/documentation/images/cvat.jpg) ![CVAT screenshot](site/content/en/images/cvat.jpg)
## Documentation ## Documentation
- [Installation guide](cvat/apps/documentation/installation.md) - [Contributing](https://openvinotoolkit.github.io/cvat/docs/contributing/)
- [User's guide](cvat/apps/documentation/user_guide.md) - [Installation guide](https://openvinotoolkit.github.io/cvat/docs/administration/basics/installation/)
- [Django REST API documentation](#rest-api) - [Manual](https://openvinotoolkit.github.io/cvat/docs/manual/)
- [Django REST API documentation](https://openvinotoolkit.github.io/cvat/docs/administration/basics/rest_api_guide/)
- [Datumaro dataset framework](https://github.com/openvinotoolkit/datumaro/blob/develop/README.md) - [Datumaro dataset framework](https://github.com/openvinotoolkit/datumaro/blob/develop/README.md)
- [Command line interface](utils/cli/) - [Command line interface](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/cli/)
- [XML annotation format](cvat/apps/documentation/xml_format.md) - [XML annotation format](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/xml_format/)
- [AWS Deployment Guide](cvat/apps/documentation/AWS-Deployment-Guide.md) - [AWS Deployment Guide](https://openvinotoolkit.github.io/cvat/docs/administration/basics/aws-deployment-guide/)
- [Frequently asked questions](cvat/apps/documentation/faq.md) - [Frequently asked questions](https://openvinotoolkit.github.io/cvat/docs/faq/)
- [Questions](#questions) - [Questions](#questions)
## Screencasts ## Screencasts
@ -47,26 +48,30 @@ dataset framework allows additional dataset transformations via its command
line tool and Python library. line tool and Python library.
For more information about supported formats look at the For more information about supported formats look at the
[documentation](cvat/apps/dataset_manager/formats/README.md#formats). [documentation](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/formats/).
| Annotation format | Import | Export | <!--lint disable maximum-line-length-->
| ----------------------------------------------------------------------------- | ------ | ------ |
| [CVAT for images](cvat/apps/documentation/xml_format.md#annotation) | X | X | | Annotation format | Import | Export |
| [CVAT for a video](cvat/apps/documentation/xml_format.md#interpolation) | X | X | | --------------------------------------------------------------------------------------------------------- | ------ | ------ |
| [Datumaro](https://github.com/openvinotoolkit/datumaro) | | X | | [CVAT for images](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/xml_format/#annotation) | X | X |
| [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X | | [CVAT for a video](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/xml_format/#interpolation) | X | X |
| Segmentation masks from [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X | | [Datumaro](https://github.com/openvinotoolkit/datumaro) | | X |
| [YOLO](https://pjreddie.com/darknet/yolo/) | X | X | | [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X | | Segmentation masks from [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tfrecord) | X | X | | [YOLO](https://pjreddie.com/darknet/yolo/) | X | X |
| [MOT](https://motchallenge.net/) | X | X | | [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X |
| [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | X | X | | [TFrecord](https://www.tensorflow.org/tutorials/load_data/tfrecord) | X | X |
| [ImageNet](http://www.image-net.org) | X | X | | [MOT](https://motchallenge.net/) | X | X |
| [CamVid](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) | X | X | | [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | X | X |
| [WIDER Face](http://shuoyang1213.me/WIDERFACE/) | X | X | | [ImageNet](http://www.image-net.org) | X | X |
| [VGGFace2](https://github.com/ox-vgg/vgg_face2) | X | X | | [CamVid](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) | X | X |
| [Market-1501](https://www.aitribune.com/dataset/2018051063) | X | X | | [WIDER Face](http://shuoyang1213.me/WIDERFACE/) | X | X |
| [ICDAR13/15](https://rrc.cvc.uab.es/?ch=2) | X | X | | [VGGFace2](https://github.com/ox-vgg/vgg_face2) | X | X |
| [Market-1501](https://www.aitribune.com/dataset/2018051063) | X | X |
| [ICDAR13/15](https://rrc.cvc.uab.es/?ch=2) | X | X |
<!--lint enable maximum-line-length-->
## Deep learning serverless functions for automatic labeling ## Deep learning serverless functions for automatic labeling
@ -86,6 +91,7 @@ For more information about supported formats look at the
| [Inside-Outside Guidance](/serverless/pytorch/shiyinzhang/iog/nuclio) | interactor | PyTorch | X | | | [Inside-Outside Guidance](/serverless/pytorch/shiyinzhang/iog/nuclio) | interactor | PyTorch | X | |
| [Faster RCNN](/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | detector | TensorFlow | X | X | | [Faster RCNN](/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | detector | TensorFlow | X | X |
| [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow | X | X | | [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow | X | X |
| [RetinaNet](serverless/pytorch/facebookresearch/detectron2/retinanet/nuclio) | detector | PyTorch | X | X |
<!--lint enable maximum-line-length--> <!--lint enable maximum-line-length-->
@ -97,7 +103,7 @@ are visible to users.
Disabled features: Disabled features:
- [Analytics: management and monitoring of data annotation team](/components/analytics/README.md) - [Analytics: management and monitoring of data annotation team](https://openvinotoolkit.github.io/cvat/docs/administration/advanced/analytics/)
Limitations: Limitations:
@ -111,15 +117,6 @@ Prebuilt docker images for CVAT releases are available on Docker Hub:
- [cvat_server](https://hub.docker.com/r/openvino/cvat_server) - [cvat_server](https://hub.docker.com/r/openvino/cvat_server)
- [cvat_ui](https://hub.docker.com/r/openvino/cvat_ui) - [cvat_ui](https://hub.docker.com/r/openvino/cvat_ui)
## REST API
Automatically generated Swagger documentation for Django REST API is 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'`.
## LICENSE ## LICENSE
Code released under the [MIT License](https://opensource.org/licenses/MIT). Code released under the [MIT License](https://opensource.org/licenses/MIT).
@ -164,10 +161,10 @@ Other ways to ask questions and get our support:
vision AI platform that fully integrates CVAT with scalable data processing vision AI platform that fully integrates CVAT with scalable data processing
and parallelized training pipelines. and parallelized training pipelines.
- [DataIsKey](https://dataiskey.eu/annotation-tool/) uses CVAT as their prime data labeling tool - [DataIsKey](https://dataiskey.eu/annotation-tool/) uses CVAT as their prime data labeling tool
to offer annotation services for projects of any size. to offer annotation services for projects of any size.
- [Human Protocol](https://hmt.ai) uses CVAT as a way of adding annotation service to the human protocol.
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<!-- Badges --> <!-- Badges -->
[docker-server-pulls-img]: https://img.shields.io/docker/pulls/openvino/cvat_server.svg?style=flat-square&label=server%20pulls [docker-server-pulls-img]: https://img.shields.io/docker/pulls/openvino/cvat_server.svg?style=flat-square&label=server%20pulls
[docker-server-image-url]: https://hub.docker.com/r/openvino/cvat_server [docker-server-image-url]: https://hub.docker.com/r/openvino/cvat_server

@ -1,12 +1,10 @@
version: '3.3' version: '3.3'
services: services:
cvat_elasticsearch: elasticsearch:
container_name: cvat_elasticsearch container_name: cvat_elasticsearch
image: cvat_elasticsearch image: cvat_elasticsearch
networks: networks:
default: - cvat
aliases:
- elasticsearch
build: build:
context: ./components/analytics/elasticsearch context: ./components/analytics/elasticsearch
args: args:
@ -15,18 +13,16 @@ services:
- cvat_events:/usr/share/elasticsearch/data - cvat_events:/usr/share/elasticsearch/data
restart: always restart: always
cvat_kibana: kibana:
container_name: cvat_kibana container_name: cvat_kibana
image: cvat_kibana image: cvat_kibana
networks: networks:
default: - cvat
aliases:
- kibana
build: build:
context: ./components/analytics/kibana context: ./components/analytics/kibana
args: args:
ELK_VERSION: 6.4.0 ELK_VERSION: 6.4.0
depends_on: ['cvat_elasticsearch'] depends_on: ['elasticsearch']
restart: always restart: always
cvat_kibana_setup: cvat_kibana_setup:
@ -35,6 +31,8 @@ services:
volumes: ['./components/analytics/kibana:/home/django/kibana:ro'] volumes: ['./components/analytics/kibana:/home/django/kibana:ro']
depends_on: ['cvat'] depends_on: ['cvat']
working_dir: '/home/django' working_dir: '/home/django'
networks:
- cvat
entrypoint: entrypoint:
[ [
'bash', 'bash',
@ -56,13 +54,11 @@ services:
environment: environment:
no_proxy: elasticsearch,kibana,${no_proxy} no_proxy: elasticsearch,kibana,${no_proxy}
cvat_logstash: logstash:
container_name: cvat_logstash container_name: cvat_logstash
image: cvat_logstash image: cvat_logstash
networks: networks:
default: - cvat
aliases:
- logstash
build: build:
context: ./components/analytics/logstash context: ./components/analytics/logstash
args: args:
@ -73,7 +69,7 @@ services:
LOGSTASH_OUTPUT_HOST: elasticsearch:9200 LOGSTASH_OUTPUT_HOST: elasticsearch:9200
LOGSTASH_OUTPUT_USER: LOGSTASH_OUTPUT_USER:
LOGSTASH_OUTPUT_PASS: LOGSTASH_OUTPUT_PASS:
depends_on: ['cvat_elasticsearch'] depends_on: ['elasticsearch']
restart: always restart: always
cvat: cvat:

@ -1,13 +1,11 @@
version: '3.3' version: '3.3'
services: services:
serverless: nuclio:
container_name: nuclio container_name: nuclio
image: quay.io/nuclio/dashboard:1.5.16-amd64 image: quay.io/nuclio/dashboard:1.5.16-amd64
restart: always restart: always
networks: networks:
default: - cvat
aliases:
- nuclio
volumes: volumes:
- /tmp:/tmp - /tmp:/tmp
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock

@ -245,4 +245,4 @@ canvas.draw({
You can call setup() during editing, dragging, and resizing only to update objects, not to change a frame. 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 You can change frame during draw only when you do not redraw an existing object
Other methods do not change state and can be used everytime. Other methods do not change state and can be used at any time.

@ -1,6 +1,6 @@
{ {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.4.3", "version": "2.5.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -1995,17 +1995,6 @@
"postcss-value-parser": "^4.0.2" "postcss-value-parser": "^4.0.2"
}, },
"dependencies": { "dependencies": {
"browserslist": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.8.2.tgz",
"integrity": "sha512-+M4oeaTplPm/f1pXDw84YohEv7B1i/2Aisei8s4s6k3QsoSHa7i5sz8u/cGQkkatCPxMASKxPualR4wwYgVboA==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001015",
"electron-to-chromium": "^1.3.322",
"node-releases": "^1.1.42"
}
},
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001016", "version": "1.0.30001016",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001016.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001016.tgz",
@ -2015,40 +2004,25 @@
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.322", "version": "1.3.322",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.322.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.322.tgz",
"integrity": "sha512-Tc8JQEfGQ1MzfSzI/bTlSr7btJv/FFO7Yh6tanqVmIWOuNCu6/D1MilIEgLtmWqIrsv+o4IjpLAhgMBr/ncNAA==", "integrity": "sha512-Tc8JQEfGQ1MzfSzI/bTlSr7btJv/FFO7Yh6tanqVmIWOuNCu6/D1MilIEgLtmWqIrsv+o4IjpLAhgMBr/ncNAA=="
"dev": true
}, },
"node-releases": { "node-releases": {
"version": "1.1.44", "version": "1.1.44",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.44.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.44.tgz",
"integrity": "sha512-NwbdvJyR7nrcGrXvKAvzc5raj/NkoJudkarh2yIpJ4t0NH4aqjUDz/486P+ynIW5eokKOfzGNRdYoLfBlomruw==", "integrity": "sha512-NwbdvJyR7nrcGrXvKAvzc5raj/NkoJudkarh2yIpJ4t0NH4aqjUDz/486P+ynIW5eokKOfzGNRdYoLfBlomruw==",
"dev": true,
"requires": { "requires": {
"semver": "^6.3.0" "semver": "^6.3.0"
} }
}, },
"postcss": {
"version": "7.0.25",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.25.tgz",
"integrity": "sha512-NXXVvWq9icrm/TgQC0O6YVFi4StfJz46M1iNd/h6B26Nvh/HKI+q4YZtFN/EjcInZliEscO/WL10BXnc1E5nwg==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
"source-map": "^0.6.1",
"supports-color": "^6.1.0"
}
},
"semver": { "semver": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
"dev": true
}, },
"source-map": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"dev": true
} }
} }
}, },
@ -2367,14 +2341,42 @@
} }
}, },
"browserslist": { "browserslist": {
"version": "4.6.6", "version": "4.16.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.6.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
"integrity": "sha512-D2Nk3W9JL9Fp/gIcWei8LrERCS+eXu9AM5cfXA8WEZ84lFks+ARnZ0q/R69m2SV3Wjma83QDDPxsNKXUwdIsyA==", "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"caniuse-lite": "^1.0.30000984", "caniuse-lite": "^1.0.30001219",
"electron-to-chromium": "^1.3.191", "colorette": "^1.2.2",
"node-releases": "^1.1.25" "electron-to-chromium": "^1.3.723",
"escalade": "^3.1.1",
"node-releases": "^1.1.71"
},
"dependencies": {
"caniuse-lite": {
"version": "1.0.30001228",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz",
"integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==",
"dev": true
},
"colorette": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
"dev": true
},
"electron-to-chromium": {
"version": "1.3.737",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.737.tgz",
"integrity": "sha512-P/B84AgUSQXaum7a8m11HUsYL8tj9h/Pt5f7Hg7Ty6bm5DxlFq+e5+ouHUoNQMsKDJ7u4yGfI8mOErCmSH9wyg==",
"dev": true
},
"node-releases": {
"version": "1.1.72",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz",
"integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==",
"dev": true
}
} }
}, },
"buffer-from": { "buffer-from": {
@ -2811,12 +2813,6 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true "dev": true
}, },
"colorette": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==",
"dev": true
},
"combined-stream": { "combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -3121,17 +3117,6 @@
"schema-utils": "^2.6.0" "schema-utils": "^2.6.0"
}, },
"dependencies": { "dependencies": {
"postcss": {
"version": "7.0.27",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz",
"integrity": "sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
"source-map": "^0.6.1",
"supports-color": "^6.1.0"
}
},
"schema-utils": { "schema-utils": {
"version": "2.6.4", "version": "2.6.4",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz",
@ -3145,8 +3130,7 @@
"source-map": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"dev": true
} }
} }
}, },
@ -3387,9 +3371,9 @@
"dev": true "dev": true
}, },
"dns-packet": { "dns-packet": {
"version": "1.3.1", "version": "1.3.4",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz",
"integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==",
"dev": true, "dev": true,
"requires": { "requires": {
"ip": "^1.1.0", "ip": "^1.1.0",
@ -3501,12 +3485,6 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
"dev": true "dev": true
}, },
"electron-to-chromium": {
"version": "1.3.199",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.199.tgz",
"integrity": "sha512-gachlDdHSK47s0N2e58GH9HMC6Z4ip0SfmYUa5iEbE50AKaOUXysaJnXMfKj0xB245jWbYcyFSH+th3rqsF8hA==",
"dev": true
},
"emoji-regex": { "emoji-regex": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
@ -6738,15 +6716,6 @@
} }
} }
}, },
"node-releases": {
"version": "1.1.25",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.25.tgz",
"integrity": "sha512-fI5BXuk83lKEoZDdH3gRhtsNgh05/wZacuXkgbiYkceE7+QIMXOg98n9ZV7mz27B+kFHnqHcUpscZZlGRSmTpQ==",
"dev": true,
"requires": {
"semver": "^5.3.0"
}
},
"node-sass": { "node-sass": {
"version": "4.14.1", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz",
@ -6915,9 +6884,9 @@
"dev": true "dev": true
}, },
"normalize-url": { "normalize-url": {
"version": "4.5.0", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
"integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
"dev": true "dev": true
}, },
"npm-run-path": { "npm-run-path": {
@ -7604,9 +7573,9 @@
"dev": true "dev": true
}, },
"postcss": { "postcss": {
"version": "7.0.17", "version": "7.0.36",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz",
"integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "^2.4.2", "chalk": "^2.4.2",
@ -10461,30 +10430,15 @@
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true "dev": true
}, },
"browserslist": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.3.tgz",
"integrity": "sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001181",
"colorette": "^1.2.1",
"electron-to-chromium": "^1.3.649",
"escalade": "^3.1.1",
"node-releases": "^1.1.70"
}
},
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001185", "version": "1.0.30001185",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001185.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001185.tgz",
"integrity": "sha512-Fpi4kVNtNvJ15H0F6vwmXtb3tukv3Zg3qhKkOGUq7KJ1J6b9kf4dnNgtEAFXhRsJo0gNj9W60+wBvn0JcTvdTg==", "integrity": "sha512-Fpi4kVNtNvJ15H0F6vwmXtb3tukv3Zg3qhKkOGUq7KJ1J6b9kf4dnNgtEAFXhRsJo0gNj9W60+wBvn0JcTvdTg=="
"dev": true
}, },
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.654", "version": "1.3.654",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.654.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.654.tgz",
"integrity": "sha512-Zy2gc/c8KYFg2GkNr7Ruzh5tPEZpFm7EyXqZTFasm1YRDJtpyBRGaOuM0H3t6SuIP53qX4kNmtO9t0WjhBjE9A==", "integrity": "sha512-Zy2gc/c8KYFg2GkNr7Ruzh5tPEZpFm7EyXqZTFasm1YRDJtpyBRGaOuM0H3t6SuIP53qX4kNmtO9t0WjhBjE9A=="
"dev": true
}, },
"enhanced-resolve": { "enhanced-resolve": {
"version": "5.7.0", "version": "5.7.0",
@ -10559,8 +10513,7 @@
"node-releases": { "node-releases": {
"version": "1.1.70", "version": "1.1.70",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.70.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.70.tgz",
"integrity": "sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw==", "integrity": "sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw=="
"dev": true
}, },
"schema-utils": { "schema-utils": {
"version": "3.0.0", "version": "3.0.0",
@ -10976,9 +10929,9 @@
} }
}, },
"ws": { "ws": {
"version": "6.2.1", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
"integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==",
"dev": true, "dev": true,
"requires": { "requires": {
"async-limiter": "~1.0.0" "async-limiter": "~1.0.0"

@ -1,6 +1,6 @@
{ {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.4.3", "version": "2.5.0",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library", "description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts", "main": "src/canvas.ts",
"scripts": { "scripts": {

@ -155,6 +155,22 @@ polyline.cvat_canvas_shape_splitting {
fill: blueviolet; fill: blueviolet;
} }
.cvat_canvas_interact_intermediate_shape {
@extend .cvat_canvas_shape;
}
.cvat_canvas_removable_interaction_point {
cursor:
url(
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K'
) 10 10,
auto;
}
.cvat_canvas_interact_intermediate_shape_point {
pointer-events: none;
}
.svg_select_boundingRect { .svg_select_boundingRect {
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -28,14 +28,14 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
private auxiliaryGroupID: number | null; private auxiliaryGroupID: number | null;
private auxiliaryClicks: number[]; private auxiliaryClicks: number[];
private listeners: Record< private listeners: Record<
number, number,
Record< Record<
number, number,
{ {
click: (event: MouseEvent) => void; click: (event: MouseEvent) => void;
dblclick: (event: MouseEvent) => void; dblclick: (event: MouseEvent) => void;
} }
> >
>; >;
public constructor(frameContent: SVGSVGElement) { public constructor(frameContent: SVGSVGElement) {
@ -172,12 +172,11 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
} else { } else {
// sign defines bypass direction // sign defines bypass direction
const landmarks = this.auxiliaryClicks; const landmarks = this.auxiliaryClicks;
const sign = const sign = Math.sign(landmarks[2] - landmarks[0])
Math.sign(landmarks[2] - landmarks[0]) * * Math.sign(landmarks[1] - landmarks[0])
Math.sign(landmarks[1] - landmarks[0]) * * Math.sign(landmarks[2] - landmarks[1]);
Math.sign(landmarks[2] - landmarks[1]);
// go via a polygon and get vertexes // go via a polygon and get vertices
// the first vertex has been already drawn // the first vertex has been already drawn
const way = []; const way = [];
for (let i = landmarks[0] + sign; ; i += sign) { for (let i = landmarks[0] + sign; ; i += sign) {

@ -58,6 +58,7 @@ export interface Configuration {
showProjections?: boolean; showProjections?: boolean;
forceDisableEditing?: boolean; forceDisableEditing?: boolean;
intelligentPolygonCrop?: boolean; intelligentPolygonCrop?: boolean;
forceFrameUpdate?: boolean;
} }
export interface DrawData { export interface DrawData {
@ -77,10 +78,14 @@ export interface InteractionData {
crosshair?: boolean; crosshair?: boolean;
minPosVertices?: number; minPosVertices?: number;
minNegVertices?: number; minNegVertices?: number;
enableNegVertices?: boolean; startWithBox?: boolean;
enableThreshold?: boolean; enableThreshold?: boolean;
enableSliding?: boolean; enableSliding?: boolean;
allowRemoveOnlyLast?: boolean; allowRemoveOnlyLast?: boolean;
intermediateShape?: {
shapeType: string;
points: number[];
};
} }
export interface InteractionResult { export interface InteractionResult {
@ -388,8 +393,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
throw Error(`Canvas is busy. Action: ${this.data.mode}`); throw Error(`Canvas is busy. Action: ${this.data.mode}`);
} }
} }
if (frameData.number === this.data.imageID && !this.data.configuration.forceFrameUpdate) {
if (frameData.number === this.data.imageID) {
this.data.zLayer = zLayer; this.data.zLayer = zLayer;
this.data.objects = objectStates; this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED); this.notify(UpdateReasons.OBJECTS_UPDATED);
@ -421,7 +425,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}) })
.catch((exception: any): void => { .catch((exception: any): void => {
this.data.exception = exception; this.data.exception = exception;
this.notify(UpdateReasons.DATA_FAILED); // don't notify when the frame is no longer needed
if (typeof exception !== 'number' || exception === this.data.imageID) {
this.notify(UpdateReasons.DATA_FAILED);
}
throw exception; throw exception;
}); });
} }
@ -548,7 +555,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
throw Error(`Canvas is busy. Action: ${this.data.mode}`); throw Error(`Canvas is busy. Action: ${this.data.mode}`);
} }
if (interactionData.enabled) { if (interactionData.enabled && !interactionData.intermediateShape) {
if (this.data.interactionData.enabled) { if (this.data.interactionData.enabled) {
throw new Error('Interaction has been already started'); throw new Error('Interaction has been already started');
} else if (!interactionData.shapeType) { } else if (!interactionData.shapeType) {
@ -645,6 +652,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.configuration.intelligentPolygonCrop = configuration.intelligentPolygonCrop; this.data.configuration.intelligentPolygonCrop = configuration.intelligentPolygonCrop;
} }
if (typeof configuration.forceFrameUpdate === 'boolean') {
this.data.configuration.forceFrameUpdate = configuration.forceFrameUpdate;
}
this.notify(UpdateReasons.CONFIG_UPDATED); this.notify(UpdateReasons.CONFIG_UPDATED);
} }

@ -23,6 +23,7 @@ import consts from './consts';
import { import {
translateToSVG, translateToSVG,
translateFromSVG, translateFromSVG,
translateToCanvas,
pointsToNumberArray, pointsToNumberArray,
parsePoints, parsePoints,
displayShapeSize, displayShapeSize,
@ -103,7 +104,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private translateToCanvas(points: number[]): number[] { private translateToCanvas(points: number[]): number[] {
const { offset } = this.controller.geometry; const { offset } = this.controller.geometry;
return points.map((coord: number): number => coord + offset); return translateToCanvas(offset, points);
} }
private translateFromCanvas(points: number[]): number[] { private translateFromCanvas(points: number[]): number[] {
@ -1267,9 +1268,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} else if (reason === UpdateReasons.INTERACT) { } else if (reason === UpdateReasons.INTERACT) {
const data: InteractionData = this.controller.interactionData; const data: InteractionData = this.controller.interactionData;
if (data.enabled && this.mode === Mode.IDLE) { if (data.enabled && (this.mode === Mode.IDLE || data.intermediateShape)) {
this.canvas.style.cursor = 'crosshair'; if (!data.intermediateShape) {
this.mode = Mode.INTERACT; this.canvas.style.cursor = 'crosshair';
this.mode = Mode.INTERACT;
}
this.interactionHandler.interact(data); this.interactionHandler.interact(data);
} else { } else {
this.canvas.style.cursor = ''; this.canvas.style.cursor = '';

@ -352,7 +352,7 @@ function setupCuboidPoints(points: Point[]): any[] {
? Math.abs(points[1].y - points[0].y) ? Math.abs(points[1].y - points[0].y)
: Math.abs(points[1].y - points[2].y); : Math.abs(points[1].y - points[2].y);
// seperate into left and right point // separate into left and right point
// we pick the first and third point because we know assume they will be on // we pick the first and third point because we know assume they will be on
// opposite corners // opposite corners
if (points[0].x < points[2].x) { if (points[0].x < points[2].x) {

@ -311,8 +311,9 @@ export class DrawHandlerImpl implements DrawHandler {
// We check if it is activated with remember function // We check if it is activated with remember function
if (this.drawInstance.remember('_paintHandler')) { if (this.drawInstance.remember('_paintHandler')) {
if ( if (
this.drawData.shapeType !== 'rectangle' ['polygon', 'polyline', 'points'].includes(this.drawData.shapeType)
&& this.drawData.cuboidDrawingMethod !== CuboidDrawingMethod.CLASSIC || (this.drawData.shapeType === 'cuboid'
&& this.drawData.cuboidDrawingMethod === CuboidDrawingMethod.CORNER_POINTS)
) { ) {
// Check for unsaved drawn shapes // Check for unsaved drawn shapes
this.drawInstance.draw('done'); this.drawInstance.draw('done');
@ -462,11 +463,11 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance.draw('point', e); this.drawInstance.draw('point', e);
} else { } else {
this.drawInstance.draw('update', e); this.drawInstance.draw('update', e);
const deltaTreshold = 15; const deltaThreshold = 15;
const dx = (e.clientX - lastDrawnPoint.x) ** 2; const dx = (e.clientX - lastDrawnPoint.x) ** 2;
const dy = (e.clientY - lastDrawnPoint.y) ** 2; const dy = (e.clientY - lastDrawnPoint.y) ** 2;
const delta = Math.sqrt(dx + dy); const delta = Math.sqrt(dx + dy);
if (delta > deltaTreshold) { if (delta > deltaThreshold) {
this.drawInstance.draw('point', e); this.drawInstance.draw('point', e);
} }
} }
@ -574,7 +575,7 @@ export class DrawHandlerImpl implements DrawHandler {
.on('drawstop', (e: Event): void => { .on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox(); const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox); const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType } = this.drawData; const { shapeType, redraw: clientID } = this.drawData;
this.release(); this.release();
if (this.canceled) return; if (this.canceled) return;
@ -584,6 +585,7 @@ export class DrawHandlerImpl implements DrawHandler {
{ {
shapeType, shapeType,
points: cuboidFrom4Points([xtl, ybr, xbr, ybr, xbr, ytl, xbr + d.x, ytl - d.y]), points: cuboidFrom4Points([xtl, ybr, xbr, ybr, xbr, ytl, xbr + d.x, ytl - d.y]),
clientID,
}, },
Date.now() - this.startTimestamp, Date.now() - this.startTimestamp,
); );

@ -93,11 +93,11 @@ export class EditHandlerImpl implements EditHandler {
if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) { if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) {
(this.editLine as any).draw('point', e); (this.editLine as any).draw('point', e);
} else { } else {
const deltaTreshold = 15; const deltaThreshold = 15;
const dxsqr = (e.clientX - lastDrawnPoint.x) ** 2; const dxsqr = (e.clientX - lastDrawnPoint.x) ** 2;
const dysqr = (e.clientY - lastDrawnPoint.y) ** 2; const dysqr = (e.clientY - lastDrawnPoint.y) ** 2;
const delta = Math.sqrt(dxsqr + dysqr); const delta = Math.sqrt(dxsqr + dysqr);
if (delta > deltaTreshold) { if (delta > deltaThreshold) {
(this.editLine as any).draw('point', e); (this.editLine as any).draw('point', e);
} }
} }

@ -5,7 +5,9 @@
import * as SVG from 'svg.js'; import * as SVG from 'svg.js';
import consts from './consts'; import consts from './consts';
import Crosshair from './crosshair'; import Crosshair from './crosshair';
import { translateToSVG } from './shared'; import {
translateToSVG, PropType, stringifyPoints, translateToCanvas,
} from './shared';
import { InteractionData, InteractionResult, Geometry } from './canvasModel'; import { InteractionData, InteractionResult, Geometry } from './canvasModel';
export interface InteractionHandler { export interface InteractionHandler {
@ -26,6 +28,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
private crosshair: Crosshair; private crosshair: Crosshair;
private threshold: SVG.Rect | null; private threshold: SVG.Rect | null;
private thresholdRectSize: number; private thresholdRectSize: number;
private intermediateShape: PropType<InteractionData, 'intermediateShape'>;
private drawnIntermediateShape: SVG.Shape;
private prepareResult(): InteractionResult[] { private prepareResult(): InteractionResult[] {
return this.interactionShapes.map( return this.interactionShapes.map(
@ -65,8 +69,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
return enabled && !ctrlKey && !!interactionShapes.length; return enabled && !ctrlKey && !!interactionShapes.length;
} }
const minPosVerticesAchieved = typeof minPosVertices === 'undefined' || minPosVertices <= positiveShapes.length; const minPosVerticesDefined = Number.isInteger(minPosVertices);
const minNegVerticesAchieved = typeof minNegVertices === 'undefined' || minPosVertices <= negativeShapes.length; const minNegVerticesDefined = Number.isInteger(minNegVertices) && minNegVertices >= 0;
const minPosVerticesAchieved = !minPosVerticesDefined || minPosVertices <= positiveShapes.length;
const minNegVerticesAchieved = !minNegVerticesDefined || minNegVertices <= negativeShapes.length;
const minimumVerticesAchieved = minPosVerticesAchieved && minNegVerticesAchieved; const minimumVerticesAchieved = minPosVerticesAchieved && minNegVerticesAchieved;
return enabled && !ctrlKey && minimumVerticesAchieved && shapesWereUpdated; return enabled && !ctrlKey && minimumVerticesAchieved && shapesWereUpdated;
} }
@ -91,10 +97,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
private interactPoints(): void { private interactPoints(): void {
const eventListener = (e: MouseEvent): void => { const eventListener = (e: MouseEvent): void => {
if ((e.button === 0 || (e.button === 2 && this.interactionData.enableNegVertices)) && !e.altKey) { if ((e.button === 0 || (e.button === 2 && this.interactionData.minNegVertices >= 0)) && !e.altKey) {
e.preventDefault(); e.preventDefault();
const [cx, cy] = translateToSVG((this.canvas.node as any) as SVGSVGElement, [e.clientX, e.clientY]); const [cx, cy] = translateToSVG((this.canvas.node as any) as SVGSVGElement, [e.clientX, e.clientY]);
if (!this.isWithingFrame(cx, cy)) return; if (!this.isWithinFrame(cx, cy)) return;
if (!this.isWithinThreshold(cx, cy)) return; if (!this.isWithinThreshold(cx, cy)) return;
this.currentInteractionShape = this.canvas this.currentInteractionShape = this.canvas
@ -121,8 +127,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
} }
} }
self.addClass('cvat_canvas_removable_interaction_point');
self.attr({ self.attr({
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale,
r: (consts.BASE_POINT_SIZE * 1.5) / this.geometry.scale,
}); });
self.on('mousedown', (_e: MouseEvent): void => { self.on('mousedown', (_e: MouseEvent): void => {
@ -132,6 +140,9 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.interactionShapes = this.interactionShapes.filter( this.interactionShapes = this.interactionShapes.filter(
(shape: SVG.Shape): boolean => shape !== self, (shape: SVG.Shape): boolean => shape !== self,
); );
if (this.interactionData.startWithBox && this.interactionShapes.length === 1) {
this.interactionShapes[0].style({ visibility: '' });
}
this.shapesWereUpdated = true; this.shapesWereUpdated = true;
if (this.shouldRaiseEvent(_e.ctrlKey)) { if (this.shouldRaiseEvent(_e.ctrlKey)) {
this.onInteraction(this.prepareResult(), true, false); this.onInteraction(this.prepareResult(), true, false);
@ -140,8 +151,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
}); });
self.on('mouseleave', (): void => { self.on('mouseleave', (): void => {
self.removeClass('cvat_canvas_removable_interaction_point');
self.attr({ self.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
r: consts.BASE_POINT_SIZE / this.geometry.scale,
}); });
self.off('mousedown'); self.off('mousedown');
@ -149,11 +162,11 @@ export class InteractionHandlerImpl implements InteractionHandler {
} }
}; };
// clear this listener in relese() // clear this listener in release()
this.canvas.on('mousedown.interaction', eventListener); this.canvas.on('mousedown.interaction', eventListener);
} }
private interactRectangle(): void { private interactRectangle(shouldFinish: boolean, onContinue?: () => void): void {
let initialized = false; let initialized = false;
const eventListener = (e: MouseEvent): void => { const eventListener = (e: MouseEvent): void => {
if (e.button === 0 && !e.altKey) { if (e.button === 0 && !e.altKey) {
@ -170,11 +183,15 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.canvas.on('mousedown.interaction', eventListener); this.canvas.on('mousedown.interaction', eventListener);
this.currentInteractionShape this.currentInteractionShape
.on('drawstop', (): void => { .on('drawstop', (): void => {
this.canvas.off('mousedown.interaction', eventListener);
this.interactionShapes.push(this.currentInteractionShape); this.interactionShapes.push(this.currentInteractionShape);
this.shapesWereUpdated = true; this.shapesWereUpdated = true;
this.canvas.off('mousedown.interaction', eventListener); if (shouldFinish) {
this.interact({ enabled: false }); this.interact({ enabled: false });
} else if (onContinue) {
onContinue();
}
}) })
.addClass('cvat_canvas_shape_drawing') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
@ -194,15 +211,25 @@ export class InteractionHandlerImpl implements InteractionHandler {
private startInteraction(): void { private startInteraction(): void {
if (this.interactionData.shapeType === 'rectangle') { if (this.interactionData.shapeType === 'rectangle') {
this.interactRectangle(); this.interactRectangle(true);
} else if (this.interactionData.shapeType === 'points') { } else if (this.interactionData.shapeType === 'points') {
this.interactPoints(); if (this.interactionData.startWithBox) {
this.interactRectangle(false, (): void => this.interactPoints());
} else {
this.interactPoints();
}
} else { } else {
throw new Error('Interactor implementation supports only rectangle and points'); throw new Error('Interactor implementation supports only rectangle and points');
} }
} }
private release(): void { private release(): void {
if (this.drawnIntermediateShape) {
this.selectize(false, this.drawnIntermediateShape);
this.drawnIntermediateShape.remove();
this.drawnIntermediateShape = null;
}
if (this.crosshair) { if (this.crosshair) {
this.removeCrosshair(); this.removeCrosshair();
} }
@ -234,13 +261,75 @@ export class InteractionHandlerImpl implements InteractionHandler {
return xDiff < this.thresholdRectSize / 2 && yDiff < this.thresholdRectSize / 2; return xDiff < this.thresholdRectSize / 2 && yDiff < this.thresholdRectSize / 2;
} }
private isWithingFrame(x: number, y: number): boolean { private isWithinFrame(x: number, y: number): boolean {
const { offset, image } = this.geometry; const { offset, image } = this.geometry;
const { width, height } = image; const { width, height } = image;
const [imageX, imageY] = [Math.round(x - offset), Math.round(y - offset)]; const [imageX, imageY] = [Math.round(x - offset), Math.round(y - offset)];
return imageX >= 0 && imageX < width && imageY >= 0 && imageY < height; return imageX >= 0 && imageX < width && imageY >= 0 && imageY < height;
} }
private updateIntermediateShape(): void {
const { intermediateShape, geometry } = this;
if (this.drawnIntermediateShape) {
this.selectize(false, this.drawnIntermediateShape);
this.drawnIntermediateShape.remove();
}
if (!intermediateShape) return;
const { shapeType, points } = intermediateShape;
if (shapeType === 'polygon') {
const erroredShape = shapeType === 'polygon' && points.length < 3 * 2;
this.drawnIntermediateShape = this.canvas
.polygon(stringifyPoints(translateToCanvas(geometry.offset, points)))
.attr({
'color-rendering': 'optimizeQuality',
'shape-rendering': 'geometricprecision',
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
stroke: erroredShape ? 'red' : 'black',
fill: 'none',
})
.addClass('cvat_canvas_interact_intermediate_shape');
this.selectize(true, this.drawnIntermediateShape, erroredShape);
} else {
throw new Error(
`Shape type "${shapeType}" was not implemented at interactionHandler::updateIntermediateShape`,
);
}
}
private selectize(value: boolean, shape: SVG.Element, erroredShape = false): void {
const self = this;
if (value) {
(shape as any).selectize(value, {
deepSelect: true,
pointSize: consts.BASE_POINT_SIZE / self.geometry.scale,
rotationPoint: false,
classPoints: 'cvat_canvas_interact_intermediate_shape_point',
pointType(cx: number, cy: number): SVG.Circle {
return this.nested
.circle(this.options.pointSize)
.stroke(erroredShape ? 'red' : 'black')
.fill('black')
.center(cx, cy)
.attr({
'fill-opacity': 1,
'stroke-width': consts.POINTS_STROKE_WIDTH / self.geometry.scale,
});
},
});
} else {
(shape as any).selectize(false, {
deepSelect: true,
});
}
const handler = shape.remember('_selectHandler');
if (handler && handler.nested) {
handler.nested.fill(shape.attr('fill'));
}
}
public constructor( public constructor(
onInteraction: ( onInteraction: (
shapes: InteractionResult[] | null, shapes: InteractionResult[] | null,
@ -264,6 +353,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.crosshair = new Crosshair(); this.crosshair = new Crosshair();
this.threshold = null; this.threshold = null;
this.thresholdRectSize = 300; this.thresholdRectSize = 300;
this.intermediateShape = null;
this.drawnIntermediateShape = null;
this.cursorPosition = { this.cursorPosition = {
x: 0, x: 0,
y: 0, y: 0,
@ -280,7 +371,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
} }
if (this.interactionData.enableSliding && this.interactionShapes.length) { if (this.interactionData.enableSliding && this.interactionShapes.length) {
if (this.isWithingFrame(x, y)) { if (this.isWithinFrame(x, y)) {
if (this.interactionData.enableThreshold && !this.isWithinThreshold(x, y)) return; if (this.interactionData.enableThreshold && !this.isWithinThreshold(x, y)) return;
this.onInteraction( this.onInteraction(
[ [
@ -334,16 +425,36 @@ export class InteractionHandlerImpl implements InteractionHandler {
: [...this.interactionShapes]; : [...this.interactionShapes];
for (const shape of shapesToBeScaled) { for (const shape of shapesToBeScaled) {
if (shape.type === 'circle') { if (shape.type === 'circle') {
(shape as SVG.Circle).radius(consts.BASE_POINT_SIZE / this.geometry.scale); if (shape.hasClass('cvat_canvas_removable_interaction_point')) {
shape.attr('stroke-width', consts.POINTS_STROKE_WIDTH / this.geometry.scale); (shape as SVG.Circle).radius((consts.BASE_POINT_SIZE * 1.5) / this.geometry.scale);
shape.attr('stroke-width', consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale);
} else {
(shape as SVG.Circle).radius(consts.BASE_POINT_SIZE / this.geometry.scale);
shape.attr('stroke-width', consts.POINTS_STROKE_WIDTH / this.geometry.scale);
}
} else { } else {
shape.attr('stroke-width', consts.BASE_STROKE_WIDTH / this.geometry.scale); shape.attr('stroke-width', consts.BASE_STROKE_WIDTH / this.geometry.scale);
} }
} }
for (const element of window.document.getElementsByClassName('cvat_canvas_interact_intermediate_shape_point')) {
element.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / (2 * this.geometry.scale)}`);
element.setAttribute('r', `${consts.BASE_POINT_SIZE / this.geometry.scale}`);
}
if (this.drawnIntermediateShape) {
this.drawnIntermediateShape.stroke({ width: consts.BASE_STROKE_WIDTH / this.geometry.scale });
}
} }
public interact(interactionData: InteractionData): void { public interact(interactionData: InteractionData): void {
if (interactionData.enabled) { if (interactionData.intermediateShape) {
this.intermediateShape = interactionData.intermediateShape;
this.updateIntermediateShape();
if (this.interactionData.startWithBox) {
this.interactionShapes[0].style({ visibility: 'hidden' });
}
} else if (interactionData.enabled) {
this.interactionData = interactionData; this.interactionData = interactionData;
this.initInteraction(); this.initInteraction();
this.startInteraction(); this.startInteraction();

@ -1,11 +1,9 @@
// Copyright (C) 2019-2020 Intel Corporation // Copyright (C) 2019-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
export interface Master { export interface Master {
subscribe(listener: Listener): void; subscribe(listener: Listener): void;
unsubscribe(listener: Listener): void;
unsubscribeAll(): void;
notify(reason: string): void; notify(reason: string): void;
} }
@ -24,18 +22,6 @@ export class MasterImpl implements Master {
this.listeners.push(listener); this.listeners.push(listener);
} }
public unsubscribe(listener: Listener): void {
for (let i = 0; i < this.listeners.length; i++) {
if (this.listeners[i] === listener) {
this.listeners.splice(i, 1);
}
}
}
public unsubscribeAll(): void {
this.listeners = [];
}
public notify(reason: string): void { public notify(reason: string): void {
for (const listener of this.listeners) { for (const listener of this.listeners) {
listener.notify(this, reason); listener.notify(this, reason);

@ -181,3 +181,9 @@ export function vectorLength(vector: Vector2D): number {
const sqrJ = vector.j ** 2; const sqrJ = vector.j ** 2;
return Math.sqrt(sqrI + sqrJ); return Math.sqrt(sqrI + sqrJ);
} }
export function translateToCanvas(offset: number, points: number[]): number[] {
return points.map((coord: number): number => coord + offset);
}
export type PropType<T, Prop extends keyof T> = T[Prop];

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation // Copyright (C) 2019-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -43,7 +43,7 @@ export class SplitHandlerImpl implements SplitHandler {
} }
private closeSplitting(): void { private closeSplitting(): void {
// Split done is true if an object was splitted // Split done is true if an object was split
// Split also can be called with { enabled: false } without splitting an object // Split also can be called with { enabled: false } without splitting an object
if (!this.splitDone) { if (!this.splitDone) {
this.onSplitDone(null); this.onSplitDone(null);

@ -26,11 +26,20 @@ npm run build -- --mode=development # without a minification
```ts ```ts
interface Canvas3d { interface Canvas3d {
html(): HTMLDivElement; html(): ViewsDOM;
setup(frameData: any): void; setup(frameData: any, objectStates: any[]): void;
mode(): Mode;
isAbleToChangeFrame(): boolean; isAbleToChangeFrame(): boolean;
mode(): Mode;
render(): void; render(): void;
keyControls(keys: KeyboardEvent): void;
draw(drawData: DrawData): void;
cancel(): void;
dragCanvas(enable: boolean): void;
activate(clientID: number | null, attributeID?: number): void;
configureShapes(shapeProperties: ShapeProperties): void;
fitCanvas(): void;
fit(): void;
group(groupData: GroupData): void;
} }
``` ```
@ -44,5 +53,9 @@ console.log('Version ', window.canvas.CanvasVersion);
console.log('Current mode is ', window.canvas.mode()); console.log('Current mode is ', window.canvas.mode());
// Put canvas to a html container // Put canvas to a html container
htmlContainer.appendChild(canvas.html()); const views = canvas.html();
htmlContainer.appendChild(views.perspective);
htmlContainer.appendChild(views.top);
htmlContainer.appendChild(views.side);
htmlContainer.appendChild(views.front);
``` ```

@ -5,10 +5,17 @@
import pjson from '../../package.json'; import pjson from '../../package.json';
import { Canvas3dController, Canvas3dControllerImpl } from './canvas3dController'; import { Canvas3dController, Canvas3dControllerImpl } from './canvas3dController';
import { import {
Canvas3dModel, Canvas3dModelImpl, Mode, DrawData, ViewType, MouseInteraction, Canvas3dModel,
Canvas3dModelImpl,
Mode,
DrawData,
ViewType,
MouseInteraction,
ShapeProperties,
GroupData,
} from './canvas3dModel'; } from './canvas3dModel';
import { import {
Canvas3dView, Canvas3dViewImpl, ViewsDOM, CAMERA_ACTION, Canvas3dView, Canvas3dViewImpl, ViewsDOM, CameraAction,
} from './canvas3dView'; } from './canvas3dView';
import { Master } from './master'; import { Master } from './master';
@ -16,19 +23,24 @@ const Canvas3dVersion = pjson.version;
interface Canvas3d { interface Canvas3d {
html(): ViewsDOM; html(): ViewsDOM;
setup(frameData: any): void; setup(frameData: any, objectStates: any[]): void;
isAbleToChangeFrame(): boolean; isAbleToChangeFrame(): boolean;
mode(): Mode; mode(): Mode;
render(): void; render(): void;
keyControls(keys: KeyboardEvent): void; keyControls(keys: KeyboardEvent): void;
mouseControls(type: string, event: MouseEvent): void;
draw(drawData: DrawData): void; draw(drawData: DrawData): void;
cancel(): void; cancel(): void;
dragCanvas(enable: boolean): void;
activate(clientID: number | null, attributeID?: number): void;
configureShapes(shapeProperties: ShapeProperties): void;
fitCanvas(): void;
fit(): void;
group(groupData: GroupData): void;
} }
class Canvas3dImpl implements Canvas3d { class Canvas3dImpl implements Canvas3d {
private model: Canvas3dModel & Master; private readonly model: Canvas3dModel & Master;
private controller: Canvas3dController; private readonly controller: Canvas3dController;
private view: Canvas3dView; private view: Canvas3dView;
public constructor() { public constructor() {
@ -45,10 +57,6 @@ class Canvas3dImpl implements Canvas3d {
this.view.keyControls(keys); this.view.keyControls(keys);
} }
public mouseControls(type: MouseInteraction, event: MouseEvent): void {
this.view.mouseControls(type, event);
}
public render(): void { public render(): void {
this.view.render(); this.view.render();
} }
@ -57,14 +65,18 @@ class Canvas3dImpl implements Canvas3d {
this.model.draw(drawData); this.model.draw(drawData);
} }
public setup(frameData: any): void { public setup(frameData: any, objectStates: any[]): void {
this.model.setup(frameData); this.model.setup(frameData, objectStates);
} }
public mode(): Mode { public mode(): Mode {
return this.model.mode; return this.model.mode;
} }
public group(groupData: GroupData): void {
this.model.group(groupData);
}
public isAbleToChangeFrame(): boolean { public isAbleToChangeFrame(): boolean {
return this.model.isAbleToChangeFrame(); return this.model.isAbleToChangeFrame();
} }
@ -72,8 +84,28 @@ class Canvas3dImpl implements Canvas3d {
public cancel(): void { public cancel(): void {
this.model.cancel(); this.model.cancel();
} }
public dragCanvas(enable: boolean): void {
this.model.dragCanvas(enable);
}
public configureShapes(shapeProperties: ShapeProperties): void {
this.model.configureShapes(shapeProperties);
}
public activate(clientID: number | null, attributeID: number | null = null): void {
this.model.activate(String(clientID), attributeID);
}
public fit(): void {
this.model.fit();
}
public fitCanvas(): void {
this.model.fit();
}
} }
export { export {
Canvas3dImpl as Canvas3d, Canvas3dVersion, ViewType, MouseInteraction, CAMERA_ACTION, Canvas3dImpl as Canvas3d, Canvas3dVersion, ViewType, MouseInteraction, CameraAction, ViewsDOM,
}; };

@ -2,11 +2,18 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { Canvas3dModel, Mode, DrawData } from './canvas3dModel'; import {
Canvas3dModel, Mode, DrawData, ActiveElement, FocusData, GroupData,
} from './canvas3dModel';
export interface Canvas3dController { export interface Canvas3dController {
readonly drawData: DrawData; readonly drawData: DrawData;
readonly activeElement: ActiveElement;
readonly selected: any;
readonly focused: FocusData;
readonly groupData: GroupData;
mode: Mode; mode: Mode;
group(groupData: GroupData): void;
} }
export class Canvas3dControllerImpl implements Canvas3dController { export class Canvas3dControllerImpl implements Canvas3dController {
@ -27,4 +34,24 @@ export class Canvas3dControllerImpl implements Canvas3dController {
public get drawData(): DrawData { public get drawData(): DrawData {
return this.model.data.drawData; return this.model.data.drawData;
} }
public get activeElement(): ActiveElement {
return this.model.data.activeElement;
}
public get selected(): any {
return this.model.data.selected;
}
public get focused(): any {
return this.model.data.focusData;
}
public get groupData(): GroupData {
return this.model.groupData;
}
public group(groupData: GroupData): void {
this.model.group(groupData);
}
} }

@ -9,6 +9,16 @@ export interface Size {
height: number; height: number;
} }
export interface ActiveElement {
clientID: string | null;
attributeID: number | null;
}
export interface GroupData {
enabled: boolean;
grouped?: [];
}
export interface Image { export interface Image {
renderWidth: number; renderWidth: number;
renderHeight: number; renderHeight: number;
@ -19,6 +29,7 @@ export interface DrawData {
enabled: boolean; enabled: boolean;
initialState?: any; initialState?: any;
redraw?: number; redraw?: number;
shapeType?: string;
} }
export enum FrameZoom { export enum FrameZoom {
@ -26,6 +37,13 @@ export enum FrameZoom {
MAX = 10, MAX = 10,
} }
export enum Planes {
TOP = 'topPlane',
SIDE = 'sidePlane',
FRONT = 'frontPlane',
PERSPECTIVE = 'perspectivePlane',
}
export enum ViewType { export enum ViewType {
PERSPECTIVE = 'perspective', PERSPECTIVE = 'perspective',
TOP = 'top', TOP = 'top',
@ -39,14 +57,29 @@ export enum MouseInteraction {
HOVER = 'hover', HOVER = 'hover',
} }
export interface FocusData {
clientID: string | null;
}
export interface ShapeProperties {
opacity: number;
outlined: boolean;
outlineColor: string;
selectedOpacity: number;
colorBy: string;
}
export enum UpdateReasons { export enum UpdateReasons {
IMAGE_CHANGED = 'image_changed', IMAGE_CHANGED = 'image_changed',
OBJECTS_UPDATED = 'objects_updated', OBJECTS_UPDATED = 'objects_updated',
FITTED_CANVAS = 'fitted_canvas',
DRAW = 'draw', DRAW = 'draw',
SELECT = 'select', SELECT = 'select',
CANCEL = 'cancel', CANCEL = 'cancel',
DATA_FAILED = 'data_failed', DATA_FAILED = 'data_failed',
DRAG_CANVAS = 'drag_canvas',
SHAPE_ACTIVATED = 'shape_activated',
GROUP = 'group',
FITTED_CANVAS = 'fitted_canvas',
} }
export enum Mode { export enum Mode {
@ -56,9 +89,13 @@ export enum Mode {
DRAW = 'draw', DRAW = 'draw',
EDIT = 'edit', EDIT = 'edit',
INTERACT = 'interact', INTERACT = 'interact',
DRAG_CANVAS = 'drag_canvas',
GROUP = 'group',
BUSY = 'busy',
} }
export interface Canvas3dDataModel { export interface Canvas3dDataModel {
activeElement: ActiveElement;
canvasSize: Size; canvasSize: Size;
image: Image | null; image: Image | null;
imageID: number | null; imageID: number | null;
@ -66,16 +103,29 @@ export interface Canvas3dDataModel {
imageSize: Size; imageSize: Size;
drawData: DrawData; drawData: DrawData;
mode: Mode; mode: Mode;
objectUpdating: boolean;
exception: Error | null; exception: Error | null;
objects: any[];
groupedObjects: any[];
focusData: FocusData;
selected: any;
shapeProperties: ShapeProperties;
groupData: GroupData;
} }
export interface Canvas3dModel { export interface Canvas3dModel {
mode: Mode; mode: Mode;
data: Canvas3dDataModel; data: Canvas3dDataModel;
setup(frameData: any): void; readonly groupData: GroupData;
setup(frameData: any, objectStates: any[]): void;
isAbleToChangeFrame(): boolean; isAbleToChangeFrame(): boolean;
draw(drawData: DrawData): void; draw(drawData: DrawData): void;
cancel(): void; cancel(): void;
dragCanvas(enable: boolean): void;
activate(clientID: string | null, attributeID: number | null): void;
configureShapes(shapeProperties: any): void;
fit(): void;
group(groupData: GroupData): void;
} }
export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel { export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
@ -84,10 +134,17 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
public constructor() { public constructor() {
super(); super();
this.data = { this.data = {
activeElement: {
clientID: null,
attributeID: null,
},
canvasSize: { canvasSize: {
height: 0, height: 0,
width: 0, width: 0,
}, },
objectUpdating: false,
objects: [],
groupedObjects: [],
image: null, image: null,
imageID: null, imageID: null,
imageOffset: 0, imageOffset: 0,
@ -101,37 +158,71 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
}, },
mode: Mode.IDLE, mode: Mode.IDLE,
exception: null, exception: null,
focusData: {
clientID: null,
},
groupData: {
enabled: false,
grouped: [],
},
selected: null,
shapeProperties: {
opacity: 40,
outlined: false,
outlineColor: '#000000',
selectedOpacity: 60,
colorBy: 'Label',
},
}; };
} }
public setup(frameData: any): void { public setup(frameData: any, objectStates: any[]): void {
if (this.data.imageID !== frameData.number) { if (this.data.imageID !== frameData.number) {
this.data.imageID = frameData.number; if ([Mode.EDIT, Mode.DRAG, Mode.RESIZE].includes(this.data.mode)) {
frameData throw Error(`Canvas is busy. Action: ${this.data.mode}`);
.data((): void => { }
this.data.image = null; }
this.notify(UpdateReasons.IMAGE_CHANGED); if ([Mode.EDIT, Mode.BUSY].includes(this.data.mode)) {
}) return;
.then((data: Image): void => { }
if (frameData.number !== this.data.imageID) {
// already another image if (frameData.number === this.data.imageID) {
return; if (this.data.objectUpdating) {
} return;
}
this.data.imageSize = { this.data.objects = objectStates;
height: frameData.height as number, this.data.objectUpdating = true;
width: frameData.width as number, this.notify(UpdateReasons.OBJECTS_UPDATED);
}; return;
this.data.image = data;
this.notify(UpdateReasons.IMAGE_CHANGED);
})
.catch((exception: any): void => {
this.data.exception = exception;
this.notify(UpdateReasons.DATA_FAILED);
throw exception;
});
} }
this.data.imageID = frameData.number;
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;
}
this.data.imageSize = {
height: frameData.height as number,
width: frameData.width as number,
};
this.data.image = data;
this.notify(UpdateReasons.IMAGE_CHANGED);
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 set mode(value: Mode) { public set mode(value: Mode) {
@ -143,23 +234,110 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
} }
public isAbleToChangeFrame(): boolean { public isAbleToChangeFrame(): boolean {
const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE, Mode.INTERACT].includes(this.data.mode) const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE, Mode.INTERACT, Mode.BUSY].includes(this.data.mode)
|| (this.data.mode === Mode.DRAW && typeof this.data.drawData.redraw === 'number'); || (this.data.mode === Mode.DRAW && typeof this.data.drawData.redraw === 'number');
return !isUnable; return !isUnable;
} }
public draw(drawData: DrawData): void { public draw(drawData: DrawData): void {
if (drawData.enabled && this.data.drawData.enabled) { if (drawData.enabled && this.data.drawData.enabled && !drawData.initialState) {
throw new Error('Drawing has been already started'); throw new Error('Drawing has been already started');
} }
if ([Mode.DRAW, Mode.EDIT].includes(this.data.mode) && !drawData.initialState) {
return;
}
this.data.drawData.enabled = drawData.enabled; this.data.drawData.enabled = drawData.enabled;
this.data.mode = Mode.DRAW; this.data.mode = Mode.DRAW;
if (typeof drawData.redraw === 'number') {
const clientID = drawData.redraw;
const [state] = this.data.objects.filter((_state: any): boolean => _state.clientID === clientID);
if (state) {
this.data.drawData = { ...drawData };
this.data.drawData.initialState = { ...this.data.drawData.initialState, label: state.label };
this.data.drawData.shapeType = state.shapeType;
} else {
return;
}
} else {
this.data.drawData = { ...drawData };
if (this.data.drawData.initialState) {
this.data.drawData.shapeType = this.data.drawData.initialState.shapeType;
}
}
this.notify(UpdateReasons.DRAW); this.notify(UpdateReasons.DRAW);
} }
public cancel(): void { public cancel(): void {
this.notify(UpdateReasons.CANCEL); this.notify(UpdateReasons.CANCEL);
} }
public dragCanvas(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.DRAG_CANVAS) {
throw Error(`Canvas is not in the drag mode. Action: ${this.data.mode}`);
}
this.data.mode = enable ? Mode.DRAG_CANVAS : Mode.IDLE;
this.notify(UpdateReasons.DRAG_CANVAS);
}
public activate(clientID: string, attributeID: number | null): void {
if (this.data.activeElement.clientID === clientID && this.data.activeElement.attributeID === attributeID) {
return;
}
if (this.data.mode !== Mode.IDLE) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (typeof clientID === 'number') {
const [state] = this.data.objects.filter((_state: any): boolean => _state.clientID === clientID);
if (!state || state.objectType === 'tag') {
return;
}
}
this.data.activeElement = {
clientID,
attributeID,
};
this.notify(UpdateReasons.SHAPE_ACTIVATED);
}
public group(groupData: GroupData): void {
if (![Mode.IDLE, Mode.GROUP].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (this.data.groupData.enabled && groupData.enabled) {
return;
}
if (!this.data.groupData.enabled && !groupData.enabled) {
return;
}
this.data.mode = groupData.enabled ? Mode.GROUP : Mode.IDLE;
this.data.groupData = { ...this.data.groupData, ...groupData };
this.notify(UpdateReasons.GROUP);
}
public configureShapes(shapeProperties: ShapeProperties): void {
this.data.drawData.enabled = false;
this.data.mode = Mode.IDLE;
this.cancel();
this.data.shapeProperties = {
...shapeProperties,
};
this.notify(UpdateReasons.OBJECTS_UPDATED);
}
public fit(): void {
this.notify(UpdateReasons.FITTED_CANVAS);
}
public get groupData(): GroupData {
return { ...this.data.groupData };
}
} }

File diff suppressed because it is too large Load Diff

@ -6,8 +6,17 @@ const BASE_GRID_WIDTH = 2;
const MOVEMENT_FACTOR = 200; const MOVEMENT_FACTOR = 200;
const DOLLY_FACTOR = 5; const DOLLY_FACTOR = 5;
const MAX_DISTANCE = 100; const MAX_DISTANCE = 100;
const MIN_DISTANCE = 0; const MIN_DISTANCE = 0.3;
const ZOOM_FACTOR = 7; const ZOOM_FACTOR = 7;
const ROTATION_HELPER_OFFSET = 0.1;
const CAMERA_REFERENCE = 'camRef';
const CUBOID_EDGE_NAME = 'edges';
const ROTATION_HELPER = 'rotationHelper';
const ROTATION_SPEED = 80;
const FOV_DEFAULT = 1;
const FOV_MAX = 2;
const FOV_MIN = 0;
const FOV_INC = 0.08;
export default { export default {
BASE_GRID_WIDTH, BASE_GRID_WIDTH,
@ -16,4 +25,13 @@ export default {
MAX_DISTANCE, MAX_DISTANCE,
MIN_DISTANCE, MIN_DISTANCE,
ZOOM_FACTOR, ZOOM_FACTOR,
ROTATION_HELPER_OFFSET,
CAMERA_REFERENCE,
CUBOID_EDGE_NAME,
ROTATION_HELPER,
ROTATION_SPEED,
FOV_DEFAULT,
FOV_MAX,
FOV_MIN,
FOV_INC,
}; };

@ -2,6 +2,13 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import * as THREE from 'three'; import * as THREE from 'three';
import { BufferGeometryUtils } from 'three/examples/jsm/utils/BufferGeometryUtils';
import { ViewType } from './canvas3dModel';
import constants from './consts';
export interface Indexable {
[key: string]: any;
}
export class CuboidModel { export class CuboidModel {
public perspective: THREE.Mesh; public perspective: THREE.Mesh;
@ -9,12 +16,174 @@ export class CuboidModel {
public side: THREE.Mesh; public side: THREE.Mesh;
public front: THREE.Mesh; public front: THREE.Mesh;
public constructor() { public constructor(outline: string, outlineColor: string) {
const geometry = new THREE.BoxGeometry(1, 1, 1); const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true }); const material = new THREE.MeshBasicMaterial({
color: 0x00ff00,
wireframe: false,
transparent: true,
opacity: 0.4,
});
this.perspective = new THREE.Mesh(geometry, material); this.perspective = new THREE.Mesh(geometry, material);
const geo = new THREE.EdgesGeometry(this.perspective.geometry);
const wireframe = new THREE.LineSegments(
geo,
outline === 'line'
? new THREE.LineBasicMaterial({ color: outlineColor, linewidth: 4 })
: new THREE.LineDashedMaterial({
color: outlineColor,
dashSize: 0.05,
gapSize: 0.05,
}),
);
wireframe.computeLineDistances();
wireframe.renderOrder = 1;
this.perspective.add(wireframe);
this.top = new THREE.Mesh(geometry, material); this.top = new THREE.Mesh(geometry, material);
this.side = new THREE.Mesh(geometry, material); this.side = new THREE.Mesh(geometry, material);
this.front = new THREE.Mesh(geometry, material); this.front = new THREE.Mesh(geometry, material);
const camRotateHelper = new THREE.Object3D();
camRotateHelper.translateX(-2);
camRotateHelper.name = 'camRefRot';
camRotateHelper.up = new THREE.Vector3(0, 0, 1);
camRotateHelper.lookAt(new THREE.Vector3(0, 0, 0));
this.front.add(camRotateHelper.clone());
}
public setPosition(x: number, y: number, z: number): void {
[ViewType.PERSPECTIVE, ViewType.TOP, ViewType.SIDE, ViewType.FRONT].forEach((view): void => {
(this as Indexable)[view].position.set(x, y, z);
});
}
public setScale(x: number, y: number, z: number): void {
[ViewType.PERSPECTIVE, ViewType.TOP, ViewType.SIDE, ViewType.FRONT].forEach((view): void => {
(this as Indexable)[view].scale.set(x, y, z);
});
}
public setRotation(x: number, y: number, z: number): void {
[ViewType.PERSPECTIVE, ViewType.TOP, ViewType.SIDE, ViewType.FRONT].forEach((view): void => {
(this as Indexable)[view].rotation.set(x, y, z);
});
}
public attachCameraReference(): void {
// Attach Cam Reference
const topCameraReference = new THREE.Object3D();
topCameraReference.translateZ(2);
topCameraReference.name = constants.CAMERA_REFERENCE;
this.top.add(topCameraReference);
this.top.userData = { ...this.top.userData, camReference: topCameraReference };
const sideCameraReference = new THREE.Object3D();
sideCameraReference.translateY(2);
sideCameraReference.name = constants.CAMERA_REFERENCE;
this.side.add(sideCameraReference);
this.side.userData = { ...this.side.userData, camReference: sideCameraReference };
const frontCameraReference = new THREE.Object3D();
frontCameraReference.translateX(2);
frontCameraReference.name = constants.CAMERA_REFERENCE;
this.front.add(frontCameraReference);
this.front.userData = { ...this.front.userData, camReference: frontCameraReference };
}
public getReferenceCoordinates(viewType: string): THREE.Vector3 {
const { elements } = (this as Indexable)[viewType].getObjectByName(constants.CAMERA_REFERENCE).matrixWorld;
return new THREE.Vector3(elements[12], elements[13], elements[14]);
}
public setName(clientId: any): void {
[ViewType.PERSPECTIVE, ViewType.TOP, ViewType.SIDE, ViewType.FRONT].forEach((view): void => {
(this as Indexable)[view].name = clientId;
});
}
public setOriginalColor(color: string): void {
[ViewType.PERSPECTIVE, ViewType.TOP, ViewType.SIDE, ViewType.FRONT].forEach((view): void => {
((this as Indexable)[view] as any).originalColor = color;
});
}
public setColor(color: string): void {
[ViewType.PERSPECTIVE, ViewType.TOP, ViewType.SIDE, ViewType.FRONT].forEach((view): void => {
((this as Indexable)[view].material as THREE.MeshBasicMaterial).color.set(color);
});
}
public setOpacity(opacity: number): void {
[ViewType.PERSPECTIVE, ViewType.TOP, ViewType.SIDE, ViewType.FRONT].forEach((view): void => {
((this as Indexable)[view].material as THREE.MeshBasicMaterial).opacity = opacity / 100;
});
}
}
export function setEdges(instance: THREE.Mesh): THREE.LineSegments {
const edges = new THREE.EdgesGeometry(instance.geometry);
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: '#ffffff', linewidth: 3 }));
line.name = constants.CUBOID_EDGE_NAME;
instance.add(line);
return line;
}
export function setTranslationHelper(instance: THREE.Mesh): void {
const sphereGeometry = new THREE.SphereGeometry(0.1);
const sphereMaterial = new THREE.MeshBasicMaterial({ color: '#ffffff', opacity: 1 });
instance.geometry.deleteAttribute('normal');
instance.geometry.deleteAttribute('uv');
// eslint-disable-next-line no-param-reassign
instance.geometry = BufferGeometryUtils.mergeVertices(instance.geometry);
const vertices = [];
const positionAttribute = instance.geometry.getAttribute('position');
for (let i = 0; i < positionAttribute.count; i++) {
const vertex = new THREE.Vector3();
vertex.fromBufferAttribute(positionAttribute, i);
vertices.push(vertex);
}
const helpers = [];
for (let i = 0; i < vertices.length; i++) {
helpers[i] = new THREE.Mesh(sphereGeometry.clone(), sphereMaterial.clone());
helpers[i].position.set(vertices[i].x, vertices[i].y, vertices[i].z);
helpers[i].up.set(0, 0, 1);
helpers[i].name = 'resizeHelper';
instance.add(helpers[i]);
helpers[i].scale.set(1 / instance.scale.x, 1 / instance.scale.y, 1 / instance.scale.z);
}
// eslint-disable-next-line no-param-reassign
instance.userData = { ...instance.userData, resizeHelpers: helpers };
}
export function createRotationHelper(instance: THREE.Mesh, viewType: ViewType): void {
const sphereGeometry = new THREE.SphereGeometry(0.1);
const sphereMaterial = new THREE.MeshBasicMaterial({ color: '#ffffff', opacity: 1 });
const rotationHelper = new THREE.Mesh(sphereGeometry, sphereMaterial);
rotationHelper.name = constants.ROTATION_HELPER;
switch (viewType) {
case ViewType.TOP:
rotationHelper.position.set(
(instance.geometry as THREE.BoxGeometry).parameters.height / 2 + constants.ROTATION_HELPER_OFFSET,
instance.position.y,
instance.position.z,
);
instance.add(rotationHelper.clone());
// eslint-disable-next-line no-param-reassign
instance.userData = { ...instance.userData, rotationHelpers: rotationHelper.clone() };
break;
case ViewType.SIDE:
case ViewType.FRONT:
rotationHelper.position.set(
instance.position.x,
instance.position.y,
(instance.geometry as THREE.BoxGeometry).parameters.depth / 2 + constants.ROTATION_HELPER_OFFSET,
);
instance.add(rotationHelper.clone());
// eslint-disable-next-line no-param-reassign
instance.userData = { ...instance.userData, rotationHelpers: rotationHelper.clone() };
break;
default:
break;
} }
} }

@ -2,7 +2,7 @@
## Description ## Description
This CVAT module is a client-side JavaScipt library to management of objects, frames, logs, etc. This CVAT module is a client-side JavaScript library for management of objects, frames, logs, etc.
It contains the core logic of the Computer Vision Annotation Tool. It contains the core logic of the Computer Vision Annotation Tool.
## Versioning ## Versioning

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "3.12.1", "version": "3.13.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "3.12.1", "version": "3.13.3",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration", "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js", "main": "babel.config.js",
"scripts": { "scripts": {

@ -788,6 +788,7 @@
() => { () => {
importedArray.forEach((object) => { importedArray.forEach((object) => {
object.removed = false; object.removed = false;
object.serverID = undefined;
}); });
}, },
importedArray.map((object) => object.clientID), importedArray.map((object) => object.clientID),
@ -868,7 +869,7 @@
const deepSearch = (deepSearchFrom, deepSearchTo) => { const deepSearch = (deepSearchFrom, deepSearchTo) => {
// deepSearchFrom is expected to be a frame that doesn't satisfy a filter // deepSearchFrom is expected to be a frame that doesn't satisfy a filter
// deepSearchTo is expected to be a frame that satifies a filter // deepSearchTo is expected to be a frame that satisfies a filter
let [prev, next] = [deepSearchFrom, deepSearchTo]; let [prev, next] = [deepSearchFrom, deepSearchTo];
// half division method instead of linear search // half division method instead of linear search

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation // Copyright (C) 2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -50,7 +50,7 @@
throw new DataError(`Points must have exact 8 points, but got ${points.length / 2}`); throw new DataError(`Points must have exact 8 points, but got ${points.length / 2}`);
} }
} else { } else {
throw new ArgumentError(`Unknown value of shapeType has been recieved ${shapeType}`); throw new ArgumentError(`Unknown value of shapeType has been received ${shapeType}`);
} }
} }
@ -248,6 +248,18 @@
this.label = label; this.label = label;
this.attributes = {}; this.attributes = {};
this.appendDefaultAttributes(label); this.appendDefaultAttributes(label);
// Try to keep old attributes if name matches and old value is still valid
for (const attribute of redoLabel.attributes) {
for (const oldAttribute of undoLabel.attributes) {
if (
attribute.name === oldAttribute.name
&& validateAttributeValue(undoAttributes[oldAttribute.id], attribute)
) {
this.attributes[attribute.id] = undoAttributes[oldAttribute.id];
}
}
}
const redoAttributes = { ...this.attributes }; const redoAttributes = { ...this.attributes };
this.history.do( this.history.do(
@ -324,11 +336,16 @@
checkObjectType('points', data.points, null, Array); checkObjectType('points', data.points, null, Array);
checkNumberOfPoints(this.shapeType, data.points); checkNumberOfPoints(this.shapeType, data.points);
// cut points // cut points
const { width, height } = this.frameMeta[frame]; const { width, height, filename } = this.frameMeta[frame];
fittedPoints = fitPoints(this.shapeType, data.points, width, height); fittedPoints = fitPoints(this.shapeType, data.points, width, height);
let check = true;
if (!checkShapeArea(this.shapeType, fittedPoints) || checkOutside(fittedPoints, width, height)) { if (filename && filename.slice(filename.length - 3) === 'pcd') {
fittedPoints = []; check = false;
}
if (check) {
if (!checkShapeArea(this.shapeType, fittedPoints) || checkOutside(fittedPoints, width, height)) {
fittedPoints = [];
}
} }
} }
@ -1447,7 +1464,7 @@
/ Math.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2)), / Math.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2)),
); );
} else { } else {
// The link below works for lines (which have infinit length) // The link below works for lines (which have infinite length)
// There is a case when perpendicular doesn't cross the edge // There is a case when perpendicular doesn't cross the edge
// In this case we don't use the computed distance // In this case we don't use the computed distance
// Instead we use just distance to the nearest point // Instead we use just distance to the nearest point

@ -1,11 +1,11 @@
// Copyright (C) 2019-2020 Intel Corporation // Copyright (C) 2019-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
(() => { (() => {
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
const { Task } = require('./session'); const { Task } = require('./session');
const { ScriptingError } = './exceptions'; const { ScriptingError } = require('./exceptions');
class AnnotationsSaver { class AnnotationsSaver {
constructor(version, collection, session) { constructor(version, collection, session) {

@ -115,6 +115,7 @@
cvat.users.get.implementation = async (filter) => { cvat.users.get.implementation = async (filter) => {
checkFilter(filter, { checkFilter(filter, {
id: isInteger, id: isInteger,
is_active: isBoolean,
self: isBoolean, self: isBoolean,
search: isString, search: isString,
limit: isInteger, limit: isInteger,
@ -227,6 +228,14 @@
checkExclusiveFields(filter, ['id', 'search'], ['page', 'withoutTasks']); checkExclusiveFields(filter, ['id', 'search'], ['page', 'withoutTasks']);
if (typeof filter.withoutTasks === 'undefined') {
if (typeof filter.id === 'undefined') {
filter.withoutTasks = true;
} else {
filter.withoutTasks = false;
}
}
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page', 'withoutTasks']) { for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page', 'withoutTasks']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) { if (Object.prototype.hasOwnProperty.call(filter, field)) {
@ -238,7 +247,10 @@
// prettier-ignore // prettier-ignore
const projects = projectsData.map((project) => { const projects = projectsData.map((project) => {
if (filter.withoutTasks) { if (filter.withoutTasks) {
project.task_ids = project.tasks;
project.tasks = []; project.tasks = [];
} else {
project.task_ids = project.tasks.map((task) => task.id);
} }
return project; return project;
}).map((project) => new Project(project)); }).map((project) => new Project(project));

@ -20,6 +20,7 @@ function build() {
const { Project } = require('./project'); const { Project } = require('./project');
const { Attribute, Label } = require('./labels'); const { Attribute, Label } = require('./labels');
const MLModel = require('./ml-model'); const MLModel = require('./ml-model');
const { FrameData } = require('./frames');
const enums = require('./enums'); const enums = require('./enums');
@ -765,6 +766,7 @@ function build() {
Comment, Comment,
Issue, Issue,
Review, Review,
FrameData,
}, },
}; };

@ -34,7 +34,7 @@
for (const prop in filter) { for (const prop in filter) {
if (Object.prototype.hasOwnProperty.call(filter, prop)) { if (Object.prototype.hasOwnProperty.call(filter, prop)) {
if (!(prop in fields)) { if (!(prop in fields)) {
throw new ArgumentError(`Unsupported filter property has been recieved: "${prop}"`); throw new ArgumentError(`Unsupported filter property has been received: "${prop}"`);
} else if (!fields[prop](filter[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`);
} }
@ -104,6 +104,37 @@
} }
negativeIDGenerator.start = -1; negativeIDGenerator.start = -1;
class FieldUpdateTrigger {
constructor(initialFields) {
const data = { ...initialFields };
Object.defineProperties(
this,
Object.freeze({
...Object.assign(
{},
...Array.from(Object.keys(data), (key) => ({
[key]: {
get: () => data[key],
set: (value) => {
data[key] = value;
},
enumerable: true,
},
})),
),
reset: {
value: () => {
Object.keys(data).forEach((key) => {
data[key] = false;
});
},
},
}),
);
}
}
module.exports = { module.exports = {
isBoolean, isBoolean,
isInteger, isInteger,
@ -114,5 +145,6 @@
negativeIDGenerator, negativeIDGenerator,
checkExclusiveFields, checkExclusiveFields,
camelToSnake, camelToSnake,
FieldUpdateTrigger,
}; };
})(); })();

@ -1,8 +1,8 @@
// Copyright (C) 2019-2020 Intel Corporation // Copyright (C) 2019-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
module.exports = { module.exports = {
backendAPI: 'http://localhost:7000/api/v1', backendAPI: '/api/v1',
proxy: false, proxy: false,
}; };

@ -19,7 +19,15 @@
*/ */
class FrameData { class FrameData {
constructor({ constructor({
width, height, name, taskID, frameNumber, startFrame, stopFrame, decodeForward, width,
height,
name,
taskID,
frameNumber,
startFrame,
stopFrame,
decodeForward,
has_related_context: hasRelatedContext,
}) { }) {
Object.defineProperties( Object.defineProperties(
this, this,
@ -72,6 +80,18 @@
value: frameNumber, value: frameNumber,
writable: false, writable: false,
}, },
/**
* True if some context images are associated with this frame
* @name hasRelatedContext
* @type {boolean}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
hasRelatedContext: {
value: hasRelatedContext,
writable: false,
},
startFrame: { startFrame: {
value: startFrame, value: startFrame,
writable: false, writable: false,
@ -104,6 +124,14 @@
const result = await PluginRegistry.apiWrapper.call(this, FrameData.prototype.data, onServerRequest); const result = await PluginRegistry.apiWrapper.call(this, FrameData.prototype.data, onServerRequest);
return result; return result;
} }
get imageData() {
return this._data.imageData;
}
set imageData(imageData) {
this._data.imageData = imageData;
}
} }
FrameData.prototype.data.implementation = async function (onServerRequest) { FrameData.prototype.data.implementation = async function (onServerRequest) {

@ -17,7 +17,8 @@ class MLModel {
this._params = { this._params = {
canvas: { canvas: {
minPosVertices: data.min_pos_points, minPosVertices: data.min_pos_points,
enableNegVertices: true, minNegVertices: data.min_neg_points,
startWithBox: data.startswith_box,
}, },
}; };
} }

@ -8,6 +8,7 @@
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { Task } = require('./session'); const { Task } = require('./session');
const { Label } = require('./labels'); const { Label } = require('./labels');
const { getPreview } = require('./frames');
const User = require('./user'); const User = require('./user');
/** /**
@ -17,7 +18,7 @@
class Project { class Project {
/** /**
* In a fact you need use the constructor only if you want to create a 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 * @param {object} initialData - Object which is used for initialization
* <br> It can contain keys: * <br> It can contain keys:
* <br> <li style="margin-left: 10px;"> name * <br> <li style="margin-left: 10px;"> name
* <br> <li style="margin-left: 10px;"> labels * <br> <li style="margin-left: 10px;"> labels
@ -34,6 +35,7 @@
updated_date: undefined, updated_date: undefined,
task_subsets: undefined, task_subsets: undefined,
training_project: undefined, training_project: undefined,
task_ids: undefined,
}; };
for (const property in data) { for (const property in data) {
@ -58,9 +60,9 @@
data.tasks.push(taskInstance); data.tasks.push(taskInstance);
} }
} }
if (!data.task_subsets && data.tasks.length) { if (!data.task_subsets) {
const subsetsSet = new Set(); const subsetsSet = new Set();
for (const task in data.tasks) { for (const task of data.tasks) {
if (task.subset) subsetsSet.add(task.subset); if (task.subset) subsetsSet.add(task.subset);
} }
data.task_subsets = Array.from(subsetsSet); data.task_subsets = Array.from(subsetsSet);
@ -254,6 +256,22 @@
); );
} }
/**
* Get the first frame of the first task of a project for preview
* @method preview
* @memberof Project
* @returns {string} - jpeg encoded image
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async preview() {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.preview);
return result;
}
/** /**
* Method updates data of a created project or creates new project from scratch * Method updates data of a created project or creates new project from scratch
* @method save * @method save
@ -331,4 +349,12 @@
const result = await serverProxy.projects.delete(this.id); const result = await serverProxy.projects.delete(this.id);
return result; return result;
}; };
Project.prototype.preview.implementation = async function () {
if (!this._internalData.task_ids.length) {
return '';
}
const frameData = await getPreview(this._internalData.task_ids[0]);
return frameData;
};
})(); })();

@ -490,6 +490,59 @@
}); });
} }
async function exportTask(id) {
const { backendAPI } = config;
const url = `${backendAPI}/tasks/${id}`;
return new Promise((resolve, reject) => {
async function request() {
try {
const response = await Axios.get(`${url}?action=export`, {
proxy: config.proxy,
});
if (response.status === 202) {
setTimeout(request, 3000);
} else {
resolve(`${url}?action=download`);
}
} catch (errorData) {
reject(generateError(errorData));
}
}
setTimeout(request);
});
}
async function importTask(file) {
const { backendAPI } = config;
let taskData = new FormData();
taskData.append('task_file', file);
return new Promise((resolve, reject) => {
async function request() {
try {
const response = await Axios.post(`${backendAPI}/tasks?action=import`, taskData, {
proxy: config.proxy,
});
if (response.status === 202) {
taskData = new FormData();
taskData.append('rq_id', response.data.rq_id);
setTimeout(request, 3000);
} else {
const importedTask = await getTasks(`?id=${response.data.id}`);
resolve(importedTask[0]);
}
} catch (errorData) {
reject(generateError(errorData));
}
}
setTimeout(request);
});
}
async function createTask(taskSpec, taskDataSpec, onUpdate) { async function createTask(taskSpec, taskDataSpec, onUpdate) {
const { backendAPI } = config; const { backendAPI } = config;
@ -756,11 +809,7 @@
}, },
); );
} catch (errorData) { } catch (errorData) {
const code = errorData.response ? errorData.response.status : errorData.code; throw generateError(errorData);
throw new ServerError(
`Could not get Image Context of the frame for the task ${tid} from the server`,
code,
);
} }
return response.data; return response.data;
@ -1161,6 +1210,8 @@
createTask, createTask,
deleteTask, deleteTask,
exportDataset, exportDataset,
exportTask,
importTask,
}), }),
writable: false, writable: false,
}, },

@ -16,8 +16,9 @@
const User = require('./user'); const User = require('./user');
const Issue = require('./issue'); const Issue = require('./issue');
const Review = require('./review'); const Review = require('./review');
const { FieldUpdateTrigger } = require('./common');
function buildDublicatedAPI(prototype) { function buildDuplicatedAPI(prototype) {
Object.defineProperties(prototype, { Object.defineProperties(prototype, {
annotations: Object.freeze({ annotations: Object.freeze({
value: { value: {
@ -575,7 +576,7 @@
* Create a log and add it to a log collection <br> * Create a log and add it to a log collection <br>
* Durable logs will be added after "close" method is called for them <br> * Durable logs will be added after "close" method is called for them <br>
* The fields "task_id" and "job_id" automatically added when add logs * The fields "task_id" and "job_id" automatically added when add logs
* throught a task or a job <br> * through a task or a job <br>
* Ignore rules exist for some logs (e.g. zoomImage, changeAttribute) <br> * Ignore rules exist for some logs (e.g. zoomImage, changeAttribute) <br>
* Payload of ignored logs are shallowly combined to previous logs of the same type * Payload of ignored logs are shallowly combined to previous logs of the same type
* @method log * @method log
@ -734,11 +735,11 @@
task: undefined, task: undefined,
}; };
let updatedFields = { const updatedFields = new FieldUpdateTrigger({
assignee: false, assignee: false,
reviewer: false, reviewer: false,
status: false, status: false,
}; });
for (const property in data) { for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property)) { if (Object.prototype.hasOwnProperty.call(data, property)) {
@ -865,9 +866,6 @@
}, },
__updatedFields: { __updatedFields: {
get: () => updatedFields, get: () => updatedFields,
set: (fields) => {
updatedFields = fields;
},
}, },
}), }),
); );
@ -1001,7 +999,7 @@
class Task extends Session { class Task extends Session {
/** /**
* In a fact you need use the constructor only if you want to create a task * In a fact you need use the constructor only if you want to create a task
* @param {object} initialData - Object which is used for initalization * @param {object} initialData - Object which is used for initialization
* <br> It can contain keys: * <br> It can contain keys:
* <br> <li style="margin-left: 10px;"> name * <br> <li style="margin-left: 10px;"> name
* <br> <li style="margin-left: 10px;"> assignee * <br> <li style="margin-left: 10px;"> assignee
@ -1040,13 +1038,14 @@
dimension: undefined, dimension: undefined,
}; };
let updatedFields = { const updatedFields = new FieldUpdateTrigger({
name: false, name: false,
assignee: false, assignee: false,
bug_tracker: false, bug_tracker: false,
subset: false, subset: false,
labels: false, labels: false,
}; project_id: false,
});
for (const property in data) { 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) {
@ -1126,11 +1125,18 @@
* @name projectId * @name projectId
* @type {integer|null} * @type {integer|null}
* @memberof module:API.cvat.classes.Task * @memberof module:API.cvat.classes.Task
* @readonly
* @instance * @instance
*/ */
projectId: { projectId: {
get: () => data.project_id, get: () => data.project_id,
set: (projectId) => {
if (!Number.isInteger(projectId) || projectId <= 0) {
throw new ArgumentError('Value must be a positive integer');
}
updatedFields.project_id = true;
data.project_id = projectId;
},
}, },
/** /**
* @name status * @name status
@ -1558,9 +1564,6 @@
}, },
__updatedFields: { __updatedFields: {
get: () => updatedFields, get: () => updatedFields,
set: (fields) => {
updatedFields = fields;
},
}, },
}), }),
); );
@ -1661,6 +1664,36 @@
const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.delete); const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.delete);
return result; return result;
} }
/**
* Method makes a backup of a task
* @method export
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async export() {
const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.export);
return result;
}
/**
* Method imports a task from a backup
* @method import
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
static async import(file) {
const result = await PluginRegistry.apiWrapper.call(this, Task.import, file);
return result;
}
} }
module.exports = { module.exports = {
@ -1694,8 +1727,8 @@
closeSession, closeSession,
} = require('./annotations'); } = require('./annotations');
buildDublicatedAPI(Job.prototype); buildDuplicatedAPI(Job.prototype);
buildDublicatedAPI(Task.prototype); buildDuplicatedAPI(Task.prototype);
Job.prototype.save.implementation = async function () { Job.prototype.save.implementation = async function () {
if (this.id) { if (this.id) {
@ -1721,11 +1754,7 @@
await serverProxy.jobs.save(this.id, jobData); await serverProxy.jobs.save(this.id, jobData);
this.__updatedFields = { this.__updatedFields.reset();
status: false,
assignee: false,
reviewer: false,
};
return this; return this;
} }
@ -2000,6 +2029,9 @@
case 'subset': case 'subset':
taskData.subset = this.subset; taskData.subset = this.subset;
break; break;
case 'project_id':
taskData.project_id = this.projectId;
break;
case 'labels': case 'labels':
taskData.labels = [...this._internalData.labels.map((el) => el.toJSON())]; taskData.labels = [...this._internalData.labels.map((el) => el.toJSON())];
break; break;
@ -2011,13 +2043,7 @@
await serverProxy.tasks.saveTask(this.id, taskData); await serverProxy.tasks.saveTask(this.id, taskData);
this.updatedFields = { this.__updatedFields.reset();
assignee: false,
name: false,
bugTracker: false,
subset: false,
labels: false,
};
return this; return this;
} }
@ -2077,6 +2103,16 @@
return result; return result;
}; };
Task.prototype.export.implementation = async function () {
const result = await serverProxy.tasks.exportTask(this.id);
return result;
};
Task.import.implementation = async function (file) {
const result = await serverProxy.tasks.importTask(file);
return result;
};
Task.prototype.frames.get.implementation = async function (frame, isPlaying, step) { Task.prototype.frames.get.implementation = async function (frame, isPlaying, step) {
if (!Number.isInteger(frame) || frame < 0) { if (!Number.isInteger(frame) || frame < 0) {
throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`);

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -211,7 +211,7 @@ describe('Feature: put annotations', () => {
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 () => { test('put shape without points and with invalid points to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0]; const task = (await window.cvat.tasks.get({ id: 101 }))[0];
await task.annotations.clear(true); await task.annotations.clear(true);
const state = new window.cvat.classes.ObjectState({ const state = new window.cvat.classes.ObjectState({
@ -311,6 +311,27 @@ describe('Feature: check unsaved changes', () => {
}); });
describe('Feature: save annotations', () => { describe('Feature: save annotations', () => {
test('create, save, undo, save, redo save', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
await task.annotations.get(0);
const state = new window.cvat.classes.ObjectState({
frame: 0,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
zOrder: 0,
});
await task.annotations.put([state]);
await task.annotations.save();
await task.actions.undo();
await task.annotations.save();
await task.actions.redo();
await task.annotations.save();
});
test('create & save annotations for a task', async () => { test('create & save annotations for a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0]; const task = (await window.cvat.tasks.get({ id: 101 }))[0];
let annotations = await task.annotations.get(0); let annotations = await task.annotations.get(0);

@ -16,7 +16,7 @@ const { Project } = require('../../src/project');
describe('Feature: get projects', () => { describe('Feature: get projects', () => {
test('get all projects', async () => { test('get all projects', async () => {
const result = await window.cvat.projects.get(); const result = await window.cvat.projects.get({ withoutTasks: false });
expect(Array.isArray(result)).toBeTruthy(); expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(2); expect(result).toHaveLength(2);
for (const el of result) { for (const el of result) {

@ -95,6 +95,7 @@ describe('Feature: save a task', () => {
result[0].bugTracker = 'newBugTracker'; result[0].bugTracker = 'newBugTracker';
result[0].name = 'New Task Name'; result[0].name = 'New Task Name';
result[0].projectId = 6;
result[0].save(); result[0].save();
@ -104,6 +105,7 @@ describe('Feature: save a task', () => {
expect(result[0].bugTracker).toBe('newBugTracker'); expect(result[0].bugTracker).toBe('newBugTracker');
expect(result[0].name).toBe('New Task Name'); expect(result[0].name).toBe('New Task Name');
expect(result[0].projectId).toBe(6);
}); });
test('save some new labels in a task', async () => { test('save some new labels in a task', async () => {

@ -984,6 +984,7 @@ const tasksDummyData = {
}, },
assignee: null, assignee: null,
bug_tracker: '', bug_tracker: '',
project_id: 2,
created_date: '2019-05-15T11:40:19.487999+03:00', created_date: '2019-05-15T11:40:19.487999+03:00',
updated_date: '2019-05-15T16:58:27.992785+03:00', updated_date: '2019-05-15T16:58:27.992785+03:00',
overlap: 5, overlap: 5,

@ -14,16 +14,18 @@ const {
frameMetaDummyData, frameMetaDummyData,
} = require('./dummy-data.mock'); } = require('./dummy-data.mock');
function QueryStringToJSON(query) { function QueryStringToJSON(query, ignoreList = []) {
const pairs = [...new URLSearchParams(query).entries()]; const pairs = [...new URLSearchParams(query).entries()];
const result = {}; const result = {};
for (const pair of pairs) { for (const pair of pairs) {
const [key, value] = pair; const [key, value] = pair;
if (['id'].includes(key)) { if (!ignoreList.includes(key)) {
result[key] = +value; if (['id'].includes(key)) {
} else { result[key] = +value;
result[key] = value; } else {
result[key] = value;
}
} }
} }
@ -73,7 +75,7 @@ class ServerProxy {
} }
async function getProjects(filter = '') { async function getProjects(filter = '') {
const queries = QueryStringToJSON(filter); const queries = QueryStringToJSON(filter, ['without_tasks']);
const result = projectsDummyData.results.filter((x) => { const result = projectsDummyData.results.filter((x) => {
for (const key in queries) { for (const key in queries) {
if (Object.prototype.hasOwnProperty.call(queries, key)) { if (Object.prototype.hasOwnProperty.call(queries, key)) {

@ -3432,28 +3432,28 @@
"dependencies": { "dependencies": {
"abbrev": { "abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "", "resolved": false,
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "", "resolved": false,
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "", "resolved": false,
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"are-we-there-yet": { "are-we-there-yet": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "", "resolved": false,
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3464,14 +3464,14 @@
}, },
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "", "resolved": false,
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "", "resolved": false,
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3482,42 +3482,42 @@
}, },
"chownr": { "chownr": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "", "resolved": false,
"integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "", "resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "", "resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "", "resolved": false,
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "", "resolved": false,
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"debug": { "debug": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "", "resolved": false,
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3527,28 +3527,28 @@
}, },
"deep-extend": { "deep-extend": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "", "resolved": false,
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"delegates": { "delegates": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "", "resolved": false,
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"detect-libc": { "detect-libc": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "", "resolved": false,
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"fs-minipass": { "fs-minipass": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "", "resolved": false,
"integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3558,14 +3558,14 @@
}, },
"fs.realpath": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "", "resolved": false,
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"gauge": { "gauge": {
"version": "2.7.4", "version": "2.7.4",
"resolved": "", "resolved": false,
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3582,7 +3582,7 @@
}, },
"glob": { "glob": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "", "resolved": false,
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3597,14 +3597,14 @@
}, },
"has-unicode": { "has-unicode": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "", "resolved": false,
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"iconv-lite": { "iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "", "resolved": false,
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3614,7 +3614,7 @@
}, },
"ignore-walk": { "ignore-walk": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "", "resolved": false,
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3624,7 +3624,7 @@
}, },
"inflight": { "inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "", "resolved": false,
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3635,7 +3635,7 @@
}, },
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "", "resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true, "dev": true,
"optional": true "optional": true
@ -3649,7 +3649,7 @@
}, },
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "", "resolved": false,
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3659,14 +3659,14 @@
}, },
"isarray": { "isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "", "resolved": false,
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "", "resolved": false,
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3676,7 +3676,7 @@
}, },
"minipass": { "minipass": {
"version": "2.3.5", "version": "2.3.5",
"resolved": "", "resolved": false,
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3687,7 +3687,7 @@
}, },
"minizlib": { "minizlib": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "", "resolved": false,
"integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3707,14 +3707,14 @@
}, },
"ms": { "ms": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "", "resolved": false,
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"needle": { "needle": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "", "resolved": false,
"integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3726,7 +3726,7 @@
}, },
"node-pre-gyp": { "node-pre-gyp": {
"version": "0.12.0", "version": "0.12.0",
"resolved": "", "resolved": false,
"integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3745,7 +3745,7 @@
}, },
"nopt": { "nopt": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "", "resolved": false,
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3756,14 +3756,14 @@
}, },
"npm-bundled": { "npm-bundled": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "", "resolved": false,
"integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"npm-packlist": { "npm-packlist": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "", "resolved": false,
"integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3774,7 +3774,7 @@
}, },
"npmlog": { "npmlog": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "", "resolved": false,
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3787,21 +3787,21 @@
}, },
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "", "resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "", "resolved": false,
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"once": { "once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "", "resolved": false,
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3811,21 +3811,21 @@
}, },
"os-homedir": { "os-homedir": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "", "resolved": false,
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"os-tmpdir": { "os-tmpdir": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "", "resolved": false,
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"osenv": { "osenv": {
"version": "0.1.5", "version": "0.1.5",
"resolved": "", "resolved": false,
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3836,21 +3836,21 @@
}, },
"path-is-absolute": { "path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "", "resolved": false,
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"process-nextick-args": { "process-nextick-args": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "", "resolved": false,
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"rc": { "rc": {
"version": "1.2.8", "version": "1.2.8",
"resolved": "", "resolved": false,
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3872,7 +3872,7 @@
}, },
"readable-stream": { "readable-stream": {
"version": "2.3.6", "version": "2.3.6",
"resolved": "", "resolved": false,
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3888,7 +3888,7 @@
}, },
"rimraf": { "rimraf": {
"version": "2.6.3", "version": "2.6.3",
"resolved": "", "resolved": false,
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3898,49 +3898,49 @@
}, },
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "", "resolved": false,
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "", "resolved": false,
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"sax": { "sax": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "", "resolved": false,
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"semver": { "semver": {
"version": "5.7.0", "version": "5.7.0",
"resolved": "", "resolved": false,
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"set-blocking": { "set-blocking": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "", "resolved": false,
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"signal-exit": { "signal-exit": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "", "resolved": false,
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"string-width": { "string-width": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "", "resolved": false,
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3952,7 +3952,7 @@
}, },
"string_decoder": { "string_decoder": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "", "resolved": false,
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3962,7 +3962,7 @@
}, },
"strip-ansi": { "strip-ansi": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "", "resolved": false,
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3972,14 +3972,14 @@
}, },
"strip-json-comments": { "strip-json-comments": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "", "resolved": false,
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"tar": { "tar": {
"version": "4.4.8", "version": "4.4.8",
"resolved": "", "resolved": false,
"integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -3995,14 +3995,14 @@
}, },
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "", "resolved": false,
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"wide-align": { "wide-align": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "", "resolved": false,
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
@ -4012,14 +4012,14 @@
}, },
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "", "resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "", "resolved": false,
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
"dev": true, "dev": true,
"optional": true "optional": true
@ -4832,9 +4832,9 @@
"dev": true "dev": true
}, },
"jszip": { "jszip": {
"version": "3.6.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.0.tgz",
"integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==", "integrity": "sha512-Y2OlFIzrDOPWUnpU0LORIcDn2xN7rC9yKffFM/7pGhQuhO+SUhfm2trkJ/S5amjFvem0Y+1EALz/MEPkvHXVNw==",
"requires": { "requires": {
"lie": "~3.3.0", "lie": "~3.3.0",
"pako": "~1.0.2", "pako": "~1.0.2",

@ -22,7 +22,7 @@
}, },
"dependencies": { "dependencies": {
"async-mutex": "^0.3.1", "async-mutex": "^0.3.1",
"jszip": "3.6.0" "jszip": "3.7.0"
}, },
"scripts": { "scripts": {
"patch": "cd src/js && patch --dry-run --forward -p0 < 3rdparty_patch.diff >> /dev/null && patch -p0 < 3rdparty_patch.diff; true", "patch": "cd src/js && patch --dry-run --forward -p0 < 3rdparty_patch.diff >> /dev/null && patch -p0 < 3rdparty_patch.diff; true",

@ -17,52 +17,52 @@ So, we have solved to write patch file for this library.
It modifies source code a little to support our scenario of using. It modifies source code a little to support our scenario of using.
### How to build awc.wasm and Decoder.js ### How to build awc.wasm and Decoder.js
1. Clone Emscripten SDK, install and activate the latest fastcomp SDK: 1. Clone Emscripten SDK, install and activate the latest fastcomp SDK:
```sh ```sh
git clone https://github.com/emscripten-core/emsdk.git && cd emsdk git clone https://github.com/emscripten-core/emsdk.git && cd emsdk
``` ```
```sh ```sh
./emsdk install latest-fastcomp ./emsdk install latest-fastcomp
``` ```
```sh ```sh
./emsdk activate latest-fastcomp ./emsdk activate latest-fastcomp
``` ```
1. Clone Broadway.js 1. Clone Broadway.js
```sh ```sh
git clone https://github.com/mbebenita/Broadway.git && cd Broadway/Decoder git clone https://github.com/mbebenita/Broadway.git && cd Broadway/Decoder
``` ```
1. Edit `make.py`: 1. Edit `make.py`:
- Remove or comment the following options: - Remove or comment the following options:
`'-s', 'NO_BROWSER=1',`\ `'-s', 'NO_BROWSER=1',`\
`'-s', 'PRECISE_I64_MATH=0',` `'-s', 'PRECISE_I64_MATH=0',`
- Remove `"HEAP8", "HEAP16", "HEAP32"` from the `EXPORTED_FUNCTIONS` list. - Remove `"HEAP8", "HEAP16", "HEAP32"` from the `EXPORTED_FUNCTIONS` list.
- Increase total memory to make possible decode 4k videos - Increase total memory to make possible decode 4k videos
(or try to enable `ALLOW_MEMORY_GROWTH`, but this option has not been tested):\ (or try to enable `ALLOW_MEMORY_GROWTH`, but this option has not been tested):\
`'-s', 'TOTAL_MEMORY=' + str(100*1024*1024),` `'-s', 'TOTAL_MEMORY=' + str(100*1024*1024),`
- Add the following options:\ - Add the following options:\
`'-s', "ENVIRONMENT='worker'",`\ `'-s', "ENVIRONMENT='worker'",`\
`'-s', 'WASM=1',` `'-s', 'WASM=1',`
1. Activate emsdk environment and build Broadway.js: 1. Activate emsdk environment and build Broadway.js:
```sh ```sh
. /tmp/emsdk/emsdk_env.sh . /tmp/emsdk/emsdk_env.sh
``` ```
```sh ```sh
python2 make.py python2 make.py
``` ```
1. Copy the following files to cvat-data 3rdparty source folder: 1. Copy the following files to cvat-data 3rdparty source folder:
```sh ```sh
cd .. cd ..
``` ```
```sh ```sh
cp Player/avc.wasm Player/Decoder.js Player/mp4.js <CVAT_FOLDER>/cvat-data/src/ cp Player/avc.wasm Player/Decoder.js Player/mp4.js <CVAT_FOLDER>/cvat-data/src/
``` ```
```sh ```sh
js/3rdparty js/3rdparty
``` ```
### How work with a patch file ### How work with a patch file
```bash ```bash

@ -36,5 +36,5 @@ npm run build
npm run build -- --mode=development # without a minification npm run build -- --mode=development # without a minification
``` ```
Important: You also have to run CVAT REST API server (please read `CONTRIBUTING.md`) Important: You also have to run CVAT REST API server (please read `https://openvinotoolkit.github.io/cvat/docs/contributing/`)
to correct working since UI gets all necessary data (tasks, users, annotations) from there to correct working since UI gets all necessary data (tasks, users, annotations) from there

File diff suppressed because it is too large Load Diff

@ -1,11 +1,11 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.19.1", "version": "1.21.1",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {
"build": "webpack --config ./webpack.config.js", "build": "webpack --config ./webpack.config.js",
"start": "REACT_APP_API_URL=http://localhost:7000 webpack-dev-server --config ./webpack.config.js --mode=development", "start": "webpack-dev-server --env.API_URL=http://localhost:7000 --config ./webpack.config.js --mode=development",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"type-check:watch": "npm run type-check -- --watch", "type-check:watch": "npm run type-check -- --watch",
"lint": "eslint './src/**/*.{ts,tsx}'", "lint": "eslint './src/**/*.{ts,tsx}'",
@ -49,14 +49,14 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.6.2", "@ant-design/icons": "^4.6.2",
"@types/lodash": "^4.14.168", "@types/lodash": "^4.14.170",
"@types/platform": "^1.3.3", "@types/platform": "^1.3.3",
"@types/react": "^16.14.5", "@types/react": "^16.14.10",
"@types/react-color": "^3.0.4", "@types/react-color": "^3.0.4",
"@types/react-dom": "^16.9.12", "@types/react-dom": "^16.9.13",
"@types/react-redux": "^7.1.16", "@types/react-redux": "^7.1.16",
"@types/react-resizable": "^1.7.2", "@types/react-resizable": "^1.7.2",
"@types/react-router": "^5.1.13", "@types/react-router": "^5.1.15",
"@types/react-router-dom": "^5.1.7", "@types/react-router-dom": "^5.1.7",
"@types/react-share": "^3.0.3", "@types/react-share": "^3.0.3",
"@types/redux-logger": "^3.0.8", "@types/redux-logger": "^3.0.8",
@ -73,6 +73,7 @@
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"platform": "^1.3.6", "platform": "^1.3.6",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"rc-menu": "^8.10.8",
"react": "^16.14.0", "react": "^16.14.0",
"react-awesome-query-builder": "^3.0.0", "react-awesome-query-builder": "^3.0.0",
"react-color": "^2.19.3", "react-color": "^2.19.3",

@ -7,7 +7,7 @@ import {
ActionCreator, AnyAction, Dispatch, Store, ActionCreator, AnyAction, Dispatch, Store,
} from 'redux'; } from 'redux';
import { ThunkAction } from 'utils/redux'; import { ThunkAction } from 'utils/redux';
import { RectDrawingMethod } from 'cvat-canvas-wrapper'; import { RectDrawingMethod, CuboidDrawingMethod, Canvas } from 'cvat-canvas-wrapper';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import logger, { LogType } from 'cvat-logger'; import logger, { LogType } from 'cvat-logger';
import { getCVATStore } from 'cvat-store'; import { getCVATStore } from 'cvat-store';
@ -146,7 +146,6 @@ export enum AnnotationActionTypes {
GROUP_ANNOTATIONS_FAILED = 'GROUP_ANNOTATIONS_FAILED', GROUP_ANNOTATIONS_FAILED = 'GROUP_ANNOTATIONS_FAILED',
SPLIT_ANNOTATIONS_SUCCESS = 'SPLIT_ANNOTATIONS_SUCCESS', SPLIT_ANNOTATIONS_SUCCESS = 'SPLIT_ANNOTATIONS_SUCCESS',
SPLIT_ANNOTATIONS_FAILED = 'SPLIT_ANNOTATIONS_FAILED', SPLIT_ANNOTATIONS_FAILED = 'SPLIT_ANNOTATIONS_FAILED',
UPDATE_TAB_CONTENT_HEIGHT = 'UPDATE_TAB_CONTENT_HEIGHT',
COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR', COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR',
COLLAPSE_APPEARANCE = 'COLLAPSE_APPEARANCE', COLLAPSE_APPEARANCE = 'COLLAPSE_APPEARANCE',
COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS', COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS',
@ -196,6 +195,8 @@ export enum AnnotationActionTypes {
GET_PREDICTIONS_SUCCESS = 'GET_PREDICTIONS_SUCCESS', GET_PREDICTIONS_SUCCESS = 'GET_PREDICTIONS_SUCCESS',
HIDE_SHOW_CONTEXT_IMAGE = 'HIDE_SHOW_CONTEXT_IMAGE', HIDE_SHOW_CONTEXT_IMAGE = 'HIDE_SHOW_CONTEXT_IMAGE',
GET_CONTEXT_IMAGE = 'GET_CONTEXT_IMAGE', GET_CONTEXT_IMAGE = 'GET_CONTEXT_IMAGE',
GET_CONTEXT_IMAGE_SUCCESS = 'GET_CONTEXT_IMAGE_SUCCESS',
GET_CONTEXT_IMAGE_FAILED = 'GET_CONTEXT_IMAGE_FAILED',
} }
export function saveLogsAsync(): ThunkAction { export function saveLogsAsync(): ThunkAction {
@ -574,15 +575,6 @@ export function activateObject(activatedStateID: number | null, activatedAttribu
}; };
} }
export function updateTabContentHeight(tabContentHeight: number): AnyAction {
return {
type: AnnotationActionTypes.UPDATE_TAB_CONTENT_HEIGHT,
payload: {
tabContentHeight,
},
};
}
export function collapseSidebar(): AnyAction { export function collapseSidebar(): AnyAction {
return { return {
type: AnnotationActionTypes.COLLAPSE_SIDEBAR, type: AnnotationActionTypes.COLLAPSE_SIDEBAR,
@ -697,7 +689,8 @@ export function getPredictionsAsync(): ThunkAction {
}; };
} }
export function changeFrameAsync(toFrame: number, fillBuffer?: boolean, frameStep?: number): ThunkAction { export function changeFrameAsync(toFrame: number, fillBuffer?: boolean, frameStep?: number,
forceUpdate?: boolean): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const state: CombinedState = getStore().getState(); const state: CombinedState = getStore().getState();
const { instance: job } = state.annotation.job; const { instance: job } = state.annotation.job;
@ -708,13 +701,14 @@ export function changeFrameAsync(toFrame: number, fillBuffer?: boolean, frameSte
throw Error(`Required frame ${toFrame} is out of the current job`); throw Error(`Required frame ${toFrame} is out of the current job`);
} }
if (toFrame === frame) { if (toFrame === frame && !forceUpdate) {
dispatch({ dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS, type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: { payload: {
number: state.annotation.player.frame.number, number: state.annotation.player.frame.number,
data: state.annotation.player.frame.data, data: state.annotation.player.frame.data,
filename: state.annotation.player.frame.filename, filename: state.annotation.player.frame.filename,
hasRelatedContext: state.annotation.player.frame.hasRelatedContext,
delay: state.annotation.player.frame.delay, delay: state.annotation.player.frame.delay,
changeTime: state.annotation.player.frame.changeTime, changeTime: state.annotation.player.frame.changeTime,
states: state.annotation.annotations.states, states: state.annotation.annotations.states,
@ -726,7 +720,6 @@ export function changeFrameAsync(toFrame: number, fillBuffer?: boolean, frameSte
return; return;
} }
// Start async requests // Start async requests
dispatch({ dispatch({
type: AnnotationActionTypes.CHANGE_FRAME, type: AnnotationActionTypes.CHANGE_FRAME,
@ -766,6 +759,7 @@ export function changeFrameAsync(toFrame: number, fillBuffer?: boolean, frameSte
number: toFrame, number: toFrame,
data, data,
filename: data.filename, filename: data.filename,
hasRelatedContext: data.hasRelatedContext,
states, states,
minZ, minZ,
maxZ, maxZ,
@ -807,7 +801,6 @@ export function undoActionAsync(sessionInstance: any, frame: number): ThunkActio
true, true,
); );
dispatch(changeFrameAsync(undo[1]));
await sessionInstance.actions.undo(); await sessionInstance.actions.undo();
const history = await sessionInstance.actions.get(); const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame, showAllInterpolationTracks, filters); const states = await sessionInstance.annotations.get(frame, showAllInterpolationTracks, filters);
@ -823,6 +816,11 @@ export function undoActionAsync(sessionInstance: any, frame: number): ThunkActio
maxZ, maxZ,
}, },
}); });
const undoOnFrame = undo[1];
if (frame !== undoOnFrame) {
dispatch(changeFrameAsync(undoOnFrame));
}
} catch (error) { } catch (error) {
dispatch({ dispatch({
type: AnnotationActionTypes.UNDO_ACTION_FAILED, type: AnnotationActionTypes.UNDO_ACTION_FAILED,
@ -851,7 +849,7 @@ export function redoActionAsync(sessionInstance: any, frame: number): ThunkActio
}, },
true, true,
); );
dispatch(changeFrameAsync(redo[1]));
await sessionInstance.actions.redo(); await sessionInstance.actions.redo();
const history = await sessionInstance.actions.get(); const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame, showAllInterpolationTracks, filters); const states = await sessionInstance.annotations.get(frame, showAllInterpolationTracks, filters);
@ -867,6 +865,11 @@ export function redoActionAsync(sessionInstance: any, frame: number): ThunkActio
maxZ, maxZ,
}, },
}); });
const redoOnFrame = redo[1];
if (frame !== redoOnFrame) {
dispatch(changeFrameAsync(redoOnFrame));
}
} catch (error) { } catch (error) {
dispatch({ dispatch({
type: AnnotationActionTypes.REDO_ACTION_FAILED, type: AnnotationActionTypes.REDO_ACTION_FAILED,
@ -1031,6 +1034,7 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
states, states,
frameNumber, frameNumber,
frameFilename: frameData.filename, frameFilename: frameData.filename,
frameHasRelatedContext: frameData.hasRelatedContext,
frameData, frameData,
colors, colors,
filters, filters,
@ -1139,6 +1143,7 @@ export function rememberObject(createParams: {
activeShapeType?: ShapeType; activeShapeType?: ShapeType;
activeNumOfPoints?: number; activeNumOfPoints?: number;
activeRectDrawingMethod?: RectDrawingMethod; activeRectDrawingMethod?: RectDrawingMethod;
activeCuboidDrawingMethod?: CuboidDrawingMethod;
}): AnyAction { }): AnyAction {
return { return {
type: AnnotationActionTypes.REMEMBER_CREATED_OBJECT, type: AnnotationActionTypes.REMEMBER_CREATED_OBJECT,
@ -1430,8 +1435,9 @@ export function pasteShapeAsync(): ThunkAction {
activeControl, activeControl,
}, },
}); });
if (canvasInstance instanceof Canvas) {
canvasInstance.cancel(); canvasInstance.cancel();
}
if (initialState.objectType === ObjectType.TAG) { if (initialState.objectType === ObjectType.TAG) {
const objectState = new cvat.classes.ObjectState({ const objectState = new cvat.classes.ObjectState({
objectType: ObjectType.TAG, objectType: ObjectType.TAG,
@ -1484,11 +1490,12 @@ export function repeatDrawShapeAsync(): ThunkAction {
activeShapeType, activeShapeType,
activeNumOfPoints, activeNumOfPoints,
activeRectDrawingMethod, activeRectDrawingMethod,
activeCuboidDrawingMethod,
}, },
} = getStore().getState().annotation; } = getStore().getState().annotation;
let activeControl = ActiveControl.CURSOR; let activeControl = ActiveControl.CURSOR;
if (activeInteractor) { if (activeInteractor && canvasInstance instanceof Canvas) {
if (activeInteractor.type === 'tracker') { if (activeInteractor.type === 'tracker') {
canvasInstance.interact({ canvasInstance.interact({
enabled: true, enabled: true,
@ -1506,7 +1513,6 @@ export function repeatDrawShapeAsync(): ThunkAction {
return; return;
} }
if (activeShapeType === ShapeType.RECTANGLE) { if (activeShapeType === ShapeType.RECTANGLE) {
activeControl = ActiveControl.DRAW_RECTANGLE; activeControl = ActiveControl.DRAW_RECTANGLE;
} else if (activeShapeType === ShapeType.POINTS) { } else if (activeShapeType === ShapeType.POINTS) {
@ -1518,7 +1524,6 @@ export function repeatDrawShapeAsync(): ThunkAction {
} else if (activeShapeType === ShapeType.CUBOID) { } else if (activeShapeType === ShapeType.CUBOID) {
activeControl = ActiveControl.DRAW_CUBOID; activeControl = ActiveControl.DRAW_CUBOID;
} }
dispatch({ dispatch({
type: AnnotationActionTypes.REPEAT_DRAW_SHAPE, type: AnnotationActionTypes.REPEAT_DRAW_SHAPE,
payload: { payload: {
@ -1526,7 +1531,9 @@ export function repeatDrawShapeAsync(): ThunkAction {
}, },
}); });
canvasInstance.cancel(); if (canvasInstance instanceof Canvas) {
canvasInstance.cancel();
}
if (activeObjectType === ObjectType.TAG) { if (activeObjectType === ObjectType.TAG) {
const objectState = new cvat.classes.ObjectState({ const objectState = new cvat.classes.ObjectState({
objectType: ObjectType.TAG, objectType: ObjectType.TAG,
@ -1538,6 +1545,7 @@ export function repeatDrawShapeAsync(): ThunkAction {
canvasInstance.draw({ canvasInstance.draw({
enabled: true, enabled: true,
rectDrawingMethod: activeRectDrawingMethod, rectDrawingMethod: activeRectDrawingMethod,
cuboidDrawingMethod: activeCuboidDrawingMethod,
numberOfPoints: activeNumOfPoints, numberOfPoints: activeNumOfPoints,
shapeType: activeShapeType, shapeType: activeShapeType,
crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID].includes(activeShapeType), crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID].includes(activeShapeType),
@ -1575,8 +1583,9 @@ export function redrawShapeAsync(): ThunkAction {
activeControl, activeControl,
}, },
}); });
if (canvasInstance instanceof Canvas) {
canvasInstance.cancel(); canvasInstance.cancel();
}
canvasInstance.draw({ canvasInstance.draw({
enabled: true, enabled: true,
redraw: activatedStateID, redraw: activatedStateID,
@ -1632,35 +1641,27 @@ export function hideShowContextImage(hidden: boolean): AnyAction {
}; };
} }
export function getContextImage(): ThunkAction { export function getContextImageAsync(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const state: CombinedState = getStore().getState(); const state: CombinedState = getStore().getState();
const { instance: job } = state.annotation.job; const { instance: job } = state.annotation.job;
const { frame, contextImage } = state.annotation.player; const { number: frameNumber } = state.annotation.player.frame;
try { try {
const context = await job.frames.contextImage(job.task.id, frame.number);
const loaded = true;
const contextImageHide = contextImage.hidden;
dispatch({ dispatch({
type: AnnotationActionTypes.GET_CONTEXT_IMAGE, type: AnnotationActionTypes.GET_CONTEXT_IMAGE,
payload: { payload: {},
context, });
loaded,
contextImageHide, const contextImageData = await job.frames.contextImage(job.task.id, frameNumber);
}, dispatch({
type: AnnotationActionTypes.GET_CONTEXT_IMAGE_SUCCESS,
payload: { contextImageData },
}); });
} catch (error) { } catch (error) {
const context = '';
const loaded = true;
const contextImageHide = contextImage.hidden;
dispatch({ dispatch({
type: AnnotationActionTypes.GET_CONTEXT_IMAGE, type: AnnotationActionTypes.GET_CONTEXT_IMAGE_FAILED,
payload: { payload: { error },
context,
loaded,
contextImageHide,
},
}); });
} }
}; };

@ -5,9 +5,8 @@
import { Dispatch, ActionCreator } from 'redux'; import { Dispatch, ActionCreator } from 'redux';
import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { ProjectsQuery, CombinedState } from 'reducers/interfaces'; import { ProjectsQuery } from 'reducers/interfaces';
import { getTasksSuccess, updateTaskSuccess } from 'actions/tasks-actions'; import { getTasksSuccess, updateTaskSuccess } from 'actions/tasks-actions';
import { getCVATStore } from 'cvat-store';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
const cvat = getCore(); const cvat = getCore();
@ -31,8 +30,8 @@ export enum ProjectsActionTypes {
// prettier-ignore // prettier-ignore
const projectActions = { const projectActions = {
getProjects: () => createAction(ProjectsActionTypes.GET_PROJECTS), getProjects: () => createAction(ProjectsActionTypes.GET_PROJECTS),
getProjectsSuccess: (array: any[], count: number) => ( getProjectsSuccess: (array: any[], previews: string[], count: number) => (
createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, count }) createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, previews, count })
), ),
getProjectsFailed: (error: any) => createAction(ProjectsActionTypes.GET_PROJECTS_FAILED, { error }), getProjectsFailed: (error: any) => createAction(ProjectsActionTypes.GET_PROJECTS_FAILED, { error }),
updateProjectsGettingQuery: (query: Partial<ProjectsQuery>) => ( updateProjectsGettingQuery: (query: Partial<ProjectsQuery>) => (
@ -60,7 +59,7 @@ const projectActions = {
export type ProjectActions = ActionUnion<typeof projectActions>; export type ProjectActions = ActionUnion<typeof projectActions>;
export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction { export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>, getState): Promise<void> => {
dispatch(projectActions.getProjects()); dispatch(projectActions.getProjects());
dispatch(projectActions.updateProjectsGettingQuery(query)); dispatch(projectActions.updateProjectsGettingQuery(query));
@ -69,6 +68,7 @@ export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
page: 1, page: 1,
...query, ...query,
}; };
for (const key in filteredQuery) { for (const key in filteredQuery) {
if (filteredQuery[key] === null || typeof filteredQuery[key] === 'undefined') { if (filteredQuery[key] === null || typeof filteredQuery[key] === 'undefined') {
delete filteredQuery[key]; delete filteredQuery[key];
@ -85,38 +85,38 @@ export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
const array = Array.from(result); const array = Array.from(result);
const tasks: any[] = []; // Appropriate tasks fetching proccess needs with retrieving only a single project
const taskPreviewPromises: Promise<any>[] = []; if (Object.keys(filteredQuery).includes('id')) {
const tasks: any[] = [];
for (const project of array) { const [project] = array;
taskPreviewPromises.push( const taskPreviewPromises: Promise<string>[] = (project as any).tasks.map((task: any): string => {
...(project as any).tasks.map((task: any): string => { tasks.push(task);
tasks.push(task); return (task as any).frames.preview().catch(() => '');
return (task as any).frames.preview().catch(() => ''); });
}),
);
}
const taskPreviews = await Promise.all(taskPreviewPromises); const taskPreviews = await Promise.all(taskPreviewPromises);
dispatch(projectActions.getProjectsSuccess(array, result.count)); const state = getState();
const store = getCVATStore(); dispatch(projectActions.getProjectsSuccess(array, taskPreviews, result.count));
const state: CombinedState = store.getState();
if (!state.tasks.fetching) {
if (!state.tasks.fetching) { dispatch(
dispatch( getTasksSuccess(tasks, taskPreviews, tasks.length, {
getTasksSuccess(tasks, taskPreviews, tasks.length, { page: 1,
page: 1, assignee: null,
assignee: null, id: null,
id: null, mode: null,
mode: null, name: null,
name: null, owner: null,
owner: null, search: null,
search: null, status: null,
status: null, }),
}), );
); }
} else {
const previewPromises = array.map((project): string => (project as any).preview().catch(() => ''));
dispatch(projectActions.getProjectsSuccess(array, await Promise.all(previewPromises), result.count));
} }
}; };
} }

@ -26,6 +26,7 @@ export enum SettingsActionTypes {
SWITCH_AUTO_SAVE = 'SWITCH_AUTO_SAVE', SWITCH_AUTO_SAVE = 'SWITCH_AUTO_SAVE',
CHANGE_AUTO_SAVE_INTERVAL = 'CHANGE_AUTO_SAVE_INTERVAL', CHANGE_AUTO_SAVE_INTERVAL = 'CHANGE_AUTO_SAVE_INTERVAL',
CHANGE_AAM_ZOOM_MARGIN = 'CHANGE_AAM_ZOOM_MARGIN', CHANGE_AAM_ZOOM_MARGIN = 'CHANGE_AAM_ZOOM_MARGIN',
CHANGE_DEFAULT_APPROX_POLY_THRESHOLD = 'CHANGE_DEFAULT_APPROX_POLY_THRESHOLD',
SWITCH_AUTOMATIC_BORDERING = 'SWITCH_AUTOMATIC_BORDERING', SWITCH_AUTOMATIC_BORDERING = 'SWITCH_AUTOMATIC_BORDERING',
SWITCH_INTELLIGENT_POLYGON_CROP = 'SWITCH_INTELLIGENT_POLYGON_CROP', SWITCH_INTELLIGENT_POLYGON_CROP = 'SWITCH_INTELLIGENT_POLYGON_CROP',
SWITCH_SHOWNIG_INTERPOLATED_TRACKS = 'SWITCH_SHOWNIG_INTERPOLATED_TRACKS', SWITCH_SHOWNIG_INTERPOLATED_TRACKS = 'SWITCH_SHOWNIG_INTERPOLATED_TRACKS',
@ -270,6 +271,15 @@ export function switchSettingsDialog(show?: boolean): AnyAction {
}; };
} }
export function changeDefaultApproxPolyAccuracy(approxPolyAccuracy: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_DEFAULT_APPROX_POLY_THRESHOLD,
payload: {
approxPolyAccuracy,
},
};
}
export function setSettings(settings: Partial<SettingsState>): AnyAction { export function setSettings(settings: Partial<SettingsState>): AnyAction {
return { return {
type: SettingsActionTypes.SET_SETTINGS, type: SettingsActionTypes.SET_SETTINGS,

@ -35,6 +35,13 @@ export enum TasksActionTypes {
UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS', UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS',
UPDATE_TASK_FAILED = 'UPDATE_TASK_FAILED', UPDATE_TASK_FAILED = 'UPDATE_TASK_FAILED',
HIDE_EMPTY_TASKS = 'HIDE_EMPTY_TASKS', HIDE_EMPTY_TASKS = 'HIDE_EMPTY_TASKS',
EXPORT_TASK = 'EXPORT_TASK',
EXPORT_TASK_SUCCESS = 'EXPORT_TASK_SUCCESS',
EXPORT_TASK_FAILED = 'EXPORT_TASK_FAILED',
IMPORT_TASK = 'IMPORT_TASK',
IMPORT_TASK_SUCCESS = 'IMPORT_TASK_SUCCESS',
IMPORT_TASK_FAILED = 'IMPORT_TASK_FAILED',
SWITCH_MOVE_TASK_MODAL_VISIBLE = 'SWITCH_MOVE_TASK_MODAL_VISIBLE',
} }
function getTasks(): AnyAction { function getTasks(): AnyAction {
@ -213,6 +220,49 @@ export function loadAnnotationsAsync(
}; };
} }
function importTask(): AnyAction {
const action = {
type: TasksActionTypes.IMPORT_TASK,
payload: {},
};
return action;
}
function importTaskSuccess(task: any): AnyAction {
const action = {
type: TasksActionTypes.IMPORT_TASK_SUCCESS,
payload: {
task,
},
};
return action;
}
function importTaskFailed(error: any): AnyAction {
const action = {
type: TasksActionTypes.IMPORT_TASK_FAILED,
payload: {
error,
},
};
return action;
}
export function importTaskAsync(file: File): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch(importTask());
const taskInstance = await cvat.classes.Task.import(file);
dispatch(importTaskSuccess(taskInstance));
} catch (error) {
dispatch(importTaskFailed(error));
}
};
}
function exportDataset(task: any, exporter: any): AnyAction { function exportDataset(task: any, exporter: any): AnyAction {
const action = { const action = {
type: TasksActionTypes.EXPORT_DATASET, type: TasksActionTypes.EXPORT_DATASET,
@ -267,6 +317,56 @@ export function exportDatasetAsync(task: any, exporter: any): ThunkAction<Promis
}; };
} }
function exportTask(taskID: number): AnyAction {
const action = {
type: TasksActionTypes.EXPORT_TASK,
payload: {
taskID,
},
};
return action;
}
function exportTaskSuccess(taskID: number): AnyAction {
const action = {
type: TasksActionTypes.EXPORT_TASK_SUCCESS,
payload: {
taskID,
},
};
return action;
}
function exportTaskFailed(taskID: number, error: Error): AnyAction {
const action = {
type: TasksActionTypes.EXPORT_TASK_FAILED,
payload: {
taskID,
error,
},
};
return action;
}
export function exportTaskAsync(taskInstance: any): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(exportTask(taskInstance.id));
try {
const url = await taskInstance.export();
const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement;
downloadAnchor.href = url;
downloadAnchor.click();
dispatch(exportTaskSuccess(taskInstance.id));
} catch (error) {
dispatch(exportTaskFailed(taskInstance.id, error));
}
};
}
function deleteTask(taskID: number): AnyAction { function deleteTask(taskID: number): AnyAction {
const action = { const action = {
type: TasksActionTypes.DELETE_TASK, type: TasksActionTypes.DELETE_TASK,
@ -519,3 +619,46 @@ export function hideEmptyTasks(hideEmpty: boolean): AnyAction {
return action; return action;
} }
export function switchMoveTaskModalVisible(visible: boolean, taskId: number | null = null): AnyAction {
const action = {
type: TasksActionTypes.SWITCH_MOVE_TASK_MODAL_VISIBLE,
payload: {
taskId,
visible,
},
};
return action;
}
interface LabelMap {
label_id: number;
new_label_name: string | null;
clear_attributes: boolean;
}
export function moveTaskToProjectAsync(
taskInstance: any,
projectId: any,
labelMap: LabelMap[],
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(updateTask());
try {
// eslint-disable-next-line no-param-reassign
taskInstance.labels = labelMap.map((mapper) => {
const [label] = taskInstance.labels.filter((_label: any) => mapper.label_id === _label.id);
label.name = mapper.new_label_name;
return label;
});
// eslint-disable-next-line no-param-reassign
taskInstance.projectId = projectId;
await taskInstance.save();
const [task] = await cvat.tasks.get({ id: taskInstance.id });
dispatch(updateTaskSuccess(task, task.id));
} catch (error) {
dispatch(updateTaskFailed(error, taskInstance));
}
};
}

@ -1 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M5.816 24.607v6.572h6.53V32H5v-7.393h.816zm29.184 0V32h-7.347v-.821h6.53v-6.572H35zM12.347 9v.821h-6.53v6.572H5V9h7.347zM35 9v7.393h-.816V9.82h-6.53V9H35z" stroke="#000" fill="#000" fill-rule="evenodd"/></svg> <svg viewBox="0 0 40 40" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><path d="M5.816 24.607v6.572h6.53V32H5v-7.393h.816zm29.184 0V32h-7.347v-.821h6.53v-6.572H35zM12.347 9v.821h-6.53v6.572H5V9h7.347zM35 9v7.393h-.816V9.82h-6.53V9H35z" stroke="#000" fill="#000" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 281 B

After

Width:  |  Height:  |  Size: 304 B

@ -1 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M20 2c9.941 0 18 8.059 18 18s-8.059 18-18 18S2 29.941 2 20 10.059 2 20 2zm0 2.4C11.384 4.4 4.4 11.384 4.4 20S11.384 35.6 20 35.6 35.6 28.616 35.6 20 28.616 4.4 20 4.4zm1.27 25.2h-2.642V14.147h2.642V29.6zm-2.856-19.552c0-.429.13-.79.393-1.086.261-.295.65-.443 1.164-.443.514 0 .904.148 1.17.443.267.295.4.657.4 1.086 0 .428-.133.785-.4 1.07-.266.286-.656.43-1.17.43-.515 0-.903-.144-1.164-.43-.262-.285-.393-.642-.393-1.07z" fill="#000" fill-rule="nonzero"/></svg> <svg viewBox="0 0 40 40" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><path d="M20 2c9.941 0 18 8.059 18 18s-8.059 18-18 18S2 29.941 2 20 10.059 2 20 2zm0 2.4C11.384 4.4 4.4 11.384 4.4 20S11.384 35.6 20 35.6 35.6 28.616 35.6 20 28.616 4.4 20 4.4zm1.27 25.2h-2.642V14.147h2.642V29.6zm-2.856-19.552c0-.429.13-.79.393-1.086.261-.295.65-.443 1.164-.443.514 0 .904.148 1.17.443.267.295.4.657.4 1.086 0 .428-.133.785-.4 1.07-.266.286-.656.43-1.17.43-.515 0-.903-.144-1.164-.43-.262-.285-.393-.642-.393-1.07z" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 535 B

After

Width:  |  Height:  |  Size: 558 B

@ -1 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M5 30h30v-3.333H5V30zm0-8.333h30v-3.334H5v3.334zM5 10v3.333h30V10H5z" fill="#000" fill-rule="evenodd"/></svg> <svg viewBox="0 0 40 40" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><path d="M5 30h30v-3.333H5V30zm0-8.333h30v-3.334H5v3.334zM5 10v3.333h30V10H5z" fill="#000" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 182 B

After

Width:  |  Height:  |  Size: 204 B

@ -1 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M35.748 2H4.253C2.254 2 1.246 4.425 2.662 5.841L15.5 18.682v13.13c0 .65.28 1.267.768 1.694l4.5 3.936c1.437 1.258 3.732.259 3.732-1.693V18.682L37.339 5.841C38.752 4.428 37.75 2 35.748 2zM22.25 17.75v18l-4.5-3.937V17.75L4.25 4.25h31.5l-13.5 13.5z" fill="#000" fill-rule="nonzero"/></svg> <svg viewBox="0 0 40 40" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><path d="M35.748 2H4.253C2.254 2 1.246 4.425 2.662 5.841L15.5 18.682v13.13c0 .65.28 1.267.768 1.694l4.5 3.936c1.437 1.258 3.732.259 3.732-1.693V18.682L37.339 5.841C38.752 4.428 37.75 2 35.748 2zM22.25 17.75v18l-4.5-3.937V17.75L4.25 4.25h31.5l-13.5 13.5z" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 357 B

After

Width:  |  Height:  |  Size: 380 B

@ -1 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="redoA" d="M0 0h34v24H0z"/></defs><g transform="translate(3 8)" fill="none" fill-rule="evenodd"><mask id="redoB" fill="#fff"><use xlink:href="#redoA"/></mask><path d="M28.262 9.331C20.935.18 8.932-.428 1.308 7.643V2.968H0v6.537c0 .45.293.817.654.817h5.232V8.688H2.35c7.125-7.322 18.2-6.683 24.987 1.8 7.14 8.92 7.14 23.437 0 32.357L28.262 44c7.65-9.557 7.65-25.111 0-34.669z" fill="#000" mask="url(#redoB)" transform="matrix(-1 0 0 1 34 0)"/></g></svg> <svg viewBox="0 0 40 40" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="redoA" d="M0 0h34v24H0z"/></defs><g transform="translate(3 8)" fill="none" fill-rule="evenodd"><mask id="redoB" fill="#fff"><use xlink:href="#redoA"/></mask><path d="M28.262 9.331C20.935.18 8.932-.428 1.308 7.643V2.968H0v6.537c0 .45.293.817.654.817h5.232V8.688H2.35c7.125-7.322 18.2-6.683 24.987 1.8 7.14 8.92 7.14 23.437 0 32.357L28.262 44c7.65-9.557 7.65-25.111 0-34.669z" fill="#000" mask="url(#redoB)" transform="matrix(-1 0 0 1 34 0)"/></g></svg>

Before

Width:  |  Height:  |  Size: 573 B

After

Width:  |  Height:  |  Size: 596 B

@ -1 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M29.131 5l5.257 5.257V35H5V5h24.131zm-.866 17.143H10.51v11.633h17.755V22.143zM10.51 6.224H6.224v27.552h3.062V20.918H29.49v12.858h3.673V10.764l-4.539-4.54h-.97v9.796H10.51V6.224zm8.878 20.205a.612.612 0 0 1 .1 1.216l-.1.008h-6.123a.612.612 0 0 1-.1-1.216l.1-.008h6.123zm2.271.177c.11.116.178.276.178.435 0 .159-.068.318-.178.435a.644.644 0 0 1-.435.177.63.63 0 0 1-.434-.177.64.64 0 0 1-.178-.435.64.64 0 0 1 .178-.435.641.641 0 0 1 .87 0zm-4.108-2.626a.612.612 0 0 1 .1 1.216l-.1.008h-4.286a.612.612 0 0 1-.1-1.216l.1-.008h4.286zm8.878-17.756H11.735v8.572h14.694V6.224zM25.51 7.45v6.122h-3.673V7.45h3.673zm-1.224 1.224H23.06v3.674h1.225V8.673z" fill="#000" fill-rule="nonzero"/></svg> <svg viewBox="0 0 40 40" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><path d="M29.131 5l5.257 5.257V35H5V5h24.131zm-.866 17.143H10.51v11.633h17.755V22.143zM10.51 6.224H6.224v27.552h3.062V20.918H29.49v12.858h3.673V10.764l-4.539-4.54h-.97v9.796H10.51V6.224zm8.878 20.205a.612.612 0 0 1 .1 1.216l-.1.008h-6.123a.612.612 0 0 1-.1-1.216l.1-.008h6.123zm2.271.177c.11.116.178.276.178.435 0 .159-.068.318-.178.435a.644.644 0 0 1-.435.177.63.63 0 0 1-.434-.177.64.64 0 0 1-.178-.435.64.64 0 0 1 .178-.435.641.641 0 0 1 .87 0zm-4.108-2.626a.612.612 0 0 1 .1 1.216l-.1.008h-4.286a.612.612 0 0 1-.1-1.216l.1-.008h4.286zm8.878-17.756H11.735v8.572h14.694V6.224zM25.51 7.45v6.122h-3.673V7.45h3.673zm-1.224 1.224H23.06v3.674h1.225V8.673z" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 756 B

After

Width:  |  Height:  |  Size: 779 B

@ -1 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="b" d="M0 0h34v24H0z"/></defs><g transform="translate(3 8)" fill="none" fill-rule="evenodd"><mask id="undoB" fill="#fff"><use xlink:href="#b"/></mask><path d="M28.62 9.331C20.935.18 8.932-.428 1.308 7.643V2.968H0v6.537c0 .45.293.817.654.817h5.232V8.688H2.35c7.125-7.322 18.2-6.683 24.987 1.8 7.14 8.92 7.14 23.437 0 32.357L28.262 44c7.65-9.557 7.65-25.111 0-34.669z" fill="#000" mask="url(#undoB)"/></g></svg> <svg viewBox="0 0 40 40" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="b" d="M0 0h34v24H0z"/></defs><g transform="translate(3 8)" fill="none" fill-rule="evenodd"><mask id="undoB" fill="#fff"><use xlink:href="#b"/></mask><path d="M28.62 9.331C20.935.18 8.932-.428 1.308 7.643V2.968H0v6.537c0 .45.293.817.654.817h5.232V8.688H2.35c7.125-7.322 18.2-6.683 24.987 1.8 7.14 8.92 7.14 23.437 0 32.357L28.262 44c7.65-9.557 7.65-25.111 0-34.669z" fill="#000" mask="url(#undoB)"/></g></svg>

Before

Width:  |  Height:  |  Size: 530 B

After

Width:  |  Height:  |  Size: 553 B

@ -13,6 +13,7 @@ $layout-lg-grid-color: rgba(0, 0, 0, 0.15);
$header-color: #d8d8d8; $header-color: #d8d8d8;
$text-color: #303030; $text-color: #303030;
$text-color-secondary: rgba(0, 0, 0, 0.45);
$hover-menu-color: rgba(24, 144, 255, 0.05); $hover-menu-color: rgba(24, 144, 255, 0.05);
$completed-progress-color: #61c200; $completed-progress-color: #61c200;
$inprogress-progress-color: #1890ff; $inprogress-progress-color: #1890ff;

@ -6,6 +6,7 @@ import './styles.scss';
import React from 'react'; import React from 'react';
import Menu from 'antd/lib/menu'; import Menu from 'antd/lib/menu';
import Modal from 'antd/lib/modal'; import Modal from 'antd/lib/modal';
import { LoadingOutlined } from '@ant-design/icons';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import { MenuInfo } from 'rc-menu/lib/interface'; import { MenuInfo } from 'rc-menu/lib/interface';
import DumpSubmenu from './dump-submenu'; import DumpSubmenu from './dump-submenu';
@ -25,6 +26,7 @@ interface Props {
inferenceIsActive: boolean; inferenceIsActive: boolean;
taskDimension: DimensionType; taskDimension: DimensionType;
onClickMenu: (params: MenuInfo, file?: File) => void; onClickMenu: (params: MenuInfo, file?: File) => void;
exportIsActive: boolean;
} }
export enum Actions { export enum Actions {
@ -33,7 +35,9 @@ export enum Actions {
EXPORT_TASK_DATASET = 'export_task_dataset', EXPORT_TASK_DATASET = 'export_task_dataset',
DELETE_TASK = 'delete_task', DELETE_TASK = 'delete_task',
RUN_AUTO_ANNOTATION = 'run_auto_annotation', RUN_AUTO_ANNOTATION = 'run_auto_annotation',
MOVE_TASK_TO_PROJECT = 'move_task_to_project',
OPEN_BUG_TRACKER = 'open_bug_tracker', OPEN_BUG_TRACKER = 'open_bug_tracker',
EXPORT_TASK = 'export_task',
} }
export default function ActionsMenuComponent(props: Props): JSX.Element { export default function ActionsMenuComponent(props: Props): JSX.Element {
@ -49,6 +53,7 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
exportActivities, exportActivities,
loadActivity, loadActivity,
taskDimension, taskDimension,
exportIsActive,
} = props; } = props;
let latestParams: MenuInfo | null = null; let latestParams: MenuInfo | null = null;
@ -127,7 +132,12 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
<Menu.Item disabled={inferenceIsActive} key={Actions.RUN_AUTO_ANNOTATION}> <Menu.Item disabled={inferenceIsActive} key={Actions.RUN_AUTO_ANNOTATION}>
Automatic annotation Automatic annotation
</Menu.Item> </Menu.Item>
<Menu.Item key={Actions.EXPORT_TASK} disabled={exportIsActive}>
{exportIsActive && <LoadingOutlined id='cvat-export-task-loading' />}
Export Task
</Menu.Item>
<hr /> <hr />
<Menu.Item key={Actions.MOVE_TASK_TO_PROJECT}>Move to project</Menu.Item>
<Menu.Item key={Actions.DELETE_TASK}>Delete</Menu.Item> <Menu.Item key={Actions.DELETE_TASK}>Delete</Menu.Item>
</Menu> </Menu>
); );

@ -47,7 +47,12 @@ export default function LoadSubmenu(props: Props): JSX.Element {
return false; return false;
}} }}
> >
<Button block type='link' disabled={disabled} className='cvat-menu-load-submenu-item-button'> <Button
block
type='link'
disabled={disabled}
className='cvat-menu-load-submenu-item-button'
>
<UploadOutlined /> <UploadOutlined />
<Text>{loader.name}</Text> <Text>{loader.name}</Text>
{pending && <LoadingOutlined style={{ marginLeft: 10 }} />} {pending && <LoadingOutlined style={{ marginLeft: 10 }} />}

@ -48,3 +48,7 @@
.cvat-menu-icon { .cvat-menu-icon {
transform: scale(0.5); transform: scale(0.5);
} }
#cvat-export-task-loading {
margin-left: 10;
}

@ -14,11 +14,8 @@ import Button from 'antd/lib/button';
import ColorPicker from 'components/annotation-page/standard-workspace/objects-side-bar/color-picker'; import ColorPicker from 'components/annotation-page/standard-workspace/objects-side-bar/color-picker';
import { ColorizeIcon } from 'icons'; import { ColorizeIcon } from 'icons';
import { ColorBy, CombinedState } from 'reducers/interfaces'; import { ColorBy, CombinedState, DimensionType } from 'reducers/interfaces';
import { import { collapseAppearance as collapseAppearanceAction } from 'actions/annotation-actions';
collapseAppearance as collapseAppearanceAction,
updateTabContentHeight as updateTabContentHeightAction,
} from 'actions/annotation-actions';
import { import {
changeShapesColorBy as changeShapesColorByAction, changeShapesColorBy as changeShapesColorByAction,
changeShapesOpacity as changeShapesOpacityAction, changeShapesOpacity as changeShapesOpacityAction,
@ -37,6 +34,7 @@ interface StateToProps {
outlineColor: string; outlineColor: string;
showBitmap: boolean; showBitmap: boolean;
showProjections: boolean; showProjections: boolean;
jobInstance: any;
} }
interface DispatchToProps { interface DispatchToProps {
@ -49,24 +47,12 @@ interface DispatchToProps {
changeShowProjections(event: CheckboxChangeEvent): void; changeShowProjections(event: CheckboxChangeEvent): void;
} }
export function computeHeight(): number {
const [sidebar] = window.document.getElementsByClassName('cvat-objects-sidebar');
const [appearance] = window.document.getElementsByClassName('cvat-objects-appearance-collapse');
const [tabs] = Array.from(window.document.querySelectorAll('.cvat-objects-sidebar-tabs > .ant-tabs-nav'));
if (sidebar && appearance && tabs) {
const maxHeight = sidebar ? sidebar.clientHeight : 0;
const appearanceHeight = appearance ? appearance.clientHeight : 0;
const tabsHeight = tabs ? tabs.clientHeight : 0;
return maxHeight - appearanceHeight - tabsHeight;
}
return 0;
}
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
const { const {
annotation: { appearanceCollapsed }, annotation: {
appearanceCollapsed,
job: { instance: jobInstance },
},
settings: { settings: {
shapes: { shapes: {
colorBy, opacity, selectedOpacity, outlined, outlineColor, showBitmap, showProjections, colorBy, opacity, selectedOpacity, outlined, outlineColor, showBitmap, showProjections,
@ -83,6 +69,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
outlineColor, outlineColor,
showBitmap, showBitmap,
showProjections, showProjections,
jobInstance,
}; };
} }
@ -90,19 +77,6 @@ function mapDispatchToProps(dispatch: Dispatch<AnyAction>): DispatchToProps {
return { return {
collapseAppearance(): void { collapseAppearance(): void {
dispatch(collapseAppearanceAction()); dispatch(collapseAppearanceAction());
const [collapser] = window.document.getElementsByClassName('cvat-objects-appearance-collapse');
if (collapser) {
const listener = (event: Event): void => {
if ((event as TransitionEvent).propertyName === 'height') {
const height = computeHeight();
dispatch(updateTabContentHeightAction(height));
collapser.removeEventListener('transitionend', listener);
}
};
collapser.addEventListener('transitionend', listener);
}
}, },
changeShapesColorBy(event: RadioChangeEvent): void { changeShapesColorBy(event: RadioChangeEvent): void {
dispatch(changeShapesColorByAction(event.target.value)); dispatch(changeShapesColorByAction(event.target.value));
@ -144,8 +118,11 @@ function AppearanceBlock(props: Props): JSX.Element {
changeShapesOutlinedBorders, changeShapesOutlinedBorders,
changeShowBitmap, changeShowBitmap,
changeShowProjections, changeShowProjections,
jobInstance,
} = props; } = props;
const is2D = jobInstance.task.dimension === DimensionType.DIM_2D;
return ( return (
<Collapse <Collapse
onChange={collapseAppearance} onChange={collapseAppearance}
@ -206,20 +183,24 @@ function AppearanceBlock(props: Props): JSX.Element {
</Button> </Button>
</ColorPicker> </ColorPicker>
</Checkbox> </Checkbox>
<Checkbox {is2D && (
className='cvat-appearance-bitmap-checkbox' <Checkbox
onChange={changeShowBitmap} className='cvat-appearance-bitmap-checkbox'
checked={showBitmap} onChange={changeShowBitmap}
> checked={showBitmap}
Show bitmap >
</Checkbox> Show bitmap
<Checkbox </Checkbox>
className='cvat-appearance-cuboid-projections-checkbox' )}
onChange={changeShowProjections} {is2D && (
checked={showProjections} <Checkbox
> className='cvat-appearance-cuboid-projections-checkbox'
Show projections onChange={changeShowProjections}
</Checkbox> checked={showProjections}
>
Show projections
</Checkbox>
)}
</div> </div>
</Collapse.Panel> </Collapse.Panel>
</Collapse> </Collapse>

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React, { useEffect, useState } from 'react'; import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
import { SelectValue } from 'antd/lib/select'; import { SelectValue } from 'antd/lib/select';
@ -10,6 +10,7 @@ import Layout, { SiderProps } from 'antd/lib/layout';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { LogType } from 'cvat-logger'; import { LogType } from 'cvat-logger';
import { import {
activateObject as activateObjectAction, activateObject as activateObjectAction,
@ -20,6 +21,7 @@ import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { ThunkDispatch } from 'utils/redux'; import { ThunkDispatch } from 'utils/redux';
import AppearanceBlock from 'components/annotation-page/appearance-block'; import AppearanceBlock from 'components/annotation-page/appearance-block';
import ObjectButtonsContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-buttons'; import ObjectButtonsContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-buttons';
import { adjustContextImagePosition } from 'components/annotation-page/standard-workspace/context-image/context-image';
import { CombinedState, ObjectType } from 'reducers/interfaces'; import { CombinedState, ObjectType } from 'reducers/interfaces';
import AttributeEditor from './attribute-editor'; import AttributeEditor from './attribute-editor';
import AttributeSwitcher from './attribute-switcher'; import AttributeSwitcher from './attribute-switcher';
@ -34,7 +36,7 @@ interface StateToProps {
jobInstance: any; jobInstance: any;
keyMap: KeyMap; keyMap: KeyMap;
normalizedKeyMap: Record<string, string>; normalizedKeyMap: Record<string, string>;
canvasInstance: Canvas; canvasInstance: Canvas | Canvas3d;
canvasIsReady: boolean; canvasIsReady: boolean;
curZLayer: number; curZLayer: number;
} }
@ -134,6 +136,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
(collapser as HTMLElement).addEventListener('transitionend', listener as any); (collapser as HTMLElement).addEventListener('transitionend', listener as any);
} }
adjustContextImagePosition(!sidebarCollapsed);
setSidebarCollapsed(!sidebarCollapsed); setSidebarCollapsed(!sidebarCollapsed);
}; };

@ -255,7 +255,7 @@ function AttributeEditor(props: Props): JSX.Element {
const { inputType, values, id: attrID } = attribute; const { inputType, values, id: attrID } = attribute;
return ( return (
<div> <div className='attribute-annotations-sidebar-attribute-editor'>
{renderList({ values, inputType, onChange })} {renderList({ values, inputType, onChange })}
<hr /> <hr />
{renderInputElement({ {renderInputElement({

@ -8,9 +8,14 @@
height: 100%; height: 100%;
} }
.attribute-annotation-sidebar { .attribute-annotation-sidebar:not(.ant-layout-sider-collapsed) {
background: $background-color-2; background: $background-color-2;
padding: 5px; padding: 5px;
> .ant-layout-sider-children {
display: flex;
flex-direction: column;
}
} }
.cvat-attribute-annotation-sidebar-object-switcher, .cvat-attribute-annotation-sidebar-object-switcher,
@ -43,6 +48,11 @@
.attribute-annotations-sidebar-not-found-wrapper { .attribute-annotations-sidebar-not-found-wrapper {
margin-top: 20px; margin-top: 20px;
text-align: center; text-align: center;
flex-grow: 10;
}
.attribute-annotations-sidebar-attribute-editor {
flex-grow: 10;
} }
.attribute-annotation-sidebar-attr-list-wrapper { .attribute-annotation-sidebar-attr-list-wrapper {

@ -14,10 +14,12 @@ import {
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { LogType } from 'cvat-logger'; import { LogType } from 'cvat-logger';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import consts from 'consts'; import consts from 'consts';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
import ImageSetupsContent from './image-setups-content'; import ImageSetupsContent from './image-setups-content';
import ContextImage from '../standard-workspace/context-image/context-image';
const cvat = getCore(); const cvat = getCore();
@ -25,7 +27,7 @@ const MAX_DISTANCE_TO_OPEN_SHAPE = 50;
interface Props { interface Props {
sidebarCollapsed: boolean; sidebarCollapsed: boolean;
canvasInstance: Canvas; canvasInstance: Canvas | Canvas3d;
jobInstance: any; jobInstance: any;
activatedStateID: number | null; activatedStateID: number | null;
activatedAttributeID: number | null; activatedAttributeID: number | null;
@ -102,10 +104,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
automaticBordering, automaticBordering,
intelligentPolygonCrop, intelligentPolygonCrop,
showObjectsTextAlways, showObjectsTextAlways,
canvasInstance,
workspace, workspace,
showProjections, showProjections,
} = this.props; } = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
// It's awful approach from the point of view React // It's awful approach from the point of view React
// But we do not have another way because cvat-canvas returns regular DOM element // But we do not have another way because cvat-canvas returns regular DOM element
@ -138,7 +140,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
frameData, frameData,
frameAngle, frameAngle,
annotations, annotations,
canvasInstance,
sidebarCollapsed, sidebarCollapsed,
activatedStateID, activatedStateID,
curZLayer, curZLayer,
@ -160,7 +161,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasBackgroundColor, canvasBackgroundColor,
onFetchAnnotation, onFetchAnnotation,
} = this.props; } = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
if ( if (
prevProps.showObjectsTextAlways !== showObjectsTextAlways || prevProps.showObjectsTextAlways !== showObjectsTextAlways ||
prevProps.automaticBordering !== automaticBordering || prevProps.automaticBordering !== automaticBordering ||
@ -305,7 +306,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
const { canvasInstance } = this.props; const { canvasInstance } = this.props as { canvasInstance: Canvas };
canvasInstance.html().removeEventListener('mousedown', this.onCanvasMouseDown); canvasInstance.html().removeEventListener('mousedown', this.onCanvasMouseDown);
canvasInstance.html().removeEventListener('click', this.onCanvasClicked); canvasInstance.html().removeEventListener('click', this.onCanvasClicked);
@ -431,7 +432,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}; };
private onCanvasClicked = (): void => { private onCanvasClicked = (): void => {
const { canvasInstance, onUpdateContextMenu } = this.props; const { onUpdateContextMenu } = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
onUpdateContextMenu(false, 0, 0, ContextMenuType.CANVAS_SHAPE); onUpdateContextMenu(false, 0, 0, ContextMenuType.CANVAS_SHAPE);
if (!canvasInstance.html().contains(document.activeElement) && document.activeElement instanceof HTMLElement) { if (!canvasInstance.html().contains(document.activeElement) && document.activeElement instanceof HTMLElement) {
document.activeElement.blur(); document.activeElement.blur();
@ -561,7 +563,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}; };
private onCanvasFindObject = async (e: any): Promise<void> => { private onCanvasFindObject = async (e: any): Promise<void> => {
const { jobInstance, canvasInstance } = this.props; const { jobInstance } = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
const result = await jobInstance.annotations.select(e.detail.states, e.detail.x, e.detail.y); const result = await jobInstance.annotations.select(e.detail.states, e.detail.x, e.detail.y);
@ -595,12 +598,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { const {
activatedStateID, activatedStateID,
activatedAttributeID, activatedAttributeID,
canvasInstance,
selectedOpacity, selectedOpacity,
aamZoomMargin, aamZoomMargin,
workspace, workspace,
annotations, annotations,
} = this.props; } = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
if (activatedStateID !== null) { if (activatedStateID !== null) {
const [activatedState] = annotations.filter((state: any): boolean => state.clientID === activatedStateID); const [activatedState] = annotations.filter((state: any): boolean => state.clientID === activatedStateID);
@ -652,7 +655,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
private updateIssueRegions(): void { private updateIssueRegions(): void {
const { canvasInstance, frameIssues } = this.props; const { frameIssues } = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
if (frameIssues === null) { if (frameIssues === null) {
canvasInstance.setupIssueRegions({}); canvasInstance.setupIssueRegions({});
} else { } else {
@ -687,12 +691,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
gridSize, gridSize,
gridColor, gridColor,
gridOpacity, gridOpacity,
canvasInstance,
brightnessLevel, brightnessLevel,
contrastLevel, contrastLevel,
saturationLevel, saturationLevel,
canvasBackgroundColor, canvasBackgroundColor,
} = this.props; } = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
// Size // Size
window.addEventListener('resize', this.fitCanvas); window.addEventListener('resize', this.fitCanvas);
@ -773,12 +777,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
maxZLayer, maxZLayer,
curZLayer, curZLayer,
minZLayer, minZLayer,
onSwitchZLayer,
onAddZLayer,
keyMap, keyMap,
switchableAutomaticBordering, switchableAutomaticBordering,
automaticBordering, automaticBordering,
onSwitchAutomaticBordering, onSwitchAutomaticBordering,
onSwitchZLayer,
onAddZLayer,
} = this.props; } = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => { const preventDefault = (event: KeyboardEvent | undefined): void => {
@ -817,6 +821,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}} }}
/> />
<ContextImage />
<Dropdown trigger='click' placement='topCenter' overlay={<ImageSetupsContent />}> <Dropdown trigger='click' placement='topCenter' overlay={<ImageSetupsContent />}>
<UpOutlined className='cvat-canvas-image-setups-trigger' /> <UpOutlined className='cvat-canvas-image-setups-trigger' />
</Dropdown> </Dropdown>

@ -10,33 +10,56 @@ import {
ArrowDownOutlined, ArrowLeftOutlined, ArrowRightOutlined, ArrowUpOutlined, ArrowDownOutlined, ArrowLeftOutlined, ArrowRightOutlined, ArrowUpOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { ResizableBox } from 'react-resizable'; import { ResizableBox } from 'react-resizable';
import { Workspace } from 'reducers/interfaces';
import { import {
CAMERA_ACTION, Canvas3d, MouseInteraction, ViewType, ColorBy, ContextMenuType, ObjectType, Workspace,
} from 'reducers/interfaces';
import {
CameraAction, Canvas3d, ViewType, ViewsDOM,
} from 'cvat-canvas3d-wrapper'; } from 'cvat-canvas3d-wrapper';
import ContextImage from '../standard3D-workspace/context-image/context-image'; import { Canvas } from 'cvat-canvas-wrapper';
import CVATTooltip from '../../common/cvat-tooltip'; import ContextImage from 'components/annotation-page/standard-workspace/context-image/context-image';
import CVATTooltip from 'components/common/cvat-tooltip';
import { LogType } from 'cvat-logger';
import getCore from 'cvat-core-wrapper';
const cvat = getCore();
interface Props { interface Props {
canvasInstance: Canvas3d; opacity: number;
selectedOpacity: number;
outlined: boolean;
outlineColor: string;
colorBy: ColorBy;
frameFetching: boolean;
canvasInstance: Canvas3d | Canvas;
jobInstance: any; jobInstance: any;
frameData: any; frameData: any;
curZLayer: number; curZLayer: number;
contextImageHide: boolean;
loaded: boolean;
data: string;
annotations: any[]; annotations: any[];
contextMenuVisibility: boolean;
activeLabelID: number;
activatedStateID: number | null;
activeObjectType: ObjectType;
onSetupCanvas: () => void; onSetupCanvas: () => void;
getContextImage(): void; onGroupObjects: (enabled: boolean) => void;
onResetCanvas(): void; onResetCanvas(): void;
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onActivateObject(activatedStateID: number | null): void;
onUpdateAnnotations(states: any[]): void;
onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onEditShape: (enabled: boolean) => void;
onDragCanvas: (enabled: boolean) => void;
onShapeDrawn: () => void;
workspace: Workspace; workspace: Workspace;
animateID: any;
automaticBordering: boolean; automaticBordering: boolean;
showObjectsTextAlways: boolean; showObjectsTextAlways: boolean;
frame: number;
} }
interface ViewSize { interface ViewSize {
fullHeight: number; fullHeight: number;
fullWidth: number;
vertical: number; vertical: number;
top: number; top: number;
side: number; side: number;
@ -45,7 +68,7 @@ interface ViewSize {
function viewSizeReducer( function viewSizeReducer(
state: ViewSize, state: ViewSize,
action: { type: ViewType | 'set'; e?: SyntheticEvent; data?: ViewSize }, action: { type: ViewType | 'set' | 'resize'; e?: SyntheticEvent; data?: ViewSize },
): ViewSize { ): ViewSize {
const event = (action.e as unknown) as MouseEvent; const event = (action.e as unknown) as MouseEvent;
const canvas3dContainer = document.getElementById('canvas3d-container'); const canvas3dContainer = document.getElementById('canvas3d-container');
@ -98,6 +121,33 @@ function viewSizeReducer(
}; };
case 'set': case 'set':
return action.data as ViewSize; return action.data as ViewSize;
case 'resize': {
const canvasPerspectiveContainer = document.getElementById('cvat-canvas3d-perspective');
let midState = { ...state };
if (canvasPerspectiveContainer) {
if (state.fullHeight !== canvas3dContainer.clientHeight) {
const diff = canvas3dContainer.clientHeight - state.fullHeight;
midState = {
...midState,
fullHeight: canvas3dContainer.clientHeight,
vertical: state.vertical + diff,
};
}
if (state.fullWidth !== canvasPerspectiveContainer.clientWidth) {
const oldWidth = state.fullWidth;
const width = canvasPerspectiveContainer.clientWidth;
midState = {
...midState,
fullWidth: width,
top: (state.top / oldWidth) * width,
side: (state.side / oldWidth) * width,
front: (state.front / oldWidth) * width,
};
}
return midState;
}
return state;
}
default: default:
throw new Error(); throw new Error();
} }
@ -109,6 +159,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
const animateId = useRef(0); const animateId = useRef(0);
const [viewSize, setViewSize] = useReducer(viewSizeReducer, { const [viewSize, setViewSize] = useReducer(viewSizeReducer, {
fullHeight: 0, fullHeight: 0,
fullWidth: 0,
vertical: 0, vertical: 0,
top: 0, top: 0,
side: 0, side: 0,
@ -120,71 +171,122 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
const frontView = useRef<HTMLDivElement | null>(null); const frontView = useRef<HTMLDivElement | null>(null);
const { const {
frameData, contextImageHide, getContextImage, loaded, data, annotations, curZLayer, opacity,
outlined,
outlineColor,
selectedOpacity,
colorBy,
contextMenuVisibility,
frameData,
onResetCanvas,
onSetupCanvas,
annotations,
frame,
jobInstance,
activeLabelID,
activeObjectType,
onShapeDrawn,
onCreateAnnotations,
frameFetching,
} = props; } = props;
const { canvasInstance } = props as { canvasInstance: Canvas3d };
const onCanvasSetup = (): void => { const onCanvasSetup = (): void => {
const { onSetupCanvas } = props;
onSetupCanvas(); onSetupCanvas();
}; };
const animateCanvas = (): void => { const onCanvasDragStart = (): void => {
const { canvasInstance } = props; const { onDragCanvas } = props;
onDragCanvas(true);
};
const onCanvasDragDone = (): void => {
const { onDragCanvas } = props;
onDragCanvas(false);
};
const animateCanvas = (): void => {
canvasInstance.render(); canvasInstance.render();
animateId.current = requestAnimationFrame(animateCanvas); animateId.current = requestAnimationFrame(animateCanvas);
}; };
const updateCanvas = (): void => { const updateCanvas = (): void => {
const { canvasInstance } = props;
if (frameData !== null) { if (frameData !== null) {
canvasInstance.setup(frameData); canvasInstance.setup(
frameData,
annotations.filter((e) => e.objectType !== ObjectType.TAG),
);
} }
}; };
const onMouseClick = (event: MouseEvent): void => { const onCanvasCancel = (): void => {
const { canvasInstance } = props; onResetCanvas();
canvasInstance.mouseControls(MouseInteraction.CLICK, event);
}; };
const onMouseDoubleClick = (event: MouseEvent): void => { const onCanvasShapeDrawn = (event: any): void => {
const { canvasInstance } = props; if (!event.detail.continue) {
canvasInstance.mouseControls(MouseInteraction.DOUBLE_CLICK, event); onShapeDrawn();
}; }
const { state, duration } = event.detail;
const isDrawnFromScratch = !state.label;
if (isDrawnFromScratch) {
jobInstance.logger.log(LogType.drawObject, { count: 1, duration });
} else {
jobInstance.logger.log(LogType.pasteObject, { count: 1, duration });
}
const onMouseHover = (event: MouseEvent): void => { state.objectType = state.objectType || activeObjectType;
const { canvasInstance } = props; state.label = state.label || jobInstance.task.labels.filter((label: any) => label.id === activeLabelID)[0];
canvasInstance.mouseControls(MouseInteraction.HOVER, event); state.occluded = state.occluded || false;
state.frame = frame;
state.zOrder = 0;
const objectState = new cvat.classes.ObjectState(state);
onCreateAnnotations(jobInstance, frame, [objectState]);
}; };
const onCanvasCancel = (): void => { const onCanvasClick = (e: MouseEvent): void => {
const { onResetCanvas } = props; const { onUpdateContextMenu } = props;
onResetCanvas(); if (contextMenuVisibility) {
onUpdateContextMenu(false, e.clientX, e.clientY, ContextMenuType.CANVAS_SHAPE);
}
}; };
const initialSetup = (): void => { const initialSetup = (): void => {
const { canvasInstance } = props; const canvasInstanceDOM = canvasInstance.html() as ViewsDOM;
const canvasInstanceDOM = canvasInstance.html();
// Events
canvasInstanceDOM.perspective.addEventListener('canvas.setup', onCanvasSetup); canvasInstanceDOM.perspective.addEventListener('canvas.setup', onCanvasSetup);
canvasInstanceDOM.perspective.addEventListener('mousemove', onMouseHover);
canvasInstanceDOM.perspective.addEventListener('canvas.canceled', onCanvasCancel); canvasInstanceDOM.perspective.addEventListener('canvas.canceled', onCanvasCancel);
canvasInstanceDOM.perspective.addEventListener(MouseInteraction.DOUBLE_CLICK, onMouseDoubleClick); canvasInstanceDOM.perspective.addEventListener('canvas.dragstart', onCanvasDragStart);
canvasInstanceDOM.perspective.addEventListener(MouseInteraction.CLICK, onMouseClick); canvasInstanceDOM.perspective.addEventListener('canvas.dragstop', onCanvasDragDone);
}; };
const keyControls = (key: KeyboardEvent): void => { const keyControlsKeyDown = (key: KeyboardEvent): void => {
const { canvasInstance } = props;
canvasInstance.keyControls(key); canvasInstance.keyControls(key);
}; };
useEffect(() => { const keyControlsKeyUp = (key: KeyboardEvent): void => {
const { canvasInstance } = props; if (key.code === 'ControlLeft') {
canvasInstance.keyControls(key);
}
};
const canvasInstanceDOM = canvasInstance.html(); const onCanvasShapeSelected = (event: any): void => {
const { onActivateObject } = props;
const { clientID } = event.detail;
onActivateObject(clientID);
canvasInstance.activate(clientID);
};
const onCanvasEditDone = (event: any): void => {
const { onEditShape, onUpdateAnnotations } = props;
onEditShape(false);
const { state, points } = event.detail;
state.points = points;
onUpdateAnnotations([state]);
};
useEffect(() => {
const canvasInstanceDOM = canvasInstance.html();
if ( if (
perspectiveView && perspectiveView &&
perspectiveView.current && perspectiveView.current &&
@ -206,6 +308,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
type: 'set', type: 'set',
data: { data: {
fullHeight: canvas3dContainer.clientHeight, fullHeight: canvas3dContainer.clientHeight,
fullWidth: canvas3dContainer.clientWidth,
vertical: canvas3dContainer.clientHeight / 2, vertical: canvas3dContainer.clientHeight / 2,
top: width, top: width,
side: width, side: width,
@ -215,7 +318,8 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
} }
} }
document.addEventListener('keydown', keyControls); document.addEventListener('keydown', keyControlsKeyDown);
document.addEventListener('keyup', keyControlsKeyUp);
initialSetup(); initialSetup();
updateCanvas(); updateCanvas();
@ -223,29 +327,89 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
return () => { return () => {
canvasInstanceDOM.perspective.removeEventListener('canvas.setup', onCanvasSetup); canvasInstanceDOM.perspective.removeEventListener('canvas.setup', onCanvasSetup);
canvasInstanceDOM.perspective.removeEventListener('mousemove', onMouseHover);
canvasInstanceDOM.perspective.removeEventListener('canvas.canceled', onCanvasCancel); canvasInstanceDOM.perspective.removeEventListener('canvas.canceled', onCanvasCancel);
canvasInstanceDOM.perspective.removeEventListener(MouseInteraction.DOUBLE_CLICK, onMouseDoubleClick); canvasInstanceDOM.perspective.removeEventListener('canvas.dragstart', onCanvasDragStart);
canvasInstanceDOM.perspective.removeEventListener(MouseInteraction.CLICK, onMouseClick); canvasInstanceDOM.perspective.removeEventListener('canvas.dragstop', onCanvasDragDone);
document.removeEventListener('keydown', keyControls); document.removeEventListener('keydown', keyControlsKeyDown);
document.removeEventListener('keyup', keyControlsKeyUp);
cancelAnimationFrame(animateId.current); cancelAnimationFrame(animateId.current);
}; };
}, []); }, []);
const updateShapesView = (): void => {
(canvasInstance as Canvas3d).configureShapes({
opacity,
outlined,
outlineColor,
selectedOpacity,
colorBy,
});
};
const onContextMenu = (event: any): void => {
const { onUpdateContextMenu, onActivateObject } = props;
onActivateObject(event.detail.clientID);
onUpdateContextMenu(
event.detail.clientID !== null,
event.detail.clientX,
event.detail.clientY,
ContextMenuType.CANVAS_SHAPE,
);
};
const onResize = (): void => {
setViewSize({
type: 'resize',
});
};
const onCanvasObjectsGroupped = (event: any): void => {
const { onGroupAnnotations, onGroupObjects } = props;
onGroupObjects(false);
const { states } = event.detail;
onGroupAnnotations(jobInstance, frame, states);
};
useEffect(() => { useEffect(() => {
updateShapesView();
}, [opacity, outlined, outlineColor, selectedOpacity, colorBy]);
useEffect(() => {
const canvasInstanceDOM = canvasInstance.html() as ViewsDOM;
updateCanvas(); updateCanvas();
}, [frameData, annotations, curZLayer]); canvasInstanceDOM.perspective.addEventListener('canvas.drawn', onCanvasShapeDrawn);
canvasInstanceDOM.perspective.addEventListener('canvas.selected', onCanvasShapeSelected);
canvasInstanceDOM.perspective.addEventListener('canvas.edited', onCanvasEditDone);
canvasInstanceDOM.perspective.addEventListener('canvas.contextmenu', onContextMenu);
canvasInstanceDOM.perspective.addEventListener('click', onCanvasClick);
canvasInstanceDOM.perspective.addEventListener('canvas.fit', onResize);
canvasInstanceDOM.perspective.addEventListener('canvas.groupped', onCanvasObjectsGroupped);
window.addEventListener('resize', onResize);
return () => {
canvasInstanceDOM.perspective.removeEventListener('canvas.drawn', onCanvasShapeDrawn);
canvasInstanceDOM.perspective.removeEventListener('canvas.selected', onCanvasShapeSelected);
canvasInstanceDOM.perspective.removeEventListener('canvas.edited', onCanvasEditDone);
canvasInstanceDOM.perspective.removeEventListener('canvas.contextmenu', onContextMenu);
canvasInstanceDOM.perspective.removeEventListener('click', onCanvasClick);
canvasInstanceDOM.perspective.removeEventListener('canvas.fit', onResize);
canvasInstanceDOM.perspective.removeEventListener('canvas.groupped', onCanvasObjectsGroupped);
window.removeEventListener('resize', onResize);
};
}, [frameData, annotations, activeLabelID, contextMenuVisibility]);
const screenKeyControl = (code: CAMERA_ACTION): void => { const screenKeyControl = (code: CameraAction, altKey: boolean, shiftKey: boolean): void => {
const { canvasInstance } = props; canvasInstance.keyControls(new KeyboardEvent('keydown', { code, altKey, shiftKey }));
canvasInstance.keyControls(new KeyboardEvent('keydown', { code, altKey: true }));
}; };
const ArrowGroup = (): ReactElement => ( const ArrowGroup = (): ReactElement => (
<span className='cvat-canvas3d-perspective-arrow-directions'> <span className='cvat-canvas3d-perspective-arrow-directions'>
<CVATTooltip title='Arrow Up' placement='topRight'> <CVATTooltip title='Shift+Arrow Up' placement='topRight'>
<button <button
onClick={() => screenKeyControl(CAMERA_ACTION.TILT_UP)} data-cy='arrow-up'
onClick={() => screenKeyControl(CameraAction.TILT_UP, false, true)}
type='button' type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-up' className='cvat-canvas3d-perspective-arrow-directions-icons-up'
> >
@ -253,27 +417,27 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</button> </button>
</CVATTooltip> </CVATTooltip>
<br /> <br />
<CVATTooltip title='Arrow Left' placement='topRight'> <CVATTooltip title='Shift+Arrow Left' placement='topRight'>
<button <button
onClick={() => screenKeyControl(CAMERA_ACTION.ROTATE_LEFT)} onClick={() => screenKeyControl(CameraAction.ROTATE_LEFT, false, true)}
type='button' type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom' className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
> >
<ArrowLeftOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' /> <ArrowLeftOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
</button> </button>
</CVATTooltip> </CVATTooltip>
<CVATTooltip title='Arrow Bottom' placement='topRight'> <CVATTooltip title='Shift+Arrow Bottom' placement='topRight'>
<button <button
onClick={() => screenKeyControl(CAMERA_ACTION.TILT_DOWN)} onClick={() => screenKeyControl(CameraAction.TILT_DOWN, false, true)}
type='button' type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom' className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
> >
<ArrowDownOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' /> <ArrowDownOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
</button> </button>
</CVATTooltip> </CVATTooltip>
<CVATTooltip title='Arrow Right' placement='topRight'> <CVATTooltip title='Shift+Arrow Right' placement='topRight'>
<button <button
onClick={() => screenKeyControl(CAMERA_ACTION.ROTATE_RIGHT)} onClick={() => screenKeyControl(CameraAction.ROTATE_RIGHT, false, true)}
type='button' type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom' className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
> >
@ -287,7 +451,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
<span className='cvat-canvas3d-perspective-directions'> <span className='cvat-canvas3d-perspective-directions'>
<CVATTooltip title='Alt+U' placement='topLeft'> <CVATTooltip title='Alt+U' placement='topLeft'>
<button <button
onClick={() => screenKeyControl(CAMERA_ACTION.MOVE_UP)} onClick={() => screenKeyControl(CameraAction.MOVE_UP, true, false)}
type='button' type='button'
className='cvat-canvas3d-perspective-directions-icon' className='cvat-canvas3d-perspective-directions-icon'
> >
@ -296,7 +460,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</CVATTooltip> </CVATTooltip>
<CVATTooltip title='Alt+I' placement='topLeft'> <CVATTooltip title='Alt+I' placement='topLeft'>
<button <button
onClick={() => screenKeyControl(CAMERA_ACTION.ZOOM_IN)} onClick={() => screenKeyControl(CameraAction.ZOOM_IN, true, false)}
type='button' type='button'
className='cvat-canvas3d-perspective-directions-icon' className='cvat-canvas3d-perspective-directions-icon'
> >
@ -305,7 +469,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</CVATTooltip> </CVATTooltip>
<CVATTooltip title='Alt+O' placement='topLeft'> <CVATTooltip title='Alt+O' placement='topLeft'>
<button <button
onClick={() => screenKeyControl(CAMERA_ACTION.MOVE_DOWN)} onClick={() => screenKeyControl(CameraAction.MOVE_DOWN, true, false)}
type='button' type='button'
className='cvat-canvas3d-perspective-directions-icon' className='cvat-canvas3d-perspective-directions-icon'
> >
@ -315,7 +479,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
<br /> <br />
<CVATTooltip title='Alt+J' placement='topLeft'> <CVATTooltip title='Alt+J' placement='topLeft'>
<button <button
onClick={() => screenKeyControl(CAMERA_ACTION.MOVE_LEFT)} onClick={() => screenKeyControl(CameraAction.MOVE_LEFT, true, false)}
type='button' type='button'
className='cvat-canvas3d-perspective-directions-icon' className='cvat-canvas3d-perspective-directions-icon'
> >
@ -324,7 +488,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</CVATTooltip> </CVATTooltip>
<CVATTooltip title='Alt+K' placement='topLeft'> <CVATTooltip title='Alt+K' placement='topLeft'>
<button <button
onClick={() => screenKeyControl(CAMERA_ACTION.ZOOM_OUT)} onClick={() => screenKeyControl(CameraAction.ZOOM_OUT, true, false)}
type='button' type='button'
className='cvat-canvas3d-perspective-directions-icon' className='cvat-canvas3d-perspective-directions-icon'
> >
@ -333,7 +497,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</CVATTooltip> </CVATTooltip>
<CVATTooltip title='Alt+L' placement='topLeft'> <CVATTooltip title='Alt+L' placement='topLeft'>
<button <button
onClick={() => screenKeyControl(CAMERA_ACTION.MOVE_RIGHT)} onClick={() => screenKeyControl(CameraAction.MOVE_RIGHT, true, false)}
type='button' type='button'
className='cvat-canvas3d-perspective-directions-icon' className='cvat-canvas3d-perspective-directions-icon'
> >
@ -345,13 +509,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
return ( return (
<Layout.Content className='cvat-canvas3d-fullsize' id='canvas3d-container'> <Layout.Content className='cvat-canvas3d-fullsize' id='canvas3d-container'>
<ContextImage <ContextImage />
frame={frameData}
contextImageHide={contextImageHide}
getContextImage={getContextImage}
loaded={loaded}
data={data}
/>
<ResizableBox <ResizableBox
className='cvat-resizable' className='cvat-resizable'
width={Infinity} width={Infinity}
@ -360,7 +518,12 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
handle={<span className='cvat-resizable-handle-horizontal' />} handle={<span className='cvat-resizable-handle-horizontal' />}
onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.PERSPECTIVE, e })} onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.PERSPECTIVE, e })}
> >
<div className='cvat-canvas3d-perspective'> {frameFetching ? (
<svg id='cvat_canvas_loading_animation'>
<circle id='cvat_canvas_loading_circle' r='30' cx='50%' cy='50%' />
</svg>
) : null}
<div className='cvat-canvas3d-perspective' id='cvat-canvas3d-perspective'>
<div className='cvat-canvas-container cvat-canvas-container-overflow' ref={perspectiveView} /> <div className='cvat-canvas-container cvat-canvas-container-overflow' ref={perspectiveView} />
<ArrowGroup /> <ArrowGroup />
<ControlGroup /> <ControlGroup />

@ -0,0 +1,89 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useEffect, useState } from 'react';
import { notification } from 'antd';
import { useDispatch, useSelector } from 'react-redux';
import { QuestionCircleOutlined, ShrinkOutlined } from '@ant-design/icons';
import Spin from 'antd/lib/spin';
import Image from 'antd/lib/image';
import { CombinedState } from 'reducers/interfaces';
import { hideShowContextImage, getContextImageAsync } from 'actions/annotation-actions';
import CVATTooltip from 'components/common/cvat-tooltip';
export function adjustContextImagePosition(sidebarCollapsed: boolean): void {
const element = window.document.getElementsByClassName('cvat-context-image-wrapper')[0] as
| HTMLDivElement
| undefined;
if (element) {
if (sidebarCollapsed) {
element.style.right = '40px';
} else {
element.style.right = '';
}
}
}
function ContextImage(): JSX.Element | null {
const dispatch = useDispatch();
const { number: frame, hasRelatedContext } = useSelector((state: CombinedState) => state.annotation.player.frame);
const { data: contextImageData, hidden: contextImageHidden, fetching: contextImageFetching } = useSelector(
(state: CombinedState) => state.annotation.player.contextImage,
);
const [requested, setRequested] = useState(false);
useEffect(() => {
if (requested) {
setRequested(false);
}
}, [frame, contextImageData]);
useEffect(() => {
if (hasRelatedContext && !contextImageHidden && !requested) {
dispatch(getContextImageAsync());
setRequested(true);
}
}, [contextImageHidden, requested, hasRelatedContext]);
if (!hasRelatedContext) {
return null;
}
return (
<div className='cvat-context-image-wrapper' {...(contextImageHidden ? { style: { width: '32px' } } : {})}>
<div className='cvat-context-image-wrapper-header' />
{contextImageFetching ? <Spin size='small' /> : null}
{contextImageHidden ? (
<CVATTooltip title='A context image is available'>
<QuestionCircleOutlined
className='cvat-context-image-switcher'
onClick={() => dispatch(hideShowContextImage(false))}
/>
</CVATTooltip>
) : (
<>
<ShrinkOutlined
className='cvat-context-image-switcher'
onClick={() => dispatch(hideShowContextImage(true))}
/>
<Image
{...(contextImageData ? { src: contextImageData } : {})}
onError={() => {
notification.error({
message: 'Could not display context image',
description: `Source is ${
contextImageData === null ? 'empty' : contextImageData.slice(0, 100)
}`,
});
}}
className='cvat-context-image'
/>
</>
)}
</div>
);
}
export default React.memo(ContextImage);

@ -0,0 +1,76 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { CSSProperties } from 'react';
import ReactDOM from 'react-dom';
import Text from 'antd/lib/typography/Text';
import Slider from 'antd/lib/slider';
import { Col, Row } from 'antd/lib/grid';
interface Props {
approxPolyAccuracy: number;
onChange(value: number): void;
}
export const MAX_ACCURACY = 13;
export const marks: Record<number, { style: CSSProperties; label: JSX.Element }> = {};
marks[0] = {
style: {
color: '#1890ff',
},
label: <strong>less</strong>,
};
marks[MAX_ACCURACY] = {
style: {
color: '#61c200',
},
label: <strong>more</strong>,
};
export function thresholdFromAccuracy(approxPolyAccuracy: number): number {
const approxPolyMaxDistance = MAX_ACCURACY - approxPolyAccuracy;
let threshold = 0;
if (approxPolyMaxDistance > 0) {
if (approxPolyMaxDistance <= 8) {
// 2.75x+7y+1=0 linear made from two points (1; 0.25) and (8; 3)
threshold = (2.75 * approxPolyMaxDistance - 1) / 7;
} else {
// 4 for 9, 8 for 10, 16 for 11, 32 for 12, 64 for 13
threshold = 2 ** (approxPolyMaxDistance - 7);
}
}
return threshold;
}
function ApproximationAccuracy(props: Props): React.ReactPortal | null {
const { approxPolyAccuracy, onChange } = props;
const target = window.document.getElementsByClassName('cvat-canvas-container')[0];
return target ?
ReactDOM.createPortal(
<Row align='middle' className='cvat-approx-poly-threshold-wrapper'>
<Col span={5}>
<Text>Points: </Text>
</Col>
<Col offset={1} span={18}>
<Slider
value={approxPolyAccuracy}
min={0}
max={MAX_ACCURACY}
step={1}
dots
tooltipVisible={false}
onChange={onChange}
marks={marks}
/>
</Col>
</Row>,
target,
) :
null;
}
export default React.memo(ApproximationAccuracy);

@ -41,7 +41,7 @@ export function ExtraControlsControl(): JSX.Element {
> >
<SmallDashOutlined <SmallDashOutlined
style={{ visibility: hasChildren ? 'visible' : 'hidden' }} style={{ visibility: hasChildren ? 'visible' : 'hidden' }}
className='cvat-extra-controls-control' className='cvat-extra-controls-control cvat-antd-icon-control'
/> />
</Popover> </Popover>
); );
@ -76,7 +76,7 @@ export default function ControlVisibilityObserver<P = {}>(
visibilityHeightThreshold = wrapper.offsetTop + wrapper.offsetHeight; visibilityHeightThreshold = wrapper.offsetTop + wrapper.offsetHeight;
// start observing parent size // start observing parent size
observer.observe(ref.current.parentElement as HTMLElement); observer.observe(ref.current.parentElement as HTMLElement);
// then put it to extra controls if parent height is not enought // then put it to extra controls if parent height is not enough
setVisible(availableHeight - reservedHeight >= visibilityHeightThreshold); setVisible(availableHeight - reservedHeight >= visibilityHeightThreshold);
} }

@ -8,10 +8,11 @@ import Icon from '@ant-design/icons';
import { CursorIcon } from 'icons'; import { CursorIcon } from 'icons';
import { ActiveControl } from 'reducers/interfaces'; import { ActiveControl } from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
export interface Props { export interface Props {
canvasInstance: Canvas; canvasInstance: Canvas | Canvas3d;
cursorShortkey: string; cursorShortkey: string;
activeControl: ActiveControl; activeControl: ActiveControl;
} }

@ -7,6 +7,7 @@ import Popover from 'antd/lib/popover';
import Icon from '@ant-design/icons'; import Icon from '@ant-design/icons';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { ShapeType } from 'reducers/interfaces'; import { ShapeType } from 'reducers/interfaces';
import { CubeIcon } from 'icons'; import { CubeIcon } from 'icons';
@ -15,7 +16,7 @@ import DrawShapePopoverContainer from 'containers/annotation-page/standard-works
import withVisibilityHandling from './handle-popover-visibility'; import withVisibilityHandling from './handle-popover-visibility';
export interface Props { export interface Props {
canvasInstance: Canvas; canvasInstance: Canvas | Canvas3d;
isDrawing: boolean; isDrawing: boolean;
disabled?: boolean; disabled?: boolean;
} }

@ -8,16 +8,14 @@ import Button from 'antd/lib/button';
import InputNumber from 'antd/lib/input-number'; import InputNumber from 'antd/lib/input-number';
import Radio, { RadioChangeEvent } from 'antd/lib/radio'; import Radio, { RadioChangeEvent } from 'antd/lib/radio';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { Canvas, RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper'; import { RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { ShapeType } from 'reducers/interfaces'; import { DimensionType, ShapeType } from 'reducers/interfaces';
import { clamp } from 'utils/math'; import { clamp } from 'utils/math';
import LabelSelector from 'components/label-selector/label-selector'; import LabelSelector from 'components/label-selector/label-selector';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
interface Props { interface Props {
canvasInstance: Canvas | Canvas3d;
shapeType: ShapeType; shapeType: ShapeType;
labels: any[]; labels: any[];
minimumPoints: number; minimumPoints: number;
@ -32,6 +30,7 @@ interface Props {
onChangeCuboidDrawingMethod(event: RadioChangeEvent): void; onChangeCuboidDrawingMethod(event: RadioChangeEvent): void;
onDrawTrack(): void; onDrawTrack(): void;
onDrawShape(): void; onDrawShape(): void;
jobInstance: any;
} }
function DrawShapePopoverComponent(props: Props): JSX.Element { function DrawShapePopoverComponent(props: Props): JSX.Element {
@ -50,10 +49,10 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
onChangePoints, onChangePoints,
onChangeRectDrawingMethod, onChangeRectDrawingMethod,
onChangeCuboidDrawingMethod, onChangeCuboidDrawingMethod,
canvasInstance, jobInstance,
} = props; } = props;
const is2D = canvasInstance instanceof Canvas; const is2D = jobInstance.task.dimension === DimensionType.DIM_2D;
return ( return (
<div className='cvat-draw-shape-popover-content'> <div className='cvat-draw-shape-popover-content'>

@ -7,21 +7,29 @@ import Icon from '@ant-design/icons';
import { GroupIcon } from 'icons'; import { GroupIcon } from 'icons';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
import { ActiveControl } from 'reducers/interfaces'; import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { ActiveControl, DimensionType } from 'reducers/interfaces';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
export interface Props { export interface Props {
canvasInstance: Canvas; canvasInstance: Canvas | Canvas3d;
activeControl: ActiveControl; activeControl: ActiveControl;
switchGroupShortcut: string; switchGroupShortcut: string;
resetGroupShortcut: string; resetGroupShortcut: string;
disabled?: boolean; disabled?: boolean;
jobInstance?: any;
groupObjects(enabled: boolean): void; groupObjects(enabled: boolean): void;
} }
function GroupControl(props: Props): JSX.Element { function GroupControl(props: Props): JSX.Element {
const { const {
switchGroupShortcut, resetGroupShortcut, activeControl, canvasInstance, groupObjects, disabled, switchGroupShortcut,
resetGroupShortcut,
activeControl,
canvasInstance,
groupObjects,
disabled,
jobInstance,
} = props; } = props;
const dynamicIconProps = const dynamicIconProps =
@ -43,7 +51,9 @@ function GroupControl(props: Props): JSX.Element {
}; };
const title = [ const title = [
`Group shapes/tracks ${switchGroupShortcut}. `, `Group shapes${
jobInstance && jobInstance.task.dimension === DimensionType.DIM_3D ? '' : '/tracks'
} ${switchGroupShortcut}. `,
`Select and press ${resetGroupShortcut} to reset a group.`, `Select and press ${resetGroupShortcut} to reset a group.`,
].join(' '); ].join(' ');

@ -8,10 +8,11 @@ import Icon from '@ant-design/icons';
import { MoveIcon } from 'icons'; import { MoveIcon } from 'icons';
import { ActiveControl } from 'reducers/interfaces'; import { ActiveControl } from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
export interface Props { export interface Props {
canvasInstance: Canvas; canvasInstance: Canvas | Canvas3d;
activeControl: ActiveControl; activeControl: ActiveControl;
} }

@ -6,7 +6,7 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Popover from 'antd/lib/popover'; import Popover from 'antd/lib/popover';
import Icon, { ScissorOutlined } from '@ant-design/icons'; import Icon, { AreaChartOutlined, ScissorOutlined } from '@ant-design/icons';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import Tabs from 'antd/lib/tabs'; import Tabs from 'antd/lib/tabs';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
@ -19,16 +19,21 @@ import getCore from 'cvat-core-wrapper';
import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper'; import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper';
import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors'; import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors';
import { import {
CombinedState, ActiveControl, OpenCVTool, ObjectType, CombinedState, ActiveControl, OpenCVTool, ObjectType, ShapeType,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { import {
interactWithCanvas, interactWithCanvas,
fetchAnnotationsAsync, fetchAnnotationsAsync,
updateAnnotationsAsync, updateAnnotationsAsync,
createAnnotationsAsync, createAnnotationsAsync,
changeFrameAsync,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import LabelSelector from 'components/label-selector/label-selector'; import LabelSelector from 'components/label-selector/label-selector';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
import ApproximationAccuracy, {
thresholdFromAccuracy,
} from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy';
import { ImageProcessing } from 'utils/opencv-wrapper/opencv-interfaces';
import withVisibilityHandling from './handle-popover-visibility'; import withVisibilityHandling from './handle-popover-visibility';
interface Props { interface Props {
@ -39,6 +44,8 @@ interface Props {
states: any[]; states: any[];
frame: number; frame: number;
curZOrder: number; curZOrder: number;
defaultApproxPolyAccuracy: number;
frameData: any;
} }
interface DispatchToProps { interface DispatchToProps {
@ -46,6 +53,7 @@ interface DispatchToProps {
updateAnnotations(statesToUpdate: any[]): void; updateAnnotations(statesToUpdate: any[]): void;
createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void; createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void;
fetchAnnotations(): void; fetchAnnotations(): void;
changeFrame(toFrame: number, fillBuffer?: boolean, frameStep?: number, forceUpdate?: boolean):void;
} }
interface State { interface State {
@ -53,6 +61,13 @@ interface State {
initializationError: boolean; initializationError: boolean;
initializationProgress: number; initializationProgress: number;
activeLabelID: number; activeLabelID: number;
approxPolyAccuracy: number;
activeImageModifiers: ImageModifier[];
}
interface ImageModifier {
modifier: ImageProcessing,
alias: string
} }
const core = getCore(); const core = getCore();
@ -68,19 +83,24 @@ function mapStateToProps(state: CombinedState): Props {
job: { instance: jobInstance, labels }, job: { instance: jobInstance, labels },
canvas: { activeControl, instance: canvasInstance }, canvas: { activeControl, instance: canvasInstance },
player: { player: {
frame: { number: frame }, frame: { number: frame, data: frameData },
}, },
}, },
settings: {
workspace: { defaultApproxPolyAccuracy },
},
} = state; } = state;
return { return {
isActivated: activeControl === ActiveControl.OPENCV_TOOLS, isActivated: activeControl === ActiveControl.OPENCV_TOOLS,
canvasInstance: canvasInstance as Canvas, canvasInstance: canvasInstance as Canvas,
defaultApproxPolyAccuracy,
jobInstance, jobInstance,
curZOrder, curZOrder,
labels, labels,
states, states,
frame, frame,
frameData,
}; };
} }
@ -89,78 +109,78 @@ const mapDispatchToProps = {
updateAnnotations: updateAnnotationsAsync, updateAnnotations: updateAnnotationsAsync,
fetchAnnotations: fetchAnnotationsAsync, fetchAnnotations: fetchAnnotationsAsync,
createAnnotations: createAnnotationsAsync, createAnnotations: createAnnotationsAsync,
changeFrame: changeFrameAsync,
}; };
class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps, State> { class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps, State> {
private activeTool: IntelligentScissors | null; private activeTool: IntelligentScissors | null;
private interactiveStateID: number | null; private latestPoints: number[];
private interactionIsDone: boolean; private canvasForceUpdateWasEnabled: boolean;
public constructor(props: Props & DispatchToProps) { public constructor(props: Props & DispatchToProps) {
super(props); super(props);
const { labels } = props; const { labels, defaultApproxPolyAccuracy } = props;
this.activeTool = null; this.activeTool = null;
this.interactiveStateID = null; this.latestPoints = [];
this.interactionIsDone = false; this.canvasForceUpdateWasEnabled = false;
this.state = { this.state = {
libraryInitialized: openCVWrapper.isInitialized, libraryInitialized: openCVWrapper.isInitialized,
initializationError: false, initializationError: false,
initializationProgress: -1, initializationProgress: -1,
approxPolyAccuracy: defaultApproxPolyAccuracy,
activeLabelID: labels.length ? labels[0].id : null, activeLabelID: labels.length ? labels[0].id : null,
activeImageModifiers: [],
}; };
} }
public componentDidMount(): void { public componentDidMount(): void {
const { canvasInstance } = this.props; const { canvasInstance } = this.props;
canvasInstance.html().addEventListener('canvas.interacted', this.interactionListener); canvasInstance.html().addEventListener('canvas.interacted', this.interactionListener);
canvasInstance.html().addEventListener('canvas.canceled', this.cancelListener); canvasInstance.html().addEventListener('canvas.setup', this.runImageModifier);
} }
public componentDidUpdate(prevProps: Props): void { public componentDidUpdate(prevProps: Props, prevState: State): void {
const { isActivated } = this.props; const { approxPolyAccuracy } = this.state;
const { isActivated, defaultApproxPolyAccuracy, canvasInstance } = this.props;
if (!prevProps.isActivated && isActivated) { if (!prevProps.isActivated && isActivated) {
// reset flags when before using a tool // reset flags & states before using a tool
this.latestPoints = [];
this.setState({
approxPolyAccuracy: defaultApproxPolyAccuracy,
});
if (this.activeTool) { if (this.activeTool) {
this.activeTool.reset(); this.activeTool.reset();
} }
this.interactiveStateID = null; }
this.interactionIsDone = false;
if (prevState.approxPolyAccuracy !== approxPolyAccuracy) {
if (isActivated) {
const approx = openCVWrapper.contours.approxPoly(
this.latestPoints,
thresholdFromAccuracy(approxPolyAccuracy),
);
canvasInstance.interact({
enabled: true,
intermediateShape: {
shapeType: ShapeType.POLYGON,
points: approx.flat(),
},
});
}
} }
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
const { canvasInstance } = this.props; const { canvasInstance } = this.props;
canvasInstance.html().removeEventListener('canvas.interacted', this.interactionListener); canvasInstance.html().removeEventListener('canvas.interacted', this.interactionListener);
canvasInstance.html().removeEventListener('canvas.canceled', this.cancelListener); canvasInstance.html().removeEventListener('canvas.setup', this.runImageModifier);
}
private getInteractiveState(): any | null {
const { states } = this.props;
return states.filter((_state: any): boolean => _state.clientID === this.interactiveStateID)[0] || null;
} }
private cancelListener = async (): Promise<void> => {
const {
fetchAnnotations, isActivated, jobInstance, frame,
} = this.props;
if (isActivated) {
if (this.interactiveStateID !== null) {
const state = this.getInteractiveState();
this.interactiveStateID = null;
await state.delete(frame);
fetchAnnotations();
}
await jobInstance.actions.freeze(false);
}
};
private interactionListener = async (e: Event): Promise<void> => { private interactionListener = async (e: Event): Promise<void> => {
const { approxPolyAccuracy } = this.state;
const { const {
fetchAnnotations, updateAnnotations, isActivated, jobInstance, frame, labels, curZOrder, createAnnotations, isActivated, jobInstance, frame, labels, curZOrder, canvasInstance,
} = this.props; } = this.props;
const { activeLabelID } = this.state; const { activeLabelID } = this.state;
if (!isActivated || !this.activeTool) { if (!isActivated || !this.activeTool) {
@ -171,65 +191,81 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
shapesUpdated, isDone, threshold, shapes, shapesUpdated, isDone, threshold, shapes,
} = (e as CustomEvent).detail; } = (e as CustomEvent).detail;
const pressedPoints = convertShapesForInteractor(shapes, 0).flat(); const pressedPoints = convertShapesForInteractor(shapes, 0).flat();
this.interactionIsDone = isDone;
try { try {
let points: number[] = [];
if (shapesUpdated) { if (shapesUpdated) {
points = await this.runCVAlgorithm(pressedPoints, threshold); this.latestPoints = await this.runCVAlgorithm(pressedPoints, threshold);
const approx = openCVWrapper.contours.approxPoly(
this.latestPoints,
thresholdFromAccuracy(approxPolyAccuracy),
false,
);
canvasInstance.interact({
enabled: true,
intermediateShape: {
shapeType: ShapeType.POLYGON,
points: approx.flat(),
},
});
} }
if (this.interactiveStateID === null) { if (isDone) {
if (!this.interactionIsDone) { // need to recalculate without the latest sliding point
await jobInstance.actions.freeze(true); const finalPoints = await this.runCVAlgorithm(pressedPoints, threshold);
} const finalObject = new core.classes.ObjectState({
const object = new core.classes.ObjectState({
...this.activeTool.params.shape,
frame, frame,
objectType: ObjectType.SHAPE, objectType: ObjectType.SHAPE,
shapeType: ShapeType.POLYGON,
label: labels.filter((label: any) => label.id === activeLabelID)[0], label: labels.filter((label: any) => label.id === activeLabelID)[0],
points, points: openCVWrapper.contours
.approxPoly(finalPoints, thresholdFromAccuracy(approxPolyAccuracy))
.flat(),
occluded: false, occluded: false,
zOrder: curZOrder, zOrder: curZOrder,
}); });
// need a clientID of a created object to interact with it further createAnnotations(jobInstance, frame, [finalObject]);
// so, we do not use createAnnotationAction
const [clientID] = await jobInstance.annotations.put([object]);
this.interactiveStateID = clientID;
// update annotations on a canvas
fetchAnnotations();
return;
} }
} catch (error) {
notification.error({
description: error.toString(),
message: 'OpenCV.js processing error occured',
});
}
};
const state = this.getInteractiveState(); private runImageModifier = async ():Promise<void> => {
if ((e as CustomEvent).detail.isDone) { const { activeImageModifiers } = this.state;
const finalObject = new core.classes.ObjectState({ const {
frame: state.frame, frameData, states, curZOrder, canvasInstance, frame,
objectType: state.objectType, } = this.props;
label: state.label, try {
shapeType: state.shapeType, if (activeImageModifiers.length !== 0 && activeImageModifiers[0].modifier.currentProcessedImage !== frame) {
// need to recalculate without the latest sliding point this.enableCanvasForceUpdate();
points: points = await this.runCVAlgorithm(pressedPoints, threshold), const canvas: HTMLCanvasElement | undefined = window.document.getElementById('cvat_canvas_background') as
occluded: state.occluded, | HTMLCanvasElement
zOrder: state.zOrder, | undefined;
}); if (!canvas) {
this.interactiveStateID = null; throw new Error('Element #cvat_canvas_background was not found');
await state.delete(frame); }
await jobInstance.actions.freeze(false); const { width, height } = canvas;
await jobInstance.annotations.put([finalObject]); const context = canvas.getContext('2d');
fetchAnnotations(); if (!context) {
} else { throw new Error('Canvas context is empty');
state.points = points; }
updateAnnotations([state]); const imageData = context.getImageData(0, 0, width, height);
fetchAnnotations(); const newImageData = activeImageModifiers.reduce((oldImageData, activeImageModifier) =>
activeImageModifier.modifier.processImage(oldImageData, frame), imageData);
const imageBitmap = await createImageBitmap(newImageData);
frameData.imageData = imageBitmap;
canvasInstance.setup(frameData, states, curZOrder);
} }
} catch (error) { } catch (error) {
notification.error({ notification.error({
description: error.toString(), description: error.toString(),
message: 'Processing error occured', message: 'OpenCV.js processing error occured',
}); });
} finally {
this.disableCanvasForceUpdate();
} }
}; };
@ -259,20 +295,46 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
// Handling via OpenCV.js // Handling via OpenCV.js
const points = await this.activeTool.run(pressedPoints, imageData, startX, startY); const points = await this.activeTool.run(pressedPoints, imageData, startX, startY);
return points;
}
// Increasing number of points artificially private imageModifier(alias: string): ImageProcessing|null {
let minNumberOfPoints = 1; const { activeImageModifiers } = this.state;
// eslint-disable-next-line: eslintdot-notation return activeImageModifiers.find((imageModifier) => imageModifier.alias === alias)?.modifier || null;
if (this.activeTool.params.shape.shapeType === 'polyline') { }
minNumberOfPoints = 2;
} else if (this.activeTool.params.shape.shapeType === 'polygon') { private disableImageModifier(alias: string):void {
minNumberOfPoints = 3; const { activeImageModifiers } = this.state;
} const index = activeImageModifiers.findIndex((imageModifier) => imageModifier.alias === alias);
while (points.length < minNumberOfPoints * 2) { if (index !== -1) {
points.push(...points.slice(points.length - 2)); activeImageModifiers.splice(index, 1);
this.setState({
activeImageModifiers: [...activeImageModifiers],
});
} }
}
return points; private enableImageModifier(modifier: ImageProcessing, alias: string): void{
this.setState((prev: State) => ({
...prev,
activeImageModifiers: [...prev.activeImageModifiers, { modifier, alias }],
}), () => {
this.runImageModifier();
});
}
private enableCanvasForceUpdate():void{
const { canvasInstance } = this.props;
canvasInstance.configure({ forceFrameUpdate: true });
this.canvasForceUpdateWasEnabled = true;
}
private disableCanvasForceUpdate():void{
if (this.canvasForceUpdateWasEnabled) {
const { canvasInstance } = this.props;
canvasInstance.configure({ forceFrameUpdate: false });
this.canvasForceUpdateWasEnabled = false;
}
} }
private renderDrawingContent(): JSX.Element { private renderDrawingContent(): JSX.Element {
@ -314,6 +376,36 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
); );
} }
private renderImageContent():JSX.Element {
return (
<Row justify='start'>
<Col>
<CVATTooltip title='Histogram equalization' className='cvat-opencv-image-tool'>
<Button
className={this.imageModifier('histogram') ? 'cvat-opencv-image-tool-active' : ''}
onClick={(e: React.MouseEvent<HTMLElement>) => {
const modifier = this.imageModifier('histogram');
if (!modifier) {
this.enableImageModifier(openCVWrapper.imgproc.hist(), 'histogram');
} else {
const button = e.target as HTMLElement;
button.blur();
this.disableImageModifier('histogram');
const { changeFrame } = this.props;
const { frame } = this.props;
this.enableCanvasForceUpdate();
changeFrame(frame, false, 1, true);
}
}}
>
<AreaChartOutlined />
</Button>
</CVATTooltip>
</Col>
</Row>
);
}
private renderContent(): JSX.Element { private renderContent(): JSX.Element {
const { libraryInitialized, initializationProgress, initializationError } = this.state; const { libraryInitialized, initializationProgress, initializationError } = this.state;
@ -331,7 +423,9 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
<Tabs.TabPane key='drawing' tab='Drawing' className='cvat-opencv-control-tabpane'> <Tabs.TabPane key='drawing' tab='Drawing' className='cvat-opencv-control-tabpane'>
{this.renderDrawingContent()} {this.renderDrawingContent()}
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane disabled key='image' tab='Image' className='cvat-opencv-control-tabpane' /> <Tabs.TabPane key='image' tab='Image' className='cvat-opencv-control-tabpane'>
{this.renderImageContent()}
</Tabs.TabPane>
</Tabs> </Tabs>
) : ( ) : (
<> <>
@ -384,6 +478,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
public render(): JSX.Element { public render(): JSX.Element {
const { isActivated, canvasInstance, labels } = this.props; const { isActivated, canvasInstance, labels } = this.props;
const { libraryInitialized, approxPolyAccuracy } = this.state;
const dynamcPopoverPros = isActivated ? const dynamcPopoverPros = isActivated ?
{ {
overlayStyle: { overlayStyle: {
@ -406,14 +501,31 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
return !labels.length ? ( return !labels.length ? (
<Icon className='cvat-opencv-control cvat-disabled-canvas-control' component={OpenCVIcon} /> <Icon className='cvat-opencv-control cvat-disabled-canvas-control' component={OpenCVIcon} />
) : ( ) : (
<CustomPopover <>
{...dynamcPopoverPros} <CustomPopover
placement='right' {...dynamcPopoverPros}
overlayClassName='cvat-opencv-control-popover' placement='right'
content={this.renderContent()} overlayClassName='cvat-opencv-control-popover'
> content={this.renderContent()}
<Icon {...dynamicIconProps} component={OpenCVIcon} /> afterVisibleChange={() => {
</CustomPopover> if (libraryInitialized !== openCVWrapper.isInitialized) {
this.setState({
libraryInitialized: openCVWrapper.isInitialized,
});
}
}}
>
<Icon {...dynamicIconProps} component={OpenCVIcon} />
</CustomPopover>
{isActivated ? (
<ApproximationAccuracy
approxPolyAccuracy={approxPolyAccuracy}
onChange={(value: number) => {
this.setState({ approxPolyAccuracy: value });
}}
/>
) : null}
</>
); );
} }
} }

@ -13,6 +13,7 @@ import Text from 'antd/lib/typography/Text';
import Tabs from 'antd/lib/tabs'; import Tabs from 'antd/lib/tabs';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import notification from 'antd/lib/notification'; import notification from 'antd/lib/notification';
import message from 'antd/lib/message';
import Progress from 'antd/lib/progress'; import Progress from 'antd/lib/progress';
import InputNumber from 'antd/lib/input-number'; import InputNumber from 'antd/lib/input-number';
@ -20,6 +21,7 @@ import { AIToolsIcon } from 'icons';
import { Canvas, convertShapesForInteractor } from 'cvat-canvas-wrapper'; import { Canvas, convertShapesForInteractor } from 'cvat-canvas-wrapper';
import range from 'utils/range'; import range from 'utils/range';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper';
import { import {
CombinedState, ActiveControl, Model, ObjectType, ShapeType, CombinedState, ActiveControl, Model, ObjectType, ShapeType,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
@ -31,6 +33,9 @@ import {
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import DetectorRunner from 'components/model-runner-modal/detector-runner'; import DetectorRunner from 'components/model-runner-modal/detector-runner';
import LabelSelector from 'components/label-selector/label-selector'; import LabelSelector from 'components/label-selector/label-selector';
import ApproximationAccuracy, {
thresholdFromAccuracy,
} from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy';
import withVisibilityHandling from './handle-popover-visibility'; import withVisibilityHandling from './handle-popover-visibility';
interface StateToProps { interface StateToProps {
@ -46,6 +51,7 @@ interface StateToProps {
trackers: Model[]; trackers: Model[];
curZOrder: number; curZOrder: number;
aiToolsRef: MutableRefObject<any>; aiToolsRef: MutableRefObject<any>;
defaultApproxPolyAccuracy: number;
} }
interface DispatchToProps { interface DispatchToProps {
@ -60,6 +66,7 @@ const CustomPopover = withVisibilityHandling(Popover, 'tools-control');
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
const { annotation } = state; const { annotation } = state;
const { settings } = state;
const { number: frame } = annotation.player.frame; const { number: frame } = annotation.player.frame;
const { instance: jobInstance } = annotation.job; const { instance: jobInstance } = annotation.job;
const { instance: canvasInstance, activeControl } = annotation.canvas; const { instance: canvasInstance, activeControl } = annotation.canvas;
@ -79,32 +86,35 @@ function mapStateToProps(state: CombinedState): StateToProps {
frame, frame,
curZOrder: annotation.annotations.zLayer.cur, curZOrder: annotation.annotations.zLayer.cur,
aiToolsRef: annotation.aiToolsRef, aiToolsRef: annotation.aiToolsRef,
defaultApproxPolyAccuracy: settings.workspace.defaultApproxPolyAccuracy,
}; };
} }
const mapDispatchToProps = { const mapDispatchToProps = {
onInteractionStart: interactWithCanvas, onInteractionStart: interactWithCanvas,
updateAnnotations: updateAnnotationsAsync, updateAnnotations: updateAnnotationsAsync,
fetchAnnotations: fetchAnnotationsAsync,
createAnnotations: createAnnotationsAsync, createAnnotations: createAnnotationsAsync,
fetchAnnotations: fetchAnnotationsAsync,
}; };
type Props = StateToProps & DispatchToProps; type Props = StateToProps & DispatchToProps;
interface State { interface State {
activeInteractor: Model | null; activeInteractor: Model | null;
activeLabelID: number; activeLabelID: number;
interactiveStateID: number | null;
activeTracker: Model | null; activeTracker: Model | null;
trackingProgress: number | null; trackingProgress: number | null;
trackingFrames: number; trackingFrames: number;
fetching: boolean; fetching: boolean;
pointsRecieved: boolean;
approxPolyAccuracy: number;
mode: 'detection' | 'interaction' | 'tracking'; mode: 'detection' | 'interaction' | 'tracking';
} }
export class ToolsControlComponent extends React.PureComponent<Props, State> { export class ToolsControlComponent extends React.PureComponent<Props, State> {
private interactionIsAborted: boolean; private interactionIsAborted: boolean;
private interactionIsDone: boolean; private interactionIsDone: boolean;
private latestResponseResult: number[][];
private latestResult: number[][];
public constructor(props: Props) { public constructor(props: Props) {
super(props); super(props);
@ -112,13 +122,16 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
activeInteractor: props.interactors.length ? props.interactors[0] : null, activeInteractor: props.interactors.length ? props.interactors[0] : null,
activeTracker: props.trackers.length ? props.trackers[0] : null, activeTracker: props.trackers.length ? props.trackers[0] : null,
activeLabelID: props.labels.length ? props.labels[0].id : null, activeLabelID: props.labels.length ? props.labels[0].id : null,
interactiveStateID: null, approxPolyAccuracy: props.defaultApproxPolyAccuracy,
trackingProgress: null, trackingProgress: null,
trackingFrames: 10, trackingFrames: 10,
fetching: false, fetching: false,
pointsRecieved: false,
mode: 'interaction', mode: 'interaction',
}; };
this.latestResponseResult = [];
this.latestResult = [];
this.interactionIsAborted = false; this.interactionIsAborted = false;
this.interactionIsDone = false; this.interactionIsDone = false;
} }
@ -130,16 +143,39 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
canvasInstance.html().addEventListener('canvas.canceled', this.cancelListener); canvasInstance.html().addEventListener('canvas.canceled', this.cancelListener);
} }
public componentDidUpdate(prevProps: Props): void { public componentDidUpdate(prevProps: Props, prevState: State): void {
const { isActivated } = this.props; const { isActivated, defaultApproxPolyAccuracy, canvasInstance } = this.props;
const { approxPolyAccuracy, activeInteractor } = this.state;
if (prevProps.isActivated && !isActivated) { if (prevProps.isActivated && !isActivated) {
window.removeEventListener('contextmenu', this.contextmenuDisabler); window.removeEventListener('contextmenu', this.contextmenuDisabler);
} else if (!prevProps.isActivated && isActivated) { } else if (!prevProps.isActivated && isActivated) {
// reset flags when start interaction/tracking // reset flags when start interaction/tracking
this.setState({
approxPolyAccuracy: defaultApproxPolyAccuracy,
pointsRecieved: false,
});
this.latestResult = [];
this.latestResponseResult = [];
this.interactionIsDone = false; this.interactionIsDone = false;
this.interactionIsAborted = false; this.interactionIsAborted = false;
window.addEventListener('contextmenu', this.contextmenuDisabler); window.addEventListener('contextmenu', this.contextmenuDisabler);
} }
if (prevState.approxPolyAccuracy !== approxPolyAccuracy) {
if (isActivated && activeInteractor !== null && this.latestResponseResult.length) {
this.approximateResponsePoints(this.latestResponseResult).then((points: number[][]) => {
this.latestResult = points;
canvasInstance.interact({
enabled: true,
intermediateShape: {
shapeType: ShapeType.POLYGON,
points: this.latestResult.flat(),
},
});
});
}
}
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
@ -149,12 +185,6 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
canvasInstance.html().removeEventListener('canvas.canceled', this.cancelListener); canvasInstance.html().removeEventListener('canvas.canceled', this.cancelListener);
} }
private getInteractiveState(): any | null {
const { states } = this.props;
const { interactiveStateID } = this.state;
return states.filter((_state: any): boolean => _state.clientID === interactiveStateID)[0] || null;
}
private contextmenuDisabler = (e: MouseEvent): void => { private contextmenuDisabler = (e: MouseEvent): void => {
if ( if (
e.target && e.target &&
@ -166,10 +196,9 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
}; };
private cancelListener = async (): Promise<void> => { private cancelListener = async (): Promise<void> => {
const { const { isActivated } = this.props;
isActivated, jobInstance, frame, fetchAnnotations, const { fetching } = this.state;
} = this.props; this.latestResult = [];
const { interactiveStateID, fetching } = this.state;
if (isActivated) { if (isActivated) {
if (fetching && !this.interactionIsDone) { if (fetching && !this.interactionIsDone) {
@ -177,15 +206,6 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
this.setState({ fetching: false }); this.setState({ fetching: false });
this.interactionIsAborted = true; this.interactionIsAborted = true;
} }
if (interactiveStateID !== null) {
const state = this.getInteractiveState();
this.setState({ interactiveStateID: null });
await state.delete(frame);
fetchAnnotations();
}
await jobInstance.actions.freeze(false);
} }
}; };
@ -197,103 +217,69 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
jobInstance, jobInstance,
isActivated, isActivated,
activeLabelID, activeLabelID,
fetchAnnotations, canvasInstance,
updateAnnotations, createAnnotations,
} = this.props; } = this.props;
const { activeInteractor, interactiveStateID, fetching } = this.state; const { activeInteractor, fetching } = this.state;
if (!isActivated) { if (!isActivated) {
return; return;
} }
try { try {
if (fetching) { this.interactionIsDone = (e as CustomEvent).detail.isDone;
this.interactionIsDone = (e as CustomEvent).detail.isDone;
return;
}
const interactor = activeInteractor as Model; const interactor = activeInteractor as Model;
let result = [];
if ((e as CustomEvent).detail.shapesUpdated) { if ((e as CustomEvent).detail.shapesUpdated) {
this.setState({ fetching: true }); this.setState({ fetching: true });
try { try {
result = await core.lambda.call(jobInstance.task, interactor, { this.latestResponseResult = await core.lambda.call(jobInstance.task, interactor, {
frame, frame,
pos_points: convertShapesForInteractor((e as CustomEvent).detail.shapes, 0), pos_points: convertShapesForInteractor((e as CustomEvent).detail.shapes, 0),
neg_points: convertShapesForInteractor((e as CustomEvent).detail.shapes, 2), neg_points: convertShapesForInteractor((e as CustomEvent).detail.shapes, 2),
}); });
this.latestResult = this.latestResponseResult;
if (this.interactionIsAborted) { if (this.interactionIsAborted) {
// while the server request // while the server request
// user has cancelled interaction (for example pressed ESC) // user has cancelled interaction (for example pressed ESC)
// need to clean variables that have been just set
this.latestResult = [];
this.latestResponseResult = [];
return; return;
} }
this.latestResult = await this.approximateResponsePoints(this.latestResponseResult);
} finally { } finally {
this.setState({ fetching: false }); this.setState({ fetching: false, pointsRecieved: !!this.latestResult.length });
} }
} }
if (this.interactionIsDone) { if (!this.latestResult.length) {
// while the server request, user has done interaction (for example pressed N) return;
}
if (this.interactionIsDone && !fetching) {
const object = new core.classes.ObjectState({ const object = new core.classes.ObjectState({
frame, frame,
objectType: ObjectType.SHAPE, objectType: ObjectType.SHAPE,
label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null, label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null,
shapeType: ShapeType.POLYGON, shapeType: ShapeType.POLYGON,
points: result.flat(), points: this.latestResult.flat(),
occluded: false, occluded: false,
zOrder: curZOrder, zOrder: curZOrder,
}); });
await jobInstance.annotations.put([object]); createAnnotations(jobInstance, frame, [object]);
fetchAnnotations();
} else { } else {
// no shape yet, then create it and save to collection canvasInstance.interact({
if (interactiveStateID === null) { enabled: true,
// freeze history for interaction time intermediateShape: {
// (points updating shouldn't cause adding new actions to history)
await jobInstance.actions.freeze(true);
const object = new core.classes.ObjectState({
frame,
objectType: ObjectType.SHAPE,
label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null,
shapeType: ShapeType.POLYGON, shapeType: ShapeType.POLYGON,
points: result.flat(), points: this.latestResult.flat(),
occluded: false, },
zOrder: curZOrder, });
});
// need a clientID of a created object to interact with it further
// so, we do not use createAnnotationAction
const [clientID] = await jobInstance.annotations.put([object]);
// update annotations on a canvas
fetchAnnotations();
this.setState({ interactiveStateID: clientID });
return;
}
const state = this.getInteractiveState();
if ((e as CustomEvent).detail.isDone) {
const finalObject = new core.classes.ObjectState({
frame: state.frame,
objectType: state.objectType,
label: state.label,
shapeType: state.shapeType,
points: result.length ? result.flat() : state.points,
occluded: state.occluded,
zOrder: state.zOrder,
});
this.setState({ interactiveStateID: null });
await state.delete(frame);
await jobInstance.actions.freeze(false);
await jobInstance.annotations.put([finalObject]);
fetchAnnotations();
} else {
state.points = result.flat();
updateAnnotations([state]);
fetchAnnotations();
}
} }
} catch (err) { } catch (err) {
notification.error({ notification.error({
@ -315,7 +301,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
const { activeLabelID } = this.state; const { activeLabelID } = this.state;
const [label] = jobInstance.task.labels.filter((_label: any): boolean => _label.id === activeLabelID); const [label] = jobInstance.task.labels.filter((_label: any): boolean => _label.id === activeLabelID);
if (!(e as CustomEvent).detail.isDone) { const { isDone, shapesUpdated } = (e as CustomEvent).detail;
if (!isDone || !shapesUpdated) {
return; return;
} }
@ -376,8 +363,27 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
}); });
}; };
private async approximateResponsePoints(points: number[][]): Promise<number[][]> {
const { approxPolyAccuracy } = this.state;
if (points.length > 3) {
if (!openCVWrapper.isInitialized) {
const hide = message.loading('OpenCV.js initialization..');
try {
await openCVWrapper.initialize(() => {});
} finally {
hide();
}
}
const threshold = thresholdFromAccuracy(approxPolyAccuracy);
return openCVWrapper.contours.approxPoly(points, threshold);
}
return points;
}
public async trackState(state: any): Promise<void> { public async trackState(state: any): Promise<void> {
const { jobInstance, frame } = this.props; const { jobInstance, frame, fetchAnnotations } = this.props;
const { activeTracker, trackingFrames } = this.state; const { activeTracker, trackingFrames } = this.state;
const { clientID, points } = state; const { clientID, points } = state;
@ -429,6 +435,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
} }
} finally { } finally {
this.setState({ trackingProgress: null, fetching: false }); this.setState({ trackingProgress: null, fetching: false });
fetchAnnotations();
} }
} }
@ -633,7 +640,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
private renderDetectorBlock(): JSX.Element { private renderDetectorBlock(): JSX.Element {
const { const {
jobInstance, detectors, curZOrder, frame, fetchAnnotations, jobInstance, detectors, curZOrder, frame, createAnnotations,
} = this.props; } = this.props;
if (!detectors.length) { if (!detectors.length) {
@ -672,8 +679,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
}), }),
); );
await jobInstance.annotations.put(states); createAnnotations(jobInstance, frame, states);
fetchAnnotations();
} catch (error) { } catch (error) {
notification.error({ notification.error({
description: error.toString(), description: error.toString(),
@ -718,7 +724,9 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
const { const {
interactors, detectors, trackers, isActivated, canvasInstance, labels, interactors, detectors, trackers, isActivated, canvasInstance, labels,
} = this.props; } = this.props;
const { fetching, trackingProgress } = this.state; const {
fetching, trackingProgress, approxPolyAccuracy, activeInteractor, pointsRecieved,
} = this.state;
if (![...interactors, ...detectors, ...trackers].length) return null; if (![...interactors, ...detectors, ...trackers].length) return null;
@ -758,6 +766,14 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
<Progress percent={+(trackingProgress * 100).toFixed(0)} status='active' /> <Progress percent={+(trackingProgress * 100).toFixed(0)} status='active' />
)} )}
</Modal> </Modal>
{isActivated && activeInteractor !== null && pointsRecieved ? (
<ApproximationAccuracy
approxPolyAccuracy={approxPolyAccuracy}
onChange={(value: number) => {
this.setState({ approxPolyAccuracy: value });
}}
/>
) : null}
<CustomPopover {...dynamcPopoverPros} placement='right' content={this.renderPopoverContent()}> <CustomPopover {...dynamcPopoverPros} placement='right' content={this.renderPopoverContent()}>
<Icon {...dynamicIconProps} component={AIToolsIcon} /> <Icon {...dynamicIconProps} component={AIToolsIcon} />
</CustomPopover> </CustomPopover>

@ -17,7 +17,6 @@ import { CombinedState } from 'reducers/interfaces';
export default function LabelsListComponent(): JSX.Element { export default function LabelsListComponent(): JSX.Element {
const dispatch = useDispatch(); const dispatch = useDispatch();
const tabContentHeight = useSelector((state: CombinedState) => state.annotation.tabContentHeight);
const frame = useSelector((state: CombinedState): number => state.annotation.player.frame.number); const frame = useSelector((state: CombinedState): number => state.annotation.player.frame.number);
const frameIssues = useSelector((state: CombinedState): any[] => state.review.frameIssues); const frameIssues = useSelector((state: CombinedState): any[] => state.review.frameIssues);
const issues = useSelector((state: CombinedState): any[] => state.review.issues); const issues = useSelector((state: CombinedState): any[] => state.review.issues);
@ -50,7 +49,7 @@ export default function LabelsListComponent(): JSX.Element {
}; };
return ( return (
<div style={{ height: tabContentHeight }}> <>
<div className='cvat-objects-sidebar-issues-list-header'> <div className='cvat-objects-sidebar-issues-list-header'>
<Row justify='start' align='middle'> <Row justify='start' align='middle'>
<Col> <Col>
@ -122,6 +121,6 @@ export default function LabelsListComponent(): JSX.Element {
), ),
)} )}
</div> </div>
</div> </>
); );
} }

@ -14,7 +14,6 @@ import GlobalHotKeys from 'utils/mousetrap-react';
function LabelsListComponent(): JSX.Element { function LabelsListComponent(): JSX.Element {
const dispatch = useDispatch(); const dispatch = useDispatch();
const labels = useSelector((state: CombinedState) => state.annotation.job.labels); const labels = useSelector((state: CombinedState) => state.annotation.job.labels);
const listHeight = useSelector((state: CombinedState) => state.annotation.tabContentHeight);
const activatedStateID = useSelector((state: CombinedState) => state.annotation.annotations.activatedStateID); const activatedStateID = useSelector((state: CombinedState) => state.annotation.annotations.activatedStateID);
const states = useSelector((state: CombinedState) => state.annotation.annotations.states); const states = useSelector((state: CombinedState) => state.annotation.annotations.states);
const keyMap = useSelector((state: CombinedState) => state.shortcuts.keyMap); const keyMap = useSelector((state: CombinedState) => state.shortcuts.keyMap);
@ -87,7 +86,7 @@ function LabelsListComponent(): JSX.Element {
}; };
return ( return (
<div style={{ height: listHeight }} className='cvat-objects-sidebar-labels-list'> <div className='cvat-objects-sidebar-labels-list'>
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} /> <GlobalHotKeys keyMap={subKeyMap} handlers={handlers} />
{labelIDs.map( {labelIDs.map(
(labelID: number): JSX.Element => ( (labelID: number): JSX.Element => (

@ -14,6 +14,7 @@ import LabelSelector from 'components/label-selector/label-selector';
import ItemMenu from './object-item-menu'; import ItemMenu from './object-item-menu';
interface Props { interface Props {
jobInstance: any;
readonly: boolean; readonly: boolean;
clientID: number; clientID: number;
serverID: number | undefined; serverID: number | undefined;
@ -76,6 +77,7 @@ function ItemTopComponent(props: Props): JSX.Element {
toForeground, toForeground,
resetCuboidPerspective, resetCuboidPerspective,
activateTracking, activateTracking,
jobInstance,
} = props; } = props;
const [menuVisible, setMenuVisible] = useState(false); const [menuVisible, setMenuVisible] = useState(false);
@ -124,6 +126,7 @@ function ItemTopComponent(props: Props): JSX.Element {
onVisibleChange={changeMenuVisible} onVisibleChange={changeMenuVisible}
placement='bottomLeft' placement='bottomLeft'
overlay={ItemMenu({ overlay={ItemMenu({
jobInstance,
readonly, readonly,
serverID, serverID,
locked, locked,

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

Loading…
Cancel
Save