Merge branch 'release-1.3.0'

main
Nikita Manovich 5 years ago
commit c7033a79ec

@ -5,6 +5,7 @@ branch = true
source =
cvat/apps/
utils/cli/
utils/dataset_manifest
omit =
cvat/settings/*

@ -1,4 +1,4 @@
// Copyright (C) 2018-2020 Intel Corporation
// Copyright (C) 2018-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -16,8 +16,8 @@ module.exports = {
extends: ['eslint:recommended', 'prettier'],
rules: {
'header/header': [2, 'line', [{
pattern: ' {1}Copyright \\(C\\) (?:20\\d{2}-)?2020 Intel Corporation',
template: ' Copyright (C) 2020 Intel Corporation'
pattern: ' {1}Copyright \\(C\\) (?:20\\d{2}-)?2021 Intel Corporation',
template: ' Copyright (C) 2021 Intel Corporation'
}, '', ' SPDX-License-Identifier: MIT']],
},
};

@ -1,5 +1,5 @@
<!---
Copyright (C) 2020 Intel Corporation
Copyright (C) 2020-2021 Intel Corporation
SPDX-License-Identifier: MIT
-->
@ -45,7 +45,7 @@ If you're unsure about any of these, don't hesitate to ask. We're here to help!
- [ ] I have updated the license header for each file (see an example below)
```python
# Copyright (C) 2020 Intel Corporation
# Copyright (C) 2021 Intel Corporation
#
# SPDX-License-Identifier: MIT
```

@ -0,0 +1,41 @@
name: Linter
on: pull_request
jobs:
Bandit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- 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 == 'py' ]]; then
changed_files_bandit+=" ${files}"
fi
done
if [[ ! -z ${changed_files_bandit} ]]; then
sudo apt-get --no-install-recommends install -y build-essential curl python3-dev python3-pip python3-venv
python3 -m venv .env
. .env/bin/activate
pip install -U pip wheel setuptools
pip install bandit
mkdir -p bandit_report
echo "Bandit version: "`bandit --version | head -1`
echo "The files will be checked: "`echo ${changed_files_bandit}`
bandit ${changed_files_bandit} --exclude '**/tests/**' -a file --ini ./.bandit -f html -o ./bandit_report/bandit_checks.html
deactivate
else
echo "No files with the \"py\" extension found"
fi
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v2
with:
name: bandit_report
path: bandit_report

@ -0,0 +1,42 @@
name: Linter
on: pull_request
jobs:
ESLint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- 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 == 'js' || $extension == 'ts' || $extension == 'jsx' || $extension == 'tsx' ]]; then
changed_files_eslint+=" ${files}"
fi
done
if [[ ! -z ${changed_files_eslint} ]]; then
for package_files in `find -maxdepth 2 -name "package.json" -type f`; do
cd $(dirname $package_files) && npm ci && cd ${{ github.workspace }}
done
npm install eslint-detailed-reporter --save-dev
mkdir -p eslint_report
echo "ESLint version: "`npx eslint --version`
echo "The files will be checked: "`echo ${changed_files_eslint}`
npx eslint ${changed_files_eslint} -f node_modules/eslint-detailed-reporter/lib/detailed.js -o ./eslint_report/eslint_checks.html
else
echo "No files with the \"js|ts|jsx|tsx\" extension found"
fi
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v2
with:
name: eslint_report
path: eslint_report

@ -0,0 +1,57 @@
name: CI
on:
push:
branches:
- 'master'
- 'develop'
pull_request:
branches:
- '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- name: Build CVAT
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 build
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 && mv .coverage ${CONTAINER_COVERAGE_DATA_DIR}'
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 && mv ./reports/coverage/lcov.info ${CONTAINER_COVERAGE_DATA_DIR} && chmod a+rwx ${CONTAINER_COVERAGE_DATA_DIR}/lcov.info'
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"
- name: Code instrumentation
run: |
npm ci
npm run coverage
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
- name: End-to-end testing
run: |
cd ./tests
npm ci
npx cypress run --headless --browser chrome
- name: Uploading cypress screenshots as an artifact
if: failure()
uses: actions/upload-artifact@v2
with:
name: cypress_screenshots
path: ${{ github.workspace }}/tests/cypress/screenshots
- name: Collect coverage data
env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
CONTAINER_COVERAGE_DATA_DIR: "/coverage_data"
COVERALLS_SERVICE_NAME: github
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./tests/.nyc_output ./
npx nyc report --reporter=text-lcov >> ${HOST_COVERAGE_DATA_DIR}/lcov.info
docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd ${CONTAINER_COVERAGE_DATA_DIR} && coveralls-lcov -v -n lcov.info > ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json'
docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'ln -s ${CONTAINER_COVERAGE_DATA_DIR}/.git . && ln -s ${CONTAINER_COVERAGE_DATA_DIR}/.coverage . && ln -s ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json . && coveralls --merge=coverage.json'

@ -0,0 +1,61 @@
name: Publish Docker images
on:
release:
types: [published]
jobs:
build_and_push_to_registry:
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"
- name: Run end-to-end tests
run: |
cd ./tests
npm ci
npm run cypress:run:chrome
- name: Uploading cypress screenshots as an artifact
if: failure()
uses: actions/upload-artifact@v2
with:
name: cypress_screenshots
path: ${{ github.workspace }}/tests/cypress/screenshots
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push to Docker Hub
env:
DOCKERHUB_WORKSPACE: 'openvino'
SERVER_IMAGE_REPO: 'cvat_server'
UI_IMAGE_REPO: 'cvat_ui'
run: |
docker tag "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:latest" "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:${{ github.event.release.tag_name }}"
docker push "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:${{ github.event.release.tag_name }}"
docker push "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:latest"
docker tag "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:latest" "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:${{ github.event.release.tag_name }}"
docker push "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:${{ github.event.release.tag_name }}"
docker push "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:latest"

@ -0,0 +1,45 @@
name: Linter
on: pull_request
jobs:
PyLint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- 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 == 'py' ]]; then
changed_files_pylint+=" ${files}"
fi
done
if [[ ! -z ${changed_files_pylint} ]]; then
sudo apt-get --no-install-recommends install -y build-essential curl python3-dev python3-pip python3-venv
python3 -m venv .env
. .env/bin/activate
pip install -U pip wheel setuptools
pip install pylint-json2html
pip install $(egrep "pylint.*" ./cvat/requirements/development.txt)
pip install $(egrep "Django.*" ./cvat/requirements/base.txt)
mkdir -p pylint_report
echo "Pylint version: "`pylint --version | head -1`
echo "The files will be checked: "`echo ${changed_files_pylint}`
pylint ${changed_files_pylint} --output-format=json > ./pylint_report/pylint_checks.json || exit_code=`echo $?` || true
pylint-json2html -o ./pylint_report/pylint_checks.html ./pylint_report/pylint_checks.json
deactivate
exit ${exit_code}
else
echo "No files with the \"py\" extension found"
fi
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v2
with:
name: pylint_report
path: pylint_report

@ -0,0 +1,34 @@
name: CI-nightly
on:
schedule:
- cron: '0 22 * * *'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- name: Build CVAT
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 -f ./tests/docker-compose.email.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
run: |
cd ./tests
npm ci
npm run cypress:run:firefox
- name: Uploading cypress screenshots as an artifact
if: failure()
uses: actions/upload-artifact@v2
with:
name: cypress_screenshots
path: ${{ github.workspace }}/tests/cypress/screenshots

@ -3,7 +3,7 @@ exports.settings = { bullet: '*', paddedTable: false };
exports.plugins = [
'remark-preset-lint-recommended',
'remark-preset-lint-consistent',
['remark-preset-lint-markdown-style-guide', 'mixed'],
['remark-lint-list-item-indent', 'space'],
['remark-lint-no-dead-urls', { skipOffline: true }],
['remark-lint-maximum-line-length', 120],
['remark-lint-maximum-heading-length', 120],

@ -1,57 +0,0 @@
language: generic
dist: focal
cache:
npm: true
directories:
- ~/.cache
addons:
firefox: 'latest'
chrome: stable
apt:
packages:
- libgconf-2-4
services:
- docker
env:
- CONTAINER_COVERAGE_DATA_DIR="/coverage_data"
HOST_COVERAGE_DATA_DIR="${TRAVIS_BUILD_DIR}"
DJANGO_SU_NAME="admin"
DJANGO_SU_EMAIL="admin@localhost.company"
DJANGO_SU_PASSWORD="12qwaszx"
NODE_VERSION="12"
API_ABOUT_PAGE="localhost:8080/api/v1/server/about"
before_install:
- nvm install ${NODE_VERSION}
before_script:
- chmod a+rwx ${HOST_COVERAGE_DATA_DIR}
script:
- if [[ $TRAVIS_EVENT_TYPE == "cron" && $TRAVIS_BRANCH == "develop" ]];
then
docker-compose -f docker-compose.yml -f ./tests/docker-compose.email.yml up -d --build;
bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done';
docker exec -it cvat bash -ic "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell";
cd ./tests && npm install && npm run cypress:run:firefox; exit $?;
fi;
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml build
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'coverage run -a manage.py test cvat/apps utils/cli && mv .coverage ${CONTAINER_COVERAGE_DATA_DIR}'
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-data && npm install && cd ../cvat-core && npm install && npm run test && mv ./reports/coverage/lcov.info ${CONTAINER_COVERAGE_DATA_DIR} && chmod a+rwx ${CONTAINER_COVERAGE_DATA_DIR}/lcov.info'
- docker-compose up -d
- docker exec -it cvat bash -ic "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
# End-to-end testing
- npm install && npm run coverage
- docker-compose up -d --build
- cd ./tests && npm install && npx cypress run --headless --browser chrome
- mv ./.nyc_output ../ && cd ..
- npx nyc report --reporter=text-lcov >> ${HOST_COVERAGE_DATA_DIR}/lcov.info
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd ${CONTAINER_COVERAGE_DATA_DIR} && coveralls-lcov -v -n lcov.info > ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json'
after_success:
# https://coveralls-python.readthedocs.io/en/latest/usage/multilang.html
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'ln -s ${CONTAINER_COVERAGE_DATA_DIR}/.git . && ln -s ${CONTAINER_COVERAGE_DATA_DIR}/.coverage . && ln -s ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json . && coveralls --merge=coverage.json'

@ -42,8 +42,11 @@
"request": "launch",
"stopOnEntry": false,
"justMyCode": false,
"pythonPath": "${command:python.interpreterPath}",
"python": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"env": {
"CVAT_SERVERLESS": "1",
},
"args": [
"runserver",
"--noreload",
@ -73,7 +76,7 @@
"request": "launch",
"stopOnEntry": false,
"justMyCode": false,
"pythonPath": "${command:python.interpreterPath}",
"python": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"rqworker",
@ -92,7 +95,7 @@
"request": "launch",
"stopOnEntry": false,
"justMyCode": false,
"pythonPath": "${command:python.interpreterPath}",
"python": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"rqscheduler",
@ -108,7 +111,7 @@
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"pythonPath":"${command:python.interpreterPath}",
"python":"${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"rqworker",
@ -127,7 +130,7 @@
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"pythonPath": "${command:python.interpreterPath}",
"python": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"update_git_states"
@ -143,7 +146,7 @@
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"pythonPath": "${command:python.interpreterPath}",
"python": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"migrate"
@ -159,7 +162,7 @@
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"pythonPath": "${command:python.interpreterPath}",
"python": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"test",

@ -1,6 +1,5 @@
{
"python.pythonPath": ".env/bin/python",
"eslint.enable": true,
"eslint.probe": [
"javascript",
"typescript",
@ -19,8 +18,10 @@
"!cwd": true
}
],
"npm.exclude": "**/.env/**",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.testing.unittestEnabled": true,
"python.linting.pycodestyleEnabled": false,
"licenser.license": "Custom",
"licenser.customHeader": "Copyright (C) @YEAR@ Intel Corporation\n\nSPDX-License-Identifier: MIT",
"files.trimTrailingWhitespace": true

@ -5,6 +5,79 @@ 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.3.0] - 3/31/2021
### Added
- CLI: Add support for saving annotations in a git repository when creating a task.
- CVAT-3D: support lidar data on the server side (<https://github.com/openvinotoolkit/cvat/pull/2534>)
- GPU support for Mask-RCNN and improvement in its deployment time (<https://github.com/openvinotoolkit/cvat/pull/2714>)
- CVAT-3D: Load all frames corresponding to the job instance
(<https://github.com/openvinotoolkit/cvat/pull/2645>)
- Intelligent scissors with OpenCV javascript (<https://github.com/openvinotoolkit/cvat/pull/2689>)
- CVAT-3D: Visualize 3D point cloud spaces in 3D View, Top View Side View and Front View (<https://github.com/openvinotoolkit/cvat/pull/2768>)
- [Inside Outside Guidance](https://github.com/shiyinzhang/Inside-Outside-Guidance) serverless
function for interactive segmentation
- Pre-built [cvat_server](https://hub.docker.com/r/openvino/cvat_server) and
[cvat_ui](https://hub.docker.com/r/openvino/cvat_ui) images were published on DockerHub (<https://github.com/openvinotoolkit/cvat/pull/2766>)
- Project task subsets (<https://github.com/openvinotoolkit/cvat/pull/2774>)
- Kubernetes templates and guide for their deployment (<https://github.com/openvinotoolkit/cvat/pull/1962>)
- [WiderFace](http://shuoyang1213.me/WIDERFACE/) format support (<https://github.com/openvinotoolkit/cvat/pull/2864>)
- [VGGFace2](https://github.com/ox-vgg/vgg_face2) format support (<https://github.com/openvinotoolkit/cvat/pull/2865>)
- [Backup/Restore guide](cvat/apps/documentation/backup_guide.md) (<https://github.com/openvinotoolkit/cvat/pull/2964>)
- Label deletion from tasks and projects (<https://github.com/openvinotoolkit/cvat/pull/2881>)
- CVAT-3D: Implemented initial cuboid placement in 3D View and select cuboid in Top, Side and Front views
(<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)
- 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>
- Filters and sorting options for job list, added tooltip for tasks filters (<https://github.com/openvinotoolkit/cvat/pull/3030>)
### Changed
- CLI - task list now returns a list of current tasks. (<https://github.com/openvinotoolkit/cvat/pull/2863>)
- Updated HTTPS install README section (cleanup and described more robust deploy)
- Logstash is improved for using with configurable elasticsearch outputs (<https://github.com/openvinotoolkit/cvat/pull/2531>)
- Bumped nuclio version to 1.5.16 (<https://github.com/openvinotoolkit/cvat/pull/2578>)
- All methods for interactive segmentation accept negative points as well
- Persistent queue added to logstash (<https://github.com/openvinotoolkit/cvat/pull/2744>)
- Improved maintenance of popups visibility (<https://github.com/openvinotoolkit/cvat/pull/2809>)
- Image visualizations settings on canvas for faster access (<https://github.com/openvinotoolkit/cvat/pull/2872>)
- Better scale management of left panel when screen is too small (<https://github.com/openvinotoolkit/cvat/pull/2880>)
- Improved error messages for annotation import (<https://github.com/openvinotoolkit/cvat/pull/2935>)
- Using manifest support instead video meta information and dummy chunks (<https://github.com/openvinotoolkit/cvat/pull/2763>)
### Fixed
- More robust execution of nuclio GPU functions by limiting the GPU memory consumption per worker (<https://github.com/openvinotoolkit/cvat/pull/2714>)
- Kibana startup initialization (<https://github.com/openvinotoolkit/cvat/pull/2659>)
- The cursor jumps to the end of the line when renaming a task (<https://github.com/openvinotoolkit/cvat/pull/2669>)
- SSLCertVerificationError when remote source is used (<https://github.com/openvinotoolkit/cvat/pull/2683>)
- 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
- 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>)
- Annotation page popups (ai tools, drawing) reset state after detecting, tracking, drawing (<https://github.com/openvinotoolkit/cvat/pull/2780>)
- Polygon editing using trailing point (<https://github.com/openvinotoolkit/cvat/pull/2808>)
- Updated the path to python for DL models inside automatic annotation documentation (<https://github.com/openvinotoolkit/cvat/pull/2847>)
- Fixed of receiving function variable (<https://github.com/openvinotoolkit/cvat/pull/2860>)
- Shortcuts with CAPSLOCK enabled and with non-US languages activated (<https://github.com/openvinotoolkit/cvat/pull/2872>)
- Prevented creating several issues for the same object (<https://github.com/openvinotoolkit/cvat/pull/2868>)
- Fixed label editor name field validator (<https://github.com/openvinotoolkit/cvat/pull/2879>)
- An error about track shapes outside of the task frames during export (<https://github.com/openvinotoolkit/cvat/pull/2890>)
- Fixed project search field updating (<https://github.com/openvinotoolkit/cvat/pull/2901>)
- Fixed export error when invalid polygons are present in overlapping frames (<https://github.com/openvinotoolkit/cvat/pull/2852>)
- Fixed image quality option for tasks created from images (<https://github.com/openvinotoolkit/cvat/pull/2963>)
- Incorrect text on the warning when specifying an incorrect link to the issue tracker (<https://github.com/openvinotoolkit/cvat/pull/2971>)
- 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
### Fixed

@ -122,10 +122,10 @@ You develop CVAT under WSL (Windows subsystem for Linux) following next steps.
### DL models as serverless functions
Install [nuclio platform](https://github.com/nuclio/nuclio):
Follow this [guide](/cvat/apps/documentation/installation_automatic_annotation.md) to install Nuclio:
- You have to install `nuctl` command line tool to build and deploy serverless
functions. Download [the latest release](https://github.com/nuclio/nuclio/blob/development/docs/reference/nuctl/nuctl.md#download).
functions.
- The simplest way to explore Nuclio is to run its graphical user interface (GUI)
of the Nuclio dashboard. All you need in order to run the dashboard is Docker. See
[nuclio documentation](https://github.com/nuclio/nuclio#quick-start-steps)

@ -34,15 +34,17 @@ RUN curl -sL https://github.com/cisco/openh264/archive/v${OPENH264_VERSION}.tar.
make -j5 && make install PREFIX=${PREFIX} && make clean
WORKDIR /tmp/ffmpeg
RUN curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
RUN curl -sL https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 --output - | \
tar -jx --strip-components=1 && \
./configure --disable-nonfree --disable-gpl --enable-libopenh264 --enable-shared --disable-static --prefix="${PREFIX}" && \
make -j5 && make install && make distclean
# make clean keeps the configuration files that let to know how the original sources were used to create the binary
make -j5 && make install && make clean && \
tar -zcf "/tmp/ffmpeg-$FFMPEG_VERSION.tar.gz" . && mv "/tmp/ffmpeg-$FFMPEG_VERSION.tar.gz" .
# Install requirements
RUN python3 -m venv /opt/venv
ENV PATH="/opt/venv/bin:${PATH}"
RUN python3 -m pip install --no-cache-dir -U pip==20.0.1 setuptools==49.6.0 wheel==0.35.1
RUN python3 -m pip install --no-cache-dir -U pip==21.0.1 setuptools==53.0.0 wheel==0.36.2
COPY cvat/requirements/ /tmp/requirements/
RUN DATUMARO_HEADLESS=1 python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt
@ -72,7 +74,10 @@ ENV DJANGO_CONFIGURATION=${DJANGO_CONFIGURATION}
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
apache2 \
ca-certificates \
libapache2-mod-xsendfile \
libgomp1 \
libgl1 \
supervisor \
libldap-2.4-2 \
libsasl2-2 \
@ -90,8 +95,17 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/* && \
echo 'application/wasm wasm' >> /etc/mime.types
ARG CLAM_AV
ENV CLAM_AV=${CLAM_AV}
# Add a non-root user
ENV USER=${USER}
ENV HOME /home/${USER}
RUN adduser --shell /bin/bash --disabled-password --gecos "" ${USER} && \
if [ -z ${socks_proxy} ]; then \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30\"" >> ${HOME}/.bashrc; \
else \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ProxyCommand='nc -X 5 -x ${socks_proxy} %h %p'\"" >> ${HOME}/.bashrc; \
fi
ARG CLAM_AV="no"
RUN if [ "$CLAM_AV" = "yes" ]; then \
apt-get update && \
apt-get --no-install-recommends install -yq \
@ -103,16 +117,6 @@ RUN if [ "$CLAM_AV" = "yes" ]; then \
rm -rf /var/lib/apt/lists/*; \
fi
# Add a non-root user
ENV USER=${USER}
ENV HOME /home/${USER}
RUN adduser --shell /bin/bash --disabled-password --gecos "" ${USER} && \
if [ -z ${socks_proxy} ]; then \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30\"" >> ${HOME}/.bashrc; \
else \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ProxyCommand='nc -X 5 -x ${socks_proxy} %h %p'\"" >> ${HOME}/.bashrc; \
fi
ARG INSTALL_SOURCES='no'
WORKDIR ${HOME}/sources
RUN if [ "$INSTALL_SOURCES" = "yes" ]; then \
@ -128,7 +132,7 @@ RUN if [ "$INSTALL_SOURCES" = "yes" ]; then \
done && \
rm -rf /var/lib/apt/lists/*; \
fi
COPY --from=build-image /tmp/openh264/openh264*.tar.gz /tmp/ffmpeg/ffmpeg*.tar.bz2 ${HOME}/sources/
COPY --from=build-image /tmp/openh264/openh264*.tar.gz /tmp/ffmpeg/ffmpeg*.tar.gz ${HOME}/sources/
# Copy python virtual enviroment and FFmpeg binaries from build-image
COPY --from=build-image /opt/venv /opt/venv

@ -1,4 +1,4 @@
FROM cvat/server
FROM openvino/cvat_server
ENV DJANGO_CONFIGURATION=testing
USER root

@ -20,6 +20,7 @@ RUN apk add python3 g++ make
# Install dependencies
COPY cvat-core/package*.json /tmp/cvat-core/
COPY cvat-canvas/package*.json /tmp/cvat-canvas/
COPY cvat-canvas3d/package*.json /tmp/cvat-canvas3d/
COPY cvat-ui/package*.json /tmp/cvat-ui/
COPY cvat-data/package*.json /tmp/cvat-data/
@ -35,6 +36,10 @@ RUN npm ci
WORKDIR /tmp/cvat-canvas/
RUN npm ci
# Install cvat-canvas dependencies
WORKDIR /tmp/cvat-canvas3d/
RUN npm ci
# Install cvat-ui dependencies
WORKDIR /tmp/cvat-ui/
RUN npm ci
@ -42,6 +47,7 @@ RUN npm ci
# Build source code
COPY cvat-data/ /tmp/cvat-data/
COPY cvat-core/ /tmp/cvat-core/
COPY cvat-canvas3d/ /tmp/cvat-canvas3d/
COPY cvat-canvas/ /tmp/cvat-canvas/
COPY cvat-ui/ /tmp/cvat-ui/
RUN npm run build

@ -1,6 +1,6 @@
MIT License
Copyright (C) 2018-2020 Intel Corporation
Copyright (C) 2018-2021 Intel Corporation
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"),
@ -20,3 +20,12 @@ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.
 
This software uses LGPL licensed libraries from the [FFmpeg](https://www.ffmpeg.org) project.
The exact steps on how FFmpeg was configured and compiled can be found in the [Dockerfile](Dockerfile).
FFmpeg is an open source framework licensed under LGPL and GPL.
See https://www.ffmpeg.org/legal.html. You are solely responsible
for determining if your use of FFmpeg requires any
additional licenses. Intel is not responsible for obtaining any
such licenses, nor liable for any licensing fees due in
connection with your use of FFmpeg.

@ -1,6 +1,6 @@
# Computer Vision Annotation Tool (CVAT)
[![Build Status](https://travis-ci.org/openvinotoolkit/cvat.svg?branch=develop)](https://travis-ci.org/openvinotoolkit/cvat)
[![CI](https://github.com/openvinotoolkit/cvat/workflows/CI/badge.svg?branch=develop)](https://github.com/openvinotoolkit/cvat/actions)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/b9899c72f2764df0b5d26390cb872e21)](https://app.codacy.com/gh/openvinotoolkit/cvat?utm_source=github.com&utm_medium=referral&utm_content=openvinotoolkit/cvat&utm_campaign=Badge_Grade_Dashboard)
[![Gitter chat](https://badges.gitter.im/opencv-cvat/gitter.png)](https://gitter.im/opencv-cvat)
[![Coverage Status](https://coveralls.io/repos/github/openvinotoolkit/cvat/badge.svg?branch=develop)](https://coveralls.io/github/openvinotoolkit/cvat?branch=develop)
@ -62,19 +62,31 @@ For more information about supported formats look at the
| [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 |
## Deep learning models for automatic labeling
## Deep learning serverless functions for automatic labeling
<!--lint disable maximum-line-length-->
| Name | Type | Framework | CPU | GPU |
| ------------------------------------------------------------------------------------------------------- | ---------- | ---------- | --- | --- |
| [Deep Extreme Cut](/serverless/openvino/dextr/nuclio) | interactor | OpenVINO | X |
| [Deep Extreme Cut](/serverless/openvino/dextr/nuclio) | interactor | OpenVINO | X | |
| [Faster RCNN](/serverless/openvino/omz/public/faster_rcnn_inception_v2_coco/nuclio) | detector | OpenVINO | X | |
| [Mask RCNN](/serverless/openvino/omz/public/mask_rcnn_inception_resnet_v2_atrous_coco/nuclio) | detector | OpenVINO | X | |
| [YOLO v3](/serverless/openvino/omz/public/yolo-v3-tf/nuclio) | detector | OpenVINO | X | |
| [Object reidentification](/serverless/openvino/omz/intel/person-reidentification-retail-300/nuclio) | reid | OpenVINO | X | |
| [Semantic segmentation for ADAS](/serverless/openvino/omz/intel/semantic-segmentation-adas-0001/nuclio) | detector | OpenVINO | X | |
| [Text detection v4](/serverless/openvino/omz/intel/text-detection-0004/nuclio) | detector | OpenVINO | X | |
| [SiamMask](/serverless/pytorch/foolwood/siammask/nuclio) | tracker | PyTorch | X | |
| [f-BRS](/serverless/pytorch/saic-vul/fbrs/nuclio) | interactor | PyTorch | X | |
| [Inside-Outside Guidance](/serverless/pytorch/shiyinzhang/iog/nuclio) | interactor | PyTorch | X | |
| [Faster RCNN](/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | detector | TensorFlow | X | X |
| [Mask RCNN](/serverless/openvino/omz/public/mask_rcnn_inception_resnet_v2_atrous_coco/nuclio) | detector | OpenVINO | X |
| [YOLO v3](/serverless/openvino/omz/public/yolo-v3-tf/nuclio) | detector | OpenVINO | X |
| [Text detection v4](/serverless/openvino/omz/intel/text-detection-0004/nuclio) | detector | OpenVINO | X |
| [Semantic segmentation for ADAS](/serverless/openvino/omz/intel/semantic-segmentation-adas-0001/nuclio) | detector | OpenVINO | X |
| [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow | X |
| [Object reidentification](/serverless/openvino/omz/intel/person-reidentification-retail-300/nuclio) | reid | OpenVINO | X |
| [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow | X | X |
<!--lint enable maximum-line-length-->
## Online demo: [cvat.org](https://cvat.org)
@ -91,18 +103,36 @@ Limitations:
- No more than 10 tasks per user
- Uploaded data is limited to 500Mb
## Prebuilt Docker images
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`).
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'`)
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).
This software uses LGPL licensed libraries from the [FFmpeg](https://www.ffmpeg.org) project.
The exact steps on how FFmpeg was configured and compiled can be found in the [Dockerfile](Dockerfile).
FFmpeg is an open source framework licensed under LGPL and GPL.
See [https://www.ffmpeg.org/legal.html](https://www.ffmpeg.org/legal.html). You are solely responsible
for determining if your use of FFmpeg requires any
additional licenses. Intel is not responsible for obtaining any
such licenses, nor liable for any licensing fees due in
connection with your use of FFmpeg.
## Questions
CVAT usage related questions or unclear concepts can be posted in our
@ -129,4 +159,6 @@ Other ways to ask questions and get our support:
## Projects using CVAT
- [Onepanel](https://github.com/onepanelio/core) - Onepanel is an open source vision AI platform that fully integrates CVAT with scalable data processing and parallelized training pipelines.
- [Onepanel](https://github.com/onepanelio/core) - Onepanel is an open source
vision AI platform that fully integrates CVAT with scalable data processing
and parallelized training pipelines.

@ -31,7 +31,7 @@ services:
cvat_kibana_setup:
container_name: cvat_kibana_setup
image: cvat/server
image: openvino/cvat_server
volumes: ['./components/analytics/kibana:/home/django/kibana:ro']
depends_on: ['cvat']
working_dir: '/home/django'
@ -49,7 +49,7 @@ services:
'-t',
'0',
'--',
'/usr/bin/python3',
'python3',
'kibana/setup.py',
'kibana/export.json',
]
@ -69,13 +69,17 @@ services:
ELK_VERSION: 6.4.0
http_proxy: ${http_proxy}
https_proxy: ${https_proxy}
environment:
LOGSTASH_OUTPUT_HOST: elasticsearch:9200
LOGSTASH_OUTPUT_USER:
LOGSTASH_OUTPUT_PASS:
depends_on: ['cvat_elasticsearch']
restart: always
cvat:
environment:
DJANGO_LOG_SERVER_HOST: logstash
DJANGO_LOG_SERVER_PORT: 5000
DJANGO_LOG_SERVER_PORT: 8080
DJANGO_LOG_VIEWER_HOST: kibana
DJANGO_LOG_VIEWER_PORT: 5601
CVAT_ANALYTICS: 1

@ -58,14 +58,21 @@
"_type": "visualization",
"_source": {
"title": "Timeline for exceptions",
"visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{\"customBucket\":{\"enabled\":true,\"id\":\"1-bucket\",\"params\":{\"filters\":[{\"input\":{\"query\":\"event:\\\"Send exception\\\"\"},\"label\":\"\"}]},\"schema\":{\"aggFilter\":[],\"deprecate\":false,\"editor\":false,\"group\":\"none\",\"max\":null,\"min\":0,\"name\":\"bucketAgg\",\"params\":[],\"title\":\"Bucket Agg\"},\"type\":\"filters\"},\"customLabel\":\"Exceptions\",\"customMetric\":{\"enabled\":true,\"id\":\"1-metric\",\"params\":{\"customLabel\":\"Exceptions\"},\"schema\":{\"aggFilter\":[\"!top_hits\",\"!percentiles\",\"!percentile_ranks\",\"!median\",\"!std_dev\",\"!sum_bucket\",\"!avg_bucket\",\"!min_bucket\",\"!max_bucket\",\"!derivative\",\"!moving_avg\",\"!serial_diff\",\"!cumulative_sum\"],\"deprecate\":false,\"editor\":false,\"group\":\"none\",\"max\":null,\"min\":0,\"name\":\"metricAgg\",\"params\":[],\"title\":\"Metric Agg\"},\"type\":\"count\"}},\"schema\":\"metric\",\"type\":\"sum_bucket\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"customInterval\":\"2h\",\"customLabel\":\"Time\",\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"}],\"params\":{\"addLegend\":true,\"addTimeMarker\":true,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"show\":true,\"truncate\":100},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{},\"type\":\"category\"}],\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"legendPosition\":\"right\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Exceptions\"},\"drawLinesBetweenPoints\":true,\"mode\":\"stacked\",\"show\":\"true\",\"showCircles\":true,\"type\":\"histogram\",\"valueAxis\":\"ValueAxis-1\"}],\"times\":[],\"type\":\"histogram\",\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"Exceptions\"},\"type\":\"value\"}]},\"title\":\"Timeline for exceptions\",\"type\":\"histogram\"}",
"visState": "{\"title\":\"Timeline for exceptions\",\"type\":\"histogram\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"sum_bucket\",\"params\":{\"customBucket\":{\"id\":\"1-bucket\",\"enabled\":true,\"type\":\"filters\",\"params\":{\"filters\":[{\"input\":{\"query\":\"event:\\\"Send exception\\\"\",\"language\":\"lucene\"},\"label\":\"\"}]}},\"customMetric\":{\"id\":\"1-metric\",\"enabled\":true,\"type\":\"count\",\"params\":{\"customLabel\":\"Exceptions\"}},\"customLabel\":\"Exceptions\"},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"now-15h\",\"to\":\"now\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{},\"customLabel\":\"Time\"},\"schema\":\"segment\"}],\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":false,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Exceptions\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Exceptions\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"labels\":{\"show\":false},\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"orderBucketsBySum\":false}}",
"uiStateJSON": "{}",
"description": "",
"version": 1,
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"index\":\"ec510550-c238-11e8-8e1b-758ef07f6de8\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}"
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
}
},
"references": [
{
"id": "ec510550-c238-11e8-8e1b-758ef07f6de8",
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
"type": "index-pattern"
}
],
"_meta": {
"savedObjectVersion": 2
}

@ -3,5 +3,6 @@ FROM docker.elastic.co/logstash/logstash-oss:${ELK_VERSION}
RUN logstash-plugin install logstash-input-http logstash-filter-aggregate \
logstash-filter-prune logstash-output-email
COPY logstash.conf /usr/share/logstash/pipeline/
EXPOSE 5000
COPY logstash.yml /usr/share/logstash/config/
COPY logstash.conf /usr/share/logstash/pipeline/
EXPOSE 8080

@ -1,11 +1,22 @@
input {
tcp {
port => 5000
http {
port => 8080
codec => json
}
}
filter {
mutate {
add_field => {"logger_name" => ""}
add_field => {"path" =>""}
}
mutate {
copy => {"[extra][logger_name]" => "logger_name" }
copy => {"[extra][path]"=>"path"}
}
prune {
blacklist_names => ["type","logsource","extra","program","pid","headers"]
}
if [logger_name] =~ /cvat.client/ {
# 1. Decode the event from json in 'message' field
# 2. Remove unnecessary field from it
@ -14,6 +25,9 @@ filter {
mutate {
rename => { "message" => "source_message" }
}
mutate {
add_field => {"[@metadata][target_index_client]" => "cvat.client.%{+YYYY}.%{+MM}"}
}
json {
source => "source_message"
@ -77,6 +91,9 @@ filter {
# 2. Remove unnecessary field from it
# 3. Type it as server
if [logger_name] =~ /cvat\.server\.task_[0-9]+/ {
mutate {
add_field => {"[@metadata][target_index_server]" => "cvat.server.%{+YYYY}.%{+MM}"}
}
mutate {
rename => { "logger_name" => "task_id" }
gsub => [ "task_id", "cvat.server.task_", "" ]
@ -106,13 +123,19 @@ output {
if [type] == "client" {
elasticsearch {
hosts => ["elasticsearch:9200"]
hosts => ["${LOGSTASH_OUTPUT_HOST}"]
index => "cvat.client"
user => "${LOGSTASH_OUTPUT_USER:}"
password => "${LOGSTASH_OUTPUT_PASS:}"
manage_template => false
}
} else if [type] == "server" {
elasticsearch {
hosts => ["elasticsearch:9200"]
hosts => ["${LOGSTASH_OUTPUT_HOST}"]
index => "cvat.server"
user => "${LOGSTASH_OUTPUT_USER:}"
password => "${LOGSTASH_OUTPUT_PASS:}"
manage_template => false
}
}
}

@ -0,0 +1,3 @@
queue.type: persisted
queue.max_bytes: 1gb
queue.checkpoint.writes: 20

@ -2,7 +2,7 @@ version: '3.3'
services:
serverless:
container_name: nuclio
image: quay.io/nuclio/dashboard:1.5.8-amd64
image: quay.io/nuclio/dashboard:1.5.16-amd64
restart: always
networks:
default:
@ -16,6 +16,7 @@ services:
https_proxy:
no_proxy: 172.28.0.1,${no_proxy}
NUCLIO_CHECK_FUNCTION_CONTAINERS_HEALTHINESS: 'true'
NUCLIO_DASHBOARD_DEFAULT_FUNCTION_MOUNT_MODE: 'volume'
ports:
- '8070:8070'

@ -1,2 +1 @@
src/*.js
dist

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 303 KiB

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "cvat-canvas",
"version": "2.2.1",
"version": "2.4.1",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts",
"scripts": {
@ -17,7 +17,7 @@
"svg.select.js": "3.0.1"
},
"devDependencies": {
"@babel/cli": "^7.5.5",
"@babel/cli": "^7.12.13",
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
@ -34,13 +34,13 @@
"eslint-config-typescript-recommended": "^1.4.17",
"eslint-plugin-import": "^2.18.2",
"node-sass": "^4.14.1",
"nodemon": "^1.19.4",
"nodemon": "^2.0.7",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"sass-loader": "^8.0.2",
"style-loader": "^1.0.0",
"typescript": "^3.5.3",
"webpack": "^4.44.2",
"webpack": "^5.20.2",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.11.0"
}

@ -51,6 +51,10 @@ polyline.cvat_shape_drawing_opacity {
stroke: red;
}
.cvat_canvas_threshold {
stroke: red;
}
.cvat_canvas_shape_grouping {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -57,6 +57,7 @@ export interface Configuration {
undefinedAttrValue?: string;
showProjections?: boolean;
forceDisableEditing?: boolean;
intelligentPolygonCrop?: boolean;
}
export interface DrawData {
@ -76,6 +77,10 @@ export interface InteractionData {
crosshair?: boolean;
minPosVertices?: number;
minNegVertices?: number;
enableNegVertices?: boolean;
enableThreshold?: boolean;
enableSliding?: boolean;
allowRemoveOnlyLast?: boolean;
}
export interface InteractionResult {
@ -617,25 +622,29 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public configure(configuration: Configuration): void {
if (typeof configuration.displayAllText !== 'undefined') {
if (typeof configuration.displayAllText === 'boolean') {
this.data.configuration.displayAllText = configuration.displayAllText;
}
if (typeof configuration.showProjections !== 'undefined') {
if (typeof configuration.showProjections === 'boolean') {
this.data.configuration.showProjections = configuration.showProjections;
}
if (typeof configuration.autoborders !== 'undefined') {
if (typeof configuration.autoborders === 'boolean') {
this.data.configuration.autoborders = configuration.autoborders;
}
if (typeof configuration.undefinedAttrValue !== 'undefined') {
if (typeof configuration.undefinedAttrValue === 'string') {
this.data.configuration.undefinedAttrValue = configuration.undefinedAttrValue;
}
if (typeof configuration.forceDisableEditing !== 'undefined') {
if (typeof configuration.forceDisableEditing === 'boolean') {
this.data.configuration.forceDisableEditing = configuration.forceDisableEditing;
}
if (typeof configuration.intelligentPolygonCrop === 'boolean') {
this.data.configuration.intelligentPolygonCrop = configuration.intelligentPolygonCrop;
}
this.notify(UpdateReasons.CONFIG_UPDATED);
}

@ -165,6 +165,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
shapes: InteractionResult[] | null,
shapesUpdated: boolean = true,
isDone: boolean = false,
threshold: number | null = null,
): void {
const { zLayer } = this.controller;
if (Array.isArray(shapes)) {
@ -176,6 +177,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
isDone,
shapes,
zOrder: zLayer || 0,
threshold,
},
});
@ -1050,6 +1052,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
});
this.content.addEventListener('wheel', (event): void => {
if (event.ctrlKey) return;
const { offset } = this.controller.geometry;
const point = translateToSVG(this.content, [event.clientX, event.clientY]);
self.controller.zoom(point[0] - offset, point[1] - offset, event.deltaY > 0 ? -1 : 1);

@ -1,9 +1,9 @@
import consts from './consts';
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
export interface Point {
x: number;
y: number;
}
import consts from './consts';
import { Point } from './shared';
export enum Orientation {
LEFT = 'left',
@ -17,7 +17,7 @@ function line(p1: Point, p2: Point): number[] {
return [a, b, c];
}
function intersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null {
export function intersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null {
const L1 = line(p1, p2);
const L2 = line(p3, p4);
@ -27,7 +27,7 @@ function intersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null
let x = null;
let y = null;
if (D !== 0) {
if (Math.abs(D) > Number.EPSILON) {
x = Dx / D;
y = Dy / D;
return { x, y };
@ -348,10 +348,9 @@ function setupCuboidPoints(points: Point[]): any[] {
let p3;
let p4;
const height =
Math.abs(points[0].x - points[1].x) < Math.abs(points[1].x - points[2].x)
? Math.abs(points[1].y - points[0].y)
: Math.abs(points[1].y - points[2].y);
const height = Math.abs(points[0].x - points[1].x) < Math.abs(points[1].x - points[2].x)
? Math.abs(points[1].y - points[0].y)
: Math.abs(points[1].y - points[2].y);
// seperate into left and right point
// we pick the first and third point because we know assume they will be on

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -15,12 +15,15 @@ import {
pointsToNumberArray,
BBox,
Box,
Point,
} from './shared';
import Crosshair from './crosshair';
import consts from './consts';
import { DrawData, Geometry, RectDrawingMethod, Configuration, CuboidDrawingMethod } from './canvasModel';
import {
DrawData, Geometry, RectDrawingMethod, Configuration, CuboidDrawingMethod,
} from './canvasModel';
import { cuboidFrom4Points } from './cuboid';
import { cuboidFrom4Points, intersection } from './cuboid';
export interface DrawHandler {
configurate(configuration: Configuration): void;
@ -73,11 +76,11 @@ export class DrawHandlerImpl implements DrawHandler {
private getFinalPolyshapeCoordinates(
targetPoints: number[],
): {
points: number[];
box: Box;
} {
points: number[];
box: Box;
} {
const { offset } = this.geometry;
const points = targetPoints.map((coord: number): number => coord - offset);
let points = targetPoints.map((coord: number): number => coord - offset);
const box = {
xtl: Number.MAX_SAFE_INTEGER,
ytl: Number.MAX_SAFE_INTEGER,
@ -87,10 +90,91 @@ export class DrawHandlerImpl implements DrawHandler {
const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height;
for (let i = 0; i < points.length - 1; i += 2) {
points[i] = Math.min(Math.max(points[i], 0), frameWidth);
points[i + 1] = Math.min(Math.max(points[i + 1], 0), frameHeight);
enum Direction {
Horizontal,
Vertical,
}
const isBetween = (x1: number, x2: number, c: number): boolean => (
c >= Math.min(x1, x2) && c <= Math.max(x1, x2)
);
const isInsideFrame = (p: Point, direction: Direction): boolean => {
if (direction === Direction.Horizontal) {
return isBetween(0, frameWidth, p.x);
}
return isBetween(0, frameHeight, p.y);
};
const findInersection = (p1: Point, p2: Point, p3: Point, p4: Point): number[] => {
const intersectionPoint = intersection(p1, p2, p3, p4);
if (
intersectionPoint
&& isBetween(p1.x, p2.x, intersectionPoint.x)
&& isBetween(p1.y, p2.y, intersectionPoint.y)
) {
return [intersectionPoint.x, intersectionPoint.y];
}
return [];
};
const findIntersectionsWithFrameBorders = (p1: Point, p2: Point, direction: Direction): number[] => {
const resultPoints = [];
if (direction === Direction.Horizontal) {
resultPoints.push(...findInersection(p1, p2, { x: 0, y: 0 }, { x: 0, y: frameHeight }));
resultPoints.push(
...findInersection(p1, p2, { x: frameWidth, y: frameHeight }, { x: frameWidth, y: 0 }),
);
} else {
resultPoints.push(
...findInersection(p1, p2, { x: 0, y: frameHeight }, { x: frameWidth, y: frameHeight }),
);
resultPoints.push(...findInersection(p1, p2, { x: frameWidth, y: 0 }, { x: 0, y: 0 }));
}
if (resultPoints.length === 4) {
if (
Math.sign(resultPoints[0] - resultPoints[2]) !== Math.sign(p1.x - p2.x)
&& Math.sign(resultPoints[1] - resultPoints[3]) !== Math.sign(p1.y - p2.y)
) {
[resultPoints[0], resultPoints[2]] = [resultPoints[2], resultPoints[0]];
[resultPoints[1], resultPoints[3]] = [resultPoints[3], resultPoints[1]];
}
}
return resultPoints;
};
const crop = (polygonPoints: number[], direction: Direction): number[] => {
const resultPoints = [];
for (let i = 0; i < polygonPoints.length - 1; i += 2) {
const curPoint = { x: polygonPoints[i], y: polygonPoints[i + 1] };
if (isInsideFrame(curPoint, direction)) {
resultPoints.push(polygonPoints[i], polygonPoints[i + 1]);
}
const isLastPoint = i === polygonPoints.length - 2;
if (
isLastPoint
&& (this.drawData.shapeType === 'polyline'
|| (this.drawData.shapeType === 'polygon' && polygonPoints.length === 4))
) {
break;
}
const nextPoint = isLastPoint
? { x: polygonPoints[0], y: polygonPoints[1] }
: { x: polygonPoints[i + 2], y: polygonPoints[i + 3] };
const intersectionPoints = findIntersectionsWithFrameBorders(curPoint, nextPoint, direction);
if (intersectionPoints.length !== 0) {
resultPoints.push(...intersectionPoints);
}
}
return resultPoints;
};
points = crop(points, Direction.Horizontal);
points = crop(points, Direction.Vertical);
for (let i = 0; i < points.length - 1; i += 2) {
box.xtl = Math.min(box.xtl, points[i]);
box.ytl = Math.min(box.ytl, points[i + 1]);
box.xbr = Math.max(box.xbr, points[i]);
@ -106,9 +190,9 @@ export class DrawHandlerImpl implements DrawHandler {
private getFinalCuboidCoordinates(
targetPoints: number[],
): {
points: number[];
box: Box;
} {
points: number[];
box: Box;
} {
const { offset } = this.geometry;
let points = targetPoints;
@ -154,8 +238,8 @@ export class DrawHandlerImpl implements DrawHandler {
if (cuboidOffsets.length === points.length / 2) {
cuboidOffsets.forEach((offsetCoords: number[]): void => {
if (Math.sqrt(offsetCoords[0] ** 2 + offsetCoords[1] ** 2) < minCuboidOffset.d) {
minCuboidOffset.d = Math.sqrt(offsetCoords[0] ** 2 + offsetCoords[1] ** 2);
if (Math.sqrt((offsetCoords[0] ** 2) + (offsetCoords[1] ** 2)) < minCuboidOffset.d) {
minCuboidOffset.d = Math.sqrt((offsetCoords[0] ** 2) + (offsetCoords[1] ** 2));
[minCuboidOffset.dx, minCuboidOffset.dy] = offsetCoords;
}
});
@ -213,8 +297,8 @@ 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
this.drawData.shapeType !== 'rectangle'
&& this.drawData.cuboidDrawingMethod !== CuboidDrawingMethod.CLASSIC
) {
// Check for unsaved drawn shapes
this.drawInstance.draw('done');
@ -365,7 +449,8 @@ export class DrawHandlerImpl implements DrawHandler {
} else {
this.drawInstance.draw('update', e);
const deltaTreshold = 15;
const delta = Math.sqrt((e.clientX - lastDrawnPoint.x) ** 2 + (e.clientY - lastDrawnPoint.y) ** 2);
const delta = Math.sqrt(((e.clientX - lastDrawnPoint.x) ** 2)
+ ((e.clientY - lastDrawnPoint.y) ** 2));
if (delta > deltaTreshold) {
this.drawInstance.draw('point', e);
}
@ -386,17 +471,16 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance.on('drawdone', (e: CustomEvent): void => {
const targetPoints = pointsToNumberArray((e.target as SVGElement).getAttribute('points'));
const { shapeType, redraw: clientID } = this.drawData;
const { points, box } =
shapeType === 'cuboid'
? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
const { points, box } = shapeType === 'cuboid'
? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
this.release();
if (this.canceled) return;
if (
shapeType === 'polygon' &&
(box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD &&
points.length >= 3 * 2
shapeType === 'polygon'
&& (box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD
&& points.length >= 3 * 2
) {
this.onDrawDone(
{
@ -407,9 +491,9 @@ export class DrawHandlerImpl implements DrawHandler {
Date.now() - this.startTimestamp,
);
} else if (
shapeType === 'polyline' &&
(box.xbr - box.xtl >= consts.SIZE_THRESHOLD || box.ybr - box.ytl >= consts.SIZE_THRESHOLD) &&
points.length >= 2 * 2
shapeType === 'polyline'
&& (box.xbr - box.xtl >= consts.SIZE_THRESHOLD || box.ybr - box.ytl >= consts.SIZE_THRESHOLD)
&& points.length >= 2 * 2
) {
this.onDrawDone(
{
@ -527,10 +611,9 @@ export class DrawHandlerImpl implements DrawHandler {
.split(/[,\s]/g)
.map((coord: string): number => +coord);
const { points } =
this.drawData.initialState.shapeType === 'cuboid'
? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
const { points } = this.drawData.initialState.shapeType === 'cuboid'
? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
if (!e.detail.originalEvent.ctrlKey) {
this.release();

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -27,6 +27,7 @@ export class EditHandlerImpl implements EditHandler {
private editLine: SVG.PolyLine;
private clones: SVG.Polygon[];
private autobordersEnabled: boolean;
private intelligentCutEnabled: boolean;
private setupTrailingPoint(circle: SVG.Circle): void {
const head = this.editedShape.attr('points').split(' ').slice(0, this.editData.pointID).join(' ');
@ -93,7 +94,9 @@ export class EditHandlerImpl implements EditHandler {
(this.editLine as any).draw('point', e);
} else {
const deltaTreshold = 15;
const delta = Math.sqrt((e.clientX - lastDrawnPoint.x) ** 2 + (e.clientY - lastDrawnPoint.y) ** 2);
const dxsqr = (e.clientX - lastDrawnPoint.x) ** 2;
const dysqr = (e.clientY - lastDrawnPoint.y) ** 2;
const delta = Math.sqrt(dxsqr + dysqr);
if (delta > deltaTreshold) {
(this.editLine as any).draw('point', e);
}
@ -103,7 +106,7 @@ export class EditHandlerImpl implements EditHandler {
this.editLine = (this.canvas as any).polyline();
if (this.editData.state.shapeType === 'polyline') {
(this.editLine as any).on('drawpoint', (e: CustomEvent): void => {
(this.editLine as any).on('drawupdate', (e: CustomEvent): void => {
const circle = (e.target as any).instance.remember('_paintHandler').set.last();
if (circle) this.setupTrailingPoint(circle);
});
@ -208,11 +211,11 @@ export class EditHandlerImpl implements EditHandler {
}
const cutIndexes1 = oldPoints.reduce(
(acc: string[], _: string, i: number) => (i >= stop || i <= start ? [...acc, i] : acc),
(acc: number[], _: string, i: number): number[] => (i >= stop || i <= start ? [...acc, i] : acc),
[],
);
const cutIndexes2 = oldPoints.reduce(
(acc: string[], _: string, i: number) => (i <= stop && i >= start ? [...acc, i] : acc),
(acc: number[], _: string, i: number): number[] => (i <= stop && i >= start ? [...acc, i] : acc),
[],
);
@ -223,7 +226,9 @@ export class EditHandlerImpl implements EditHandler {
.map((point: string[]): number[] => [+point[0], +point[1]]);
let length = 0;
for (let i = 1; i < points.length; i++) {
length += Math.sqrt((points[i][0] - points[i - 1][0]) ** 2 + (points[i][1] - points[i - 1][1]) ** 2);
const dxsqr = (points[i][0] - points[i - 1][0]) ** 2;
const dysqr = (points[i][1] - points[i - 1][1]) ** 2;
length += Math.sqrt(dxsqr + dysqr);
}
return length;
@ -255,11 +260,11 @@ export class EditHandlerImpl implements EditHandler {
this.editLine.remove();
this.editLine = null;
if (pointsCriteria && lengthCriteria) {
if (pointsCriteria && lengthCriteria && this.intelligentCutEnabled) {
this.clones.push(this.canvas.polygon(firstPart.join(' ')));
this.selectPolygon(this.clones[0]);
// left indexes1 and
} else if (!pointsCriteria && !lengthCriteria) {
} else if (!pointsCriteria && !lengthCriteria && this.intelligentCutEnabled) {
this.clones.push(this.canvas.polygon(secondPart.join(' ')));
this.selectPolygon(this.clones[0]);
} else {
@ -380,6 +385,7 @@ export class EditHandlerImpl implements EditHandler {
) {
this.autoborderHandler = autoborderHandler;
this.autobordersEnabled = false;
this.intelligentCutEnabled = false;
this.onEditDone = onEditDone;
this.canvas = canvas;
this.editData = null;
@ -419,6 +425,10 @@ export class EditHandlerImpl implements EditHandler {
}
}
}
if (typeof configuration.intelligentPolygonCrop === 'boolean') {
this.intelligentCutEnabled = configuration.intelligentPolygonCrop;
}
}
public transform(geometry: Geometry): void {

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -24,6 +24,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
private interactionShapes: SVG.Shape[];
private currentInteractionShape: SVG.Shape | null;
private crosshair: Crosshair;
private threshold: SVG.Rect | null;
private thresholdRectSize: number;
private prepareResult(): InteractionResult[] {
return this.interactionShapes.map(
@ -63,12 +65,21 @@ export class InteractionHandlerImpl implements InteractionHandler {
return enabled && !ctrlKey && !!interactionShapes.length;
}
const minimumVerticesAchieved =
(typeof minPosVertices === 'undefined' || minPosVertices <= positiveShapes.length) &&
(typeof minNegVertices === 'undefined' || minPosVertices <= negativeShapes.length);
const minPosVerticesAchieved = typeof minPosVertices === 'undefined' || minPosVertices <= positiveShapes.length;
const minNegVerticesAchieved = typeof minNegVertices === 'undefined' || minPosVertices <= negativeShapes.length;
const minimumVerticesAchieved = minPosVerticesAchieved && minNegVerticesAchieved;
return enabled && !ctrlKey && minimumVerticesAchieved && shapesWereUpdated;
}
private addThreshold(): void {
const { x, y } = this.cursorPosition;
this.threshold = this.canvas
.rect(this.thresholdRectSize, this.thresholdRectSize)
.fill('none')
.addClass('cvat_canvas_threshold');
this.threshold.center(x, y);
}
private addCrosshair(): void {
const { x, y } = this.cursorPosition;
this.crosshair.show(this.canvas, x, y, this.geometry.scale);
@ -80,9 +91,12 @@ export class InteractionHandlerImpl implements InteractionHandler {
private interactPoints(): void {
const eventListener = (e: MouseEvent): void => {
if ((e.button === 0 || e.button === 2) && !e.altKey) {
if ((e.button === 0 || (e.button === 2 && this.interactionData.enableNegVertices)) && !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.isWithinThreshold(cx, cy)) return;
this.currentInteractionShape = this.canvas
.circle((consts.BASE_POINT_SIZE * 2) / this.geometry.scale)
.center(cx, cy)
@ -101,6 +115,12 @@ export class InteractionHandlerImpl implements InteractionHandler {
const self = this.currentInteractionShape;
self.on('mouseenter', (): void => {
if (this.interactionData.allowRemoveOnlyLast) {
if (this.interactionShapes.indexOf(self) !== this.interactionShapes.length - 1) {
return;
}
}
self.attr({
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale,
});
@ -166,6 +186,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
if (this.interactionData.crosshair) {
this.addCrosshair();
}
if (this.interactionData.enableThreshold) {
this.addThreshold();
}
}
private startInteraction(): void {
@ -183,6 +207,11 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.removeCrosshair();
}
if (this.threshold) {
this.threshold.remove();
this.threshold = null;
}
this.canvas.off('mousedown.interaction');
this.interactionShapes.forEach((shape: SVG.Shape): SVG.Shape => shape.remove());
this.interactionShapes = [];
@ -192,14 +221,39 @@ export class InteractionHandlerImpl implements InteractionHandler {
}
}
private isWithinThreshold(x: number, y: number): boolean {
const [prev] = this.interactionShapes.slice(-1);
if (!this.interactionData.enableThreshold || !prev) {
return true;
}
const [prevCx, prevCy] = [(prev as SVG.Circle).cx(), (prev as SVG.Circle).cy()];
const xDiff = Math.abs(prevCx - x);
const yDiff = Math.abs(prevCy - y);
return xDiff < this.thresholdRectSize / 2 && yDiff < this.thresholdRectSize / 2;
}
private isWithingFrame(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;
}
public constructor(
onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void,
onInteraction: (
shapes: InteractionResult[] | null,
shapesUpdated?: boolean,
isDone?: boolean,
threshold?: number,
) => void,
canvas: SVG.Container,
geometry: Geometry,
) {
this.onInteraction = (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean): void => {
this.shapesWereUpdated = false;
onInteraction(shapes, shapesUpdated, isDone);
onInteraction(shapes, shapesUpdated, isDone, this.threshold ? this.thresholdRectSize / 2 : null);
};
this.canvas = canvas;
this.geometry = geometry;
@ -208,6 +262,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.interactionData = { enabled: false };
this.currentInteractionShape = null;
this.crosshair = new Crosshair();
this.threshold = null;
this.thresholdRectSize = 300;
this.cursorPosition = {
x: 0,
y: 0,
@ -219,6 +275,43 @@ export class InteractionHandlerImpl implements InteractionHandler {
if (this.crosshair) {
this.crosshair.move(x, y);
}
if (this.threshold) {
this.threshold.center(x, y);
}
if (this.interactionData.enableSliding && this.interactionShapes.length) {
if (this.isWithingFrame(x, y)) {
if (this.interactionData.enableThreshold && !this.isWithinThreshold(x, y)) return;
this.onInteraction(
[
...this.prepareResult(),
{
points: [x - this.geometry.offset, y - this.geometry.offset],
shapeType: 'points',
button: 0,
},
],
true,
false,
);
}
}
});
this.canvas.on('wheel.interaction', (e: WheelEvent): void => {
if (e.ctrlKey) {
if (this.threshold) {
const { x, y } = this.cursorPosition;
e.preventDefault();
if (e.deltaY > 0) {
this.thresholdRectSize *= 6 / 5;
} else {
this.thresholdRectSize *= 5 / 6;
}
this.threshold.size(this.thresholdRectSize, this.thresholdRectSize);
this.threshold.center(x, y);
}
}
});
document.body.addEventListener('keyup', (e: KeyboardEvent): void => {

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -66,7 +66,7 @@ export class RegionSelectorImpl implements RegionSelector {
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
})
.addClass('cvat_canvas_shape_region_selection');
this.selectionRect.attr({ ...this.startSelectionPoint });
this.selectionRect.attr({ ...this.startSelectionPoint, width: 1, height: 1 });
}
};
@ -78,7 +78,7 @@ export class RegionSelectorImpl implements RegionSelector {
} = this.selectionRect.bbox();
this.selectionRect.remove();
this.selectionRect = null;
if (w === 0 && h === 0) {
if (w <= 1 && h <= 1) {
this.onRegionSelected([x - offset, y - offset]);
} else {
this.onRegionSelected([x - offset, y - offset, x2 - offset, y2 - offset]);

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -25,7 +25,7 @@ export interface BBox {
y: number;
}
interface Point {
export interface Point {
x: number;
y: number;
}
@ -176,5 +176,5 @@ export function scalarProduct(a: Vector2D, b: Vector2D): number {
}
export function vectorLength(vector: Vector2D): number {
return Math.sqrt(vector.i ** 2 + vector.j ** 2);
return Math.sqrt((vector.i ** 2) + (vector.j ** 2));
}

@ -167,7 +167,12 @@ SVG.Element.prototype.resize = function constructor(...args: any): any {
handler = this.remember('_resizeHandler');
handler.resize = function (e: any) {
const { event } = e.detail;
if (event.button === 0 && !event.shiftKey && !event.altKey) {
if (
event.button === 0 &&
// ignore shift key for cuboid change perspective
(!event.shiftKey || this.el.parent().hasClass('cvat_canvas_shape_cuboid')) &&
!event.altKey
) {
return handler.constructor.prototype.resize.call(this, e);
}
};
@ -233,6 +238,7 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
this.hideProjections();
this._attr('points', points);
this.addClass('cvat_canvas_shape_cuboid');
return this;
},

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

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

@ -0,0 +1 @@
dist

@ -0,0 +1,48 @@
# Module CVAT-CANVAS-3D
## Description
The CVAT module written in TypeScript language.
It presents a canvas to viewing, drawing and editing of 3D annotations.
## Versioning
If you make changes in this package, please do following:
- After not important changes (typos, backward compatible bug fixes, refactoring) do: `npm version patch`
- After changing API (backward compatible new features) do: `npm version minor`
- After changing API (changes that break backward compatibility) do: `npm version major`
## Commands
- Building of the module from sources in the `dist` directory:
```bash
npm run build
npm run build -- --mode=development # without a minification
```
### API Methods
```ts
interface Canvas3d {
html(): HTMLDivElement;
setup(frameData: any): void;
mode(): Mode;
isAbleToChangeFrame(): boolean;
render(): void;
}
```
### WEB
```js
// Create an instance of a canvas
const canvas = new window.canvas.Canvas3d();
console.log('Version ', window.canvas.CanvasVersion);
console.log('Current mode is ', window.canvas.mode());
// Put canvas to a html container
htmlContainer.appendChild(canvas.html());
```

File diff suppressed because it is too large Load Diff

@ -0,0 +1,45 @@
{
"name": "cvat-canvas3d",
"version": "0.0.1",
"description": "Part of Computer Vision Annotation Tool which presents its canvas3D library",
"main": "src/canvas3d.ts",
"scripts": {
"build": "tsc && webpack --config ./webpack.config.js",
"server": "nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'"
},
"author": "Intel",
"license": "MIT",
"devDependencies": {
"@babel/cli": "^7.5.5",
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
"@babel/preset-env": "^7.5.5",
"@babel/preset-typescript": "^7.3.3",
"@types/node": "^12.6.8",
"@typescript-eslint/eslint-plugin": "^1.13.0",
"@typescript-eslint/parser": "^1.13.0",
"babel-loader": "^8.0.6",
"css-loader": "^3.4.2",
"dts-bundle-webpack": "^1.0.2",
"eslint": "^6.1.0",
"eslint-config-airbnb-typescript": "^4.0.1",
"eslint-config-typescript-recommended": "^1.4.17",
"eslint-plugin-import": "^2.18.2",
"node-sass": "^4.14.1",
"nodemon": "^1.19.4",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"sass-loader": "^8.0.2",
"style-loader": "^1.0.0",
"typescript": "^3.5.3",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
"@types/three": "^0.125.3",
"camera-controls": "^1.25.3",
"three": "^0.125.0"
}
}

@ -0,0 +1,12 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
module.exports = {
parser: false,
plugins: {
'postcss-preset-env': {
browsers: '> 2.5%', // https://github.com/browserslist/browserslist
},
},
};

@ -0,0 +1,79 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import pjson from '../../package.json';
import { Canvas3dController, Canvas3dControllerImpl } from './canvas3dController';
import {
Canvas3dModel, Canvas3dModelImpl, Mode, DrawData, ViewType, MouseInteraction,
} from './canvas3dModel';
import {
Canvas3dView, Canvas3dViewImpl, ViewsDOM, CAMERA_ACTION,
} from './canvas3dView';
import { Master } from './master';
const Canvas3dVersion = pjson.version;
interface Canvas3d {
html(): ViewsDOM;
setup(frameData: any): void;
isAbleToChangeFrame(): boolean;
mode(): Mode;
render(): void;
keyControls(keys: KeyboardEvent): void;
mouseControls(type: string, event: MouseEvent): void;
draw(drawData: DrawData): void;
cancel(): void;
}
class Canvas3dImpl implements Canvas3d {
private model: Canvas3dModel & Master;
private controller: Canvas3dController;
private view: Canvas3dView;
public constructor() {
this.model = new Canvas3dModelImpl();
this.controller = new Canvas3dControllerImpl(this.model);
this.view = new Canvas3dViewImpl(this.model, this.controller);
}
public html(): ViewsDOM {
return this.view.html();
}
public keyControls(keys: KeyboardEvent): void {
this.view.keyControls(keys);
}
public mouseControls(type: MouseInteraction, event: MouseEvent): void {
this.view.mouseControls(type, event);
}
public render(): void {
this.view.render();
}
public draw(drawData: DrawData): void {
this.model.draw(drawData);
}
public setup(frameData: any): void {
this.model.setup(frameData);
}
public mode(): Mode {
return this.model.mode;
}
public isAbleToChangeFrame(): boolean {
return this.model.isAbleToChangeFrame();
}
public cancel(): void {
this.model.cancel();
}
}
export {
Canvas3dImpl as Canvas3d, Canvas3dVersion, ViewType, MouseInteraction, CAMERA_ACTION,
};

@ -0,0 +1,30 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { Canvas3dModel, Mode, DrawData } from './canvas3dModel';
export interface Canvas3dController {
readonly drawData: DrawData;
mode: Mode;
}
export class Canvas3dControllerImpl implements Canvas3dController {
private model: Canvas3dModel;
public constructor(model: Canvas3dModel) {
this.model = model;
}
public set mode(value: Mode) {
this.model.mode = value;
}
public get mode(): Mode {
return this.model.mode;
}
public get drawData(): DrawData {
return this.model.data.drawData;
}
}

@ -0,0 +1,165 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { MasterImpl } from './master';
export interface Size {
width: number;
height: number;
}
export interface Image {
renderWidth: number;
renderHeight: number;
imageData: ImageData | CanvasImageSource;
}
export interface DrawData {
enabled: boolean;
initialState?: any;
redraw?: number;
}
export enum FrameZoom {
MIN = 0.1,
MAX = 10,
}
export enum ViewType {
PERSPECTIVE = 'perspective',
TOP = 'top',
SIDE = 'side',
FRONT = 'front',
}
export enum MouseInteraction {
CLICK = 'click',
DOUBLE_CLICK = 'dblclick',
HOVER = 'hover',
}
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',
}
export enum Mode {
IDLE = 'idle',
DRAG = 'drag',
RESIZE = 'resize',
DRAW = 'draw',
EDIT = 'edit',
INTERACT = 'interact',
}
export interface Canvas3dDataModel {
canvasSize: Size;
image: Image | null;
imageID: number | null;
imageOffset: number;
imageSize: Size;
drawData: DrawData;
mode: Mode;
exception: Error | null;
}
export interface Canvas3dModel {
mode: Mode;
data: Canvas3dDataModel;
setup(frameData: any): void;
isAbleToChangeFrame(): boolean;
draw(drawData: DrawData): void;
cancel(): void;
}
export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
public data: Canvas3dDataModel;
public constructor() {
super();
this.data = {
canvasSize: {
height: 0,
width: 0,
},
image: null,
imageID: null,
imageOffset: 0,
imageSize: {
height: 0,
width: 0,
},
drawData: {
enabled: false,
initialState: null,
},
mode: Mode.IDLE,
exception: null,
};
}
public setup(frameData: 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;
});
}
}
public set mode(value: Mode) {
this.data.mode = value;
}
public get mode(): Mode {
return this.data.mode;
}
public isAbleToChangeFrame(): boolean {
const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE, Mode.INTERACT].includes(this.data.mode)
|| (this.data.mode === Mode.DRAW && typeof this.data.drawData.redraw === 'number');
return !isUnable;
}
public draw(drawData: DrawData): void {
if (drawData.enabled && this.data.drawData.enabled) {
throw new Error('Drawing has been already started');
}
this.data.drawData.enabled = drawData.enabled;
this.data.mode = Mode.DRAW;
this.notify(UpdateReasons.DRAW);
}
public cancel(): void {
this.notify(UpdateReasons.CANCEL);
}
}

@ -0,0 +1,437 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as THREE from 'three';
import { PCDLoader } from 'three/examples/jsm/loaders/PCDLoader';
import CameraControls from 'camera-controls';
import { Canvas3dController } from './canvas3dController';
import { Listener, Master } from './master';
import CONST from './consts';
import {
Canvas3dModel, UpdateReasons, Mode, DrawData, ViewType, MouseInteraction,
} from './canvas3dModel';
import { CuboidModel } from './cuboid';
export interface Canvas3dView {
html(): ViewsDOM;
render(): void;
keyControls(keys: KeyboardEvent): void;
mouseControls(type: MouseInteraction, event: MouseEvent): void;
}
export enum CAMERA_ACTION {
ZOOM_IN = 'KeyI',
MOVE_UP = 'KeyU',
MOVE_DOWN = 'KeyO',
MOVE_LEFT = 'KeyJ',
ZOOM_OUT = 'KeyK',
MOVE_RIGHT = 'KeyL',
TILT_UP = 'ArrowUp',
TILT_DOWN = 'ArrowDown',
ROTATE_RIGHT = 'ArrowRight',
ROTATE_LEFT = 'ArrowLeft',
}
export interface RayCast {
renderer: THREE.Raycaster;
mouseVector: THREE.Vector2;
}
export interface Views {
perspective: RenderView;
top: RenderView;
side: RenderView;
front: RenderView;
}
export interface CubeObject {
perspective: THREE.Mesh;
top: THREE.Mesh;
side: THREE.Mesh;
front: THREE.Mesh;
}
export interface RenderView {
renderer: THREE.WebGLRenderer;
scene: THREE.Scene;
camera?: THREE.PerspectiveCamera | THREE.OrthographicCamera;
controls?: CameraControls;
rayCaster?: RayCast;
}
export interface ViewsDOM {
perspective: HTMLCanvasElement;
top: HTMLCanvasElement;
side: HTMLCanvasElement;
front: HTMLCanvasElement;
}
export class Canvas3dViewImpl implements Canvas3dView, Listener {
private controller: Canvas3dController;
private views: Views;
private clock: THREE.Clock;
private speed: number;
private cube: CuboidModel;
private highlighted: boolean;
private selected: CubeObject;
private set mode(value: Mode) {
this.controller.mode = value;
}
private get mode(): Mode {
return this.controller.mode;
}
public constructor(model: Canvas3dModel & Master, controller: Canvas3dController) {
this.controller = controller;
this.clock = new THREE.Clock();
this.speed = CONST.MOVEMENT_FACTOR;
this.cube = new CuboidModel();
this.highlighted = false;
this.selected = this.cube;
this.views = {
perspective: {
renderer: new THREE.WebGLRenderer({ antialias: true }),
scene: new THREE.Scene(),
rayCaster: {
renderer: new THREE.Raycaster(),
mouseVector: new THREE.Vector2(),
},
},
top: {
renderer: new THREE.WebGLRenderer({ antialias: true }),
scene: new THREE.Scene(),
},
side: {
renderer: new THREE.WebGLRenderer({ antialias: true }),
scene: new THREE.Scene(),
},
front: {
renderer: new THREE.WebGLRenderer({ antialias: true }),
scene: new THREE.Scene(),
},
};
CameraControls.install({ THREE });
this.mode = Mode.IDLE;
Object.keys(this.views).forEach((view: string): void => {
this.views[view as keyof Views].scene.background = new THREE.Color(0x000000);
});
const viewSize = CONST.ZOOM_FACTOR;
const height = window.innerHeight;
const width = window.innerWidth;
const aspectRatio = window.innerWidth / window.innerHeight;
// setting up the camera and adding it in the scene
this.views.perspective.camera = new THREE.PerspectiveCamera(50, aspectRatio, 1, 500);
this.views.perspective.camera.position.set(-15, 0, 4);
this.views.perspective.camera.up.set(0, 0, 1);
this.views.perspective.camera.lookAt(10, 0, 0);
this.views.top.camera = new THREE.OrthographicCamera(
(-aspectRatio * viewSize) / 2 - 2,
(aspectRatio * viewSize) / 2 + 2,
viewSize / 2 + 2,
-viewSize / 2 - 2,
-10,
10,
);
this.views.side.camera = new THREE.OrthographicCamera(
(-aspectRatio * viewSize) / 2,
(aspectRatio * viewSize) / 2,
viewSize / 2,
-viewSize / 2,
-10,
10,
);
this.views.side.camera.position.set(0, 5, 0);
this.views.side.camera.lookAt(0, 0, 0);
this.views.side.camera.up.set(0, 0, 1);
this.views.front.camera = new THREE.OrthographicCamera(
(-aspectRatio * viewSize) / 2,
(aspectRatio * viewSize) / 2,
viewSize / 2,
-viewSize / 2,
-10,
10,
);
this.views.front.camera.position.set(-7, 0, 0);
this.views.front.camera.up.set(0, 0, 1);
this.views.front.camera.lookAt(0, 0, 0);
Object.keys(this.views).forEach((view: string): void => {
const viewType = this.views[view as keyof Views];
viewType.renderer.setSize(width, height);
if (view !== ViewType.PERSPECTIVE) {
viewType.controls = new CameraControls(viewType.camera, viewType.renderer.domElement);
viewType.controls.mouseButtons.left = CameraControls.ACTION.NONE;
viewType.controls.mouseButtons.right = CameraControls.ACTION.NONE;
} else {
viewType.controls = new CameraControls(viewType.camera, viewType.renderer.domElement);
}
viewType.controls.minDistance = CONST.MIN_DISTANCE;
viewType.controls.maxDistance = CONST.MAX_DISTANCE;
});
model.subscribe(this);
}
public notify(model: Canvas3dModel & Master, reason: UpdateReasons): void {
if (reason === UpdateReasons.IMAGE_CHANGED) {
const loader = new PCDLoader();
this.clearScene();
const objectURL = URL.createObjectURL(model.data.image.imageData);
loader.load(objectURL, this.addScene.bind(this));
URL.revokeObjectURL(objectURL);
const event: CustomEvent = new CustomEvent('canvas.setup');
this.views.perspective.renderer.domElement.dispatchEvent(event);
} else if (reason === UpdateReasons.DRAW) {
const data: DrawData = this.controller.drawData;
if (data.enabled && this.mode === Mode.IDLE) {
this.mode = Mode.DRAW;
this.cube = new CuboidModel();
} else if (this.mode !== Mode.IDLE) {
this.cube = new CuboidModel();
}
} else if (reason === UpdateReasons.CANCEL) {
if (this.mode === Mode.DRAW) {
this.controller.drawData.enabled = false;
Object.keys(this.views).forEach((view: string): void => {
this.views[view as keyof Views].scene.children[0].remove(this.cube[view as keyof Views]);
});
}
this.mode = Mode.IDLE;
const event: CustomEvent = new CustomEvent('canvas.canceled');
this.views.perspective.renderer.domElement.dispatchEvent(event);
}
}
private clearScene(): void {
Object.keys(this.views).forEach((view: string): void => {
this.views[view as keyof Views].scene.children = [];
});
}
private addScene(points: any): void {
// eslint-disable-next-line no-param-reassign
points.material.size = 0.08;
// eslint-disable-next-line no-param-reassign
points.material.color = new THREE.Color(0x0000ff);
const sphereCenter = points.geometry.boundingSphere.center;
const { radius } = points.geometry.boundingSphere;
const xRange = -radius / 2 < this.views.perspective.camera.position.x - sphereCenter.x
&& radius / 2 > this.views.perspective.camera.position.x - sphereCenter.x;
const yRange = -radius / 2 < this.views.perspective.camera.position.y - sphereCenter.y
&& radius / 2 > this.views.perspective.camera.position.y - sphereCenter.y;
const zRange = -radius / 2 < this.views.perspective.camera.position.z - sphereCenter.z
&& radius / 2 > this.views.perspective.camera.position.z - sphereCenter.z;
let newX = 0;
let newY = 0;
let newZ = 0;
if (!xRange) {
newX = sphereCenter.x;
}
if (!yRange) {
newY = sphereCenter.y;
}
if (!zRange) {
newZ = sphereCenter.z;
}
if (newX || newY || newZ) {
this.positionAllViews(newX, newY, newZ);
}
this.views.perspective.scene.add(points);
this.views.top.scene.add(points.clone());
this.views.side.scene.add(points.clone());
this.views.front.scene.add(points.clone());
}
private positionAllViews(x: number, y: number, z: number): void {
this.views.perspective.controls.setLookAt(x - 8, y - 8, z + 3, x, y, z, false);
this.views.top.controls.setLookAt(x, y, z + 8, x, y, z, false);
this.views.side.controls.setLookAt(x, y + 8, z, x, y, z, false);
this.views.front.controls.setLookAt(x + 8, y, z, x, y, z, false);
}
private static resizeRendererToDisplaySize(viewName: string, view: RenderView): void {
const { camera, renderer } = view;
const canvas = renderer.domElement;
const width = canvas.parentElement.clientWidth;
const height = canvas.parentElement.clientHeight;
const needResize = canvas.clientWidth !== width || canvas.clientHeight !== height;
if (needResize) {
if (camera instanceof THREE.PerspectiveCamera) {
camera.aspect = width / height;
} else {
const topViewFactor = 0; // viewName === ViewType.TOP ? 2 : 0;
const viewSize = CONST.ZOOM_FACTOR;
const aspectRatio = width / height;
if (!(camera instanceof THREE.PerspectiveCamera)) {
camera.left = (-aspectRatio * viewSize) / 2 - topViewFactor;
camera.right = (aspectRatio * viewSize) / 2 + topViewFactor;
camera.top = viewSize / 2 + topViewFactor;
camera.bottom = -viewSize / 2 - topViewFactor;
}
camera.near = -10;
camera.far = 10;
}
view.renderer.setSize(width, height);
view.camera.updateProjectionMatrix();
}
}
private renderRayCaster = (viewType: RenderView): void => {
viewType.rayCaster.renderer.setFromCamera(viewType.rayCaster.mouseVector, viewType.camera);
if (this.mode === Mode.DRAW) {
const intersects = this.views.perspective.rayCaster.renderer.intersectObjects(
this.views.perspective.scene.children,
false,
);
if (intersects.length > 0) {
this.views.perspective.scene.children[0].add(this.cube.perspective);
const newPoints = intersects[0].point;
this.cube.perspective.position.copy(newPoints);
}
} else if (this.mode === Mode.IDLE) {
const intersects = this.views.perspective.rayCaster.renderer.intersectObjects(
this.views.perspective.scene.children[0].children,
false,
);
if (intersects.length !== 0) {
this.views.perspective.scene.children[0].children.forEach((sceneItem: THREE.Mesh): void => {
if (this.selected.perspective !== sceneItem) {
// eslint-disable-next-line no-param-reassign
sceneItem.material.color = new THREE.Color(0xff0000);
}
});
const selectedObject = intersects[0].object as THREE.Mesh;
if (this.selected.perspective !== selectedObject) {
selectedObject.material.color = new THREE.Color(0xffff00);
this.highlighted = true;
}
} else {
if (this.highlighted) {
this.views.perspective.scene.children[0].children.forEach((sceneItem: THREE.Mesh): void => {
if (this.selected.perspective !== sceneItem) {
// eslint-disable-next-line no-param-reassign
sceneItem.material.color = new THREE.Color(0xff0000);
}
});
}
this.highlighted = false;
}
}
};
public render(): void {
Object.keys(this.views).forEach((view: string): void => {
const viewType = this.views[view as keyof Views];
Canvas3dViewImpl.resizeRendererToDisplaySize(view, viewType);
viewType.controls.update(this.clock.getDelta());
viewType.renderer.render(viewType.scene, viewType.camera);
if (view === ViewType.PERSPECTIVE && viewType.scene.children.length !== 0) {
this.renderRayCaster(viewType);
}
});
}
public keyControls(key: any): void {
const { controls } = this.views.perspective;
switch (key.code) {
case CAMERA_ACTION.ROTATE_RIGHT:
controls.rotate(0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true);
break;
case CAMERA_ACTION.ROTATE_LEFT:
controls.rotate(-0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true);
break;
case CAMERA_ACTION.TILT_UP:
controls.rotate(0, -0.05 * THREE.MathUtils.DEG2RAD * this.speed, true);
break;
case CAMERA_ACTION.TILT_DOWN:
controls.rotate(0, 0.05 * THREE.MathUtils.DEG2RAD * this.speed, true);
break;
default:
break;
}
if (key.altKey === true) {
switch (key.code) {
case CAMERA_ACTION.ZOOM_IN:
controls.dolly(CONST.DOLLY_FACTOR, true);
break;
case CAMERA_ACTION.ZOOM_OUT:
controls.dolly(-CONST.DOLLY_FACTOR, true);
break;
case CAMERA_ACTION.MOVE_LEFT:
controls.truck(-0.01 * this.speed, 0, true);
break;
case CAMERA_ACTION.MOVE_RIGHT:
controls.truck(0.01 * this.speed, 0, true);
break;
case CAMERA_ACTION.MOVE_DOWN:
controls.truck(0, -0.01 * this.speed, true);
break;
case CAMERA_ACTION.MOVE_UP:
controls.truck(0, 0.01 * this.speed, true);
break;
default:
break;
}
}
}
public mouseControls(type: MouseInteraction, event: MouseEvent): void {
event.preventDefault();
if (type === MouseInteraction.DOUBLE_CLICK && this.mode === Mode.DRAW) {
this.controller.drawData.enabled = false;
this.mode = Mode.IDLE;
const cancelEvent: CustomEvent = new CustomEvent('canvas.canceled');
this.views.perspective.renderer.domElement.dispatchEvent(cancelEvent);
} else {
const canvas = this.views.perspective.renderer.domElement;
const rect = canvas.getBoundingClientRect();
const { mouseVector } = this.views.perspective.rayCaster;
mouseVector.x = ((event.clientX - (canvas.offsetLeft + rect.left)) / canvas.clientWidth) * 2 - 1;
mouseVector.y = -((event.clientY - (canvas.offsetTop + rect.top)) / canvas.clientHeight) * 2 + 1;
if (type === MouseInteraction.CLICK && this.mode === Mode.IDLE) {
const intersects = this.views.perspective.rayCaster.renderer.intersectObjects(
this.views.perspective.scene.children[0].children,
false,
);
if (intersects.length !== 0) {
this.views.perspective.scene.children[0].children.forEach((sceneItem: THREE.Mesh): void => {
// eslint-disable-next-line no-param-reassign
sceneItem.material.color = new THREE.Color(0xff0000);
});
const selectedObject = intersects[0].object;
selectedObject.material.color = new THREE.Color(0x00ffff);
Object.keys(this.views).forEach((view: string): void => {
if (view !== ViewType.PERSPECTIVE) {
this.views[view as keyof Views].scene.children[0].children = [selectedObject.clone()];
this.views[view as keyof Views].controls.fitToBox(selectedObject, false);
this.views[view as keyof Views].controls.zoom(view === ViewType.TOP ? -5 : -5, false);
}
this.views[view as keyof Views].scene.background = new THREE.Color(0x000000);
});
this.selected.perspective = selectedObject as THREE.Mesh;
}
}
}
}
public html(): ViewsDOM {
return {
perspective: this.views.perspective.renderer.domElement,
top: this.views.top.renderer.domElement,
side: this.views.side.renderer.domElement,
front: this.views.front.renderer.domElement,
};
}
}

@ -0,0 +1,19 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
const BASE_GRID_WIDTH = 2;
const MOVEMENT_FACTOR = 200;
const DOLLY_FACTOR = 5;
const MAX_DISTANCE = 100;
const MIN_DISTANCE = 0;
const ZOOM_FACTOR = 7;
export default {
BASE_GRID_WIDTH,
MOVEMENT_FACTOR,
DOLLY_FACTOR,
MAX_DISTANCE,
MIN_DISTANCE,
ZOOM_FACTOR,
};

@ -0,0 +1,20 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as THREE from 'three';
export class CuboidModel {
public perspective: THREE.Mesh;
public top: THREE.Mesh;
public side: THREE.Mesh;
public front: THREE.Mesh;
public constructor() {
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true });
this.perspective = new THREE.Mesh(geometry, material);
this.top = new THREE.Mesh(geometry, material);
this.side = new THREE.Mesh(geometry, material);
this.front = new THREE.Mesh(geometry, material);
}
}

@ -0,0 +1,44 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
export interface Master {
subscribe(listener: Listener): void;
unsubscribe(listener: Listener): void;
unsubscribeAll(): void;
notify(reason: string): void;
}
export interface Listener {
notify(master: Master, reason: string): void;
}
export class MasterImpl implements Master {
private listeners: Listener[];
public constructor() {
this.listeners = [];
}
public subscribe(listener: Listener): void {
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);
}
}
}

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

@ -0,0 +1,138 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const DtsBundleWebpack = require('dts-bundle-webpack');
const nodeConfig = {
target: 'node',
mode: 'production',
devtool: 'source-map',
entry: './src/typescript/canvas3d.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cvat-canvas3d.node.js',
library: 'canvas3d',
libraryTarget: 'commonjs',
},
resolve: {
extensions: ['.ts', '.js', '.json'],
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining',
],
presets: [['@babel/preset-env'], ['@babel/typescript']],
sourceType: 'unambiguous',
},
},
},
{
test: /\.(css|scss)$/,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'sass-loader',
],
},
],
},
plugins: [
new DtsBundleWebpack({
name: 'cvat-canvas3d.node',
main: 'dist/declaration/src/typescript/canvas3d.d.ts',
out: '../cvat-canvas3d.node.d.ts',
}),
],
};
const webConfig = {
target: 'web',
mode: 'production',
devtool: 'source-map',
entry: {
'cvat-canvas3d': './src/typescript/canvas3d.ts',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
library: 'canvas3d',
libraryTarget: 'window',
},
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: false,
inline: true,
port: 3000,
},
resolve: {
extensions: ['.ts', '.js', '.json'],
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: ['@babel/plugin-proposal-class-properties'],
presets: [
[
'@babel/preset-env',
{
targets: '> 2.5%', // https://github.com/browserslist/browserslist
},
],
['@babel/typescript'],
],
sourceType: 'unambiguous',
},
},
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'sass-loader',
],
},
],
},
plugins: [
new DtsBundleWebpack({
name: 'cvat-canvas3d',
main: 'dist/declaration/src/typescript/canvas3d.d.ts',
out: '../cvat-canvas3d.d.ts',
}),
],
};
module.exports = [webConfig, nodeConfig];

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -6,7 +6,7 @@ const { defaults } = require('jest-config');
module.exports = {
coverageDirectory: 'reports/coverage',
coverageReporters: ['lcov'],
coverageReporters: ['json', ['lcov', { projectRoot: '../' }]],
moduleFileExtensions: [...defaults.moduleFileExtensions, 'ts', 'tsx'],
reporters: ['default', ['jest-junit', { outputDirectory: 'reports/junit' }]],
testMatch: ['**/tests/**/*.js'],

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.10.0",
"version": "3.12.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {
@ -26,7 +26,7 @@
"eslint-plugin-no-unsanitized": "^3.0.2",
"eslint-plugin-security": "^1.4.0",
"eslint-plugin-jest": "^24.1.0",
"jest": "^24.8.0",
"jest": "^26.6.3",
"jest-junit": "^6.4.0",
"jsdoc": "^3.6.4",
"webpack": "^4.31.0",
@ -40,8 +40,8 @@
"error-stack-parser": "^2.0.2",
"form-data": "^2.5.0",
"jest-config": "^26.6.3",
"json-logic-js": "^2.0.0",
"js-cookie": "^2.2.0",
"jsonpath": "^1.0.2",
"platform": "^1.3.5",
"quickhull": "^1.0.3",
"store": "^2.0.12",

