Merge branch 'release-1.5.0'

main
Nikita Manovich 5 years ago
commit 56c9626a3a

@ -8,3 +8,4 @@ keys/
logs/
static/
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
tags: openvino/cvat_server:latest
load: true
- name: Runing unit tests
- name: Running unit tests
env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
CONTAINER_COVERAGE_DATA_DIR: "/coverage_data"
@ -80,7 +80,7 @@ jobs:
strategy:
fail-fast: false
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:
- uses: actions/checkout@v2
- name: Getting SHA from the default branch
@ -151,7 +151,7 @@ jobs:
run: |
npm ci
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
env:
DJANGO_SU_NAME: 'admin'
@ -159,16 +159,24 @@ jobs:
DJANGO_SU_PASSWORD: '12qwaszx'
API_ABOUT_PAGE: "localhost:8080/api/v1/server/about"
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'
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
npm ci
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
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
- name: Creating a log file from "cvat" container logs
if: failure()
@ -199,7 +207,7 @@ jobs:
needs: [Unit_testing, E2E_testing]
steps:
- uses: actions/checkout@v2
- name: Geting SHA from the default branch
- name: Getting SHA from the default branch
id: get-sha
run: |
URL_get_default_branch="https://api.github.com/repos/${{ github.repository }}"

@ -4,36 +4,40 @@ on:
types: [published]
jobs:
build_and_push_to_registry:
Unit_testing:
runs-on: ubuntu-latest
steps:
- 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
env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
CONTAINER_COVERAGE_DATA_DIR: '/coverage_data'
DJANGO_SU_NAME: 'admin'
DJANGO_SU_EMAIL: 'admin@localhost.company'
DJANGO_SU_PASSWORD: '12qwaszx'
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 '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
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: |
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
npm ci
npm run cypress:run:chrome
npm run cypress:run:chrome:canvas3d
- name: Uploading cypress screenshots as an artifact
if: failure()
uses: actions/upload-artifact@v2
@ -41,6 +45,14 @@ jobs:
name: 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
uses: docker/login-action@v1
with:

@ -11,31 +11,16 @@ jobs:
- name: Run checks
run: |
URL="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files"
PR_FILES=$(curl -s -X GET -G $URL | jq -r '.[] | select(.status != "removed") | .filename')
for files in $PR_FILES; do
extension="${files##*.}"
if [[ $extension == 'md' ]]; then
changed_files_remark+=" ${files}"
fi
done
npm ci
mkdir -p remark_report
if [[ ! -z ${changed_files_remark} ]]; then
npm ci
npm install remark-cli@9.0.0 vfile-reporter-json@2.0.2
mkdir -p remark_report
echo "Remark version: "`npx remark --version`
echo "The files will be checked: "`echo ${changed_files_remark}`
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"
echo "Remark version: "`npx remark --version`
npx remark --quiet --report json --no-stdout . 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
- name: Upload artifacts

@ -18,7 +18,7 @@ jobs:
DJANGO_SU_PASSWORD: "12qwaszx"
API_ABOUT_PAGE: "localhost:8080/api/v1/server/about"
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'
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

6
.gitignore vendored