@ -15,6 +15,7 @@
format: initialData.ext,
version: initialData.version,
enabled: initialData.enabled,
dimension: initialData.dimension,
};
Object.defineProperties(this, {
@ -58,6 +59,16 @@
*/
get: () => data.enabled,
},
dimension: {
/**
* @name dimension
* @type {string}
* @memberof module:API.cvat.enums.DimensionType
* @readonly
* @instance
*/
get: () => data.dimension,
},
});
}
}
@ -74,6 +85,7 @@
format: initialData.ext,
version: initialData.version,
enabled: initialData.enabled,
dimension: initialData.dimension,
};
Object.defineProperties(this, {
@ -117,6 +129,16 @@
*/
get: () => data.enabled,
},
dimension: {
/**
* @name dimension
* @type {string}
* @memberof module:API.cvat.enums.DimensionType
* @readonly
* @instance
*/
get: () => data.dimension,
},
});
}
}

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -213,13 +213,9 @@
visible.data.push(stateData);
}
const [, query] = this.annotationsFilter.toJSONQuery(filters);
let filtered = [];
if (filters.length) {
filtered = this.annotationsFilter.filter(visible.data, query);
}
const objectStates = [];
const filtered = this.annotationsFilter.filter(visible.data, filters);
visible.data.forEach((stateData, idx) => {
if (!filters.length || filtered.includes(stateData.clientID)) {
const model = visible.models[idx];
@ -777,6 +773,7 @@
}
// Add constructed objects to a collection
// eslint-disable-next-line no-unsanitized/method
const imported = this.import(constructed);
const importedArray = imported.tags.concat(imported.tracks).concat(imported.shapes);
@ -865,13 +862,9 @@
}
search(filters, frameFrom, frameTo) {
const [groups, query] = this.annotationsFilter.toJSONQuery(filters);
const sign = Math.sign(frameTo - frameFrom);
const flattenedQuery = groups.flat(Number.MAX_SAFE_INTEGER);
const containsDifficultProperties = flattenedQuery.some(
(fragment) => fragment.match(/^width/) || fragment.match(/^height/),
);
const filtersStr = JSON.stringify(filters);
const containsDifficultProperties = filtersStr.match(/"var":"width"/) || filtersStr.match(/"var":"height"/);
const deepSearch = (deepSearchFrom, deepSearchTo) => {
// deepSearchFrom is expected to be a frame that doesn't satisfy a filter
@ -882,7 +875,7 @@
while (!(Math.abs(prev - next) === 1)) {
const middle = next + Math.floor((prev - next) / 2);
const shapesData = this.tracks.map((track) => track.get(middle));
const filtered = this.annotationsFilter.filter(shapesData, query);
const filtered = this.annotationsFilter.filter(shapesData, filters);
if (filtered.length) {
next = middle;
} else {
@ -919,7 +912,7 @@
}
// Filtering
const filtered = this.annotationsFilter.filter(statesData, query);
const filtered = this.annotationsFilter.filter(statesData, filters);
// Now we are checking whether we need deep search or not
// Deep search is needed in some difficult cases

@ -1,143 +1,15 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
const jsonpath = require('jsonpath');
const jsonLogic = require('json-logic-js');
const { AttributeType, ObjectType } = require('./enums');
const { ArgumentError } = require('./exceptions');
class AnnotationsFilter {
constructor() {
// eslint-disable-next-line security/detect-unsafe-regex
this.operatorRegex = /(==|!=|<=|>=|>|<)(?=(?:[^"]*(["])[^"]*\2)*[^"]*$)/g;
}
// Method splits expression by operators that are outside of any brackets
_splitWithOperator(container, expression) {
const operators = ['|', '&'];
const splitted = [];
let nestedCounter = 0;
let isQuotes = false;
let start = -1;
for (let i = 0; i < expression.length; i++) {
if (expression[i] === '"') {
// all quotes inside other quotes must
// be escaped by a user and changed to ` above
isQuotes = !isQuotes;
}
// We don't split with operator inside brackets
// It will be done later in recursive call
if (!isQuotes && expression[i] === '(') {
nestedCounter++;
}
if (!isQuotes && expression[i] === ')') {
nestedCounter--;
}
if (operators.includes(expression[i])) {
if (!nestedCounter) {
const subexpression = expression.substr(start + 1, i - start - 1).trim();
splitted.push(subexpression);
splitted.push(expression[i]);
start = i;
}
}
}
const subexpression = expression.substr(start + 1).trim();
splitted.push(subexpression);
splitted.forEach((internalExpression) => {
if (internalExpression === '|' || internalExpression === '&') {
container.push(internalExpression);
} else {
this._groupByBrackets(container, internalExpression);
}
});
}
// Method groups bracket containings to nested arrays of container
_groupByBrackets(container, expression) {
if (!(expression.startsWith('(') && expression.endsWith(')'))) {
container.push(expression);
}
let nestedCounter = 0;
let startBracket = null;
let endBracket = null;
let isQuotes = false;
for (let i = 0; i < expression.length; i++) {
if (expression[i] === '"') {
// all quotes inside other quotes must
// be escaped by a user and changed to ` above
isQuotes = !isQuotes;
}
if (!isQuotes && expression[i] === '(') {
nestedCounter++;
if (startBracket === null) {
startBracket = i;
}
}
if (!isQuotes && expression[i] === ')') {
nestedCounter--;
if (!nestedCounter) {
endBracket = i;
const subcontainer = [];
const subexpression = expression.substr(startBracket + 1, endBracket - 1 - startBracket);
this._splitWithOperator(subcontainer, subexpression);
container.push(subcontainer);
startBracket = null;
endBracket = null;
}
}
}
if (startBracket !== null) {
throw Error('Extra opening bracket found');
}
if (endBracket !== null) {
throw Error('Extra closing bracket found');
}
}
_parse(expression) {
const groups = [];
this._splitWithOperator(groups, expression);
}
_join(groups) {
let expression = '';
for (const group of groups) {
if (Array.isArray(group)) {
expression += `(${this._join(group)})`;
} else if (typeof group === 'string') {
// it can be operator or expression
if (group === '|' || group === '&') {
expression += group;
} else {
let [field, operator, , value] = group.split(this.operatorRegex);
field = `@.${field.trim()}`;
operator = operator.trim();
value = value.trim();
if (value === 'width' || value === 'height' || value.startsWith('attr')) {
value = `@.${value}`;
}
expression += [field, operator, value].join('');
}
}
}
return expression;
}
function adjustName(name) {
return name.replace(/\./g, '\u2219');
}
class AnnotationsFilter {
_convertObjects(statesData) {
const objects = statesData.map((state) => {
const labelAttributes = state.label.attributes.reduce((acc, attr) => {
@ -169,63 +41,38 @@ class AnnotationsFilter {
const attributes = {};
Object.keys(state.attributes).reduce((acc, key) => {
const attr = labelAttributes[key];
let value = state.attributes[key].replace(/\\"/g, '`');
let value = state.attributes[key];
if (attr.inputType === AttributeType.NUMBER) {
value = +value;
} else if (attr.inputType === AttributeType.CHECKBOX) {
value = value === 'true';
}
acc[attr.name] = value;
acc[adjustName(attr.name)] = value;
return acc;
}, attributes);
return {
width,
height,
attr: attributes,
label: state.label.name.replace(/\\"/g, '`'),
attr: Object.fromEntries([[adjustName(state.label.name), attributes]]),
label: state.label.name,
serverID: state.serverID,
clientID: state.clientID,
objectID: state.clientID,
type: state.objectType,
shape: state.shapeType,
occluded: state.occluded,
};
});
return {
objects,
};
}
toJSONQuery(filters) {
try {
if (!Array.isArray(filters) || filters.some((value) => typeof value !== 'string')) {
throw Error('Argument must be an array of strings');
}
if (!filters.length) {
return [[], '$.objects[*].clientID'];
}
const groups = [];
const expression = filters
.map((filter) => `(${filter})`)
.join('|')
.replace(/\\"/g, '`');
this._splitWithOperator(groups, expression);
return [groups, `$.objects[?(${this._join(groups)})].clientID`];
} catch (error) {
throw new ArgumentError(`Wrong filter expression. ${error.toString()}`);
}
return objects;
}
filter(statesData, query) {
try {
const objects = this._convertObjects(statesData);
return jsonpath.query(objects, query);
} catch (error) {
throw new ArgumentError(`Could not apply the filter. ${error.toString()}`);
}
filter(statesData, filters) {
if (!filters.length) return statesData;
const converted = this._convertObjects(statesData);
return converted
.map((state) => state.objectID)
.filter((_, index) => jsonLogic.apply(filters[0], converted[index]));
}
}

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -7,10 +7,16 @@
const serverProxy = require('./server-proxy');
const lambdaManager = require('./lambda-manager');
const {
isBoolean, isInteger, isEnum, isString, checkFilter,
isBoolean,
isInteger,
isEnum,
isString,
checkFilter,
checkExclusiveFields,
camelToSnake,
} = require('./common');
const { TaskStatus, TaskMode } = require('./enums');
const { TaskStatus, TaskMode, DimensionType } = require('./enums');
const User = require('./user');
const { AnnotationFormats } = require('./annotation-formats');
@ -176,29 +182,24 @@
search: isString,
status: isEnum.bind(TaskStatus),
mode: isEnum.bind(TaskMode),
dimension: isEnum.bind(DimensionType),
});
if ('search' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError('Do not use the filter field "search" with others');
}
}
if ('id' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError('Do not use the filter field "id" with others');
}
}
if (
'projectId' in filter
&& (('page' in filter && Object.keys(filter).length > 2) || Object.keys(filter).length > 2)
) {
throw new ArgumentError('Do not use the filter field "projectId" with other');
}
checkExclusiveFields(filter, ['id', 'search', 'projectId'], ['page']);
const searchParams = new URLSearchParams();
for (const field of ['name', 'owner', 'assignee', 'search', 'status', 'mode', 'id', 'page', 'projectId']) {
for (const field of [
'name',
'owner',
'assignee',
'search',
'status',
'mode',
'id',
'page',
'projectId',
'dimension',
]) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]);
}
@ -221,30 +222,26 @@
owner: isString,
search: isString,
status: isEnum.bind(TaskStatus),
withoutTasks: isBoolean,
});
if ('search' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError('Do not use the filter field "search" with others');
}
}
if ('id' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError('Do not use the filter field "id" with others');
}
}
checkExclusiveFields(filter, ['id', 'search'], ['page', 'withoutTasks']);
const searchParams = new URLSearchParams();
for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page']) {
for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page', 'withoutTasks']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]);
searchParams.set(camelToSnake(field), filter[field]);
}
}
const projectsData = await serverProxy.projects.get(searchParams.toString());
// prettier-ignore
const projects = projectsData.map((project) => new Project(project));
const projects = projectsData.map((project) => {
if (filter.withoutTasks) {
project.tasks = [];
}
return project;
}).map((project) => new Project(project));
projects.count = projectsData.count;

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -42,6 +42,25 @@
}
}
function checkExclusiveFields(obj, exclusive, ignore) {
const fields = {
exclusive: [],
other: [],
};
for (const field in Object.keys(obj)) {
if (!(field in ignore)) {
if (field in exclusive) {
if (fields.other.length) {
throw new ArgumentError(`Do not use the filter field "${field}" with others`);
}
fields.exclusive.push(field);
} else {
fields.other.push(field);
}
}
}
}
function checkObjectType(name, value, type, instance) {
if (type) {
if (typeof value !== type) {
@ -68,6 +87,16 @@
return true;
}
function camelToSnake(str) {
if (typeof str !== 'string') {
throw new ArgumentError('str is expected to be string');
}
return (
str[0].toLowerCase() + str.slice(1, str.length).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
);
}
function negativeIDGenerator() {
const value = negativeIDGenerator.start;
negativeIDGenerator.start -= 1;
@ -83,5 +112,7 @@
checkFilter,
checkObjectType,
negativeIDGenerator,
checkExclusiveFields,
camelToSnake,
};
})();

@ -33,6 +33,20 @@
COMPLETED: 'completed',
});
/**
* Task dimension
* @enum
* @name DimensionType
* @memberof module:API.cvat.enums
* @property {string} DIMENSION_2D '2d'
* @property {string} DIMENSION_3D '3d'
* @readonly
*/
const DimensionType = Object.freeze({
DIMENSION_2D: '2d',
DIMENSION_3D: '3d',
});
/**
* Review statuses
* @enum {string}
@ -333,5 +347,6 @@
RQStatus,
colors,
Source,
DimensionType,
};
})();

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -341,6 +341,7 @@
constructor(size, chunkSize, stopFrame, taskID) {
this._size = size;
this._buffer = {};
this._contextImage = {};
this._requestedChunks = {};
this._chunkSize = chunkSize;
this._stopFrame = stopFrame;
@ -348,6 +349,18 @@
this._taskID = taskID;
}
isContextImageAvailable(frame) {
return frame in this._contextImage;
}
getContextImage(frame) {
return this._contextImage[frame] || null;
}
addContextImage(frame, data) {
this._contextImage[frame] = data;
}
getFreeBufferSize() {
let requestedFrameCount = 0;
for (const chunk of Object.values(this._requestedChunks)) {
@ -535,6 +548,37 @@
}
}
async function getImageContext(taskID, frame) {
return new Promise((resolve, reject) => {
serverProxy.frames
.getImageContext(taskID, frame)
.then((result) => {
if (isNode) {
// eslint-disable-next-line no-undef
resolve(global.Buffer.from(result, 'binary').toString('base64'));
} else if (isBrowser) {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.readAsDataURL(result);
}
})
.catch((error) => {
reject(error);
});
});
}
async function getContextImage(taskID, frame) {
if (frameDataCache[taskID].frameBuffer.isContextImageAvailable(frame)) {
return frameDataCache[taskID].frameBuffer.getContextImage(frame);
}
const response = getImageContext(taskID, frame);
frameDataCache[taskID].frameBuffer.addContextImage(frame, response);
return frameDataCache[taskID].frameBuffer.getContextImage(frame);
}
async function getPreview(taskID) {
return new Promise((resolve, reject) => {
// Just go to server and get preview (no any cache)
@ -558,7 +602,18 @@
});
}
async function getFrame(taskID, chunkSize, chunkType, mode, frame, startFrame, stopFrame, isPlaying, step) {
async function getFrame(
taskID,
chunkSize,
chunkType,
mode,
frame,
startFrame,
stopFrame,
isPlaying,
step,
dimension,
) {
if (!(taskID in frameDataCache)) {
const blockType = chunkType === 'video' ? cvatData.BlockType.MP4VIDEO : cvatData.BlockType.ARCHIVE;
@ -584,6 +639,7 @@
Math.max(decodedBlocksCacheSize, 9),
decodedBlocksCacheSize,
1,
dimension,
),
frameBuffer: new FrameBuffer(
Math.min(180, decodedBlocksCacheSize * chunkSize),
@ -630,5 +686,6 @@
getRanges,
getPreview,
clear,
getContextImage,
};
})();

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -133,6 +133,7 @@
id: undefined,
name: undefined,
color: undefined,
deleted: false,
};
for (const key in data) {
@ -171,17 +172,21 @@
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
name: {
get: () => data.name,
set: (name) => {
if (typeof name !== 'string') {
throw new ArgumentError(`Name must be a string, but ${typeof name} was given`);
}
data.name = name;
},
},
/**
* @name color
* @type {string}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
color: {
@ -204,6 +209,12 @@
attributes: {
get: () => [...data.attributes],
},
deleted: {
get: () => data.deleted,
set: (value) => {
data.deleted = value;
},
},
}),
);
}
@ -219,6 +230,10 @@
object.id = this.id;
}
if (this.deleted) {
object.deleted = this.deleted;
}
return object;
}
}

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -17,6 +17,7 @@ class MLModel {
this._params = {
canvas: {
minPosVertices: data.min_pos_points,
enableNegVertices: true,
},
};
}

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -32,6 +32,7 @@
bug_tracker: undefined,
created_date: undefined,
updated_date: undefined,
task_subsets: undefined,
};
for (const property in data) {
@ -56,6 +57,13 @@
data.tasks.push(taskInstance);
}
}
if (!data.task_subsets && data.tasks.length) {
const subsetsSet = new Set();
for (const task in data.tasks) {
if (task.subset) subsetsSet.add(task.subset);
}
data.task_subsets = Array.from(subsetsSet);
}
Object.defineProperties(
this,
@ -178,7 +186,13 @@
);
}
data.labels = [...labels];
const IDs = labels.map((_label) => _label.id);
const deletedLabels = data.labels.filter((_label) => !IDs.includes(_label.id));
deletedLabels.forEach((_label) => {
_label.deleted = true;
});
data.labels = [...deletedLabels, ...labels];
},
},
/**
@ -192,6 +206,20 @@
tasks: {
get: () => [...data.tasks],
},
/**
* Subsets array for linked tasks
* @name subsets
* @type {string[]}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
subsets: {
get: () => [...data.task_subsets],
},
_internalData: {
get: () => data,
},
}),
);
}
@ -238,7 +266,7 @@
name: this.name,
assignee_id: this.assignee ? this.assignee.id : null,
bug_tracker: this.bugTracker,
labels: [...this.labels.map((el) => el.toJSON())],
labels: [...this._internalData.labels.map((el) => el.toJSON())],
};
await serverProxy.projects.save(this.id, projectData);

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -718,6 +718,29 @@
return response.data;
}
async function getImageContext(tid, frame) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(
`${backendAPI}/tasks/${tid}/data?quality=original&type=context_image&number=${frame}`,
{
proxy: config.proxy,
responseType: 'blob',
},
);
} 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,
);
}
return response.data;
}
async function getData(tid, chunk) {
const { backendAPI } = config;
@ -1053,6 +1076,7 @@
getData,
getMeta,
getPreview,
getImageContext,
}),
writable: false,
},

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -8,7 +8,7 @@
const loggerStorage = require('./logger-storage');
const serverProxy = require('./server-proxy');
const {
getFrame, getRanges, getPreview, clear: clearFrames,
getFrame, getRanges, getPreview, clear: clearFrames, getContextImage,
} = require('./frames');
const { ArgumentError } = require('./exceptions');
const { TaskStatus } = require('./enums');
@ -183,6 +183,15 @@
const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.preview);
return result;
},
async contextImage(taskId, frameId) {
const result = await PluginRegistry.apiWrapper.call(
this,
prototype.frames.contextImage,
taskId,
frameId,
);
return result;
},
},
writable: true,
}),
@ -370,7 +379,7 @@
* @param {integer} frame get objects from the frame
* @param {boolean} allTracks show all tracks
* even if they are outside and not keyframe
* @param {string[]} [filters = []]
* @param {any[]} [filters = []]
* get only objects that satisfied to specific filters
* @returns {module:API.cvat.classes.ObjectState[]}
* @memberof Session.annotations
@ -850,6 +859,7 @@
get: Object.getPrototypeOf(this).frames.get.bind(this),
ranges: Object.getPrototypeOf(this).frames.ranges.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
contextImage: Object.getPrototypeOf(this).frames.contextImage.bind(this),
};
this.logger = {
@ -962,6 +972,7 @@
created_date: undefined,
updated_date: undefined,
bug_tracker: undefined,
subset: undefined,
overlap: undefined,
segment_size: undefined,
image_quality: undefined,
@ -974,12 +985,14 @@
use_zip_chunks: undefined,
use_cache: undefined,
copy_data: undefined,
dimension: undefined,
};
let updatedFields = {
name: false,
assignee: false,
bug_tracker: false,
subset: false,
labels: false,
};
@ -1156,10 +1169,36 @@
bugTracker: {
get: () => data.bug_tracker,
set: (tracker) => {
if (typeof tracker !== 'string') {
throw new ArgumentError(
`Subset value must be a string. But ${typeof tracker} has been got.`,
);
}
updatedFields.bug_tracker = true;
data.bug_tracker = tracker;
},
},
/**
* @name subset
* @type {string}
* @memberof module:API.cvat.classes.Task
* @instance
* @throws {module:API.cvat.exception.ArgumentError}
*/
subset: {
get: () => data.subset,
set: (subset) => {
if (typeof subset !== 'string') {
throw new ArgumentError(
`Subset value must be a string. But ${typeof subset} has been got.`,
);
}
updatedFields.subset = true;
data.subset = subset;
},
},
/**
* @name overlap
* @type {integer}
@ -1265,7 +1304,7 @@
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
labels: {
get: () => [...data.labels],
get: () => data.labels.filter((_label) => !_label.deleted),
set: (labels) => {
if (!Array.isArray(labels)) {
throw new ArgumentError('Value must be an array of Labels');
@ -1279,8 +1318,14 @@
}
}
const IDs = labels.map((_label) => _label.id);
const deletedLabels = data.labels.filter((_label) => !IDs.includes(_label.id));
deletedLabels.forEach((_label) => {
_label.deleted = true;
});
updatedFields.labels = true;
data.labels = [...labels];
data.labels = [...deletedLabels, ...labels];
},
},
/**
@ -1446,6 +1491,19 @@
dataChunkType: {
get: () => data.data_compressed_chunk_type,
},
dimension: {
/**
* @name enabled
* @type {string}
* @memberof module:API.cvat.enums.DimensionType
* @readonly
* @instance
*/
get: () => data.dimension,
},
_internalData: {
get: () => data,
},
__updatedFields: {
get: () => updatedFields,
set: (fields) => {
@ -1490,6 +1548,7 @@
get: Object.getPrototypeOf(this).frames.get.bind(this),
ranges: Object.getPrototypeOf(this).frames.ranges.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
contextImage: Object.getPrototypeOf(this).frames.contextImage.bind(this),
};
this.logger = {
@ -1672,6 +1731,7 @@
this.stopFrame,
isPlaying,
step,
this.task.dimension,
);
return frameData;
};
@ -1683,8 +1743,8 @@
// TODO: Check filter for annotations
Job.prototype.annotations.get.implementation = async function (frame, allTracks, filters) {
if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) {
throw new ArgumentError('The filters argument must be an array of strings');
if (!Array.isArray(filters)) {
throw new ArgumentError('Filters must be an array');
}
if (!Number.isInteger(frame)) {
@ -1700,8 +1760,8 @@
};
Job.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) {
if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) {
throw new ArgumentError('The filters argument must be an array of strings');
if (!Array.isArray(filters)) {
throw new ArgumentError('Filters must be an array');
}
if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) {
@ -1865,8 +1925,11 @@
case 'bug_tracker':
taskData.bug_tracker = this.bugTracker;
break;
case 'subset':
taskData.subset = this.subset;
break;
case 'labels':
taskData.labels = [...this.labels.map((el) => el.toJSON())];
taskData.labels = [...this._internalData.labels.map((el) => el.toJSON())];
break;
default:
break;
@ -1880,6 +1943,7 @@
assignee: false,
name: false,
bugTracker: false,
subset: false,
labels: false,
};
@ -1903,6 +1967,9 @@
if (typeof this.projectId !== 'undefined') {
taskSpec.project_id = this.projectId;
}
if (typeof this.subset !== 'undefined') {
taskSpec.subset = this.subset;
}
const taskDataSpec = {
client_files: this.clientFiles,
@ -2131,4 +2198,9 @@
const result = await loggerStorage.log(logType, { ...payload, task_id: this.id }, wait);
return result;
};
Job.prototype.frames.contextImage.implementation = async function (taskId, frameId) {
const result = await getContextImage(taskId, frameId);
return result;
};
})();

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -168,3 +168,20 @@ describe('Feature: delete a project', () => {
expect(result).toHaveLength(0);
});
});
describe('Feature: delete a label', () => {
test('delete a label', async () => {
let result = await window.cvat.projects.get({
id: 2,
});
const labelsLength = result[0].labels.length;
const deletedLabels = result[0].labels.filter((el) => el.name !== 'bicycle');
result[0].labels = deletedLabels;
result[0].save();
result = await window.cvat.projects.get({
id: 2,
});
expect(result[0].labels).toHaveLength(labelsLength - 1);
});
});

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -196,3 +196,20 @@ describe('Feature: delete a task', () => {
expect(result).toHaveLength(0);
});
});
describe('Feature: delete a label', () => {
test('delete a label', async () => {
let result = await window.cvat.tasks.get({
id: 100,
});
const labelsLength = result[0].labels.length;
const deletedLabels = result[0].labels.filter((el) => el.name !== 'person');
result[0].labels = deletedLabels;
result[0].save();
result = await window.cvat.tasks.get({
id: 100,
});
expect(result[0].labels).toHaveLength(labelsLength - 1);
});
});