@ -41,3 +41,9 @@ yarn-error.log*
/helm-chart/values.*.yaml
/helm-chart/*.values.yaml
/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": [
"**/3rdparty/*",
"**/tests/*"
"**/tests/*",
"cvat-ui/src/actions/boundaries-actions.ts",
"cvat-ui/src/utils/platform-checker.ts"
],
"parser-plugins": [
"typescript"

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

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

@ -1,5 +1,4 @@
{
"eslint.enable": true,
"eslint.probe": [
"javascript",
"typescript",
@ -24,5 +23,14 @@
"python.linting.pycodestyleEnabled": false,
"licenser.license": "Custom",
"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",
"tasks": [
{
"label": "ui.js: server",
"type": "npm",
"script": "start",
"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/),
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
@ -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>)
- 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>)
- Initial implementation of moving tasks between projects (<https://github.com/openvinotoolkit/cvat/pull/3164>)
### 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>)
- 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
@ -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>)
- [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>)
- 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>)
- [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>
@ -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 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>)
- 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>)
- 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>)
@ -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>)
- 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
@ -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>)
- 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
@ -129,7 +175,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- PATCH requests from cvat-core submit only changed fields (<https://github.com/openvinotoolkit/cvat/pull/2445>)
- deploy.sh in serverless folder is seperated into deploy_cpu.sh and deploy_gpu.sh (<https://github.com/openvinotoolkit/cvat/pull/2546>)
- 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
- 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>)
- 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
- Ability to login into CVAT-UI with token from api/v1/auth/login (<https://github.com/openvinotoolkit/cvat/pull/2234>)
- Added layout grids toggling ('ctrl + alt + Enter')
- Added password reset functionality (<https://github.com/opencv/cvat/pull/2058>)
- Ability to work with data on the fly (https://github.com/opencv/cvat/pull/2007)
- Ability to work with data on the fly (<https://github.com/opencv/cvat/pull/2007>)
- Annotation in process outline color wheel (<https://github.com/opencv/cvat/pull/2084>)
- On the fly annotation using DL detectors (<https://github.com/opencv/cvat/pull/2102>)
- Displaying automatic annotation progress on a task view (<https://github.com/opencv/cvat/pull/2148>)
- Automatic tracking of bounding boxes using serverless functions (<https://github.com/opencv/cvat/pull/2136>)
- [Datumaro] CLI command for dataset equality comparison (<https://github.com/opencv/cvat/pull/1989>)
- [Datumaro] Merging of datasets with different labels (<https://github.com/opencv/cvat/pull/2098>)
- \[Datumaro] CLI command for dataset equality comparison (<https://github.com/opencv/cvat/pull/1989>)
- \[Datumaro] Merging of datasets with different labels (<https://github.com/opencv/cvat/pull/2098>)
- Add FBRS interactive segmentation serverless function (<https://github.com/openvinotoolkit/cvat/pull/2094>)
- Ability to change default behaviour of previous/next buttons of a player.
It supports regular navigation, searching a frame according to annotations
@ -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>)
- Right colors of label tags in label mapping when a user runs automatic detection (<https://github.com/openvinotoolkit/cvat/pull/2162>)
- Nuclio became an optional component of CVAT (<https://github.com/openvinotoolkit/cvat/pull/2192>)
- A key to remove a point from a polyshape [Ctrl => Alt] (<https://github.com/openvinotoolkit/cvat/pull/2204>)
- A key to remove a point from a polyshape (Ctrl => Alt) (<https://github.com/openvinotoolkit/cvat/pull/2204>)
- Updated `docker-compose` file version from `2.3` to `3.3`(<https://github.com/openvinotoolkit/cvat/pull/2235>)
- Added auto inference of url schema from host in CLI, if provided (<https://github.com/openvinotoolkit/cvat/pull/2240>)
- Track frames in skips between annotation is presented in MOT and MOTS formats are marked `outside` (<https://github.com/openvinotoolkit/cvat/pull/2198>)
@ -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>)
- 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
- Siammask tracker as DL serverless function (<https://github.com/opencv/cvat/pull/1988>)
- [Datumaro] Added model info and source info commands (<https://github.com/opencv/cvat/pull/1973>)
- [Datumaro] Dataset statistics (<https://github.com/opencv/cvat/pull/1668>)
- \[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>)
- 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>)
- 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>)
@ -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>)
- 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
@ -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 CVAT Dumper/Loader (<https://github.com/opencv/cvat/pull/1192>)
- 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>)
- 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>)
@ -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>)
- 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>)
- 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>)
- `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>)
- 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>)
- 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>)
@ -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>)
- 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
@ -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>)
- 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>)
- 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
like image mean and std (https://github.com/opencv/cvat/pull/1734)
- 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
like image mean and std (<https://github.com/opencv/cvat/pull/1734>)
- Add option to upload annotations upon task creation on CLI
- 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>)
@ -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>)
- 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>)
- [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 image copying when exporting datasets, if possible (<https://github.com/opencv/cvat/pull/1799>)
- \[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 image copying when exporting datasets, if possible (<https://github.com/opencv/cvat/pull/1799>)
### 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>)
- 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>)
- 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 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>)
- 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
@ -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>)
## [1.0.0] - 2020-05-29
## \[1.0.0] - 2020-05-29
### 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 debug error message on incorrect XPath (<https://github.com/opencv/cvat/pull/1352>)
- 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>)
- 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>)
@ -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>)
## [1.0.0-beta.2] - 2020-04-30
## \[1.0.0-beta.2] - 2020-04-30
### Added
@ -434,14 +480,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
- Annotation convertation utils, currently supported natively via Datumaro framework
(https://github.com/opencv/cvat/pull/1477)
- Annotation conversion utils, currently supported natively via Datumaro framework
(<https://github.com/opencv/cvat/pull/1477>)
### Fixed
- Auto annotation, TF annotation and Auto segmentation apps (https://github.com/opencv/cvat/pull/1409)
- Auto annotation, TF annotation and Auto segmentation apps (<https://github.com/opencv/cvat/pull/1409>)
- Import works with truncated images now: "OSError:broken data stream" on corrupt images
(https://github.com/opencv/cvat/pull/1430)
(<https://github.com/opencv/cvat/pull/1430>)
- 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>)
- 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>)
- 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>)
- 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>)
- 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)
- Open task button doesn't work (https://github.com/opencv/cvat/pull/1474)
- 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>)
## [1.0.0-beta.1] - 2020-04-15
## \[1.0.0-beta.1] - 2020-04-15
### 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
- Button to reset colors settings (brightness, saturation, contrast) in the new UI
- Option to display shape text always
- 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)
- 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 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
- 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
@ -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
- Merge is allowed for points, but clicks on points conflict with frame dragging logic
- 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)
- UI fails when annotations saving occurs during drag/resize/edit (https://github.com/opencv/cvat/pull/1383)
- 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>)
- 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)
- 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)
- 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)
- Task creation from remote files (https://github.com/opencv/cvat/pull/1392)
(<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>)
- 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>)
- 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
(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)
- 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)
- Uploading annotations for tasks with multiple jobs (https://github.com/opencv/cvat/pull/1396)
(<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>)
- 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>)
- 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
- Data streaming using chunks (https://github.com/opencv/cvat/pull/1007)
- New UI: showing file names in UI (https://github.com/opencv/cvat/pull/1311)
- New UI: delete a point from context menu (https://github.com/opencv/cvat/pull/1292)
- Data streaming using chunks (<https://github.com/opencv/cvat/pull/1007>)
- New UI: showing file names in UI (<https://github.com/opencv/cvat/pull/1311>)
- New UI: delete a point from context menu (<https://github.com/opencv/cvat/pull/1292>)
### Fixed
- Git app cannot clone a repository (https://github.com/opencv/cvat/pull/1330)
- New UI: preview position in task details (https://github.com/opencv/cvat/pull/1312)
- AWS deployment (https://github.com/opencv/cvat/pull/1316)
- Git app cannot clone a repository (<https://github.com/opencv/cvat/pull/1330>)
- New UI: preview position in task details (<https://github.com/opencv/cvat/pull/1312>)
- AWS deployment (<https://github.com/opencv/cvat/pull/1316>)
## [0.6.1] - 2020-03-21
## \[0.6.1] - 2020-03-21
### 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))
## [0.6.0] - 2020-03-15
## \[0.6.0] - 2020-03-15
### 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)
- 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
- 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
- Integration with Zenodo.org (DOI)
## [0.5.0] - 2019-09-12
## \[0.5.0] - 2019-09-12
### 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
- 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, ...
- 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/)
### 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
## [0.4.2] - 2019-06-03
## \[0.4.2] - 2019-06-03
### Fixed
- Fixed interaction with the server share in the auto annotation plugin
## [0.4.1] - 2019-05-14
## \[0.4.1] - 2019-05-14
### Fixed
- JavaScript syntax incompatibility with Google Chrome versions less than 72
## [0.4.0] - 2019-05-04
## \[0.4.0] - 2019-05-04
### Added
- OpenVINO auto annotation: it is possible to upload a custom model and annotate images automatically.
- Ability to rotate images/video in the client part (Ctrl+R, Shift+Ctrl+R shortcuts) (#305)
- The ReID application for automatic bounding box merging has been added (#299)
- Keyboard shortcuts to switch next/previous default shape type (box, polygon etc) [Alt + <, Alt + >] (#316)
- Keyboard shortcuts to switch next/previous default shape type (box, polygon etc) (Alt + <, Alt + >) (#316)
- Converter for VOC now supports interpolation tracks
- REST API (/api/v1/\*, /api/docs)
- 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
- 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)
## [0.3.0] - 2018-12-29
## \[0.3.0] - 2018-12-29
### 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.
- 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, ...)
- 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
- Documentation was improved
- 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)
- Text drawing outside of a frame in some cases (#202)
## [0.2.0] - 2018-09-28
## \[0.2.0] - 2018-09-28
### Added
@ -760,7 +806,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Several memory leaks
- Inconsistent extensions between filenames in an annotation file and real filenames
## [0.1.2] - 2018-08-07
## \[0.1.2] - 2018-08-07
### 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
## [0.1.1] - 2018-07-6
## \[0.1.1] - 2018-07-6
### Added
@ -789,7 +835,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- GitHub documentation
## 0.1.0 - 2018-06-29
## \[0.1.0] - 2018-06-29
### Added

@ -134,7 +134,7 @@ RUN if [ "$INSTALL_SOURCES" = "yes" ]; then \
fi
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
ENV PATH="/opt/venv/bin:${PATH}"
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 https_proxy
@ -15,8 +15,6 @@ ENV TERM=xterm \
LANG='C.UTF-8' \
LC_ALL='C.UTF-8'
RUN apk add python3 g++ make
# Install dependencies
COPY cvat-core/package*.json /tmp/cvat-core/
COPY cvat-canvas/package*.json /tmp/cvat-canvas/

@ -13,18 +13,19 @@ annotate million of objects with different properties. Many UI
and UX decisions are based on feedbacks from professional data
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
- [Installation guide](cvat/apps/documentation/installation.md)
- [User's guide](cvat/apps/documentation/user_guide.md)
- [Django REST API documentation](#rest-api)
- [Contributing](https://openvinotoolkit.github.io/cvat/docs/contributing/)
- [Installation guide](https://openvinotoolkit.github.io/cvat/docs/administration/basics/installation/)
- [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)
- [Command line interface](utils/cli/)
- [XML annotation format](cvat/apps/documentation/xml_format.md)
- [AWS Deployment Guide](cvat/apps/documentation/AWS-Deployment-Guide.md)
- [Frequently asked questions](cvat/apps/documentation/faq.md)
- [Command line interface](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/cli/)
- [XML annotation format](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/xml_format/)
- [AWS Deployment Guide](https://openvinotoolkit.github.io/cvat/docs/administration/basics/aws-deployment-guide/)
- [Frequently asked questions](https://openvinotoolkit.github.io/cvat/docs/faq/)
- [Questions](#questions)
## Screencasts
@ -47,26 +48,30 @@ dataset framework allows additional dataset transformations via its command
line tool and Python library.
For more information about supported formats look at the
[documentation](cvat/apps/dataset_manager/formats/README.md#formats).
| Annotation format | Import | Export |
| ----------------------------------------------------------------------------- | ------ | ------ |
| [CVAT for images](cvat/apps/documentation/xml_format.md#annotation) | X | X |
| [CVAT for a video](cvat/apps/documentation/xml_format.md#interpolation) | X | X |
| [Datumaro](https://github.com/openvinotoolkit/datumaro) | | X |
| [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| Segmentation masks from [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| [YOLO](https://pjreddie.com/darknet/yolo/) | X | X |
| [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X |
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tfrecord) | X | X |
| [MOT](https://motchallenge.net/) | X | X |
| [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | X | X |
| [ImageNet](http://www.image-net.org) | X | X |
| [CamVid](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) | X | X |
| [WIDER Face](http://shuoyang1213.me/WIDERFACE/) | 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 |
[documentation](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/formats/).
<!--lint disable maximum-line-length-->
| Annotation format | Import | Export |
| --------------------------------------------------------------------------------------------------------- | ------ | ------ |
| [CVAT for images](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/xml_format/#annotation) | X | X |
| [CVAT for a video](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/xml_format/#interpolation) | X | X |
| [Datumaro](https://github.com/openvinotoolkit/datumaro) | | X |
| [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| Segmentation masks from [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| [YOLO](https://pjreddie.com/darknet/yolo/) | X | X |
| [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X |
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tfrecord) | X | X |
| [MOT](https://motchallenge.net/) | X | X |
| [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | X | X |
| [ImageNet](http://www.image-net.org) | X | X |
| [CamVid](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) | X | X |
| [WIDER Face](http://shuoyang1213.me/WIDERFACE/) | 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
@ -86,6 +91,7 @@ For more information about supported formats look at the
| [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 |
| [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-->
@ -97,7 +103,7 @@ are visible to users.
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:
@ -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_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
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
and parallelized training pipelines.
- [DataIsKey](https://dataiskey.eu/annotation-tool/) uses CVAT as their prime data labeling tool
to offer annotation services for projects of any size.
<!-- prettier-ignore-start -->
<!-- Badges -->
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 -->
<!-- Badges -->
[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

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

@ -1,13 +1,11 @@
version: '3.3'
services:
serverless:
nuclio:
container_name: nuclio
image: quay.io/nuclio/dashboard:1.5.16-amd64
restart: always
networks:
default:
aliases:
- nuclio
- cvat
volumes:
- /tmp:/tmp
- /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 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",
"version": "2.4.3",
"version": "2.5.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -1995,17 +1995,6 @@
"postcss-value-parser": "^4.0.2"
},
"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": {
"version": "1.0.30001016",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001016.tgz",
@ -2015,40 +2004,25 @@
"electron-to-chromium": {
"version": "1.3.322",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.322.tgz",
"integrity": "sha512-Tc8JQEfGQ1MzfSzI/bTlSr7btJv/FFO7Yh6tanqVmIWOuNCu6/D1MilIEgLtmWqIrsv+o4IjpLAhgMBr/ncNAA==",
"dev": true
"integrity": "sha512-Tc8JQEfGQ1MzfSzI/bTlSr7btJv/FFO7Yh6tanqVmIWOuNCu6/D1MilIEgLtmWqIrsv+o4IjpLAhgMBr/ncNAA=="
},
"node-releases": {
"version": "1.1.44",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.44.tgz",
"integrity": "sha512-NwbdvJyR7nrcGrXvKAvzc5raj/NkoJudkarh2yIpJ4t0NH4aqjUDz/486P+ynIW5eokKOfzGNRdYoLfBlomruw==",
"dev": true,
"requires": {
"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": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
@ -2367,14 +2341,42 @@
}
},
"browserslist": {
"version": "4.6.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.6.tgz",
"integrity": "sha512-D2Nk3W9JL9Fp/gIcWei8LrERCS+eXu9AM5cfXA8WEZ84lFks+ARnZ0q/R69m2SV3Wjma83QDDPxsNKXUwdIsyA==",
"version": "4.16.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
"integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30000984",
"electron-to-chromium": "^1.3.191",
"node-releases": "^1.1.25"
"caniuse-lite": "^1.0.30001219",
"colorette": "^1.2.2",
"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": {
@ -2811,12 +2813,6 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"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": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -3121,17 +3117,6 @@
"schema-utils": "^2.6.0"
},
"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": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz",
@ -3145,8 +3130,7 @@
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
@ -3387,9 +3371,9 @@
"dev": true
},
"dns-packet": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz",
"integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz",
"integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==",
"dev": true,
"requires": {
"ip": "^1.1.0",
@ -3501,12 +3485,6 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
"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": {
"version": "7.0.3",
"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": {
"version": "4.14.1",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz",
@ -6915,9 +6884,9 @@
"dev": true
},
"normalize-url": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz",
"integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==",
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
"integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
"dev": true
},
"npm-run-path": {
@ -7604,9 +7573,9 @@
"dev": true
},
"postcss": {
"version": "7.0.17",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz",
"integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==",
"version": "7.0.36",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz",
"integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
@ -10461,30 +10430,15 @@
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"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": {
"version": "1.0.30001185",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001185.tgz",
"integrity": "sha512-Fpi4kVNtNvJ15H0F6vwmXtb3tukv3Zg3qhKkOGUq7KJ1J6b9kf4dnNgtEAFXhRsJo0gNj9W60+wBvn0JcTvdTg==",
"dev": true
"integrity": "sha512-Fpi4kVNtNvJ15H0F6vwmXtb3tukv3Zg3qhKkOGUq7KJ1J6b9kf4dnNgtEAFXhRsJo0gNj9W60+wBvn0JcTvdTg=="
},
"electron-to-chromium": {
"version": "1.3.654",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.654.tgz",
"integrity": "sha512-Zy2gc/c8KYFg2GkNr7Ruzh5tPEZpFm7EyXqZTFasm1YRDJtpyBRGaOuM0H3t6SuIP53qX4kNmtO9t0WjhBjE9A==",
"dev": true
"integrity": "sha512-Zy2gc/c8KYFg2GkNr7Ruzh5tPEZpFm7EyXqZTFasm1YRDJtpyBRGaOuM0H3t6SuIP53qX4kNmtO9t0WjhBjE9A=="
},
"enhanced-resolve": {
"version": "5.7.0",
@ -10559,8 +10513,7 @@
"node-releases": {
"version": "1.1.70",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.70.tgz",
"integrity": "sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw==",
"dev": true
"integrity": "sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw=="
},
"schema-utils": {
"version": "3.0.0",
@ -10976,9 +10929,9 @@
}
},
"ws": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
"integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
"integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==",
"dev": true,
"requires": {
"async-limiter": "~1.0.0"

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

@ -155,6 +155,22 @@ polyline.cvat_canvas_shape_splitting {
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 {
opacity: 0;
pointer-events: none;

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

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

@ -23,6 +23,7 @@ import consts from './consts';
import {
translateToSVG,
translateFromSVG,
translateToCanvas,
pointsToNumberArray,
parsePoints,
displayShapeSize,
@ -103,7 +104,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private translateToCanvas(points: number[]): number[] {
const { offset } = this.controller.geometry;
return points.map((coord: number): number => coord + offset);
return translateToCanvas(offset, points);
}
private translateFromCanvas(points: number[]): number[] {
@ -1267,9 +1268,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
} else if (reason === UpdateReasons.INTERACT) {
const data: InteractionData = this.controller.interactionData;
if (data.enabled && this.mode === Mode.IDLE) {
this.canvas.style.cursor = 'crosshair';
this.mode = Mode.INTERACT;
if (data.enabled && (this.mode === Mode.IDLE || data.intermediateShape)) {
if (!data.intermediateShape) {
this.canvas.style.cursor = 'crosshair';
this.mode = Mode.INTERACT;
}
this.interactionHandler.interact(data);
} else {
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[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
// opposite corners
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
if (this.drawInstance.remember('_paintHandler')) {
if (
this.drawData.shapeType !== 'rectangle'
&& this.drawData.cuboidDrawingMethod !== CuboidDrawingMethod.CLASSIC
['polygon', 'polyline', 'points'].includes(this.drawData.shapeType)
|| (this.drawData.shapeType === 'cuboid'
&& this.drawData.cuboidDrawingMethod === CuboidDrawingMethod.CORNER_POINTS)
) {
// Check for unsaved drawn shapes
this.drawInstance.draw('done');
@ -462,11 +463,11 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance.draw('point', e);
} else {
this.drawInstance.draw('update', e);
const deltaTreshold = 15;
const deltaThreshold = 15;
const dx = (e.clientX - lastDrawnPoint.x) ** 2;
const dy = (e.clientY - lastDrawnPoint.y) ** 2;
const delta = Math.sqrt(dx + dy);
if (delta > deltaTreshold) {
if (delta > deltaThreshold) {
this.drawInstance.draw('point', e);
}
}
@ -574,7 +575,7 @@ export class DrawHandlerImpl implements DrawHandler {
.on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType } = this.drawData;
const { shapeType, redraw: clientID } = this.drawData;
this.release();
if (this.canceled) return;
@ -584,6 +585,7 @@ export class DrawHandlerImpl implements DrawHandler {
{
shapeType,
points: cuboidFrom4Points([xtl, ybr, xbr, ybr, xbr, ytl, xbr + d.x, ytl - d.y]),
clientID,
},
Date.now() - this.startTimestamp,
);

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

@ -5,7 +5,9 @@
import * as SVG from 'svg.js';
import consts from './consts';
import Crosshair from './crosshair';
import { translateToSVG } from './shared';
import {
translateToSVG, PropType, stringifyPoints, translateToCanvas,
} from './shared';
import { InteractionData, InteractionResult, Geometry } from './canvasModel';
export interface InteractionHandler {
@ -26,6 +28,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
private crosshair: Crosshair;
private threshold: SVG.Rect | null;
private thresholdRectSize: number;
private intermediateShape: PropType<InteractionData, 'intermediateShape'>;
private drawnIntermediateShape: SVG.Shape;
private prepareResult(): InteractionResult[] {
return this.interactionShapes.map(
@ -65,8 +69,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
return enabled && !ctrlKey && !!interactionShapes.length;
}
const minPosVerticesAchieved = typeof minPosVertices === 'undefined' || minPosVertices <= positiveShapes.length;
const minNegVerticesAchieved = typeof minNegVertices === 'undefined' || minPosVertices <= negativeShapes.length;
const minPosVerticesDefined = Number.isInteger(minPosVertices);
const minNegVerticesDefined = Number.isInteger(minNegVertices) && minNegVertices >= 0;
const minPosVerticesAchieved = !minPosVerticesDefined || minPosVertices <= positiveShapes.length;
const minNegVerticesAchieved = !minNegVerticesDefined || minNegVertices <= negativeShapes.length;
const minimumVerticesAchieved = minPosVerticesAchieved && minNegVerticesAchieved;
return enabled && !ctrlKey && minimumVerticesAchieved && shapesWereUpdated;
}
@ -91,10 +97,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
private interactPoints(): 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();
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;
this.currentInteractionShape = this.canvas
@ -121,8 +127,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
}
}
self.addClass('cvat_canvas_removable_interaction_point');
self.attr({
'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 => {
@ -132,6 +140,9 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.interactionShapes = this.interactionShapes.filter(
(shape: SVG.Shape): boolean => shape !== self,
);
if (this.interactionData.startWithBox && this.interactionShapes.length === 1) {
this.interactionShapes[0].style({ visibility: '' });
}
this.shapesWereUpdated = true;
if (this.shouldRaiseEvent(_e.ctrlKey)) {
this.onInteraction(this.prepareResult(), true, false);
@ -140,8 +151,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
});
self.on('mouseleave', (): void => {
self.removeClass('cvat_canvas_removable_interaction_point');
self.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
r: consts.BASE_POINT_SIZE / this.geometry.scale,
});
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);
}
private interactRectangle(): void {
private interactRectangle(shouldFinish: boolean, onContinue?: () => void): void {
let initialized = false;
const eventListener = (e: MouseEvent): void => {
if (e.button === 0 && !e.altKey) {
@ -170,11 +183,15 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.canvas.on('mousedown.interaction', eventListener);
this.currentInteractionShape
.on('drawstop', (): void => {
this.canvas.off('mousedown.interaction', eventListener);
this.interactionShapes.push(this.currentInteractionShape);
this.shapesWereUpdated = true;
this.canvas.off('mousedown.interaction', eventListener);
this.interact({ enabled: false });
if (shouldFinish) {
this.interact({ enabled: false });
} else if (onContinue) {
onContinue();
}
})
.addClass('cvat_canvas_shape_drawing')
.attr({
@ -194,15 +211,25 @@ export class InteractionHandlerImpl implements InteractionHandler {
private startInteraction(): void {
if (this.interactionData.shapeType === 'rectangle') {
this.interactRectangle();
this.interactRectangle(true);
} else if (this.interactionData.shapeType === 'points') {
this.interactPoints();
if (this.interactionData.startWithBox) {
this.interactRectangle(false, (): void => this.interactPoints());
} else {
this.interactPoints();
}
} else {
throw new Error('Interactor implementation supports only rectangle and points');
}
}
private release(): void {
if (this.drawnIntermediateShape) {
this.selectize(false, this.drawnIntermediateShape);
this.drawnIntermediateShape.remove();
this.drawnIntermediateShape = null;
}
if (this.crosshair) {
this.removeCrosshair();
}
@ -234,13 +261,75 @@ export class InteractionHandlerImpl implements InteractionHandler {
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 { width, height } = image;
const [imageX, imageY] = [Math.round(x - offset), Math.round(y - offset)];
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(
onInteraction: (
shapes: InteractionResult[] | null,
@ -264,6 +353,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.crosshair = new Crosshair();
this.threshold = null;
this.thresholdRectSize = 300;
this.intermediateShape = null;
this.drawnIntermediateShape = null;
this.cursorPosition = {
x: 0,
y: 0,
@ -280,7 +371,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
}
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;
this.onInteraction(
[
@ -334,16 +425,36 @@ export class InteractionHandlerImpl implements InteractionHandler {
: [...this.interactionShapes];
for (const shape of shapesToBeScaled) {
if (shape.type === 'circle') {
(shape as SVG.Circle).radius(consts.BASE_POINT_SIZE / this.geometry.scale);
shape.attr('stroke-width', consts.POINTS_STROKE_WIDTH / this.geometry.scale);
if (shape.hasClass('cvat_canvas_removable_interaction_point')) {
(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 {
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 {
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.initInteraction();
this.startInteraction();

@ -1,11 +1,9 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
export interface Master {
subscribe(listener: Listener): void;
unsubscribe(listener: Listener): void;
unsubscribeAll(): void;
notify(reason: string): void;
}
@ -24,18 +22,6 @@ export class MasterImpl implements Master {
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 {
for (const listener of this.listeners) {
listener.notify(this, reason);

@ -181,3 +181,9 @@ export function vectorLength(vector: Vector2D): number {
const sqrJ = vector.j ** 2;
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
@ -43,7 +43,7 @@ export class SplitHandlerImpl implements SplitHandler {
}
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
if (!this.splitDone) {
this.onSplitDone(null);

@ -26,11 +26,20 @@ npm run build -- --mode=development # without a minification
```ts
interface Canvas3d {
html(): HTMLDivElement;
setup(frameData: any): void;
mode(): Mode;
html(): ViewsDOM;
setup(frameData: any, objectStates: any[]): void;
isAbleToChangeFrame(): boolean;
mode(): Mode;
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());
// 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 { Canvas3dController, Canvas3dControllerImpl } from './canvas3dController';
import {
Canvas3dModel, Canvas3dModelImpl, Mode, DrawData, ViewType, MouseInteraction,
Canvas3dModel,
Canvas3dModelImpl,
Mode,
DrawData,
ViewType,
MouseInteraction,
ShapeProperties,
GroupData,
} from './canvas3dModel';
import {
Canvas3dView, Canvas3dViewImpl, ViewsDOM, CAMERA_ACTION,
Canvas3dView, Canvas3dViewImpl, ViewsDOM, CameraAction,
} from './canvas3dView';
import { Master } from './master';
@ -16,19 +23,24 @@ const Canvas3dVersion = pjson.version;
interface Canvas3d {
html(): ViewsDOM;
setup(frameData: any): void;
setup(frameData: any, objectStates: any[]): void;
isAbleToChangeFrame(): boolean;
mode(): Mode;
render(): void;
keyControls(keys: KeyboardEvent): void;
mouseControls(type: string, event: MouseEvent): 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;
}
class Canvas3dImpl implements Canvas3d {
private model: Canvas3dModel & Master;
private controller: Canvas3dController;
private readonly model: Canvas3dModel & Master;
private readonly controller: Canvas3dController;
private view: Canvas3dView;
public constructor() {
@ -45,10 +57,6 @@ class Canvas3dImpl implements Canvas3d {
this.view.keyControls(keys);
}
public mouseControls(type: MouseInteraction, event: MouseEvent): void {
this.view.mouseControls(type, event);
}
public render(): void {
this.view.render();
}
@ -57,14 +65,18 @@ class Canvas3dImpl implements Canvas3d {
this.model.draw(drawData);
}
public setup(frameData: any): void {
this.model.setup(frameData);
public setup(frameData: any, objectStates: any[]): void {
this.model.setup(frameData, objectStates);
}
public mode(): Mode {
return this.model.mode;
}
public group(groupData: GroupData): void {
this.model.group(groupData);
}
public isAbleToChangeFrame(): boolean {
return this.model.isAbleToChangeFrame();
}
@ -72,8 +84,28 @@ class Canvas3dImpl implements Canvas3d {
public cancel(): void {
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 {
Canvas3dImpl as Canvas3d, Canvas3dVersion, ViewType, MouseInteraction, CAMERA_ACTION,
Canvas3dImpl as Canvas3d, Canvas3dVersion, ViewType, MouseInteraction, CameraAction, ViewsDOM,
};

@ -2,11 +2,18 @@
//
// SPDX-License-Identifier: MIT
import { Canvas3dModel, Mode, DrawData } from './canvas3dModel';
import {
Canvas3dModel, Mode, DrawData, ActiveElement, FocusData, GroupData,
} from './canvas3dModel';
export interface Canvas3dController {
readonly drawData: DrawData;
readonly activeElement: ActiveElement;
readonly selected: any;
readonly focused: FocusData;
readonly groupData: GroupData;
mode: Mode;
group(groupData: GroupData): void;
}
export class Canvas3dControllerImpl implements Canvas3dController {
@ -27,4 +34,24 @@ export class Canvas3dControllerImpl implements Canvas3dController {
public get drawData(): 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;
}
export interface ActiveElement {
clientID: string | null;
attributeID: number | null;
}
export interface GroupData {
enabled: boolean;
grouped?: [];
}
export interface Image {
renderWidth: number;
renderHeight: number;
@ -19,6 +29,7 @@ export interface DrawData {
enabled: boolean;
initialState?: any;
redraw?: number;
shapeType?: string;
}
export enum FrameZoom {
@ -26,6 +37,13 @@ export enum FrameZoom {
MAX = 10,
}
export enum Planes {
TOP = 'topPlane',
SIDE = 'sidePlane',
FRONT = 'frontPlane',
PERSPECTIVE = 'perspectivePlane',
}
export enum ViewType {
PERSPECTIVE = 'perspective',
TOP = 'top',
@ -39,14 +57,29 @@ export enum MouseInteraction {
HOVER = 'hover',
}
export interface FocusData {
clientID: string | null;
}
export interface ShapeProperties {
opacity: number;
outlined: boolean;
outlineColor: string;
selectedOpacity: number;
colorBy: string;
}
export enum UpdateReasons {
IMAGE_CHANGED = 'image_changed',
OBJECTS_UPDATED = 'objects_updated',
FITTED_CANVAS = 'fitted_canvas',
DRAW = 'draw',
SELECT = 'select',
CANCEL = 'cancel',
DATA_FAILED = 'data_failed',
DRAG_CANVAS = 'drag_canvas',
SHAPE_ACTIVATED = 'shape_activated',
GROUP = 'group',
FITTED_CANVAS = 'fitted_canvas',
}
export enum Mode {
@ -56,9 +89,13 @@ export enum Mode {
DRAW = 'draw',
EDIT = 'edit',
INTERACT = 'interact',
DRAG_CANVAS = 'drag_canvas',
GROUP = 'group',
BUSY = 'busy',
}
export interface Canvas3dDataModel {
activeElement: ActiveElement;
canvasSize: Size;
image: Image | null;
imageID: number | null;
@ -66,16 +103,29 @@ export interface Canvas3dDataModel {
imageSize: Size;
drawData: DrawData;
mode: Mode;
objectUpdating: boolean;
exception: Error | null;
objects: any[];
groupedObjects: any[];
focusData: FocusData;
selected: any;
shapeProperties: ShapeProperties;
groupData: GroupData;
}
export interface Canvas3dModel {
mode: Mode;
data: Canvas3dDataModel;
setup(frameData: any): void;
readonly groupData: GroupData;
setup(frameData: any, objectStates: any[]): void;
isAbleToChangeFrame(): boolean;
draw(drawData: DrawData): 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 {
@ -84,10 +134,17 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
public constructor() {
super();
this.data = {
activeElement: {
clientID: null,
attributeID: null,
},
canvasSize: {
height: 0,
width: 0,
},
objectUpdating: false,
objects: [],
groupedObjects: [],
image: null,
imageID: null,
imageOffset: 0,
@ -101,37 +158,71 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
},
mode: Mode.IDLE,
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) {
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);
})
.catch((exception: any): void => {
this.data.exception = exception;
this.notify(UpdateReasons.DATA_FAILED);
throw exception;
});
if ([Mode.EDIT, Mode.DRAG, Mode.RESIZE].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
}
if ([Mode.EDIT, Mode.BUSY].includes(this.data.mode)) {
return;
}
if (frameData.number === this.data.imageID) {
if (this.data.objectUpdating) {
return;
}
this.data.objects = objectStates;
this.data.objectUpdating = true;
this.notify(UpdateReasons.OBJECTS_UPDATED);
return;
}
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) {
@ -143,23 +234,110 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
}
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');
return !isUnable;
}
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');
}
if ([Mode.DRAW, Mode.EDIT].includes(this.data.mode) && !drawData.initialState) {
return;
}
this.data.drawData.enabled = drawData.enabled;
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);
}
public cancel(): void {
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 DOLLY_FACTOR = 5;
const MAX_DISTANCE = 100;
const MIN_DISTANCE = 0;
const MIN_DISTANCE = 0.3;
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 {
BASE_GRID_WIDTH,
@ -16,4 +25,13 @@ export default {
MAX_DISTANCE,
MIN_DISTANCE,
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
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 {
public perspective: THREE.Mesh;
@ -9,12 +16,174 @@ export class CuboidModel {
public side: THREE.Mesh;
public front: THREE.Mesh;
public constructor() {
public constructor(outline: string, outlineColor: string) {
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);
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.side = 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
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.
## Versioning

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

@ -1,6 +1,6 @@
{
"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",
"main": "babel.config.js",
"scripts": {

@ -788,6 +788,7 @@
() => {
importedArray.forEach((object) => {
object.removed = false;
object.serverID = undefined;
});
},
importedArray.map((object) => object.clientID),
@ -868,7 +869,7 @@
const deepSearch = (deepSearchFrom, deepSearchTo) => {
// 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];
// 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
@ -50,7 +50,7 @@
throw new DataError(`Points must have exact 8 points, but got ${points.length / 2}`);
}
} 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.attributes = {};
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 };
this.history.do(
@ -324,11 +336,16 @@
checkObjectType('points', data.points, null, Array);
checkNumberOfPoints(this.shapeType, data.points);
// cut points
const { width, height } = this.frameMeta[frame];
const { width, height, filename } = this.frameMeta[frame];
fittedPoints = fitPoints(this.shapeType, data.points, width, height);
if (!checkShapeArea(this.shapeType, fittedPoints) || checkOutside(fittedPoints, width, height)) {
fittedPoints = [];
let check = true;
if (filename && filename.slice(filename.length - 3) === 'pcd') {
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)),
);
} 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
// In this case we don't use the computed distance
// 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
(() => {
const serverProxy = require('./server-proxy');
const { Task } = require('./session');
const { ScriptingError } = './exceptions';
const { ScriptingError } = require('./exceptions');
class AnnotationsSaver {
constructor(version, collection, session) {

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

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

@ -34,7 +34,7 @@
for (const prop in filter) {
if (Object.prototype.hasOwnProperty.call(filter, prop)) {
if (!(prop in fields)) {
throw new ArgumentError(`Unsupported filter property has been recieved: "${prop}"`);
throw new ArgumentError(`Unsupported filter property has been received: "${prop}"`);
} else if (!fields[prop](filter[prop])) {
throw new ArgumentError(`Received filter property "${prop}" is not satisfied for checker`);
}
@ -104,6 +104,37 @@
}
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 = {
isBoolean,
isInteger,
@ -114,5 +145,6 @@
negativeIDGenerator,
checkExclusiveFields,
camelToSnake,
FieldUpdateTrigger,
};
})();

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

@ -19,7 +19,15 @@
*/
class FrameData {
constructor({
width, height, name, taskID, frameNumber, startFrame, stopFrame, decodeForward,
width,
height,
name,
taskID,
frameNumber,
startFrame,
stopFrame,
decodeForward,
has_related_context: hasRelatedContext,
}) {
Object.defineProperties(
this,
@ -72,6 +80,18 @@
value: frameNumber,
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: {
value: startFrame,
writable: false,
@ -104,6 +124,14 @@
const result = await PluginRegistry.apiWrapper.call(this, FrameData.prototype.data, onServerRequest);
return result;
}
get imageData() {
return this._data.imageData;
}
set imageData(imageData) {
this._data.imageData = imageData;
}
}
FrameData.prototype.data.implementation = async function (onServerRequest) {

@ -17,7 +17,8 @@ class MLModel {
this._params = {
canvas: {
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 { Task } = require('./session');
const { Label } = require('./labels');
const { getPreview } = require('./frames');
const User = require('./user');
/**
@ -17,7 +18,7 @@
class Project {
/**
* In a fact you need use the constructor only if you want to create a project
* @param {object} initialData - Object which is used for initalization
* @param {object} initialData - Object which is used for initialization
* <br> It can contain keys:
* <br> <li style="margin-left: 10px;"> name
* <br> <li style="margin-left: 10px;"> labels
@ -34,6 +35,7 @@
updated_date: undefined,
task_subsets: undefined,
training_project: undefined,
task_ids: undefined,
};
for (const property in data) {
@ -58,9 +60,9 @@
data.tasks.push(taskInstance);
}
}
if (!data.task_subsets && data.tasks.length) {
if (!data.task_subsets) {
const subsetsSet = new Set();
for (const task in data.tasks) {
for (const task of data.tasks) {
if (task.subset) subsetsSet.add(task.subset);
}
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 save
@ -331,4 +349,12 @@
const result = await serverProxy.projects.delete(this.id);
return result;
};
Project.prototype.preview.implementation = async function () {
if (!this._internalData.task_ids.length) {
return '';
}
const frameData = await getPreview(this._internalData.task_ids[0]);
return frameData;
};
})();

@ -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) {
const { backendAPI } = config;
@ -756,11 +809,7 @@
},
);
} catch (errorData) {
const code = errorData.response ? errorData.response.status : errorData.code;
throw new ServerError(
`Could not get Image Context of the frame for the task ${tid} from the server`,
code,
);
throw generateError(errorData);
}
return response.data;
@ -1161,6 +1210,8 @@
createTask,
deleteTask,
exportDataset,
exportTask,
importTask,
}),
writable: false,
},

@ -16,8 +16,9 @@
const User = require('./user');
const Issue = require('./issue');
const Review = require('./review');
const { FieldUpdateTrigger } = require('./common');
function buildDublicatedAPI(prototype) {
function buildDuplicatedAPI(prototype) {
Object.defineProperties(prototype, {
annotations: Object.freeze({
value: {
@ -575,7 +576,7 @@
* Create a log and add it to a log collection <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
* 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>
* Payload of ignored logs are shallowly combined to previous logs of the same type
* @method log
@ -734,11 +735,11 @@
task: undefined,
};
let updatedFields = {
const updatedFields = new FieldUpdateTrigger({
assignee: false,
reviewer: false,
status: false,
};
});
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property)) {
@ -865,9 +866,6 @@
},
__updatedFields: {
get: () => updatedFields,
set: (fields) => {
updatedFields = fields;
},
},
}),
);
@ -1001,7 +999,7 @@
class Task extends Session {
/**
* 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> <li style="margin-left: 10px;"> name
* <br> <li style="margin-left: 10px;"> assignee
@ -1040,13 +1038,14 @@
dimension: undefined,
};
let updatedFields = {
const updatedFields = new FieldUpdateTrigger({
name: false,
assignee: false,
bug_tracker: false,
subset: false,
labels: false,
};
project_id: false,
});
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
@ -1126,11 +1125,18 @@
* @name projectId
* @type {integer|null}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
*/
projectId: {
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
@ -1558,9 +1564,6 @@
},
__updatedFields: {
get: () => updatedFields,
set: (fields) => {
updatedFields = fields;
},
},
}),
);
@ -1661,6 +1664,36 @@
const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.delete);
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 = {
@ -1694,8 +1727,8 @@
closeSession,
} = require('./annotations');
buildDublicatedAPI(Job.prototype);
buildDublicatedAPI(Task.prototype);
buildDuplicatedAPI(Job.prototype);
buildDuplicatedAPI(Task.prototype);
Job.prototype.save.implementation = async function () {
if (this.id) {
@ -1721,11 +1754,7 @@
await serverProxy.jobs.save(this.id, jobData);
this.__updatedFields = {
status: false,
assignee: false,
reviewer: false,
};
this.__updatedFields.reset();
return this;
}
@ -2000,6 +2029,9 @@
case 'subset':
taskData.subset = this.subset;
break;
case 'project_id':
taskData.project_id = this.projectId;
break;
case 'labels':
taskData.labels = [...this._internalData.labels.map((el) => el.toJSON())];
break;
@ -2011,13 +2043,7 @@
await serverProxy.tasks.saveTask(this.id, taskData);
this.updatedFields = {
assignee: false,
name: false,
bugTracker: false,
subset: false,
labels: false,
};
this.__updatedFields.reset();
return this;
}
@ -2077,6 +2103,16 @@
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) {
if (!Number.isInteger(frame) || frame < 0) {
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
@ -211,7 +211,7 @@ describe('Feature: put annotations', () => {
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];
await task.annotations.clear(true);
const state = new window.cvat.classes.ObjectState({
@ -311,6 +311,27 @@ describe('Feature: check unsaved changes', () => {
});
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 () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
let annotations = await task.annotations.get(0);

@ -16,7 +16,7 @@ const { Project } = require('../../src/project');
describe('Feature: get projects', () => {
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(result).toHaveLength(2);
for (const el of result) {

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

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

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

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

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

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

@ -36,5 +36,5 @@ npm run build
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

File diff suppressed because it is too large Load Diff

@ -1,11 +1,11 @@
{
"name": "cvat-ui",
"version": "1.19.1",
"version": "1.21.1",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
"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:watch": "npm run type-check -- --watch",
"lint": "eslint './src/**/*.{ts,tsx}'",
@ -49,14 +49,14 @@
},
"dependencies": {
"@ant-design/icons": "^4.6.2",
"@types/lodash": "^4.14.168",
"@types/lodash": "^4.14.170",
"@types/platform": "^1.3.3",
"@types/react": "^16.14.5",
"@types/react": "^16.14.10",
"@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-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-share": "^3.0.3",
"@types/redux-logger": "^3.0.8",
@ -73,6 +73,7 @@
"mousetrap": "^1.6.5",
"platform": "^1.3.6",
"prop-types": "^15.7.2",
"rc-menu": "^8.10.8",
"react": "^16.14.0",
"react-awesome-query-builder": "^3.0.0",
"react-color": "^2.19.3",

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

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

@ -35,6 +35,13 @@ export enum TasksActionTypes {
UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS',
UPDATE_TASK_FAILED = 'UPDATE_TASK_FAILED',
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 {
@ -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 {
const action = {
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 {
const action = {
type: TasksActionTypes.DELETE_TASK,
@ -519,3 +619,46 @@ export function hideEmptyTasks(hideEmpty: boolean): AnyAction {
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;
$text-color: #303030;
$text-color-secondary: rgba(0, 0, 0, 0.45);
$hover-menu-color: rgba(24, 144, 255, 0.05);
$completed-progress-color: #61c200;
$inprogress-progress-color: #1890ff;

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

@ -47,7 +47,12 @@ export default function LoadSubmenu(props: Props): JSX.Element {
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 />
<Text>{loader.name}</Text>
{pending && <LoadingOutlined style={{ marginLeft: 10 }} />}

@ -48,3 +48,7 @@
.cvat-menu-icon {
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 { ColorizeIcon } from 'icons';
import { ColorBy, CombinedState } from 'reducers/interfaces';
import {
collapseAppearance as collapseAppearanceAction,
updateTabContentHeight as updateTabContentHeightAction,
} from 'actions/annotation-actions';
import { ColorBy, CombinedState, DimensionType } from 'reducers/interfaces';
import { collapseAppearance as collapseAppearanceAction } from 'actions/annotation-actions';
import {
changeShapesColorBy as changeShapesColorByAction,
changeShapesOpacity as changeShapesOpacityAction,
@ -37,6 +34,7 @@ interface StateToProps {
outlineColor: string;
showBitmap: boolean;
showProjections: boolean;
jobInstance: any;
}
interface DispatchToProps {
@ -49,24 +47,12 @@ interface DispatchToProps {
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 {
const {
annotation: { appearanceCollapsed },
annotation: {
appearanceCollapsed,
job: { instance: jobInstance },
},
settings: {
shapes: {
colorBy, opacity, selectedOpacity, outlined, outlineColor, showBitmap, showProjections,
@ -83,6 +69,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
outlineColor,
showBitmap,
showProjections,
jobInstance,
};
}
@ -90,19 +77,6 @@ function mapDispatchToProps(dispatch: Dispatch<AnyAction>): DispatchToProps {
return {
collapseAppearance(): void {
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 {
dispatch(changeShapesColorByAction(event.target.value));
@ -144,8 +118,11 @@ function AppearanceBlock(props: Props): JSX.Element {
changeShapesOutlinedBorders,
changeShowBitmap,
changeShowProjections,
jobInstance,
} = props;
const is2D = jobInstance.task.dimension === DimensionType.DIM_2D;
return (
<Collapse
onChange={collapseAppearance}
@ -206,20 +183,24 @@ function AppearanceBlock(props: Props): JSX.Element {
</Button>
</ColorPicker>
</Checkbox>
<Checkbox
className='cvat-appearance-bitmap-checkbox'
onChange={changeShowBitmap}
checked={showBitmap}
>
Show bitmap
</Checkbox>
<Checkbox
className='cvat-appearance-cuboid-projections-checkbox'
onChange={changeShowProjections}
checked={showProjections}
>
Show projections
</Checkbox>
{is2D && (
<Checkbox
className='cvat-appearance-bitmap-checkbox'
onChange={changeShowBitmap}
checked={showBitmap}
>
Show bitmap
</Checkbox>
)}
{is2D && (
<Checkbox
className='cvat-appearance-cuboid-projections-checkbox'
onChange={changeShowProjections}
checked={showProjections}
>
Show projections
</Checkbox>
)}
</div>
</Collapse.Panel>
</Collapse>

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MIT
import React, { useEffect, useState } from 'react';
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
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 { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { LogType } from 'cvat-logger';
import {
activateObject as activateObjectAction,
@ -20,6 +21,7 @@ import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { ThunkDispatch } from 'utils/redux';
import AppearanceBlock from 'components/annotation-page/appearance-block';
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 AttributeEditor from './attribute-editor';
import AttributeSwitcher from './attribute-switcher';
@ -34,7 +36,7 @@ interface StateToProps {
jobInstance: any;
keyMap: KeyMap;
normalizedKeyMap: Record<string, string>;
canvasInstance: Canvas;
canvasInstance: Canvas | Canvas3d;
canvasIsReady: boolean;
curZLayer: number;
}
@ -134,6 +136,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
(collapser as HTMLElement).addEventListener('transitionend', listener as any);
}
adjustContextImagePosition(!sidebarCollapsed);
setSidebarCollapsed(!sidebarCollapsed);
};

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

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

@ -14,10 +14,12 @@ import {
} from 'reducers/interfaces';
import { LogType } from 'cvat-logger';
import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import getCore from 'cvat-core-wrapper';
import consts from 'consts';
import CVATTooltip from 'components/common/cvat-tooltip';
import ImageSetupsContent from './image-setups-content';
import ContextImage from '../standard-workspace/context-image/context-image';
const cvat = getCore();
@ -25,7 +27,7 @@ const MAX_DISTANCE_TO_OPEN_SHAPE = 50;
interface Props {
sidebarCollapsed: boolean;
canvasInstance: Canvas;
canvasInstance: Canvas | Canvas3d;
jobInstance: any;
activatedStateID: number | null;
activatedAttributeID: number | null;
@ -102,10 +104,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
automaticBordering,
intelligentPolygonCrop,
showObjectsTextAlways,
canvasInstance,
workspace,
showProjections,
} = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
// It's awful approach from the point of view React
// 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,
frameAngle,
annotations,
canvasInstance,
sidebarCollapsed,
activatedStateID,
curZLayer,
@ -160,7 +161,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasBackgroundColor,
onFetchAnnotation,
} = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
if (
prevProps.showObjectsTextAlways !== showObjectsTextAlways ||
prevProps.automaticBordering !== automaticBordering ||
@ -305,7 +306,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
public componentWillUnmount(): void {
const { canvasInstance } = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
canvasInstance.html().removeEventListener('mousedown', this.onCanvasMouseDown);
canvasInstance.html().removeEventListener('click', this.onCanvasClicked);
@ -431,7 +432,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
};
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);
if (!canvasInstance.html().contains(document.activeElement) && document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
@ -561,7 +563,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
};
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);
@ -595,12 +598,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const {
activatedStateID,
activatedAttributeID,
canvasInstance,
selectedOpacity,
aamZoomMargin,
workspace,
annotations,
} = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
if (activatedStateID !== null) {
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 {
const { canvasInstance, frameIssues } = this.props;
const { frameIssues } = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
if (frameIssues === null) {
canvasInstance.setupIssueRegions({});
} else {
@ -687,12 +691,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
gridSize,
gridColor,
gridOpacity,
canvasInstance,
brightnessLevel,
contrastLevel,
saturationLevel,
canvasBackgroundColor,
} = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
// Size
window.addEventListener('resize', this.fitCanvas);
@ -773,12 +777,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
maxZLayer,
curZLayer,
minZLayer,
onSwitchZLayer,
onAddZLayer,
keyMap,
switchableAutomaticBordering,
automaticBordering,
onSwitchAutomaticBordering,
onSwitchZLayer,
onAddZLayer,
} = this.props;
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 />}>
<UpOutlined className='cvat-canvas-image-setups-trigger' />
</Dropdown>

@ -10,33 +10,56 @@ import {
ArrowDownOutlined, ArrowLeftOutlined, ArrowRightOutlined, ArrowUpOutlined,
} from '@ant-design/icons';
import { ResizableBox } from 'react-resizable';
import { Workspace } from 'reducers/interfaces';
import {
CAMERA_ACTION, Canvas3d, MouseInteraction, ViewType,
ColorBy, ContextMenuType, ObjectType, Workspace,
} from 'reducers/interfaces';
import {
CameraAction, Canvas3d, ViewType, ViewsDOM,
} from 'cvat-canvas3d-wrapper';
import ContextImage from '../standard3D-workspace/context-image/context-image';
import CVATTooltip from '../../common/cvat-tooltip';
import { Canvas } from 'cvat-canvas-wrapper';
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 {
canvasInstance: Canvas3d;
opacity: number;
selectedOpacity: number;
outlined: boolean;
outlineColor: string;
colorBy: ColorBy;
frameFetching: boolean;
canvasInstance: Canvas3d | Canvas;
jobInstance: any;
frameData: any;
curZLayer: number;
contextImageHide: boolean;
loaded: boolean;
data: string;
annotations: any[];
contextMenuVisibility: boolean;
activeLabelID: number;
activatedStateID: number | null;
activeObjectType: ObjectType;
onSetupCanvas: () => void;
getContextImage(): void;
onGroupObjects: (enabled: boolean) => 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;
animateID: any;
automaticBordering: boolean;
showObjectsTextAlways: boolean;
frame: number;
}
interface ViewSize {
fullHeight: number;
fullWidth: number;
vertical: number;
top: number;
side: number;
@ -45,7 +68,7 @@ interface ViewSize {
function viewSizeReducer(
state: ViewSize,
action: { type: ViewType | 'set'; e?: SyntheticEvent; data?: ViewSize },
action: { type: ViewType | 'set' | 'resize'; e?: SyntheticEvent; data?: ViewSize },
): ViewSize {
const event = (action.e as unknown) as MouseEvent;
const canvas3dContainer = document.getElementById('canvas3d-container');
@ -98,6 +121,33 @@ function viewSizeReducer(
};
case 'set':
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:
throw new Error();
}
@ -109,6 +159,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
const animateId = useRef(0);
const [viewSize, setViewSize] = useReducer(viewSizeReducer, {
fullHeight: 0,
fullWidth: 0,
vertical: 0,
top: 0,
side: 0,
@ -120,71 +171,122 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
const frontView = useRef<HTMLDivElement | null>(null);
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;
const { canvasInstance } = props as { canvasInstance: Canvas3d };
const onCanvasSetup = (): void => {
const { onSetupCanvas } = props;
onSetupCanvas();
};
const animateCanvas = (): void => {
const { canvasInstance } = props;
const onCanvasDragStart = (): void => {
const { onDragCanvas } = props;
onDragCanvas(true);
};
const onCanvasDragDone = (): void => {
const { onDragCanvas } = props;
onDragCanvas(false);
};
const animateCanvas = (): void => {
canvasInstance.render();
animateId.current = requestAnimationFrame(animateCanvas);
};
const updateCanvas = (): void => {
const { canvasInstance } = props;
if (frameData !== null) {
canvasInstance.setup(frameData);
canvasInstance.setup(
frameData,
annotations.filter((e) => e.objectType !== ObjectType.TAG),
);
}
};
const onMouseClick = (event: MouseEvent): void => {
const { canvasInstance } = props;
canvasInstance.mouseControls(MouseInteraction.CLICK, event);
const onCanvasCancel = (): void => {
onResetCanvas();
};
const onMouseDoubleClick = (event: MouseEvent): void => {
const { canvasInstance } = props;
canvasInstance.mouseControls(MouseInteraction.DOUBLE_CLICK, event);
};
const onCanvasShapeDrawn = (event: any): void => {
if (!event.detail.continue) {
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 => {
const { canvasInstance } = props;
canvasInstance.mouseControls(MouseInteraction.HOVER, event);
state.objectType = state.objectType || activeObjectType;
state.label = state.label || jobInstance.task.labels.filter((label: any) => label.id === activeLabelID)[0];
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 { onResetCanvas } = props;
onResetCanvas();
const onCanvasClick = (e: MouseEvent): void => {
const { onUpdateContextMenu } = props;
if (contextMenuVisibility) {
onUpdateContextMenu(false, e.clientX, e.clientY, ContextMenuType.CANVAS_SHAPE);
}
};
const initialSetup = (): void => {
const { canvasInstance } = props;
const canvasInstanceDOM = canvasInstance.html();
// Events
const canvasInstanceDOM = canvasInstance.html() as ViewsDOM;
canvasInstanceDOM.perspective.addEventListener('canvas.setup', onCanvasSetup);
canvasInstanceDOM.perspective.addEventListener('mousemove', onMouseHover);
canvasInstanceDOM.perspective.addEventListener('canvas.canceled', onCanvasCancel);
canvasInstanceDOM.perspective.addEventListener(MouseInteraction.DOUBLE_CLICK, onMouseDoubleClick);
canvasInstanceDOM.perspective.addEventListener(MouseInteraction.CLICK, onMouseClick);
canvasInstanceDOM.perspective.addEventListener('canvas.dragstart', onCanvasDragStart);
canvasInstanceDOM.perspective.addEventListener('canvas.dragstop', onCanvasDragDone);
};
const keyControls = (key: KeyboardEvent): void => {
const { canvasInstance } = props;
const keyControlsKeyDown = (key: KeyboardEvent): void => {
canvasInstance.keyControls(key);
};
useEffect(() => {
const { canvasInstance } = props;
const keyControlsKeyUp = (key: KeyboardEvent): void => {
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 (
perspectiveView &&
perspectiveView.current &&
@ -206,6 +308,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
type: 'set',
data: {
fullHeight: canvas3dContainer.clientHeight,
fullWidth: canvas3dContainer.clientWidth,
vertical: canvas3dContainer.clientHeight / 2,
top: 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();
updateCanvas();
@ -223,29 +327,89 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
return () => {
canvasInstanceDOM.perspective.removeEventListener('canvas.setup', onCanvasSetup);
canvasInstanceDOM.perspective.removeEventListener('mousemove', onMouseHover);
canvasInstanceDOM.perspective.removeEventListener('canvas.canceled', onCanvasCancel);
canvasInstanceDOM.perspective.removeEventListener(MouseInteraction.DOUBLE_CLICK, onMouseDoubleClick);
canvasInstanceDOM.perspective.removeEventListener(MouseInteraction.CLICK, onMouseClick);
document.removeEventListener('keydown', keyControls);
canvasInstanceDOM.perspective.removeEventListener('canvas.dragstart', onCanvasDragStart);
canvasInstanceDOM.perspective.removeEventListener('canvas.dragstop', onCanvasDragDone);
document.removeEventListener('keydown', keyControlsKeyDown);
document.removeEventListener('keyup', keyControlsKeyUp);
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(() => {
updateShapesView();
}, [opacity, outlined, outlineColor, selectedOpacity, colorBy]);
useEffect(() => {
const canvasInstanceDOM = canvasInstance.html() as ViewsDOM;
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 { canvasInstance } = props;
canvasInstance.keyControls(new KeyboardEvent('keydown', { code, altKey: true }));
const screenKeyControl = (code: CameraAction, altKey: boolean, shiftKey: boolean): void => {
canvasInstance.keyControls(new KeyboardEvent('keydown', { code, altKey, shiftKey }));
};
const ArrowGroup = (): ReactElement => (
<span className='cvat-canvas3d-perspective-arrow-directions'>
<CVATTooltip title='Arrow Up' placement='topRight'>
<CVATTooltip title='Shift+Arrow Up' placement='topRight'>
<button
onClick={() => screenKeyControl(CAMERA_ACTION.TILT_UP)}
data-cy='arrow-up'
onClick={() => screenKeyControl(CameraAction.TILT_UP, false, true)}
type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-up'
>
@ -253,27 +417,27 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</button>
</CVATTooltip>
<br />
<CVATTooltip title='Arrow Left' placement='topRight'>
<CVATTooltip title='Shift+Arrow Left' placement='topRight'>
<button
onClick={() => screenKeyControl(CAMERA_ACTION.ROTATE_LEFT)}
onClick={() => screenKeyControl(CameraAction.ROTATE_LEFT, false, true)}
type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
>
<ArrowLeftOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
</button>
</CVATTooltip>
<CVATTooltip title='Arrow Bottom' placement='topRight'>
<CVATTooltip title='Shift+Arrow Bottom' placement='topRight'>
<button
onClick={() => screenKeyControl(CAMERA_ACTION.TILT_DOWN)}
onClick={() => screenKeyControl(CameraAction.TILT_DOWN, false, true)}
type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
>
<ArrowDownOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
</button>
</CVATTooltip>
<CVATTooltip title='Arrow Right' placement='topRight'>
<CVATTooltip title='Shift+Arrow Right' placement='topRight'>
<button
onClick={() => screenKeyControl(CAMERA_ACTION.ROTATE_RIGHT)}
onClick={() => screenKeyControl(CameraAction.ROTATE_RIGHT, false, true)}
type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
>
@ -287,7 +451,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
<span className='cvat-canvas3d-perspective-directions'>
<CVATTooltip title='Alt+U' placement='topLeft'>
<button
onClick={() => screenKeyControl(CAMERA_ACTION.MOVE_UP)}
onClick={() => screenKeyControl(CameraAction.MOVE_UP, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
@ -296,7 +460,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</CVATTooltip>
<CVATTooltip title='Alt+I' placement='topLeft'>
<button
onClick={() => screenKeyControl(CAMERA_ACTION.ZOOM_IN)}
onClick={() => screenKeyControl(CameraAction.ZOOM_IN, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
@ -305,7 +469,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</CVATTooltip>
<CVATTooltip title='Alt+O' placement='topLeft'>
<button
onClick={() => screenKeyControl(CAMERA_ACTION.MOVE_DOWN)}
onClick={() => screenKeyControl(CameraAction.MOVE_DOWN, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
@ -315,7 +479,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
<br />
<CVATTooltip title='Alt+J' placement='topLeft'>
<button
onClick={() => screenKeyControl(CAMERA_ACTION.MOVE_LEFT)}
onClick={() => screenKeyControl(CameraAction.MOVE_LEFT, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
@ -324,7 +488,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</CVATTooltip>
<CVATTooltip title='Alt+K' placement='topLeft'>
<button
onClick={() => screenKeyControl(CAMERA_ACTION.ZOOM_OUT)}
onClick={() => screenKeyControl(CameraAction.ZOOM_OUT, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
@ -333,7 +497,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</CVATTooltip>
<CVATTooltip title='Alt+L' placement='topLeft'>
<button
onClick={() => screenKeyControl(CAMERA_ACTION.MOVE_RIGHT)}
onClick={() => screenKeyControl(CameraAction.MOVE_RIGHT, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
@ -345,13 +509,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
return (
<Layout.Content className='cvat-canvas3d-fullsize' id='canvas3d-container'>
<ContextImage
frame={frameData}
contextImageHide={contextImageHide}
getContextImage={getContextImage}
loaded={loaded}
data={data}
/>
<ContextImage />
<ResizableBox
className='cvat-resizable'
width={Infinity}
@ -360,7 +518,12 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
handle={<span className='cvat-resizable-handle-horizontal' />}
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} />
<ArrowGroup />
<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
style={{ visibility: hasChildren ? 'visible' : 'hidden' }}
className='cvat-extra-controls-control'
className='cvat-extra-controls-control cvat-antd-icon-control'
/>
</Popover>
);
@ -76,7 +76,7 @@ export default function ControlVisibilityObserver<P = {}>(
visibilityHeightThreshold = wrapper.offsetTop + wrapper.offsetHeight;
// start observing parent size
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);
}

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

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

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

@ -7,21 +7,29 @@ import Icon from '@ant-design/icons';
import { GroupIcon } from 'icons';
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';
export interface Props {
canvasInstance: Canvas;
canvasInstance: Canvas | Canvas3d;
activeControl: ActiveControl;
switchGroupShortcut: string;
resetGroupShortcut: string;
disabled?: boolean;
jobInstance?: any;
groupObjects(enabled: boolean): void;
}
function GroupControl(props: Props): JSX.Element {
const {
switchGroupShortcut, resetGroupShortcut, activeControl, canvasInstance, groupObjects, disabled,
switchGroupShortcut,
resetGroupShortcut,
activeControl,
canvasInstance,
groupObjects,
disabled,
jobInstance,
} = props;
const dynamicIconProps =
@ -43,7 +51,9 @@ function GroupControl(props: Props): JSX.Element {
};
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.`,
].join(' ');

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

@ -6,7 +6,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { Row, Col } from 'antd/lib/grid';
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 Tabs from 'antd/lib/tabs';
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 { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors';
import {
CombinedState, ActiveControl, OpenCVTool, ObjectType,
CombinedState, ActiveControl, OpenCVTool, ObjectType, ShapeType,
} from 'reducers/interfaces';
import {
interactWithCanvas,
fetchAnnotationsAsync,
updateAnnotationsAsync,
createAnnotationsAsync,
changeFrameAsync,
} from 'actions/annotation-actions';
import LabelSelector from 'components/label-selector/label-selector';
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';
interface Props {
@ -39,6 +44,8 @@ interface Props {
states: any[];
frame: number;
curZOrder: number;
defaultApproxPolyAccuracy: number;
frameData: any;
}
interface DispatchToProps {
@ -46,6 +53,7 @@ interface DispatchToProps {
updateAnnotations(statesToUpdate: any[]): void;
createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void;
fetchAnnotations(): void;
changeFrame(toFrame: number, fillBuffer?: boolean, frameStep?: number, forceUpdate?: boolean):void;
}
interface State {
@ -53,6 +61,13 @@ interface State {
initializationError: boolean;
initializationProgress: number;
activeLabelID: number;
approxPolyAccuracy: number;
activeImageModifiers: ImageModifier[];
}
interface ImageModifier {
modifier: ImageProcessing,
alias: string
}
const core = getCore();
@ -68,19 +83,24 @@ function mapStateToProps(state: CombinedState): Props {
job: { instance: jobInstance, labels },
canvas: { activeControl, instance: canvasInstance },
player: {
frame: { number: frame },
frame: { number: frame, data: frameData },
},
},
settings: {
workspace: { defaultApproxPolyAccuracy },
},
} = state;
return {
isActivated: activeControl === ActiveControl.OPENCV_TOOLS,
canvasInstance: canvasInstance as Canvas,
defaultApproxPolyAccuracy,
jobInstance,
curZOrder,
labels,
states,
frame,
frameData,
};
}
@ -89,78 +109,78 @@ const mapDispatchToProps = {
updateAnnotations: updateAnnotationsAsync,
fetchAnnotations: fetchAnnotationsAsync,
createAnnotations: createAnnotationsAsync,
changeFrame: changeFrameAsync,
};
class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps, State> {
private activeTool: IntelligentScissors | null;
private interactiveStateID: number | null;
private interactionIsDone: boolean;
private latestPoints: number[];
private canvasForceUpdateWasEnabled: boolean;
public constructor(props: Props & DispatchToProps) {
super(props);
const { labels } = props;
const { labels, defaultApproxPolyAccuracy } = props;
this.activeTool = null;
this.interactiveStateID = null;
this.interactionIsDone = false;
this.latestPoints = [];
this.canvasForceUpdateWasEnabled = false;
this.state = {
libraryInitialized: openCVWrapper.isInitialized,
initializationError: false,
initializationProgress: -1,
approxPolyAccuracy: defaultApproxPolyAccuracy,
activeLabelID: labels.length ? labels[0].id : null,
activeImageModifiers: [],
};
}
public componentDidMount(): void {
const { canvasInstance } = this.props;
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 {
const { isActivated } = this.props;
public componentDidUpdate(prevProps: Props, prevState: State): void {
const { approxPolyAccuracy } = this.state;
const { isActivated, defaultApproxPolyAccuracy, canvasInstance } = this.props;
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) {
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 {
const { canvasInstance } = this.props;
canvasInstance.html().removeEventListener('canvas.interacted', this.interactionListener);
canvasInstance.html().removeEventListener('canvas.canceled', this.cancelListener);
}
private getInteractiveState(): any | null {
const { states } = this.props;
return states.filter((_state: any): boolean => _state.clientID === this.interactiveStateID)[0] || null;
canvasInstance.html().removeEventListener('canvas.setup', this.runImageModifier);
}
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> => {
const { approxPolyAccuracy } = this.state;
const {
fetchAnnotations, updateAnnotations, isActivated, jobInstance, frame, labels, curZOrder,
createAnnotations, isActivated, jobInstance, frame, labels, curZOrder, canvasInstance,
} = this.props;
const { activeLabelID } = this.state;
if (!isActivated || !this.activeTool) {
@ -171,65 +191,81 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
shapesUpdated, isDone, threshold, shapes,
} = (e as CustomEvent).detail;
const pressedPoints = convertShapesForInteractor(shapes, 0).flat();
this.interactionIsDone = isDone;
try {
let points: number[] = [];
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 (!this.interactionIsDone) {
await jobInstance.actions.freeze(true);
}
const object = new core.classes.ObjectState({
...this.activeTool.params.shape,
if (isDone) {
// need to recalculate without the latest sliding point
const finalPoints = await this.runCVAlgorithm(pressedPoints, threshold);
const finalObject = new core.classes.ObjectState({
frame,
objectType: ObjectType.SHAPE,
shapeType: ShapeType.POLYGON,
label: labels.filter((label: any) => label.id === activeLabelID)[0],
points,
points: openCVWrapper.contours
.approxPoly(finalPoints, thresholdFromAccuracy(approxPolyAccuracy))
.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]);
this.interactiveStateID = clientID;
// update annotations on a canvas
fetchAnnotations();
return;
createAnnotations(jobInstance, frame, [finalObject]);
}
} catch (error) {
notification.error({
description: error.toString(),
message: 'OpenCV.js processing error occured',
});
}
};
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,
// need to recalculate without the latest sliding point
points: points = await this.runCVAlgorithm(pressedPoints, threshold),
occluded: state.occluded,
zOrder: state.zOrder,
});
this.interactiveStateID = null;
await state.delete(frame);
await jobInstance.actions.freeze(false);
await jobInstance.annotations.put([finalObject]);
fetchAnnotations();
} else {
state.points = points;
updateAnnotations([state]);
fetchAnnotations();
private runImageModifier = async ():Promise<void> => {
const { activeImageModifiers } = this.state;
const {
frameData, states, curZOrder, canvasInstance, frame,
} = this.props;
try {
if (activeImageModifiers.length !== 0 && activeImageModifiers[0].modifier.currentProcessedImage !== frame) {
this.enableCanvasForceUpdate();
const canvas: HTMLCanvasElement | undefined = window.document.getElementById('cvat_canvas_background') as
| HTMLCanvasElement
| undefined;
if (!canvas) {
throw new Error('Element #cvat_canvas_background was not found');
}
const { width, height } = canvas;
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Canvas context is empty');
}
const imageData = context.getImageData(0, 0, width, height);
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) {
notification.error({
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
const points = await this.activeTool.run(pressedPoints, imageData, startX, startY);
return points;
}
// Increasing number of points artificially
let minNumberOfPoints = 1;
// eslint-disable-next-line: eslintdot-notation
if (this.activeTool.params.shape.shapeType === 'polyline') {
minNumberOfPoints = 2;
} else if (this.activeTool.params.shape.shapeType === 'polygon') {
minNumberOfPoints = 3;
}
while (points.length < minNumberOfPoints * 2) {
points.push(...points.slice(points.length - 2));
private imageModifier(alias: string): ImageProcessing|null {
const { activeImageModifiers } = this.state;
return activeImageModifiers.find((imageModifier) => imageModifier.alias === alias)?.modifier || null;
}
private disableImageModifier(alias: string):void {
const { activeImageModifiers } = this.state;
const index = activeImageModifiers.findIndex((imageModifier) => imageModifier.alias === alias);
if (index !== -1) {
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 {
@ -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 {
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'>
{this.renderDrawingContent()}
</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>
) : (
<>
@ -384,6 +478,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
public render(): JSX.Element {
const { isActivated, canvasInstance, labels } = this.props;
const { libraryInitialized, approxPolyAccuracy } = this.state;
const dynamcPopoverPros = isActivated ?
{
overlayStyle: {
@ -406,14 +501,31 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
return !labels.length ? (
<Icon className='cvat-opencv-control cvat-disabled-canvas-control' component={OpenCVIcon} />
) : (
<CustomPopover
{...dynamcPopoverPros}
placement='right'
overlayClassName='cvat-opencv-control-popover'
content={this.renderContent()}
>
<Icon {...dynamicIconProps} component={OpenCVIcon} />
</CustomPopover>
<>
<CustomPopover
{...dynamcPopoverPros}
placement='right'
overlayClassName='cvat-opencv-control-popover'
content={this.renderContent()}
afterVisibleChange={() => {
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 { Row, Col } from 'antd/lib/grid';
import notification from 'antd/lib/notification';
import message from 'antd/lib/message';
import Progress from 'antd/lib/progress';
import InputNumber from 'antd/lib/input-number';
@ -20,6 +21,7 @@ import { AIToolsIcon } from 'icons';
import { Canvas, convertShapesForInteractor } from 'cvat-canvas-wrapper';
import range from 'utils/range';
import getCore from 'cvat-core-wrapper';
import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper';
import {
CombinedState, ActiveControl, Model, ObjectType, ShapeType,
} from 'reducers/interfaces';
@ -31,6 +33,9 @@ import {
} from 'actions/annotation-actions';
import DetectorRunner from 'components/model-runner-modal/detector-runner';
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';
interface StateToProps {
@ -46,6 +51,7 @@ interface StateToProps {
trackers: Model[];
curZOrder: number;
aiToolsRef: MutableRefObject<any>;
defaultApproxPolyAccuracy: number;
}
interface DispatchToProps {
@ -60,6 +66,7 @@ const CustomPopover = withVisibilityHandling(Popover, 'tools-control');
function mapStateToProps(state: CombinedState): StateToProps {
const { annotation } = state;
const { settings } = state;
const { number: frame } = annotation.player.frame;
const { instance: jobInstance } = annotation.job;
const { instance: canvasInstance, activeControl } = annotation.canvas;
@ -79,32 +86,35 @@ function mapStateToProps(state: CombinedState): StateToProps {
frame,
curZOrder: annotation.annotations.zLayer.cur,
aiToolsRef: annotation.aiToolsRef,
defaultApproxPolyAccuracy: settings.workspace.defaultApproxPolyAccuracy,
};
}
const mapDispatchToProps = {
onInteractionStart: interactWithCanvas,
updateAnnotations: updateAnnotationsAsync,
fetchAnnotations: fetchAnnotationsAsync,
createAnnotations: createAnnotationsAsync,
fetchAnnotations: fetchAnnotationsAsync,
};
type Props = StateToProps & DispatchToProps;
interface State {
activeInteractor: Model | null;
activeLabelID: number;
interactiveStateID: number | null;
activeTracker: Model | null;
trackingProgress: number | null;
trackingFrames: number;
fetching: boolean;
pointsRecieved: boolean;
approxPolyAccuracy: number;
mode: 'detection' | 'interaction' | 'tracking';
}
export class ToolsControlComponent extends React.PureComponent<Props, State> {
private interactionIsAborted: boolean;
private interactionIsDone: boolean;
private latestResponseResult: number[][];
private latestResult: number[][];
public constructor(props: Props) {
super(props);
@ -112,13 +122,16 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
activeInteractor: props.interactors.length ? props.interactors[0] : null,
activeTracker: props.trackers.length ? props.trackers[0] : null,
activeLabelID: props.labels.length ? props.labels[0].id : null,
interactiveStateID: null,
approxPolyAccuracy: props.defaultApproxPolyAccuracy,
trackingProgress: null,
trackingFrames: 10,
fetching: false,
pointsRecieved: false,
mode: 'interaction',
};
this.latestResponseResult = [];
this.latestResult = [];
this.interactionIsAborted = false;
this.interactionIsDone = false;
}
@ -130,16 +143,39 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
canvasInstance.html().addEventListener('canvas.canceled', this.cancelListener);
}
public componentDidUpdate(prevProps: Props): void {
const { isActivated } = this.props;
public componentDidUpdate(prevProps: Props, prevState: State): void {
const { isActivated, defaultApproxPolyAccuracy, canvasInstance } = this.props;
const { approxPolyAccuracy, activeInteractor } = this.state;
if (prevProps.isActivated && !isActivated) {
window.removeEventListener('contextmenu', this.contextmenuDisabler);
} else if (!prevProps.isActivated && isActivated) {
// reset flags when start interaction/tracking
this.setState({
approxPolyAccuracy: defaultApproxPolyAccuracy,
pointsRecieved: false,
});
this.latestResult = [];
this.latestResponseResult = [];
this.interactionIsDone = false;
this.interactionIsAborted = false;
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 {
@ -149,12 +185,6 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
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 => {
if (
e.target &&
@ -166,10 +196,9 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
};
private cancelListener = async (): Promise<void> => {
const {
isActivated, jobInstance, frame, fetchAnnotations,
} = this.props;
const { interactiveStateID, fetching } = this.state;
const { isActivated } = this.props;
const { fetching } = this.state;
this.latestResult = [];
if (isActivated) {
if (fetching && !this.interactionIsDone) {
@ -177,15 +206,6 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
this.setState({ fetching: false });
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,
isActivated,
activeLabelID,
fetchAnnotations,
updateAnnotations,
canvasInstance,
createAnnotations,
} = this.props;
const { activeInteractor, interactiveStateID, fetching } = this.state;
const { activeInteractor, fetching } = this.state;
if (!isActivated) {
return;
}
try {
if (fetching) {
this.interactionIsDone = (e as CustomEvent).detail.isDone;
return;
}
this.interactionIsDone = (e as CustomEvent).detail.isDone;
const interactor = activeInteractor as Model;
let result = [];
if ((e as CustomEvent).detail.shapesUpdated) {
this.setState({ fetching: true });
try {
result = await core.lambda.call(jobInstance.task, interactor, {
this.latestResponseResult = await core.lambda.call(jobInstance.task, interactor, {
frame,
pos_points: convertShapesForInteractor((e as CustomEvent).detail.shapes, 0),
neg_points: convertShapesForInteractor((e as CustomEvent).detail.shapes, 2),
});
this.latestResult = this.latestResponseResult;
if (this.interactionIsAborted) {
// while the server request
// user has cancelled interaction (for example pressed ESC)
// need to clean variables that have been just set
this.latestResult = [];
this.latestResponseResult = [];
return;
}
this.latestResult = await this.approximateResponsePoints(this.latestResponseResult);
} finally {
this.setState({ fetching: false });
this.setState({ fetching: false, pointsRecieved: !!this.latestResult.length });
}
}
if (this.interactionIsDone) {
// while the server request, user has done interaction (for example pressed N)
if (!this.latestResult.length) {
return;
}
if (this.interactionIsDone && !fetching) {
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,
points: result.flat(),
points: this.latestResult.flat(),
occluded: false,
zOrder: curZOrder,
});
await jobInstance.annotations.put([object]);
fetchAnnotations();
createAnnotations(jobInstance, frame, [object]);
} else {
// no shape yet, then create it and save to collection
if (interactiveStateID === null) {
// freeze history for interaction time
// (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,
canvasInstance.interact({
enabled: true,
intermediateShape: {
shapeType: ShapeType.POLYGON,
points: result.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();
}
points: this.latestResult.flat(),
},
});
}
} catch (err) {
notification.error({
@ -315,7 +301,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
const { activeLabelID } = this.state;
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;
}
@ -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> {
const { jobInstance, frame } = this.props;
const { jobInstance, frame, fetchAnnotations } = this.props;
const { activeTracker, trackingFrames } = this.state;
const { clientID, points } = state;
@ -429,6 +435,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
}
} finally {
this.setState({ trackingProgress: null, fetching: false });
fetchAnnotations();
}
}
@ -633,7 +640,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
private renderDetectorBlock(): JSX.Element {
const {
jobInstance, detectors, curZOrder, frame, fetchAnnotations,
jobInstance, detectors, curZOrder, frame, createAnnotations,
} = this.props;
if (!detectors.length) {
@ -672,8 +679,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
}),
);
await jobInstance.annotations.put(states);
fetchAnnotations();
createAnnotations(jobInstance, frame, states);
} catch (error) {
notification.error({
description: error.toString(),
@ -718,7 +724,9 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
const {
interactors, detectors, trackers, isActivated, canvasInstance, labels,
} = this.props;
const { fetching, trackingProgress } = this.state;
const {
fetching, trackingProgress, approxPolyAccuracy, activeInteractor, pointsRecieved,
} = this.state;
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' />
)}
</Modal>
{isActivated && activeInteractor !== null && pointsRecieved ? (
<ApproximationAccuracy
approxPolyAccuracy={approxPolyAccuracy}
onChange={(value: number) => {
this.setState({ approxPolyAccuracy: value });
}}
/>
) : null}
<CustomPopover {...dynamcPopoverPros} placement='right' content={this.renderPopoverContent()}>
<Icon {...dynamicIconProps} component={AIToolsIcon} />
</CustomPopover>

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

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

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

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

Loading…
Cancel
Save