@ -1,121 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock');
return mock;
});
const AnnotationsFilter = require('../../src/annotations-filter');
// Initialize api
window.cvat = require('../../src/api');
// Test cases
describe('Feature: toJSONQuery', () => {
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter.toJSONQuery([]);
expect(Array.isArray(groups)).toBeTruthy();
expect(typeof query).toBe('string');
});
test('convert empty fitlers to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [, query] = annotationsFilter.toJSONQuery([]);
expect(query).toBe('$.objects[*].clientID');
});
test('convert wrong fitlers (empty string) to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
expect(() => {
annotationsFilter.toJSONQuery(['']);
}).toThrow(window.cvat.exceptions.ArgumentError);
});
test('convert wrong fitlers (wrong number argument) to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
expect(() => {
annotationsFilter.toJSONQuery(1);
}).toThrow(window.cvat.exceptions.ArgumentError);
});
test('convert wrong fitlers (wrong array argument) to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
expect(() => {
annotationsFilter.toJSONQuery(['clientID ==6', 1]);
}).toThrow(window.cvat.exceptions.ArgumentError);
});
test('convert wrong filters (wrong expression) to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
expect(() => {
annotationsFilter.toJSONQuery(['clientID=5']);
}).toThrow(window.cvat.exceptions.ArgumentError);
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter.toJSONQuery(['clientID==5 & shape=="rectangle" & label==["car"]']);
expect(groups).toEqual([['clientID==5', '&', 'shape=="rectangle"', '&', 'label==["car"]']]);
expect(query).toBe('$.objects[?((@.clientID==5&@.shape=="rectangle"&@.label==["car"]))].clientID');
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter.toJSONQuery(['label=="car" | width >= height & type=="track"']);
expect(groups).toEqual([['label=="car"', '|', 'width >= height', '&', 'type=="track"']]);
expect(query).toBe('$.objects[?((@.label=="car"|@.width>=@.height&@.type=="track"))].clientID');
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter.toJSONQuery([
'label=="person" & attr["Attribute 1"] ==attr["Attribute 2"]',
]);
expect(groups).toEqual([['label=="person"', '&', 'attr["Attribute 1"] ==attr["Attribute 2"]']]);
expect(query).toBe('$.objects[?((@.label=="person"&@.attr["Attribute 1"]==@.attr["Attribute 2"]))].clientID');
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter.toJSONQuery([
'label=="car" & attr["parked"]==true',
'label=="pedestrian" & width > 150',
]);
expect(groups).toEqual([
['label=="car"', '&', 'attr["parked"]==true'],
'|',
['label=="pedestrian"', '&', 'width > 150'],
]);
expect(query).toBe(
'$.objects[?((@.label=="car"&@.attr["parked"]==true)|(@.label=="pedestrian"&@.width>150))].clientID',
);
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter.toJSONQuery([
// eslint-disable-next-line
'(( label==["car \\"mazda\\""]) & (attr["sunglass ( help ) es"]==true | (width > 150 | height > 150 & (clientID == serverID))))) ',
]);
expect(groups).toEqual([
[
[
['label==["car `mazda`"]'],
'&',
[
'attr["sunglass ( help ) es"]==true',
'|',
['width > 150', '|', 'height > 150', '&', ['clientID == serverID']],
],
],
],
]);
expect(query).toBe(
// eslint-disable-next-line
'$.objects[?((((@.label==["car `mazda`"])&(@.attr["sunglass ( help ) es"]==true|(@.width>150|@.height>150&(@.clientID==serverID))))))].clientID',
);
});
});

@ -189,6 +189,12 @@ const projectsDummyData = {
},
],
},
{
id: 2,
name: 'bicycle',
color: '#bb20c0',
attributes: [],
},
],
tasks: [
{

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -97,7 +97,11 @@ class ServerProxy {
Object.prototype.hasOwnProperty.call(projectData, prop)
&& Object.prototype.hasOwnProperty.call(object, prop)
) {
object[prop] = projectData[prop];
if (prop === 'labels') {
object[prop] = projectData[prop].filter((label) => !label.deleted);
} else {
object[prop] = projectData[prop];
}
}
}
}
@ -156,7 +160,11 @@ class ServerProxy {
Object.prototype.hasOwnProperty.call(taskData, prop)
&& Object.prototype.hasOwnProperty.call(object, prop)
) {
object[prop] = taskData[prop];
if (prop === 'labels') {
object[prop] = taskData[prop].filter((label) => !label.deleted);
} else {
object[prop] = taskData[prop];
}
}
}
}

File diff suppressed because it is too large Load Diff

@ -8,7 +8,7 @@
"@babel/core": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"babel-loader": "^8.0.6",
"copy-webpack-plugin": "^5.0.5",
"copy-webpack-plugin": "^7.0.0",
"eslint": "^6.4.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-plugin-import": "^2.18.2",
@ -16,13 +16,13 @@
"eslint-plugin-no-unsanitized": "^3.0.2",
"eslint-plugin-security": "^1.4.0",
"nodemon": "^1.19.2",
"webpack": "^4.39.3",
"webpack": "^5.20.2",
"webpack-cli": "^3.3.7",
"worker-loader": "^2.0.0"
},
"dependencies": {
"async-mutex": "^0.2.6",
"jszip": "3.5.0"
"async-mutex": "^0.3.1",
"jszip": "3.6.0"
},
"scripts": {
"patch": "cd src/js && patch --dry-run --forward -p0 < 3rdparty_patch.diff >> /dev/null && patch -p0 < 3rdparty_patch.diff; true",

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -13,8 +13,20 @@ const BlockType = Object.freeze({
ARCHIVE: 'archive',
});
const DimensionType = Object.freeze({
DIM_3D: '3d',
DIM_2D: '2d',
});
class FrameProvider {
constructor(blockType, blockSize, cachedBlockCount, decodedBlocksCacheSize = 5, maxWorkerThreadCount = 2) {
constructor(
blockType,
blockSize,
cachedBlockCount,
decodedBlocksCacheSize = 5,
maxWorkerThreadCount = 2,
dimension = DimensionType.DIM_2D,
) {
this._frames = {};
this._cachedBlockCount = Math.max(1, cachedBlockCount); // number of stored blocks
this._decodedBlocksCacheSize = decodedBlocksCacheSize;
@ -33,6 +45,7 @@ class FrameProvider {
this._mutex = new Mutex();
this._promisedFrames = {};
this._maxWorkerThreadCount = maxWorkerThreadCount;
this._dimension = dimension;
}
async _worker() {
@ -291,7 +304,7 @@ class FrameProvider {
};
worker.onmessage = async (event) => {
if (event.data.isRaw) {
if (this._dimension === DimensionType.DIM_2D && event.data.isRaw) {
// safary doesn't support createImageBitmap
// there is a way to polyfill it with using document.createElement
// but document.createElement doesn't work in worker
@ -328,8 +341,14 @@ class FrameProvider {
}
index++;
};
worker.postMessage({ block, start, end });
const dimension = this._dimension;
worker.postMessage({
block,
start,
end,
dimension,
dimension2D: DimensionType.DIM_2D,
});
this._decodeThreadCount++;
}
} finally {
@ -357,4 +376,5 @@ class FrameProvider {
module.exports = {
FrameProvider,
BlockType,
DimensionType,
};

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -7,7 +7,9 @@ const JSZip = require('jszip');
onmessage = (e) => {
const zip = new JSZip();
if (e.data) {
const { start, end, block } = e.data;
const {
start, end, block, dimension, dimension2D,
} = e.data;
zip.loadAsync(block).then((_zip) => {
let index = start;
@ -18,7 +20,7 @@ onmessage = (e) => {
.async('blob')
.then((fileData) => {
// eslint-disable-next-line no-restricted-globals
if (self.createImageBitmap) {
if (dimension === dimension2D && self.createImageBitmap) {
createImageBitmap(fileData).then((img) => {
postMessage({
fileName: relativePath,

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -10,6 +10,7 @@ module.exports = {
parser: '@typescript-eslint/parser',
ecmaVersion: 6,
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint', 'import'],
extends: [
@ -19,6 +20,7 @@ module.exports = {
'plugin:import/warnings',
'plugin:import/typescript',
],
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/indent': ['warn', 4],
'@typescript-eslint/lines-between-class-members': 0,
@ -53,11 +55,17 @@ module.exports = {
},
},
],
'import/order': [
'error',
{
'groups': ['builtin', 'external', 'internal'],
}
]
},
settings: {
'import/resolver': {
node: {
paths: ['src'],
paths: ['src', `${__dirname}/src`],
},
},
},

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.13.3",
"version": "1.18.1",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
@ -20,6 +20,7 @@
"@babel/preset-env": "^7.6.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.6.0",
"@types/mousetrap": "^1.6.5",
"@typescript-eslint/eslint-plugin": "^4.5.0",
"@typescript-eslint/parser": "^4.5.0",
"babel-loader": "^8.0.6",
@ -47,39 +48,43 @@
"worker-loader": "^2.0.0"
},
"dependencies": {
"@ant-design/icons": "^4.3.0",
"@types/lodash": "^4.14.165",
"@ant-design/icons": "^4.5.0",
"@types/lodash": "^4.14.168",
"@types/platform": "^1.3.3",
"@types/react": "^16.14.2",
"@types/react": "^16.14.5",
"@types/react-color": "^3.0.4",
"@types/react-dom": "^16.9.10",
"@types/react-redux": "^7.1.11",
"@types/react-router": "^5.0.5",
"@types/react-router-dom": "^5.1.6",
"@types/react-dom": "^16.9.11",
"@types/react-redux": "^7.1.16",
"@types/react-router": "^5.1.12",
"@types/react-router-dom": "^5.1.7",
"@types/react-share": "^3.0.3",
"@types/redux-logger": "^3.0.8",
"antd": "^4.9.1",
"@types/resize-observer-browser": "^0.1.5",
"antd": "^4.13.0",
"copy-to-clipboard": "^3.3.1",
"cvat-canvas": "file:../cvat-canvas",
"cvat-canvas3d": "file:../cvat-canvas3d",
"cvat-core": "file:../cvat-core",
"dotenv-webpack": "^1.8.0",
"error-stack-parser": "^2.0.6",
"lodash": "^4.17.20",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"mousetrap": "^1.6.5",
"platform": "^1.3.6",
"prop-types": "^15.7.2",
"rc-virtual-list": "^3.2.3",
"react": "^16.14.0",
"react-awesome-query-builder": "^3.0.0",
"react-color": "^2.19.3",
"react-cookie": "^4.0.3",
"react-dom": "^16.14.0",
"react-hotkeys": "^2.0.0",
"react-redux": "^7.2.2",
"react-resizable": "^1.11.1",
"@types/react-resizable": "^1.7.2",
"react-router": "^5.1.0",
"react-router-dom": "^5.1.0",
"react-share": "^3.0.1",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.8",
"redux-devtools-extension": "^2.13.9",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0"
}

@ -1,31 +1,31 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { MutableRefObject } from 'react';
import {
AnyAction, Dispatch, ActionCreator, Store,
ActionCreator, AnyAction, Dispatch, Store,
} from 'redux';
import { ThunkAction } from 'utils/redux';
import { RectDrawingMethod } from 'cvat-canvas-wrapper';
import getCore from 'cvat-core-wrapper';
import logger, { LogType } from 'cvat-logger';
import { getCVATStore } from 'cvat-store';
import {
CombinedState,
ActiveControl,
ShapeType,
ObjectType,
Task,
CombinedState,
ContextMenuType,
DimensionType,
FrameSpeed,
Model,
ObjectType,
OpenCVTool,
Rotation,
ContextMenuType,
ShapeType,
Task,
Workspace,
Model,
} from 'reducers/interfaces';
import getCore from 'cvat-core-wrapper';
import logger, { LogType } from 'cvat-logger';
import { RectDrawingMethod } from 'cvat-canvas-wrapper';
import { getCVATStore } from 'cvat-store';
import { MutableRefObject } from 'react';
interface AnnotationsParameters {
filters: string[];
frame: number;
@ -159,6 +159,7 @@ export enum AnnotationActionTypes {
PROPAGATE_OBJECT_FAILED = 'PROPAGATE_OBJECT_FAILED',
CHANGE_PROPAGATE_FRAMES = 'CHANGE_PROPAGATE_FRAMES',
SWITCH_SHOWING_STATISTICS = 'SWITCH_SHOWING_STATISTICS',
SWITCH_SHOWING_FILTERS = 'SWITCH_SHOWING_FILTERS',
COLLECT_STATISTICS = 'COLLECT_STATISTICS',
COLLECT_STATISTICS_SUCCESS = 'COLLECT_STATISTICS_SUCCESS',
COLLECT_STATISTICS_FAILED = 'COLLECT_STATISTICS_FAILED',
@ -189,6 +190,8 @@ export enum AnnotationActionTypes {
SWITCH_REQUEST_REVIEW_DIALOG = 'SWITCH_REQUEST_REVIEW_DIALOG',
SWITCH_SUBMIT_REVIEW_DIALOG = 'SWITCH_SUBMIT_REVIEW_DIALOG',
SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG = 'SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG',
HIDE_SHOW_CONTEXT_IMAGE = 'HIDE_SHOW_CONTEXT_IMAGE',
GET_CONTEXT_IMAGE = 'GET_CONTEXT_IMAGE',
}
export function saveLogsAsync(): ThunkAction {
@ -272,24 +275,10 @@ export function fetchAnnotationsAsync(): ThunkAction {
};
}
export function changeAnnotationsFilters(filters: string[]): AnyAction {
const state: CombinedState = getStore().getState();
const { filtersHistory, filters: oldFilters } = state.annotation.annotations;
filters.forEach((element: string) => {
if (!(filtersHistory.includes(element) || oldFilters.includes(element))) {
filtersHistory.push(element);
}
});
window.localStorage.setItem('filtersHistory', JSON.stringify(filtersHistory.slice(-10)));
export function changeAnnotationsFilters(filters: any[]): AnyAction {
return {
type: AnnotationActionTypes.CHANGE_ANNOTATIONS_FILTERS,
payload: {
filters,
filtersHistory: filtersHistory.slice(-10),
},
payload: { filters },
};
}
@ -439,6 +428,14 @@ export function showStatistics(visible: boolean): AnyAction {
},
};
}
export function showFilters(visible: boolean): AnyAction {
return {
type: AnnotationActionTypes.SWITCH_SHOWING_FILTERS,
payload: {
visible,
},
};
}
export function propagateObjectAsync(sessionInstance: any, objectState: any, from: number, to: number): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
@ -872,7 +869,7 @@ export function closeJob(): ThunkAction {
};
}
export function getJobAsync(tid: number, jid: number, initialFrame: number, initialFilters: string[]): ThunkAction {
export function getJobAsync(tid: number, jid: number, initialFrame: number, initialFilters: object[]): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const state: CombinedState = getStore().getState();
@ -915,10 +912,6 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
throw new Error(`Task ${tid} doesn't contain the job ${jid}`);
}
if (!task.labels.length && task.projectId) {
throw new Error(`Project ${task.projectId} does not contain any label`);
}
const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame);
const frameData = await job.frames.get(frameNumber);
// call first getting of frame data before rendering interface
@ -957,6 +950,10 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
maxZ,
},
});
if (job.task.dimension === DimensionType.DIM_3D) {
const workspace = Workspace.STANDARD3D;
dispatch(changeWorkspace(workspace));
}
dispatch(changeFrameAsync(frameNumber, false));
} catch (error) {
dispatch({
@ -1354,7 +1351,7 @@ export function pasteShapeAsync(): ThunkAction {
};
}
export function interactWithCanvas(activeInteractor: Model, activeLabelID: number): AnyAction {
export function interactWithCanvas(activeInteractor: Model | OpenCVTool, activeLabelID: number): AnyAction {
return {
type: AnnotationActionTypes.INTERACT_WITH_CANVAS,
payload: {
@ -1518,3 +1515,46 @@ export function setForceExitAnnotationFlag(forceExit: boolean): AnyAction {
},
};
}
export function hideShowContextImage(hidden: boolean): AnyAction {
return {
type: AnnotationActionTypes.HIDE_SHOW_CONTEXT_IMAGE,
payload: {
hidden,
},
};
}
export function getContextImage(): 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;
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,
},
});
} catch (error) {
const context = '';
const loaded = true;
const contextImageHide = contextImage.hidden;
dispatch({
type: AnnotationActionTypes.GET_CONTEXT_IMAGE,
payload: {
context,
loaded,
contextImageHide,
},
});
}
};
}

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT

@ -1,9 +1,9 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { AnyAction } from 'redux';
import { GridColor, ColorBy } from 'reducers/interfaces';
import { GridColor, ColorBy, SettingsState } from 'reducers/interfaces';
export enum SettingsActionTypes {
SWITCH_ROTATE_ALL = 'SWITCH_ROTATE_ALL',
@ -27,10 +27,12 @@ export enum SettingsActionTypes {
CHANGE_AUTO_SAVE_INTERVAL = 'CHANGE_AUTO_SAVE_INTERVAL',
CHANGE_AAM_ZOOM_MARGIN = 'CHANGE_AAM_ZOOM_MARGIN',
SWITCH_AUTOMATIC_BORDERING = 'SWITCH_AUTOMATIC_BORDERING',
SWITCH_INTELLIGENT_POLYGON_CROP = 'SWITCH_INTELLIGENT_POLYGON_CROP',
SWITCH_SHOWNIG_INTERPOLATED_TRACKS = 'SWITCH_SHOWNIG_INTERPOLATED_TRACKS',
SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS = 'SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS',
CHANGE_CANVAS_BACKGROUND_COLOR = 'CHANGE_CANVAS_BACKGROUND_COLOR',
SWITCH_SETTINGS_DIALOG = 'SWITCH_SETTINGS_DIALOG',
SET_SETTINGS = 'SET_SETTINGS',
}
export function changeShapesOpacity(opacity: number): AnyAction {
@ -241,6 +243,15 @@ export function switchAutomaticBordering(automaticBordering: boolean): AnyAction
};
}
export function switchIntelligentPolygonCrop(intelligentPolygonCrop: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_INTELLIGENT_POLYGON_CROP,
payload: {
intelligentPolygonCrop,
},
};
}
export function changeCanvasBackgroundColor(color: string): AnyAction {
return {
type: SettingsActionTypes.CHANGE_CANVAS_BACKGROUND_COLOR,
@ -258,3 +269,12 @@ export function switchSettingsDialog(show?: boolean): AnyAction {
},
};
}
export function setSettings(settings: Partial<SettingsState>): AnyAction {
return {
type: SettingsActionTypes.SET_SETTINGS,
payload: {
settings,
},
};
}

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -397,6 +397,9 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
if (data.advanced.copyData) {
description.copy_data = data.advanced.copyData;
}
if (data.subset) {
description.subset = data.subset;
}
const taskInstance = new cvat.classes.Task(description);
taskInstance.clientFiles = data.files.local;

@ -0,0 +1,10 @@
<!--
The file has been downloaded from: https://icon-icons.com/ru/%D0%B7%D0%BD%D0%B0%D1%87%D0%BE%D0%BA/%D0%92-%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82%D0%B5-OpenCV/132129
License: Attribution 4.0 International (CC BY 4.0) https://creativecommons.org/licenses/by/4.0/
The file has been modified
-->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="40" height="40">
<g style="transform: scale(0.078)">
<path d="M148.6458282,81.0641403C191.8570709-0.3458452,307.612915-4.617764,356.5062561,73.3931732c37.8880615,60.4514771,13.7960815,135.4847717-41.8233948,167.7876129l-36.121521-62.5643005c22.1270447-12.8510284,31.7114563-42.7013397,16.6385498-66.750618c-19.4511414-31.034935-65.5021057-29.3354645-82.692749,3.0517044c-12.7206879,23.9658356-2.6391449,51.5502472,18.3088379,63.7294922l-36.1482544,62.6105804C142.0118256,210.643219,116.6704254,141.3057709,148.6458282,81.0641403z M167.9667206,374.4708557c-0.0435791,24.2778625-18.934967,46.8978271-46.092804,47.9000549c-36.6418304,1.3522339-61.0877724-37.6520386-43.8971252-70.0392151c13.2918015-25.0418091,43.8297424-31.7192383,65.9928284-19.1222839l36.2165222-62.7288513c-55.7241974-31.7991638-132.6246796-15.0146027-166.0706635,47.9976501c-43.2111893,81.4099731,18.2372913,179.4530945,110.3418884,176.0540161c68.1375427-2.5146179,115.5750122-59.1652527,115.8612366-120.0613708H167.9667206z M451.714386,270.7571411l-36.1215515,62.5642395c22.2027588,12.816864,31.8418274,42.7249451,16.744751,66.8127441c-19.4511414,31.0349426-65.5021057,29.3354797-82.692688-3.0516968c-12.742218-24.0063782-2.6048279-51.643219,18.4154358-63.7908325l-36.1482544-62.6105652c-52.7280579,30.5827942-78.1254272,99.9726562-46.128479,160.2548218c43.2111816,81.4099731,158.9670105,85.6818848,207.8603821,7.6710205C531.5561523,378.1168213,507.4096069,303.0259705,451.714386,270.7571411z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -11,6 +11,7 @@ import { MenuInfo } from 'rc-menu/lib/interface';
import DumpSubmenu from './dump-submenu';
import LoadSubmenu from './load-submenu';
import ExportSubmenu from './export-submenu';
import { DimensionType } from '../../reducers/interfaces';
interface Props {
taskID: number;
@ -22,7 +23,7 @@ interface Props {
dumpActivities: string[] | null;
exportActivities: string[] | null;
inferenceIsActive: boolean;
taskDimension: DimensionType;
onClickMenu: (params: MenuInfo, file?: File) => void;
}
@ -47,6 +48,7 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
dumpActivities,
exportActivities,
loadActivity,
taskDimension,
} = props;
let latestParams: MenuInfo | null = null;
@ -64,6 +66,7 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
Modal.confirm({
title: 'Current annotation will be lost',
content: 'You are going to upload new annotations to this task. Continue?',
className: 'cvat-modal-content-load-task-annotation',
onOk: () => {
onClickMenu(copyParams, file);
},
@ -81,6 +84,7 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
Modal.confirm({
title: `The task ${taskID} will be deleted`,
content: 'All related data (images, annotations) will be lost. Continue?',
className: 'cvat-modal-confirm-delete-task',
onOk: () => {
onClickMenu(copyParams);
},
@ -102,6 +106,7 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
dumpers,
dumpActivities,
menuKey: Actions.DUMP_TASK_ANNO,
taskDimension,
})}
{LoadSubmenu({
loaders,
@ -110,11 +115,13 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
onClickMenuWrapper(null, file);
},
menuKey: Actions.LOAD_TASK_ANNO,
taskDimension,
})}
{ExportSubmenu({
exporters: dumpers,
exportActivities,
menuKey: Actions.EXPORT_TASK_DATASET,
taskDimension,
})}
{!!bugTracker && <Menu.Item key={Actions.OPEN_BUG_TRACKER}>Open bug tracker</Menu.Item>}
<Menu.Item disabled={inferenceIsActive} key={Actions.RUN_AUTO_ANNOTATION}>

@ -6,6 +6,7 @@ import React from 'react';
import Menu from 'antd/lib/menu';
import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons';
import Text from 'antd/lib/typography/Text';
import { DimensionType } from '../../reducers/interfaces';
function isDefaultFormat(dumperName: string, taskMode: string): boolean {
return (
@ -19,15 +20,19 @@ interface Props {
menuKey: string;
dumpers: any[];
dumpActivities: string[] | null;
taskDimension: DimensionType;
}
export default function DumpSubmenu(props: Props): JSX.Element {
const { taskMode, menuKey, dumpers, dumpActivities } = props;
const {
taskMode, menuKey, dumpers, dumpActivities, taskDimension,
} = props;
return (
<Menu.SubMenu key={menuKey} title='Dump annotations'>
{dumpers
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.filter((dumper: any): boolean => dumper.dimension === taskDimension)
.map(
(dumper: any): JSX.Element => {
const pending = (dumpActivities || []).includes(dumper.name);

@ -6,20 +6,25 @@ import React from 'react';
import Menu from 'antd/lib/menu';
import Text from 'antd/lib/typography/Text';
import { ExportOutlined, LoadingOutlined } from '@ant-design/icons';
import { DimensionType } from '../../reducers/interfaces';
interface Props {
menuKey: string;
exporters: any[];
exportActivities: string[] | null;
taskDimension: DimensionType;
}
export default function ExportSubmenu(props: Props): JSX.Element {
const { menuKey, exporters, exportActivities } = props;
const {
menuKey, exporters, exportActivities, taskDimension,
} = props;
return (
<Menu.SubMenu key={menuKey} title='Export as a dataset'>
{exporters
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.filter((exporter: any): boolean => exporter.dimension === taskDimension)
.map(
(exporter: any): JSX.Element => {
const pending = (exportActivities || []).includes(exporter.name);

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -8,23 +8,26 @@ import Upload from 'antd/lib/upload';
import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text';
import { UploadOutlined, LoadingOutlined } from '@ant-design/icons';
import { DimensionType } from '../../reducers/interfaces';
interface Props {
menuKey: string;
loaders: any[];
loadActivity: string | null;
onFileUpload(file: File): void;
taskDimension: DimensionType;
}
export default function LoadSubmenu(props: Props): JSX.Element {
const {
menuKey, loaders, loadActivity, onFileUpload,
menuKey, loaders, loadActivity, onFileUpload, taskDimension,
} = props;
return (
<Menu.SubMenu key={menuKey} title='Upload annotations'>
{loaders
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.filter((loader: any): boolean => loader.dimension === taskDimension)
.map(
(loader: any): JSX.Element => {
const accept = loader.format
@ -44,7 +47,7 @@ export default function LoadSubmenu(props: Props): JSX.Element {
return false;
}}
>
<Button block type='link' disabled={disabled}>
<Button block type='link' disabled={disabled} className='cvat-menu-load-submenu-item-button'>
<UploadOutlined />
<Text>{loader.name}</Text>
{pending && <LoadingOutlined style={{ marginLeft: 10 }} />}

@ -1,23 +1,27 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useEffect } from 'react';
import { useHistory } from 'react-router';
import Layout from 'antd/lib/layout';
import Spin from 'antd/lib/spin';
import Result from 'antd/lib/result';
import Spin from 'antd/lib/spin';
import notification from 'antd/lib/notification';
import { Workspace } from 'reducers/interfaces';
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal';
import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace';
import AttributeAnnotationWorkspace from 'components/annotation-page/attribute-annotation-workspace/attribute-annotation-workspace';
import TagAnnotationWorkspace from 'components/annotation-page/tag-annotation-workspace/tag-annotation-workspace';
import ReviewAnnotationsWorkspace from 'components/annotation-page/review-workspace/review-workspace';
import SubmitAnnotationsModal from 'components/annotation-page/request-review-modal';
import ReviewAnnotationsWorkspace from 'components/annotation-page/review-workspace/review-workspace';
import SubmitReviewModal from 'components/annotation-page/review/submit-review-modal';
import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace';
import StandardWorkspace3DComponent from 'components/annotation-page/standard3D-workspace/standard3D-workspace';
import TagAnnotationWorkspace from 'components/annotation-page/tag-annotation-workspace/tag-annotation-workspace';
import FiltersModalContainer from 'containers/annotation-page/top-bar/filters-modal';
import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal';
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import { Workspace } from 'reducers/interfaces';
import { usePrevious } from 'utils/hooks';
import './styles.scss';
interface Props {
job: any | null | undefined;
@ -32,13 +36,15 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
const {
job, fetching, getJob, closeJob, saveLogs, workspace,
} = props;
const prevJob = usePrevious(job);
const prevFetching = usePrevious(fetching);
const history = useHistory();
useEffect(() => {
saveLogs();
const root = window.document.getElementById('root');
if (root) {
root.style.minHeight = '768px';
root.style.minHeight = '600px';
}
return () => {
@ -59,6 +65,27 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
}
}, [job, fetching]);
useEffect(() => {
if (prevFetching && !fetching && !prevJob && job && !job.task.labels.length) {
notification.warning({
message: 'No labels',
description: (
<span>
{`${job.task.projectId ? 'Project' : 'Task'} ${
job.task.projectId || job.task.id
} does not contain any label. `}
<a href={`/${job.task.projectId ? 'projects' : 'tasks'}/${job.task.projectId || job.task.id}/`}>
Add
</a>
{' the first one for editing annotation.'}
</span>
),
placement: 'topRight',
className: 'cvat-notification-no-labels',
});
}
}, [job, fetching, prevJob, prevFetching]);
if (job === null) {
return <Spin size='large' className='cvat-spinner' />;
}
@ -79,26 +106,32 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
<Layout.Header className='cvat-annotation-header'>
<AnnotationTopBarContainer />
</Layout.Header>
{workspace === Workspace.STANDARD3D && (
<Layout.Content className='cvat-annotation-layout-content'>
<StandardWorkspace3DComponent />
</Layout.Content>
)}
{workspace === Workspace.STANDARD && (
<Layout.Content style={{ height: '100%' }}>
<Layout.Content className='cvat-annotation-layout-content'>
<StandardWorkspaceComponent />
</Layout.Content>
)}
{workspace === Workspace.ATTRIBUTE_ANNOTATION && (
<Layout.Content style={{ height: '100%' }}>
<Layout.Content className='cvat-annotation-layout-content'>
<AttributeAnnotationWorkspace />
</Layout.Content>
)}
{workspace === Workspace.TAG_ANNOTATION && (
<Layout.Content style={{ height: '100%' }}>
<Layout.Content className='cvat-annotation-layout-content'>
<TagAnnotationWorkspace />
</Layout.Content>
)}
{workspace === Workspace.REVIEW_WORKSPACE && (
<Layout.Content style={{ height: '100%' }}>
<Layout.Content className='cvat-annotation-layout-content'>
<ReviewAnnotationsWorkspace />
</Layout.Content>
)}
<FiltersModalContainer visible={false} />
<StatisticsModalContainer />
<SubmitAnnotationsModal />
<SubmitReviewModal />

@ -1,170 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState } from 'react';
import { connect } from 'react-redux';
import Select, { SelectValue, LabeledValue } from 'antd/lib/select';
import Title from 'antd/lib/typography/Title';
import Text from 'antd/lib/typography/Text';
import Paragraph from 'antd/lib/typography/Paragraph';
import Tooltip from 'antd/lib/tooltip';
import Modal from 'antd/lib/modal';
import { FilterOutlined } from '@ant-design/icons';
import {
changeAnnotationsFilters as changeAnnotationsFiltersAction,
fetchAnnotationsAsync,
} from 'actions/annotation-actions';
import { CombinedState } from 'reducers/interfaces';
interface StateToProps {
annotationsFilters: string[];
annotationsFiltersHistory: string[];
searchForwardShortcut: string;
searchBackwardShortcut: string;
}
interface DispatchToProps {
changeAnnotationsFilters(value: SelectValue): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
annotations: { filters: annotationsFilters, filtersHistory: annotationsFiltersHistory },
},
shortcuts: { normalizedKeyMap },
} = state;
return {
annotationsFilters,
annotationsFiltersHistory,
searchForwardShortcut: normalizedKeyMap.SEARCH_FORWARD,
searchBackwardShortcut: normalizedKeyMap.SEARCH_BACKWARD,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
changeAnnotationsFilters(value: SelectValue) {
if (typeof value === 'string') {
dispatch(changeAnnotationsFiltersAction([value]));
dispatch(fetchAnnotationsAsync());
} else if (
Array.isArray(value) &&
value.every((element: string | number | LabeledValue): boolean => typeof element === 'string')
) {
dispatch(changeAnnotationsFiltersAction(value as string[]));
dispatch(fetchAnnotationsAsync());
}
},
};
}
function filtersHelpModalContent(searchForwardShortcut: string, searchBackwardShortcut: string): JSX.Element {
return (
<>
<Paragraph>
<Title level={3}>General</Title>
</Paragraph>
<Paragraph>
You can use filters to display only subset of objects on a frame or to search objects that satisfy the
filters using hotkeys
<Text strong>{` ${searchForwardShortcut} `}</Text>
and
<Text strong>{` ${searchBackwardShortcut} `}</Text>
</Paragraph>
<Paragraph>
<Text strong>Supported properties: </Text>
width, height, label, serverID, clientID, type, shape, occluded
<br />
<Text strong>Supported operators: </Text>
==, !=, &gt;, &gt;=, &lt;, &lt;=, (), &amp; and |
<br />
<Text strong>
If you have double quotes in your query string, please escape them using back slash: \&quot; (see
the latest example)
</Text>
<br />
All properties and values are case-sensitive. CVAT uses json queries to perform search.
</Paragraph>
<Paragraph>
<Title level={3}>Examples</Title>
<ul>
<li>label==&quot;car&quot; | label==[&quot;road sign&quot;]</li>
<li>shape == &quot;polygon&quot;</li>
<li>width &gt;= height</li>
<li>attr[&quot;Attribute 1&quot;] == attr[&quot;Attribute 2&quot;]</li>
<li>clientID == 50</li>
<li>
(label==&quot;car&quot; &amp; attr[&quot;parked&quot;]==true) | (label==&quot;pedestrian&quot;
&amp; width &gt; 150)
</li>
<li>
(( label==[&quot;car \&quot;mazda\&quot;&quot;]) &amp; (attr[&quot;sunglasses&quot;]==true |
(width &gt; 150 | height &gt; 150 &amp; (clientID == serverID)))))
</li>
</ul>
</Paragraph>
</>
);
}
function AnnotationsFiltersInput(props: StateToProps & DispatchToProps): JSX.Element {
const {
annotationsFilters,
annotationsFiltersHistory,
searchForwardShortcut,
searchBackwardShortcut,
changeAnnotationsFilters,
} = props;
const [underCursor, setUnderCursor] = useState(false);
return (
<Select
className='cvat-annotations-filters-input'
allowClear
value={annotationsFilters}
mode='tags'
style={{ width: '100%' }}
placeholder={
underCursor ? (
<>
<Tooltip title='Click to open help' mouseLeaveDelay={0}>
<FilterOutlined
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
Modal.info({
width: 700,
title: 'How to use filters?',
content: filtersHelpModalContent(searchForwardShortcut, searchBackwardShortcut),
});
}}
/>
</Tooltip>
</>
) : (
<>
<FilterOutlined style={{ transform: 'scale(0.9)' }} />
<span style={{ marginLeft: 5 }}>Annotations filters</span>
</>
)
}
onChange={changeAnnotationsFilters}
onMouseEnter={() => setUnderCursor(true)}
onMouseLeave={() => setUnderCursor(false)}
>
{annotationsFiltersHistory.map(
(element: string): JSX.Element => (
<Select.Option key={element} value={element} className='cvat-annotations-filters-input-history-element'>
{element}
</Select.Option>
),
)}
</Select>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(AnnotationsFiltersInput);

@ -1,33 +1,30 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState, useEffect } from 'react';
import { GlobalHotKeys, ExtendedKeyMapOptions } from 'react-hotkeys';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import Layout, { SiderProps } from 'antd/lib/layout';
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
import { SelectValue } from 'antd/lib/select';
import { Row, Col } from 'antd/lib/grid';
import Layout, { SiderProps } from 'antd/lib/layout';
import Text from 'antd/lib/typography/Text';
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
import { ThunkDispatch } from 'utils/redux';
import { Canvas } from 'cvat-canvas-wrapper';
import { LogType } from 'cvat-logger';
import {
activateObject as activateObjectAction,
updateAnnotationsAsync,
changeFrameAsync,
updateAnnotationsAsync,
} from 'actions/annotation-actions';
import { CombinedState, ObjectType } from 'reducers/interfaces';
import AnnotationsFiltersInput from 'components/annotation-page/annotations-filters-input';
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 ObjectSwitcher from './object-switcher';
import { CombinedState, ObjectType } from 'reducers/interfaces';
import AttributeEditor from './attribute-editor';
import AttributeSwitcher from './attribute-switcher';
import ObjectBasicsEditor from './object-basics-edtior';
import AttributeEditor from './attribute-editor';
import ObjectSwitcher from './object-switcher';
interface StateToProps {
activatedStateID: number | null;
@ -35,7 +32,7 @@ interface StateToProps {
states: any[];
labels: any[];
jobInstance: any;
keyMap: Record<string, ExtendedKeyMapOptions>;
keyMap: KeyMap;
normalizedKeyMap: Record<string, string>;
canvasInstance: Canvas;
canvasIsReady: boolean;
@ -295,12 +292,8 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
>
{sidebarCollapsed ? <MenuFoldOutlined title='Show' /> : <MenuUnfoldOutlined title='Hide' />}
</span>
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} allowChanges />
<Row>
<Col span={24}>
<AnnotationsFiltersInput />
</Col>
</Row>
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} />
<div className='cvat-sidebar-collapse-button-spacer' />
<ObjectSwitcher
currentLabel={activeObjectState.label.name}
clientID={activeObjectState.clientID}
@ -375,11 +368,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
>
{sidebarCollapsed ? <MenuFoldOutlined title='Show' /> : <MenuUnfoldOutlined title='Hide' />}
</span>
<Row>
<Col span={24}>
<AnnotationsFiltersInput />
</Col>
</Row>
<div className='cvat-sidebar-collapse-button-spacer' />
<div className='attribute-annotations-sidebar-not-found-wrapper'>
<Text strong>No objects found</Text>
</div>

@ -1,9 +1,9 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import Text from 'antd/lib/typography/Text';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
import Select, { SelectValue } from 'antd/lib/select';
@ -150,7 +150,7 @@ function renderList(parameters: ListParameters): JSX.Element | null {
keyMap[key] = {
name: `Set value "${value}"`,
description: `Change current value for the attribute to "${value}"`,
sequence: `${index}`,
sequences: [`${index}`],
action: 'keydown',
};
@ -165,7 +165,7 @@ function renderList(parameters: ListParameters): JSX.Element | null {
return (
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers} allowChanges />
<GlobalHotKeys keyMap={keyMap} handlers={handlers} />
<div>
<Text strong>0:</Text>
<Text>{` ${sortedValues[0]}`}</Text>
@ -190,7 +190,7 @@ function renderList(parameters: ListParameters): JSX.Element | null {
keyMap[key] = {
name: `Set value "${value}"`,
description: `Change current value for the attribute to "${value}"`,
sequence: `${index}`,
sequences: [`${index}`],
action: 'keydown',
};
@ -205,7 +205,7 @@ function renderList(parameters: ListParameters): JSX.Element | null {
return (
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers} allowChanges />
<GlobalHotKeys keyMap={keyMap} handlers={handlers} />
{filteredValues.map(
(value: string, index: number): JSX.Element => (
<div key={value}>

@ -1,13 +1,14 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Text from 'antd/lib/typography/Text';
import Tooltip from 'antd/lib/tooltip';
import Button from 'antd/lib/button';
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import CVATTooltip from 'components/common/cvat-tooltip';
interface Props {
currentAttribute: string;
currentIndex: number;
@ -24,20 +25,28 @@ function AttributeSwitcher(props: Props): JSX.Element {
const title = `${currentAttribute} [${currentIndex + 1}/${attributesCount}]`;
return (
<div className='cvat-attribute-annotation-sidebar-attribute-switcher'>
<Tooltip title={`Previous attribute ${normalizedKeyMap.PREVIOUS_ATTRIBUTE}`} mouseLeaveDelay={0}>
<Button className='cvat-attribute-annotation-sidebar-attribute-switcher-left' disabled={attributesCount <= 1} onClick={() => nextAttribute(-1)}>
<CVATTooltip title={`Previous attribute ${normalizedKeyMap.PREVIOUS_ATTRIBUTE}`}>
<Button
className='cvat-attribute-annotation-sidebar-attribute-switcher-left'
disabled={attributesCount <= 1}
onClick={() => nextAttribute(-1)}
>
<LeftOutlined />
</Button>
</Tooltip>
<Tooltip title={title} mouseLeaveDelay={0}>
</CVATTooltip>
<CVATTooltip title={title}>
<Text className='cvat-text'>{currentAttribute}</Text>
<Text strong>{` [${currentIndex + 1}/${attributesCount}]`}</Text>
</Tooltip>
<Tooltip title={`Next attribute ${normalizedKeyMap.NEXT_ATTRIBUTE}`} mouseLeaveDelay={0}>
<Button className='cvat-attribute-annotation-sidebar-attribute-switcher-right' disabled={attributesCount <= 1} onClick={() => nextAttribute(1)}>
</CVATTooltip>
<CVATTooltip title={`Next attribute ${normalizedKeyMap.NEXT_ATTRIBUTE}`}>
<Button
className='cvat-attribute-annotation-sidebar-attribute-switcher-right'
disabled={attributesCount <= 1}
onClick={() => nextAttribute(1)}
>
<RightOutlined />
</Button>
</Tooltip>
</CVATTooltip>
</div>
);
}

@ -1,13 +1,14 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Text from 'antd/lib/typography/Text';
import Tooltip from 'antd/lib/tooltip';
import Button from 'antd/lib/button';
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import CVATTooltip from 'components/common/cvat-tooltip';
interface Props {
currentLabel: string;
clientID: number;
@ -26,21 +27,29 @@ function ObjectSwitcher(props: Props): JSX.Element {
const title = `${currentLabel} ${clientID} [${currentIndex + 1}/${objectsCount}]`;
return (
<div className='cvat-attribute-annotation-sidebar-object-switcher'>
<Tooltip title={`Previous object ${normalizedKeyMap.PREVIOUS_OBJECT}`} mouseLeaveDelay={0}>
<Button className='cvat-attribute-annotation-sidebar-object-switcher-left' disabled={objectsCount <= 1} onClick={() => nextObject(-1)}>
<CVATTooltip title={`Previous object ${normalizedKeyMap.PREVIOUS_OBJECT}`}>
<Button
className='cvat-attribute-annotation-sidebar-object-switcher-left'
disabled={objectsCount <= 1}
onClick={() => nextObject(-1)}
>
<LeftOutlined />
</Button>
</Tooltip>
<Tooltip title={title} mouseLeaveDelay={0}>
</CVATTooltip>
<CVATTooltip title={title}>
<Text className='cvat-text'>{currentLabel}</Text>
<Text className='cvat-text'>{` ${clientID} `}</Text>
<Text strong>{`[${currentIndex + 1}/${objectsCount}]`}</Text>
</Tooltip>
<Tooltip title={`Next object ${normalizedKeyMap.NEXT_OBJECT}`} mouseLeaveDelay={0}>
<Button className='cvat-attribute-annotation-sidebar-object-switcher-right' disabled={objectsCount <= 1} onClick={() => nextObject(1)}>
</CVATTooltip>
<CVATTooltip title={`Next object ${normalizedKeyMap.NEXT_OBJECT}`}>
<Button
className='cvat-attribute-annotation-sidebar-object-switcher-right'
disabled={objectsCount <= 1}
onClick={() => nextObject(1)}
>
<RightOutlined />
</Button>
</Tooltip>
</CVATTooltip>
</div>
);
}

@ -63,3 +63,7 @@
align-items: center;
justify-content: space-around;
}
.cvat-sidebar-collapse-button-spacer {
height: 32px;
}

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

Loading…
Cancel
Save