Merge branch 'develop.cvat.ai' into develop

main
Nikita Manovich 4 years ago
commit e1c90477e7

@ -4,7 +4,8 @@ branch = true
source =
cvat/apps/
utils/cli/
cvat-sdk/
cvat-cli/
utils/dataset_manifest
omit =

@ -7,27 +7,27 @@ module.exports = {
env: {
node: true,
browser: true,
es6: true,
es2020: true,
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
parser: '@typescript-eslint/parser',
},
ignorePatterns: [
'.eslintrc.js',
'lint-staged.config.js',
],
plugins: ['security', 'no-unsanitized', 'eslint-plugin-header', 'import'],
plugins: ['@typescript-eslint', 'security', 'no-unsanitized', 'eslint-plugin-header', 'import'],
extends: [
'eslint:recommended', 'plugin:security/recommended', 'plugin:no-unsanitized/DOM',
'airbnb-base', 'plugin:import/errors', 'plugin:import/warnings',
'plugin:import/typescript',
'plugin:import/typescript', 'plugin:@typescript-eslint/recommended', 'airbnb-typescript/base',
],
rules: {
'header/header': [2, 'line', [{
pattern: ' {1}Copyright \\(C\\) (?:20\\d{2}-)?2022 Intel Corporation',
template: ' Copyright (C) 2022 Intel Corporation'
}, '', ' SPDX-License-Identifier: MIT']],
// 'header/header': [2, 'line', [{
// pattern: ' {1}Copyright \\(C\\) (?:20\\d{2}-)?2022 Intel Corporation',
// template: ' Copyright (C) 2022 Intel Corporation'
// }, '', ' SPDX-License-Identifier: MIT']],
'no-plusplus': 0,
'no-continue': 0,
'no-console': 0,
@ -51,5 +51,22 @@ module.exports = {
'security/detect-object-injection': 0, // the rule is relevant for user input data on the node.js environment
'import/order': ['error', {'groups': ['builtin', 'external', 'internal']}],
'import/prefer-default-export': 0, // works incorrect with interfaces
'@typescript-eslint/ban-ts-comment': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/indent': ['error', 4],
'@typescript-eslint/lines-between-class-members': 0,
'@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }],
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/ban-types': [
'error',
{
types: {
'{}': false, // TODO: try to fix with Record<string, unknown>
object: false, // TODO: try to fix with Record<string, unknown>
Function: false, // TODO: try to fix somehow
},
},
],
},
};

@ -1,11 +1,5 @@
<!---
Copyright (C) 2020-2021 Intel Corporation
SPDX-License-Identifier: MIT
-->
### My actions before raising this issue
- [ ] Read/searched [the docs](https://github.com/opencv/cvat/tree/master#documentation)
- [ ] Read/searched [the docs](https://github.com/cvat-ai/cvat/tree/master#documentation)
- [ ] Searched [past issues](/issues)
<!--- Provide a general summary of the issue in the Title above -->
@ -49,5 +43,3 @@ the bug in -->
<summary>Logs from `cvat` container</summary>
</details>
### Next steps
You may [join our Gitter](https://gitter.im/opencv-cvat/public) channel for community support.

@ -1,13 +1,7 @@
<!---
Copyright (C) 2020-2022 Intel Corporation
SPDX-License-Identifier: MIT
-->
<!-- Raised an issue to propose your change (https://github.com/opencv/cvat/issues).
<!-- Raised an issue to propose your change (https://github.com/cvat-ai/cvat/issues).
It helps to avoid duplication of efforts from multiple independent contributors.
Discuss your ideas with maintainers to be sure that changes will be approved and merged.
Read the [CONTRIBUTION](https://github.com/opencv/cvat/blob/develop/CONTRIBUTING.md)
Read the [CONTRIBUTION](https://github.com/cvat-ai/cvat/blob/develop/CONTRIBUTING.md)
guide. -->
<!-- Provide a general summary of your changes in the Title above -->
@ -28,24 +22,18 @@ If an item isn't applicable by a reason then ~~explicitly strikethrough~~ the wh
line. If you don't do that github will show an incorrect process for the pull request.
If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] I submit my changes into the `develop` branch
- [ ] I have added a description of my changes into [CHANGELOG](https://github.com/opencv/cvat/blob/develop/CHANGELOG.md) file
- [ ] I have added a description of my changes into [CHANGELOG](https://github.com/cvat-ai/cvat/blob/develop/CHANGELOG.md) file
- [ ] I have updated the [documentation](
https://github.com/opencv/cvat/blob/develop/README.md#documentation) accordingly
https://github.com/cvat-ai/cvat/blob/develop/README.md#documentation) accordingly
- [ ] I have added tests to cover my changes
- [ ] I have linked related issues ([read github docs](
https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword))
- [ ] I have increased versions of npm packages if it is necessary ([cvat-canvas](https://github.com/opencv/cvat/tree/develop/cvat-canvas#versioning),
[cvat-core](https://github.com/opencv/cvat/tree/develop/cvat-core#versioning), [cvat-data](https://github.com/opencv/cvat/tree/develop/cvat-data#versioning) and [cvat-ui](https://github.com/opencv/cvat/tree/develop/cvat-ui#versioning))
- [ ] I have increased versions of npm packages if it is necessary ([cvat-canvas](https://github.com/cvat-ai/cvat/tree/develop/cvat-canvas#versioning),
[cvat-core](https://github.com/cvat-ai/cvat/tree/develop/cvat-core#versioning), [cvat-data](https://github.com/cvat-ai/cvat/tree/develop/cvat-data#versioning) and [cvat-ui](https://github.com/cvat-ai/cvat/tree/develop/cvat-ui#versioning))
### License
- [ ] I submit _my code changes_ under the same [MIT License](
https://github.com/opencv/cvat/blob/develop/LICENSE) that covers the project.
https://github.com/cvat-ai/cvat/blob/develop/LICENSE) that covers the project.
Feel free to contact the maintainers if that's a concern.
- [ ] I have updated the license header for each file (see an example below)
```python
# Copyright (C) 2022 Intel Corporation
#
# SPDX-License-Identifier: MIT
```

@ -1,7 +1,7 @@
name: Linter
name: Bandit
on: pull_request
jobs:
Bandit:
Linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -17,7 +17,8 @@ jobs:
PR_FILES="$PR_FILES_AM $PR_FILES_RENAMED"
for FILE in $PR_FILES; do
EXTENSION="${FILE##*.}"
if [[ $EXTENSION == 'py' ]]; then
DIRECTORY="${FILE%%/*}"
if [[ "$EXTENSION" == 'py' && "$DIRECTORY" != 'cvat-sdk' ]]; then
CHANGED_FILES+=" $FILE"
fi
done

@ -0,0 +1,82 @@
name: Black
on: pull_request
jobs:
Linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- id: files
uses: jitterbit/get-changed-files@v1
continue-on-error: true
- name: Run checks
env:
PR_FILES_AM: ${{ steps.files.outputs.added_modified }}
PR_FILES_RENAMED: ${{ steps.files.outputs.renamed }}
run: |
# If different modules use different Black configs,
# we need to run Black for each python component group separately.
# Otherwise, they all will use the same config.
ENABLED_DIRS=("cvat-sdk" "cvat-cli" "tests/python/sdk" "tests/python/cli")
isValueIn () {
# Checks if a value is in an array
# https://stackoverflow.com/a/8574392
# args: value, array
local e match="$1"
shift
for e; do
[[ "$e" == "$match" ]] && return 0;
done
return 1
}
startswith () {
# Inspired by https://stackoverflow.com/a/2172367
# Checks if the first arg starts with the second one
local value="$1"
local beginning="$2"
return $([[ $value == ${beginning}* ]])
}
PR_FILES="$PR_FILES_AM $PR_FILES_RENAMED"
UPDATED_DIRS=""
for FILE in $PR_FILES; do
EXTENSION="${FILE##*.}"
DIRECTORY="$(dirname $FILE)"
if [[ "$EXTENSION" == "py" ]]; then
for EDIR in ${ENABLED_DIRS[@]}; do
if startswith "${DIRECTORY}/" "${EDIR}/" && ! isValueIn "${EDIR}" ${UPDATED_DIRS[@]};
then
UPDATED_DIRS+=" ${EDIR}"
fi
done
fi
done
if [[ ! -z $UPDATED_DIRS ]]; 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 $(egrep "black.*" ./cvat-cli/requirements/development.txt)
mkdir -p black_report
echo "Black version: "$(black --version)
echo "The dirs will be checked: $UPDATED_DIRS"
EXIT_CODE=0
for DIR in $UPDATED_DIRS; do
black --check $DIR >> ./black_report/black_checks.txt || EXIT_CODE=$(($? | $EXIT_CODE)) || true
done
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: black_report
path: black_report

@ -6,27 +6,74 @@ on:
jobs:
Caching_CVAT:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
- name: Getting SHA with cache from the default branch
id: get-sha
run: |
DEFAULT_BRANCH=$(gh api /repos/$REPO | jq -r '.default_branch')
for sha in $(gh api "/repos/$REPO/commits?per_page=100&sha=${DEFAULT_BRANCH}" | jq -r '.[].sha');
do
RUN_status=$(gh api /repos/${REPO}/actions/workflows/cache.yml/runs | \
jq -r ".workflow_runs[]? | select((.head_sha == \"${sha}\") and (.conclusion == \"success\")) | .status")
if [[ ${RUN_status} == "completed" ]]; then
SHA=$sha
break
fi
done
echo Default branch is ${DEFAULT_BRANCH}
echo Workflow will try to get cache from commit: ${SHA}
echo ::set-output name=default_branch::${DEFAULT_BRANCH}
echo ::set-output name=sha::${SHA}
- uses: actions/cache@v3
id: server-cache-action
with:
path: /tmp/cvat_cache_server
key: ${{ runner.os }}-build-server-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-server-${{ steps.get-sha.outputs.sha }}
${{ runner.os }}-build-server-
- uses: actions/cache@v2
- uses: actions/cache@v3
id: ui-cache-action
with:
path: /tmp/cvat_cache_ui
key: ${{ runner.os }}-build-ui-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-ui-${{ steps.get-sha.outputs.sha }}
${{ runner.os }}-build-ui-
- uses: actions/cache@v3
id: elasticsearch-cache-action
with:
path: /tmp/cvat_cache_elasticsearch
key: ${{ runner.os }}-build-elasticsearch-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-elasticsearch-${{ steps.get-sha.outputs.sha }}
${{ runner.os }}-build-elasticsearch-
- uses: actions/cache@v3
id: logstash-cache-action
with:
path: /tmp/cvat_cache_logstash
key: ${{ runner.os }}-build-logstash-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-logstash-${{ steps.get-sha.outputs.sha }}
${{ runner.os }}-build-logstash-
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.1.2
uses: docker/setup-buildx-action@v2
- name: Caching CVAT server
- name: Caching CVAT Server
uses: docker/build-push-action@v2
with:
context: .
@ -42,9 +89,34 @@ jobs:
cache-from: type=local,src=/tmp/cvat_cache_ui
cache-to: type=local,dest=/tmp/cvat_cache_ui-new
- name: Caching CVAT Elasticsearch
uses: docker/build-push-action@v2
with:
context: ./components/analytics/elasticsearch/
file: ./components/analytics/elasticsearch/Dockerfile
cache-from: type=local,src=/tmp/cvat_cache_elasticsearch
cache-to: type=local,dest=/tmp/cvat_cache_elasticsearch-new
build-args: ELK_VERSION=6.8.23
- name: Caching CVAT Logstash
uses: docker/build-push-action@v2
with:
context: ./components/analytics/logstash/
file: ./components/analytics/logstash/Dockerfile
cache-from: type=local,src=/tmp/cvat_cache_logstash
cache-to: type=local,dest=/tmp/cvat_cache_logstash-new
build-args: ELK_VERSION=6.8.23
- name: Moving cache
run: |
rm -rf /tmp/cvat_cache_server
mv /tmp/cvat_cache_server-new /tmp/cvat_cache_server
rm -rf /tmp/cvat_cache_ui
mv /tmp/cvat_cache_ui-new /tmp/cvat_cache_ui
rm -rf /tmp/cvat_cache_elasticsearch
mv /tmp/cvat_cache_elasticsearch-new /tmp/cvat_cache_elasticsearch
rm -rf /tmp/cvat_cache_logstash
mv /tmp/cvat_cache_logstash-new /tmp/cvat_cache_logstash

@ -0,0 +1,21 @@
# Workflow deletes image artifacts that created by CI workflow
name: Delete image artifacts
on:
workflow_run:
workflows: [CI, Comment]
types:
- completed
jobs:
cleanup:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Clean up
run: |
wri=${{ github.event.workflow_run.id }}
for ai in $(gh api /repos/${{ github.repository }}/actions/runs/$wri/artifacts | jq '.artifacts[] | select( .name | startswith("cvat")) | .id');
do
gh api --method DELETE /repos/${{ github.repository }}/actions/artifacts/$ai
done

@ -13,12 +13,12 @@ name: "CodeQL"
on:
push:
branches: [ develop, hotfix-*, master, release-* ]
branches: [ "develop", master, release-* ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ develop ]
branches: [ "develop" ]
schedule:
- cron: '25 19 * * 6'
- cron: '27 19 * * 4'
jobs:
analyze:
@ -33,39 +33,40 @@ jobs:
fail-fast: false
matrix:
language: [ 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
#- run: |
# make bootstrap
# make release
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

@ -0,0 +1,89 @@
name: Comment
on:
issue_comment:
types: [created]
env:
WORKFLOW_RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
jobs:
verify_author:
if: contains(github.event.issue.html_url, '/pull') &&
contains(github.event.comment.body, '/check')
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.get-ref.outputs.ref }}
cid: ${{ steps.send-status.outputs.cid }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Check author of comment
id: check-author
run: |
PERM=$(gh api repos/${{ github.repository }}/collaborators/${{ github.event.comment.user.login }}/permission | jq -r '.permission')
if [[ $PERM == "write" || $PERM == "maintain" || $PERM == "admin" ]];
then
ALLOW="true"
fi
echo ::set-output name=allow::${ALLOW}
- name: Verify that author of comment is collaborator
if: steps.check-author.outputs.allow == ''
uses: actions/github-script@v3
with:
script: |
core.setFailed('User that send comment with /check command is not collaborator')
- name: Get branch name
id: get-ref
run: |
BRANCH=$(gh api /repos/${{ github.repository }}/pulls/${{ github.event.issue.number }} | jq -r '.head.ref')
echo ::set-output name=ref::${BRANCH}
- name: Send comment. Test are executing
id: send-status
run: |
BODY=":hourglass: Tests are executing, see more information [here](${{ env.WORKFLOW_RUN_URL }})"
BODY=$BODY"\n :warning: Cancel [this](${{ env.WORKFLOW_RUN_URL }}) workflow manually first, if you want to restart full check"
BODY=$(echo -e $BODY)
COMMENT_ID=$(gh api --method POST \
/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments \
-f body="${BODY}" | jq '.id')
echo ::set-output name=cid::${COMMENT_ID}
run-full:
needs: verify_author
uses: ./.github/workflows/full.yml
with:
ref: ${{ needs.verify_author.outputs.ref }}
send_status:
runs-on: ubuntu-latest
needs: [run-full, verify_author]
if: needs.run-full.result != 'skipped' && always()
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Send status in comments
run: |
BODY=""
if [[ "${{ needs.run-full.result }}" == "failure" ]]
then
BODY=":x: Some checks failed"
elif [[ "${{ needs.run-full.result }}" == "success" ]]
then
BODY=":heavy_check_mark: All checks completed successfully"
elif [[ "${{ needs.run-full.result }}" == "cancelled" ]]
then
BODY=":no_entry_sign: Workflows has been canceled"
fi
BODY=$BODY"\n :page_facing_up: See logs [here](${WORKFLOW_RUN_URL})"
BODY=$(echo -e $BODY)
gh api --method PATCH \
/repos/${{ github.repository }}/issues/comments/${{ needs.verify_author.outputs.cid }} \
-f body="${BODY}"

@ -1,7 +1,7 @@
name: Linter
name: ESLint
on: pull_request
jobs:
ESLint:
Linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -26,9 +26,8 @@ jobs:
done
if [[ ! -z $CHANGED_FILES ]]; then
npm ci
cd tests && npm ci && cd ..
npm install eslint-detailed-reporter --save-dev --legacy-peer-deps
yarn install --frozen-lockfile && cd tests && yarn install --frozen-lockfile && cd ..
yarn add eslint-detailed-reporter -D -W
mkdir -p eslint_report
echo "ESLint version: "$(npx eslint --version)

@ -0,0 +1,364 @@
name: Full
on:
workflow_call:
inputs:
ref:
type: string
required: true
workflow_dispatch:
inputs:
ref:
type: string
required: true
env:
WORKFLOW_RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
jobs:
search_cache:
runs-on: ubuntu-latest
outputs:
sha: ${{ steps.get-sha.outputs.sha}}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
steps:
- name: Getting SHA with cache from the default branch
id: get-sha
run: |
DEFAULT_BRANCH=$(gh api /repos/$REPO | jq -r '.default_branch')
for sha in $(gh api "/repos/$REPO/commits?per_page=100&sha=$DEFAULT_BRANCH" | jq -r '.[].sha');
do
RUN_status=$(gh api /repos/${REPO}/actions/workflows/cache.yml/runs | \
jq -r ".workflow_runs[]? | select((.head_sha == \"${sha}\") and (.conclusion == \"success\")) | .status")
if [[ ${RUN_status} == "completed" ]]; then
SHA=$sha
break
fi
done
echo Default branch is ${DEFAULT_BRANCH}
echo Workflow will try to get cache from commit: ${SHA}
echo ::set-output name=default_branch::${DEFAULT_BRANCH}
echo ::set-output name=sha::${SHA}
build:
needs: search_cache
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ inputs.ref }}
- name: CVAT server. Getting cache from the default branch
uses: actions/cache@v3
with:
path: /tmp/cvat_cache_server
key: ${{ runner.os }}-build-server-${{ needs.search_cache.outputs.sha }}
- name: CVAT UI. Getting cache from the default branch
uses: actions/cache@v3
with:
path: /tmp/cvat_cache_ui
key: ${{ runner.os }}-build-ui-${{ needs.search_cache.outputs.sha }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Create image directory
run: |
mkdir /tmp/cvat_server
mkdir /tmp/cvat_ui
- name: CVAT server. Build and push
uses: docker/build-push-action@v3
with:
cache-from: type=local,src=/tmp/cvat_cache_server
context: .
file: Dockerfile
tags: cvat/server
outputs: type=docker,dest=/tmp/cvat_server/image.tar
- name: CVAT UI. Build and push
uses: docker/build-push-action@v3
with:
cache-from: type=local,src=/tmp/cvat_cache_ui
context: .
file: Dockerfile.ui
tags: cvat/ui
outputs: type=docker,dest=/tmp/cvat_ui/image.tar
- name: Upload CVAT server artifact
uses: actions/upload-artifact@v3
with:
name: cvat_server
path: /tmp/cvat_server/image.tar
- name: Upload CVAT UI artifact
uses: actions/upload-artifact@v3
with:
name: cvat_ui
path: /tmp/cvat_ui/image.tar
rest_api:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ inputs.ref }}
- uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@master
- name: Getting CVAT Elasticsearch cache from the default branch
uses: actions/cache@v3
with:
path: /tmp/cvat_cache_elasticsearch
key: ${{ runner.os }}-build-elasticsearch-${{ needs.search_cache.outputs.sha }}
- name: Getting CVAT Logstash cache from the default branch
uses: actions/cache@v3
with:
path: /tmp/cvat_cache_logstash
key: ${{ runner.os }}-build-logstash-${{ needs.search_cache.outputs.sha }}
- name: Building CVAT Elasticsearch
uses: docker/build-push-action@v2
with:
context: ./components/analytics/elasticsearch/
file: ./components/analytics/elasticsearch/Dockerfile
cache-from: type=local,src=/tmp/cvat_cache_elasticsearch
tags: cvat_elasticsearch:latest
load: true
build-args: ELK_VERSION=6.8.23
- name: Building CVAT Logstash
uses: docker/build-push-action@v2
with:
context: ./components/analytics/logstash/
file: ./components/analytics/logstash/Dockerfile
cache-from: type=local,src=/tmp/cvat_cache_logstash
tags: cvat_logstash:latest
load: true
build-args: ELK_VERSION=6.8.23
- name: Download CVAT server image
uses: actions/download-artifact@v3
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Download CVAT UI images
uses: actions/download-artifact@v3
with:
name: cvat_ui
path: /tmp/cvat_ui/
- name: Load Docker images
run: |
docker load --input /tmp/cvat_server/image.tar
docker load --input /tmp/cvat_ui/image.tar
docker image ls -a
- name: Running REST API and SDK tests
run: |
docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \
--entrypoint /bin/bash -u root cvat/server \
-c 'python manage.py spectacular --file /transfer/schema.yml'
pip3 install --user -r cvat-sdk/gen/requirements.txt
cd cvat-sdk/
gen/generate.sh
cd ..
pip3 install --user cvat-sdk/
pip3 install --user cvat-cli/
pip3 install --user -r tests/python/requirements.txt
pytest tests/python -s -v
- name: Creating a log file from cvat containers
if: failure()
env:
LOGS_DIR: "${{ github.workspace }}/rest_api"
run: |
mkdir $LOGS_DIR
docker logs test_cvat_server_1 > $LOGS_DIR/cvat.log
docker logs test_cvat_opa_1 2> $LOGS_DIR/cvat_opa.log
- name: Uploading "cvat" container logs as an artifact
if: failure()
uses: actions/upload-artifact@v2
env:
LOGS_DIR: "${{ github.workspace }}/rest_api"
with:
name: container_logs
path: $LOGS_DIR
unit_testing:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ inputs.ref }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@master
- name: Download CVAT server image
uses: actions/download-artifact@v3
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Load Docker images
run: |
docker load --input /tmp/cvat_server/image.tar
docker image ls -a
- name: Running OPA tests
run: |
curl -L -o opa https://openpolicyagent.org/downloads/v0.34.2/opa_linux_amd64_static
chmod +x ./opa
./opa test cvat/apps/iam/rules
- name: Running unit tests
env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
CONTAINER_COVERAGE_DATA_DIR: "/coverage_data"
run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d cvat_opa
max_tries=12
while [[ $(curl -s -o /dev/null -w "%{http_code}" localhost:8181/health) != "200" && max_tries -gt 0 ]]; do (( max_tries-- )); sleep 5; done
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash \
-c 'python manage.py test cvat/apps -v 2'
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash \
-c 'yarn --frozen-lockfile --ignore-scripts && yarn workspace cvat-core run test'
- name: Creating a log file from cvat containers
if: failure()
env:
LOGS_DIR: "${{ github.workspace }}/unit_testing"
run: |
mkdir $LOGS_DIR
docker logs cvat > $LOGS_DIR/cvat.log
docker logs cvat_opa 2> $LOGS_DIR/cvat_opa.log
- name: Uploading "cvat" container logs as an artifact
if: failure()
uses: actions/upload-artifact@v2
env:
LOGS_DIR: "${{ github.workspace }}/unit_testing"
with:
name: container_logs
path: $LOGS_DIR
e2e_testing:
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
specs: ['actions_tasks', 'actions_tasks2', 'actions_tasks3',
'actions_objects', 'actions_objects2', 'actions_users',
'actions_projects_models', 'actions_organizations', 'canvas3d_functionality',
'canvas3d_functionality_2', 'issues_prs', 'issues_prs2']
steps:
- uses: actions/checkout@v3
with:
ref: ${{ inputs.ref }}
- uses: actions/setup-node@v2
with:
node-version: '16.x'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@master
- name: Download CVAT server image
uses: actions/download-artifact@v3
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Download CVAT UI image
uses: actions/download-artifact@v3
with:
name: cvat_ui
path: /tmp/cvat_ui/
- name: Load Docker images
run: |
docker load --input /tmp/cvat_server/image.tar
docker load --input /tmp/cvat_ui/image.tar
docker image ls -a
- name: Run CVAT instance
run: |
docker-compose \
-f docker-compose.yml \
-f docker-compose.dev.yml \
-f components/serverless/docker-compose.serverless.yml \
-f tests/docker-compose.file_share.yml up -d
- name: Waiting for server
env:
API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: |
max_tries=60
status_code=$(curl -s -o /tmp/server_response -w "%{http_code}" ${API_ABOUT_PAGE})
while [[ $status_code != "401" && max_tries -gt 0 ]]
do
echo Number of attempts left: $max_tries
echo Status code of response: $status_code
sleep 5
status_code=$(curl -s -o /tmp/server_response -w "%{http_code}" ${API_ABOUT_PAGE})
(( max_tries-- ))
done
- name: Run E2E tests
env:
DJANGO_SU_NAME: 'admin'
DJANGO_SU_EMAIL: 'admin@localhost.company'
DJANGO_SU_PASSWORD: '12qwaszx'
run: |
docker exec -i cvat /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
cd ./tests
yarn --frozen-lockfile
shopt -s extglob
if [[ ${{ matrix.specs }} == canvas3d_* ]]; then
npx cypress run \
--headed \
--browser chrome \
--config-file cypress_canvas3d.json \
--spec 'cypress/integration/${{ matrix.specs }}/**/*.js,cypress/integration/remove_users_tasks_projects_organizations.js'
else
npx cypress run \
--browser chrome \
--spec 'cypress/integration/${{ matrix.specs }}/**/*.js,cypress/integration/remove_users_tasks_projects_organizations.js'
fi
- name: Creating a log file from "cvat" container logs
if: failure()
run: |
docker logs cvat > ${{ github.workspace }}/tests/cvat_${{ matrix.specs }}.log
- name: Uploading "cvat" container logs as an artifact
if: failure()
uses: actions/upload-artifact@v2
with:
name: container_logs
path: ${{ github.workspace }}/tests/cvat_${{ matrix.specs }}.log
- name: Uploading cypress screenshots as an artifact
if: failure()
uses: actions/upload-artifact@v2
with:
name: cypress_screenshots_${{ matrix.specs }}
path: ${{ github.workspace }}/tests/cypress/screenshots

@ -7,7 +7,7 @@ on:
jobs:
deploy:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:

@ -1,7 +1,7 @@
name: Linter
name: HadoLint
on: pull_request
jobs:
HadoLint:
Linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

@ -0,0 +1,82 @@
name: isort
on: pull_request
jobs:
Linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- id: files
uses: jitterbit/get-changed-files@v1
continue-on-error: true
- name: Run checks
env:
PR_FILES_AM: ${{ steps.files.outputs.added_modified }}
PR_FILES_RENAMED: ${{ steps.files.outputs.renamed }}
run: |
# If different modules use different isort configs,
# we need to run isort for each python component group separately.
# Otherwise, they all will use the same config.
ENABLED_DIRS=("cvat-sdk" "cvat-cli" "tests/python/sdk" "tests/python/cli")
isValueIn () {
# Checks if a value is in an array
# https://stackoverflow.com/a/8574392
# args: value, array
local e match="$1"
shift
for e; do
[[ "$e" == "$match" ]] && return 0;
done
return 1
}
startswith () {
# Inspired by https://stackoverflow.com/a/2172367
# Checks if the first arg starts with the second one
local value="$1"
local beginning="$2"
return $([[ $value == ${beginning}* ]])
}
PR_FILES="$PR_FILES_AM $PR_FILES_RENAMED"
UPDATED_DIRS=""
for FILE in $PR_FILES; do
EXTENSION="${FILE##*.}"
DIRECTORY="$(dirname $FILE)"
if [[ "$EXTENSION" == "py" ]]; then
for EDIR in ${ENABLED_DIRS[@]}; do
if startswith "${DIRECTORY}/" "${EDIR}/" && ! isValueIn "${EDIR}" ${UPDATED_DIRS[@]};
then
UPDATED_DIRS+=" ${EDIR}"
fi
done
fi
done
if [[ ! -z $UPDATED_DIRS ]]; 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 $(egrep "isort.*" ./cvat-cli/requirements/development.txt)
mkdir -p isort_report
echo "isort version: "$(isort --version)
echo "The dirs will be checked: $UPDATED_DIRS"
EXIT_CODE=0
for DIR in $UPDATED_DIRS; do
isort --check $DIR >> ./isort_report/isort_checks.txt || EXIT_CODE=$(($? | $EXIT_CODE)) || true
done
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: isort_report
path: isort_report

@ -6,276 +6,355 @@ on:
- 'develop'
pull_request:
types: [edited, ready_for_review, opened, synchronize, reopened]
paths-ignore:
- 'site/**'
- '**/*.md'
jobs:
Unit_testing:
if: |
github.event.pull_request.draft == false &&
!startsWith(github.event.pull_request.title, '[WIP]') &&
!startsWith(github.event.pull_request.title, '[Dependent]')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Getting SHA from the default branch
search_cache:
if: |
github.event.pull_request.draft == false &&
!startsWith(github.event.pull_request.title, '[WIP]') &&
!startsWith(github.event.pull_request.title, '[Dependent]')
runs-on: ubuntu-latest
outputs:
sha: ${{ steps.get-sha.outputs.sha}}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
steps:
- name: Getting SHA with cache from the default branch
id: get-sha
run: |
URL_get_default_branch="https://api.github.com/repos/${{ github.repository }}"
DEFAULT_BRANCH=$(curl -s -X GET -G ${URL_get_default_branch} | jq -r '.default_branch')
URL_get_sha_default_branch="https://api.github.com/repos/${{ github.repository }}/git/ref/heads/${DEFAULT_BRANCH}"
SHA=$(curl -s -X GET -G ${URL_get_sha_default_branch} | jq .object.sha | tr -d '"')
echo ::set-output name=default_branch::${DEFAULT_BRANCH}
echo ::set-output name=sha::${SHA}
- name: Waiting a cache creation in the default branch
run: |
URL_runs="https://api.github.com/repos/${{ github.repository }}/actions/workflows/cache.yml/runs"
SLEEP=45
NUMBER_ATTEMPTS=10
while [[ ${NUMBER_ATTEMPTS} -gt 0 ]]; do
RUN_status=$(curl -s -X GET -G ${URL_runs} | jq -r '.workflow_runs[]? | select((.head_sha == "${{ steps.get-sha.outputs.sha }}") and (.event == "push") and (.name == "Cache") and (.head_branch == "${{ steps.get-sha.outputs.default_branch }}")) | .status')
DEFAULT_BRANCH=$(gh api /repos/$REPO | jq -r '.default_branch')
for sha in $(gh api "/repos/$REPO/commits?per_page=100&sha=$DEFAULT_BRANCH" | jq -r '.[].sha');
do
RUN_status=$(gh api /repos/${REPO}/actions/workflows/cache.yml/runs | \
jq -r ".workflow_runs[]? | select((.head_sha == \"${sha}\") and (.conclusion == \"success\")) | .status")
if [[ ${RUN_status} == "completed" ]]; then
echo "The cache creation on the '${{ steps.get-sha.outputs.default_branch }}' branch has finished. Status: ${RUN_status}"
SHA=$sha
break
else
echo "The creation of the cache is not yet complete."
echo "There are still attempts to check the cache: ${NUMBER_ATTEMPTS}"
echo "Status of caching in the '${{ steps.get-sha.outputs.default_branch }}' branch: ${RUN_status}"
echo "sleep ${SLEEP}"
sleep ${SLEEP}
((NUMBER_ATTEMPTS--))
fi
done
if [[ ${NUMBER_ATTEMPTS} -eq 0 ]]; then
echo "Number of attempts expired!"
echo "Probably the creation of the cache is not yet complete. Will continue working without the cache."
fi
- name: Getting CVAT server cache from the default branch
uses: actions/cache@v2
echo Default branch is ${DEFAULT_BRANCH}
echo Workflow will try to get cache from commit: ${SHA}
echo ::set-output name=default_branch::${DEFAULT_BRANCH}
echo ::set-output name=sha::${SHA}
build:
needs: search_cache
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: CVAT server. Getting cache from the default branch
uses: actions/cache@v3
with:
path: /tmp/cvat_cache_server
key: ${{ runner.os }}-build-server-${{ steps.get-sha.outputs.sha }}
key: ${{ runner.os }}-build-server-${{ needs.search_cache.outputs.sha }}
- name: CVAT UI. Getting cache from the default branch
uses: actions/cache@v3
with:
path: /tmp/cvat_cache_ui
key: ${{ runner.os }}-build-ui-${{ needs.search_cache.outputs.sha }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.1.2
- name: Building CVAT server image
uses: docker/build-push-action@v2
uses: docker/setup-buildx-action@v2
- name: Create image directory
run: |
mkdir /tmp/cvat_server
mkdir /tmp/cvat_ui
- name: CVAT server. Build and push
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
cache-from: type=local,src=/tmp/cvat_cache_server
tags: openvino/cvat_server:latest
load: true
context: .
file: Dockerfile
tags: cvat/server
outputs: type=docker,dest=/tmp/cvat_server/image.tar
- name: CVAT UI. Build and push
uses: docker/build-push-action@v3
with:
cache-from: type=local,src=/tmp/cvat_cache_ui
context: .
file: Dockerfile.ui
tags: cvat/ui
outputs: type=docker,dest=/tmp/cvat_ui/image.tar
- name: Upload CVAT server artifact
uses: actions/upload-artifact@v3
with:
name: cvat_server
path: /tmp/cvat_server/image.tar
- name: Upload CVAT UI artifact
uses: actions/upload-artifact@v3
with:
name: cvat_ui
path: /tmp/cvat_ui/image.tar
rest_api:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Download CVAT server image
uses: actions/download-artifact@v3
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Download CVAT UI images
uses: actions/download-artifact@v3
with:
name: cvat_ui
path: /tmp/cvat_ui/
- name: Load Docker images
run: |
docker load --input /tmp/cvat_server/image.tar
docker load --input /tmp/cvat_ui/image.tar
docker image ls -a
- name: Running REST API tests
run: |
docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \
--entrypoint /bin/bash -u root cvat/server \
-c 'python manage.py spectacular --file /transfer/schema.yml'
pip3 install --user -r cvat-sdk/gen/requirements.txt
cd cvat-sdk/
gen/generate.sh
cd ..
pip3 install --user cvat-sdk/
pip3 install --user -r tests/python/requirements.txt
pytest tests/python/rest_api -k 'GET' -s
- name: Creating a log file from cvat containers
if: failure()
env:
LOGS_DIR: "${{ github.workspace }}/rest_api"
run: |
mkdir $LOGS_DIR
docker logs test_cvat_server_1 > $LOGS_DIR/cvat.log
docker logs test_cvat_opa_1 2> $LOGS_DIR/cvat_opa.log
- name: Uploading "cvat" container logs as an artifact
if: failure()
uses: actions/upload-artifact@v2
env:
LOGS_DIR: "${{ github.workspace }}/rest_api"
with:
name: container_logs
path: $LOGS_DIR
unit_testing:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Download CVAT server image
uses: actions/download-artifact@v3
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Load Docker server image
run: |
docker load --input /tmp/cvat_server/image.tar
docker image ls -a
- name: Running OPA tests
run: |
curl -L -o opa https://openpolicyagent.org/downloads/v0.34.2/opa_linux_amd64_static
chmod +x ./opa
./opa test cvat/apps/iam/rules
- name: Running REST API tests
env:
API_ABOUT_PAGE: "localhost:8080/api/server/about"
# Access key length should be at least 3, and secret key length at least 8 characters
MINIO_ACCESS_KEY: "minio_access_key"
MINIO_SECRET_KEY: "minio_secret_key"
run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml -f components/analytics/docker-compose.analytics.yml -f tests/rest_api/docker-compose.minio.yml up -d
/bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done'
pip3 install --user -r tests/rest_api/requirements.txt
pytest tests/rest_api/
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml -f components/analytics/docker-compose.analytics.yml -f tests/rest_api/docker-compose.minio.yml down -v
- name: Running unit tests
env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
CONTAINER_COVERAGE_DATA_DIR: "/coverage_data"
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 && 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'
- name: Uploading code coverage results as an artifact
if: github.ref == 'refs/heads/develop'
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d cvat_opa
max_tries=12
while [[ $(curl -s -o /dev/null -w "%{http_code}" localhost:8181/health) != "200" && max_tries -gt 0 ]]; do (( max_tries-- )); sleep 5; done
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash \
-c 'python manage.py test cvat/apps -k tasks_id -k lambda -k share -v 2'
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash \
-c 'yarn --frozen-lockfile --ignore-scripts && yarn workspace cvat-core run test'
- name: Creating a log file from cvat containers
if: failure()
env:
LOGS_DIR: "${{ github.workspace }}/unit_testing"
run: |
mkdir $LOGS_DIR
docker logs cvat > $LOGS_DIR/cvat.log
docker logs cvat_opa 2> $LOGS_DIR/cvat_opa.log
- name: Uploading "cvat" container logs as an artifact
if: failure()
uses: actions/upload-artifact@v2
env:
LOGS_DIR: "${{ github.workspace }}/unit_testing"
with:
name: coverage_results
path: |
${{ github.workspace }}/.coverage
${{ github.workspace }}/lcov.info
name: container_logs
path: $LOGS_DIR
E2E_testing:
if: |
github.event.pull_request.draft == false &&
!startsWith(github.event.pull_request.title, '[WIP]') &&
!startsWith(github.event.pull_request.title, '[Dependent]')
e2e_testing:
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
specs: ['actions_tasks', 'actions_tasks2', 'actions_tasks3', 'actions_objects', 'actions_objects2', 'actions_users', 'actions_projects_models', 'actions_organizations', 'canvas3d_functionality', 'canvas3d_functionality_2', 'issues_prs', 'issues_prs2']
specs: ['canvas3d_functionality', 'actions']
steps:
- uses: actions/checkout@v2
- name: Getting SHA from the default branch
id: get-sha
run: |
URL_get_default_branch="https://api.github.com/repos/${{ github.repository }}"
DEFAULT_BRANCH=$(curl -s -X GET -G ${URL_get_default_branch} | jq -r '.default_branch')
URL_get_sha_default_branch="https://api.github.com/repos/${{ github.repository }}/git/ref/heads/${DEFAULT_BRANCH}"
SHA=$(curl -s -X GET -G ${URL_get_sha_default_branch} | jq .object.sha | tr -d '"')
echo ::set-output name=default_branch::${DEFAULT_BRANCH}
echo ::set-output name=sha::${SHA}
- name: Waiting a cache creation in the default branch
run: |
URL_runs="https://api.github.com/repos/${{ github.repository }}/actions/workflows/cache.yml/runs"
SLEEP=45
NUMBER_ATTEMPTS=10
while [[ ${NUMBER_ATTEMPTS} -gt 0 ]]; do
RUN_status=$(curl -s -X GET -G ${URL_runs} | jq -r '.workflow_runs[]? | select((.head_sha == "${{ steps.get-sha.outputs.sha }}") and (.event == "push") and (.name == "Cache") and (.head_branch == "${{ steps.get-sha.outputs.default_branch }}")) | .status')
if [[ ${RUN_status} == "completed" ]]; then
echo "The cache creation on the '${{ steps.get-sha.outputs.default_branch }}' branch has finished. Status: ${RUN_status}"
break
else
echo "The creation of the cache is not yet complete."
echo "There are still attempts to check the cache: ${NUMBER_ATTEMPTS}"
echo "Status of caching in the '${{ steps.get-sha.outputs.default_branch }}' branch: ${RUN_status}"
echo "sleep ${SLEEP}"
sleep ${SLEEP}
((NUMBER_ATTEMPTS--))
fi
done
if [[ ${NUMBER_ATTEMPTS} -eq 0 ]]; then
echo "Number of attempts expired!"
echo "Probably the creation of the cache is not yet complete. Will continue working without the cache."
fi
- name: Getting CVAT server cache from the default branch
uses: actions/cache@v2
with:
path: /tmp/cvat_cache_server
key: ${{ runner.os }}-build-server-${{ steps.get-sha.outputs.sha }}
- name: Getting cache CVAT UI from the default branch
uses: actions/cache@v2
with:
path: /tmp/cvat_cache_ui
key: ${{ runner.os }}-build-ui-${{ steps.get-sha.outputs.sha }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- uses: actions/setup-node@v2
with:
node-version: '16.x'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.1.2
- name: Building CVAT server image
uses: docker/build-push-action@v2
- name: Download CVAT server images
uses: actions/download-artifact@v3
with:
context: .
file: ./Dockerfile
cache-from: type=local,src=/tmp/cvat_cache_server
tags: openvino/cvat_server:latest
load: true
- name: Building CVAT UI image
uses: docker/build-push-action@v2
name: cvat_server
path: /tmp/cvat_server/
- name: Download CVAT UI images
uses: actions/download-artifact@v3
with:
context: .
file: ./Dockerfile.ui
cache-from: type=local,src=/tmp/cvat_cache_ui
tags: openvino/cvat_ui:latest
load: true
- name: Instrumentation of the code then rebuilding the CVAT UI
if: github.ref == 'refs/heads/develop'
name: cvat_ui
path: /tmp/cvat_ui/
- name: Load Docker images
run: |
docker load --input /tmp/cvat_server/image.tar
docker load --input /tmp/cvat_ui/image.tar
docker image ls -a
- name: Run CVAT instance
run: |
npm ci
npm run coverage
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml build cvat_ui
- name: Running e2e tests
docker-compose \
-f docker-compose.yml \
-f docker-compose.dev.yml \
-f components/serverless/docker-compose.serverless.yml \
-f tests/docker-compose.file_share.yml up -d
- name: Waiting for server
env:
API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: |
max_tries=60
status_code=$(curl -s -o /tmp/server_response -w "%{http_code}" ${API_ABOUT_PAGE})
while [[ $status_code != "401" && max_tries -gt 0 ]]
do
echo Number of attempts left: $max_tries
echo Status code of response: $status_code
sleep 5
status_code=$(curl -s -o /tmp/server_response -w "%{http_code}" ${API_ABOUT_PAGE})
(( max_tries-- ))
done
- name: Run E2E tests
env:
DJANGO_SU_NAME: 'admin'
DJANGO_SU_EMAIL: 'admin@localhost.company'
DJANGO_SU_PASSWORD: '12qwaszx'
API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml -f tests/docker-compose.file_share.yml up -d
/bin/bash -c 'while [[ $(curl -s -o /dev/null -w "%{http_code}" ${API_ABOUT_PAGE}) != "401" ]]; do sleep 5; done'
docker exec -i cvat /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
docker exec -i cvat_server /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
cd ./tests
npm ci
if [[ ${{ github.ref }} == 'refs/heads/develop' ]]; then
if [ ${{ matrix.specs }} == 'canvas3d_functionality' ] || [ ${{ matrix.specs }} == 'canvas3d_functionality_2' ]; then
npx cypress run --headed --browser chrome --config-file cypress_canvas3d.json --spec 'cypress/integration/${{ matrix.specs }}/**/*.js,cypress/integration/remove_users_tasks_projects_organizations.js'
else
npx cypress run --browser chrome --spec 'cypress/integration/${{ matrix.specs }}/**/*.js,cypress/integration/remove_users_tasks_projects_organizations.js'
fi
mv ./.nyc_output/out.json ./.nyc_output/out_${{ matrix.specs }}.json
yarn --frozen-lockfile
if [ ${{ matrix.specs }} == 'canvas3d_functionality' ]; then
npx cypress run --headed --browser chrome --config-file pr_cypress_canvas3d.json
else
if [ ${{ matrix.specs }} == 'canvas3d_functionality' ] || [ ${{ matrix.specs }} == 'canvas3d_functionality_2' ]; then
npx cypress run --headed --browser chrome --env coverage=false --config-file cypress_canvas3d.json --spec 'cypress/integration/${{ matrix.specs }}/**/*.js,cypress/integration/remove_users_tasks_projects_organizations.js'
else
npx cypress run --browser chrome --env coverage=false --spec 'cypress/integration/${{ matrix.specs }}/**/*.js,cypress/integration/remove_users_tasks_projects_organizations.js'
fi
npx cypress run --browser chrome --config-file pr_cypress.json
fi
- name: Creating a log file from "cvat" container logs
if: failure()
run: |
docker logs cvat > ${{ github.workspace }}/tests/cvat_${{ matrix.specs }}.log
- name: Uploading cypress screenshots as an artifact
if: failure()
uses: actions/upload-artifact@v2
with:
name: cypress_screenshots_${{ matrix.specs }}
path: ${{ github.workspace }}/tests/cypress/screenshots
- name: Uploading "cvat" container logs as an artifact
if: failure()
uses: actions/upload-artifact@v2
with:
name: cvat_container_logs
name: container_logs
path: ${{ github.workspace }}/tests/cvat_${{ matrix.specs }}.log
- name: Uploading code coverage results as an artifact
if: github.ref == 'refs/heads/develop'
- name: Uploading cypress screenshots as an artifact
if: failure()
uses: actions/upload-artifact@v2
with:
name: coverage_results
path: ${{ github.workspace }}/tests/.nyc_output
name: cypress_screenshots_${{ matrix.specs }}
path: ${{ github.workspace }}/tests/cypress/screenshots
Coveralls:
publish_dev_images:
if: github.ref == 'refs/heads/develop'
needs: [rest_api, unit_testing, e2e_testing]
runs-on: ubuntu-latest
needs: [Unit_testing, E2E_testing]
steps:
- uses: actions/checkout@v2
- name: Getting SHA from the default branch
id: get-sha
run: |
URL_get_default_branch="https://api.github.com/repos/${{ github.repository }}"
DEFAULT_BRANCH=$(curl -s -X GET -G ${URL_get_default_branch} | jq -r '.default_branch')
URL_get_sha_default_branch="https://api.github.com/repos/${{ github.repository }}/git/ref/heads/${DEFAULT_BRANCH}"
SHA=$(curl -s -X GET -G ${URL_get_sha_default_branch} | jq .object.sha | tr -d '"')
echo ::set-output name=sha::${SHA}
- name: Getting CVAT server cache from the default branch
uses: actions/cache@v2
with:
path: /tmp/cvat_cache_server
key: ${{ runner.os }}-build-server-${{ steps.get-sha.outputs.sha }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.1.2
- name: Building CVAT server image
uses: docker/build-push-action@v2
- name: Download CVAT server images
uses: actions/download-artifact@v3
with:
context: .
file: ./Dockerfile
cache-from: type=local,src=/tmp/cvat_cache_server
tags: openvino/cvat_server:latest
load: true
- name: Downloading coverage results
uses: actions/download-artifact@v2
name: cvat_server
path: /tmp/cvat_server/
- name: Download CVAT UI images
uses: actions/download-artifact@v3
with:
name: coverage_results
- name: Combining coverage results
name: cvat_ui
path: /tmp/cvat_ui/
- name: Load Docker images
run: |
mkdir -p ./nyc_output_tmp
mv ./out_*.json ./nyc_output_tmp
mkdir -p ./.nyc_output
npm ci
npx nyc merge ./nyc_output_tmp ./.nyc_output/out.json
- name: Sending results to Coveralls
docker load --input /tmp/cvat_server/image.tar
docker load --input /tmp/cvat_ui/image.tar
- 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:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
CONTAINER_COVERAGE_DATA_DIR: "/coverage_data"
COVERALLS_SERVICE_NAME: github
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SERVER_IMAGE_REPO: ${{ secrets.DOCKERHUB_WORKSPACE }}/server
UI_IMAGE_REPO: ${{ secrets.DOCKERHUB_WORKSPACE }}/ui
run: |
npx nyc report --reporter=text-lcov >> ${HOST_COVERAGE_DATA_DIR}/lcov.info
docker-compose -f docker-compose.yml -f docker-compose.dev.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.dev.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'
docker tag cvat/server:latest "${SERVER_IMAGE_REPO}:dev"
docker push "${SERVER_IMAGE_REPO}:dev"
docker tag cvat/ui:latest "${UI_IMAGE_REPO}:dev"
docker push "${UI_IMAGE_REPO}:dev"

@ -1,53 +1,11 @@
name: Publish Docker images
on:
release:
types: [published]
types: [released]
jobs:
Unit_testing:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run unit tests
env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
CONTAINER_COVERAGE_DATA_DIR: '/coverage_data'
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'
E2E_testing:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16.x'
- name: Run end-to-end tests
env:
DJANGO_SU_NAME: 'admin'
DJANGO_SU_EMAIL: 'admin@localhost.company'
DJANGO_SU_PASSWORD: '12qwaszx'
API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml build
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml -f tests/docker-compose.file_share.yml up -d
/bin/bash -c 'while [[ $(curl -s -o /dev/null -w "%{http_code}" ${API_ABOUT_PAGE}) != "401" ]]; do sleep 5; done'
docker exec -i cvat /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
cd ./tests
npm ci
npm run cypress:run:chrome
npm run cypress:run:chrome:canvas3d
- name: Uploading cypress screenshots as an artifact
if: failure()
uses: actions/upload-artifact@v2
with:
name: cypress_screenshots
path: ${{ github.workspace }}/tests/cypress/screenshots
Push_to_registry:
runs-on: ubuntu-latest
needs: [Unit_testing, E2E_testing]
steps:
- uses: actions/checkout@v2
- name: Build images
@ -60,9 +18,9 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push to Docker Hub
env:
DOCKERHUB_WORKSPACE: 'openvino'
SERVER_IMAGE_REPO: 'cvat_server'
UI_IMAGE_REPO: 'cvat_ui'
DOCKERHUB_WORKSPACE: ${{ secrets.DOCKERHUB_WORKSPACE }}
SERVER_IMAGE_REPO: 'server'
UI_IMAGE_REPO: '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 }}"

@ -1,7 +1,7 @@
name: Linter
name: Pylint
on: pull_request
jobs:
PyLint:
Linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -17,7 +17,7 @@ jobs:
PR_FILES="$PR_FILES_AM $PR_FILES_RENAMED"
for FILE in $PR_FILES; do
EXTENSION="${FILE##*.}"
if [[ $EXTENSION == 'py' ]]; then
if [[ "$EXTENSION" == 'py' ]]; then
CHANGED_FILES+=" $FILE"
fi
done

@ -1,7 +1,7 @@
name: Linter
name: Remark
on: pull_request
jobs:
Remark:
Linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -11,11 +11,11 @@ jobs:
- name: Run checks
run: |
npm ci
yarn install --frozen-lockfile
mkdir -p remark_report
echo "Remark version: "`npx remark --version`
npx remark --quiet --report json --no-stdout . 2> ./remark_report/remark_report.json
npx remark --quiet --report json --no-stdout -i .remarkignore . 2> ./remark_report/remark_report.json
get_report=`cat ./remark_report/remark_report.json | jq -r '.[] | select(.messages | length > 0)'`
if [[ ! -z ${get_report} ]]; then
pip install json2html

@ -3,32 +3,469 @@ on:
schedule:
- cron: '0 22 * * *'
workflow_dispatch:
env:
SERVER_IMAGE_TEST_REPO: cvat_server
UI_IMAGE_TEST_REPO: instrumentation_cvat_ui
jobs:
check_updates:
runs-on: ubuntu-latest
env:
REPO: ${{ github.repository }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
outputs:
last_commit_time: ${{ steps.check_updates.outputs.last_commit_time }}
last_night_time: ${{ steps.check_updates.outputs.last_night_time }}
steps:
- id: check_updates
run: |
default_branch=$(gh api /repos/$REPO | jq -r '.default_branch')
last_commit_date=$(gh api /repos/${REPO}/branches/${default_branch} | jq -r '.commit.commit.author.date')
last_night_date=$(gh api /repos/${REPO}/actions/workflows/schedule.yml/runs | \
jq -r '.workflow_runs[]? | select((.status == "completed")) | .updated_at' \
| sort | tail -1)
last_night_time=$(date +%s -d $last_night_date)
last_commit_time=$(date +%s -d $last_commit_date)
echo Last CI-nightly workflow run time: $last_night_date
echo Last commit time in develop branch: $last_commit_date
echo ::set-output name=last_commit_time::${last_commit_time}
echo ::set-output name=last_night_time::${last_night_time}
search_cache:
needs: check_updates
if:
needs.check_updates.outputs.last_commit_time > needs.check_updates.outputs.last_night_time
runs-on: ubuntu-latest
outputs:
sha: ${{ steps.get-sha.outputs.sha}}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
steps:
- name: Getting SHA with cache from the default branch
id: get-sha
run: |
DEFAULT_BRANCH=$(gh api /repos/$REPO | jq -r '.default_branch')
for sha in $(gh api "/repos/$REPO/commits?per_page=100&sha=$DEFAULT_BRANCH" | jq -r '.[].sha');
do
RUN_status=$(gh api /repos/${REPO}/actions/workflows/cache.yml/runs | \
jq -r ".workflow_runs[]? | select((.head_sha == \"${sha}\") and (.conclusion == \"success\")) | .status")
if [[ ${RUN_status} == "completed" ]]; then
SHA=$sha
break
fi
done
echo Default branch is ${DEFAULT_BRANCH}
echo Workflow will try to get cache from commit: ${SHA}
echo ::set-output name=default_branch::${DEFAULT_BRANCH}
echo ::set-output name=sha::${SHA}
build:
needs: search_cache
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_CI_USERNAME }}
password: ${{ secrets.DOCKERHUB_CI_TOKEN }}
- name: CVAT server. Getting cache from the default branch
uses: actions/cache@v3
with:
path: /tmp/cvat_cache_server
key: ${{ runner.os }}-build-server-${{ needs.search_cache.outputs.sha }}
- name: CVAT UI. Getting cache from the default branch
uses: actions/cache@v3
with:
path: /tmp/cvat_cache_ui
key: ${{ runner.os }}-build-ui-${{ needs.search_cache.outputs.sha }}
- name: CVAT server. Extract metadata (tags, labels) for Docker
id: meta-server
uses: docker/metadata-action@master
with:
images: ${{ secrets.DOCKERHUB_CI_WORKSPACE }}/${{ env.SERVER_IMAGE_TEST_REPO }}
tags:
type=raw,value=nightly
- name: CVAT UI. Extract metadata (tags, labels) for Docker
id: meta-ui
uses: docker/metadata-action@master
with:
images: ${{ secrets.DOCKERHUB_CI_WORKSPACE }}/${{ env.UI_IMAGE_TEST_REPO }}
tags:
type=raw,value=nightly
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: CVAT server. Build and push
uses: docker/build-push-action@v3
with:
cache-from: type=local,src=/tmp/cvat_cache_server
context: .
file: Dockerfile
push: true
tags: ${{ steps.meta-server.outputs.tags }}
labels: ${{ steps.meta-server.outputs.labels }}
- name: Instrumentation of the code then rebuilding the CVAT UI
run: |
yarn --frozen-lockfile
yarn run coverage
- name: CVAT UI. Build and push
uses: docker/build-push-action@v3
with:
cache-from: type=local,src=/tmp/cvat_cache_ui
context: .
file: Dockerfile.ui
push: true
tags: ${{ steps.meta-ui.outputs.tags }}
labels: ${{ steps.meta-ui.outputs.labels }}
unit_testing:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Getting CVAT UI cache from the default branch
uses: actions/cache@v3
with:
path: /tmp/cvat_cache_ui
key: ${{ runner.os }}-build-ui-${{ needs.search_cache.outputs.sha }}
- name: Getting CVAT Logstash cache from the default branch
uses: actions/cache@v3
with:
path: /tmp/cvat_cache_logstash
key: ${{ runner.os }}-build-logstash-${{ needs.search_cache.outputs.sha }}
- name: Getting CVAT Elasticsearch cache from the default branch
uses: actions/cache@v3
with:
path: /tmp/cvat_cache_elasticsearch
key: ${{ runner.os }}-build-elasticsearch-${{ needs.search_cache.outputs.sha }}
- name: Building CVAT UI image
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile.ui
cache-from: type=local,src=/tmp/cvat_cache_ui
tags: cvat/ui:latest
load: true
- name: Building CVAT Logstash image
uses: docker/build-push-action@v2
with:
context: ./components/analytics/logstash/
file: ./components/analytics/logstash/Dockerfile
cache-from: type=local,src=/tmp/cvat_cache_logstash
build-args: ELK_VERSION=6.8.23
tags: cvat_logstash
load: true
- name: Building CVAT Elasticsearch image
uses: docker/build-push-action@v2
with:
context: ./components/analytics/elasticsearch/
file: ./components/analytics/elasticsearch/Dockerfile
cache-from: type=local,src=/tmp/cvat_cache_elasticsearch
build-args: ELK_VERSION=6.8.23
tags: cvat_elasticsearch
load: true
- name: CVAT server. Extract metadata (tags, labels) for Docker
id: meta-server
uses: docker/metadata-action@master
with:
images: ${{ secrets.DOCKERHUB_CI_WORKSPACE }}/${{ env.SERVER_IMAGE_TEST_REPO }}
tags:
type=raw,value=nightly
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_CI_USERNAME }}
password: ${{ secrets.DOCKERHUB_CI_TOKEN }}
- name: Pull CVAT server image
run: |
docker pull ${{ steps.meta-server.outputs.tags }}
docker tag ${{ steps.meta-server.outputs.tags }} cvat/server
- name: OPA tests
run: |
curl -L -o opa https://openpolicyagent.org/downloads/v0.34.2/opa_linux_amd64_static
chmod +x ./opa
./opa test cvat/apps/iam/rules
- name: REST API and SDK tests
run: |
docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \
--entrypoint /bin/bash -u root cvat/server \
-c 'python manage.py spectacular --file /transfer/schema.yml'
pip3 install --user -r cvat-sdk/gen/requirements.txt
cd cvat-sdk/
gen/generate.sh
cd ..
pip3 install --user cvat-sdk/
pip3 install --user cvat-cli/
pip3 install --user -r tests/python/requirements.txt
pytest tests/python/
pytest tests/python/ --stop-services
- name: Unit tests
env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
CONTAINER_COVERAGE_DATA_DIR: "/coverage_data"
run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d cvat_opa
max_tries=12
while [[ $(curl -s -o /dev/null -w "%{http_code}" localhost:8181/health) != "200" && max_tries -gt 0 ]]; do (( max_tries-- )); sleep 5; done
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 && 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 'yarn --frozen-lockfile --ignore-scripts && yarn workspace cvat-core run test'
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml down -v
- name: Uploading code coverage results as an artifact
uses: actions/upload-artifact@v2
with:
name: coverage_results
path: |
${{ github.workspace }}/lcov.info
${{ github.workspace }}/.coverage
e2e_testing:
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
specs: ['actions_tasks', 'actions_tasks2', 'actions_tasks3',
'actions_objects', 'actions_objects2', 'actions_users',
'actions_projects_models', 'actions_organizations', 'canvas3d_functionality',
'canvas3d_functionality_2', 'issues_prs', 'issues_prs2']
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16.x'
- name: Build CVAT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_CI_USERNAME }}
password: ${{ secrets.DOCKERHUB_CI_TOKEN }}
- name: CVAT server. Extract metadata (tags, labels) for Docker
id: meta-server
uses: docker/metadata-action@master
with:
images: ${{ secrets.DOCKERHUB_CI_WORKSPACE }}/${{ env.SERVER_IMAGE_TEST_REPO }}
tags:
type=raw,value=nightly
- name: CVAT UI. Extract metadata (tags, labels) for Docker
id: meta-ui
uses: docker/metadata-action@master
with:
images: ${{ secrets.DOCKERHUB_CI_USERNAME }}/${{ env.UI_IMAGE_TEST_REPO }}
tags:
type=raw,value=nightly
- name: Pull CVAT UI image
run: |
docker pull ${{ steps.meta-server.outputs.tags }}
docker tag ${{ steps.meta-server.outputs.tags }} cvat/server
docker pull ${{ steps.meta-ui.outputs.tags }}
docker tag ${{ steps.meta-ui.outputs.tags }} cvat/ui
- name: Run CVAT instance
run: |
docker-compose \
-f docker-compose.yml \
-f docker-compose.dev.yml \
-f tests/docker-compose.file_share.yml \
-f components/serverless/docker-compose.serverless.yml up -d
- name: Waiting for server
id: wait-server
env:
API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: |
max_tries=60
status_code=$(curl -s -o /tmp/server_response -w "%{http_code}" ${API_ABOUT_PAGE})
while [[ $status_code != "401" && max_tries -gt 0 ]]
do
echo Number of attempts left: $max_tries
echo Status code of response: $status_code
sleep 5
status_code=$(curl -s -o /tmp/server_response -w "%{http_code}" ${API_ABOUT_PAGE})
(( max_tries-- ))
done
if [[ $status_code != "401" ]]; then
echo Response from server is incorrect, output:
cat /tmp/server_response
fi
echo ::set-output name=status_code::${status_code}
- name: Fail on bad response from server
if: steps.wait-server.outputs.status_code != '401'
uses: actions/github-script@v3
with:
script: |
core.setFailed('Workflow failed: incorrect response from server. See logs artifact to get more info')
- name: Add user for tests
env:
DJANGO_SU_NAME: "admin"
DJANGO_SU_EMAIL: "admin@localhost.company"
DJANGO_SU_PASSWORD: "12qwaszx"
API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f ./tests/docker-compose.email.yml -f tests/docker-compose.file_share.yml -f components/serverless/docker-compose.serverless.yml up -d --build
/bin/bash -c 'while [[ $(curl -s -o /dev/null -w "%{http_code}" ${API_ABOUT_PAGE}) != "401" ]]; do sleep 5; done'
docker exec -i cvat /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
- name: End-to-end testing
- name: Run tests
run: |
cd ./tests
npm ci
npm run cypress:run:firefox
yarn --frozen-lockfile
shopt -s extglob
if [[ ${{ matrix.specs }} == canvas3d_* ]]; then
npx cypress run \
--headed \
--browser chrome \
--config-file cypress_canvas3d.json \
--spec 'cypress/integration/${{ matrix.specs }}/**/*.js,cypress/integration/remove_users_tasks_projects_organizations.js'
mv ./.nyc_output/out.json ./.nyc_output/out_${{ matrix.specs }}.json
else
npx cypress run \
--browser chrome \
--spec 'cypress/integration/${{ matrix.specs }}/**/*.js,cypress/integration/remove_users_tasks_projects_organizations.js'
mv ./.nyc_output/out.json ./.nyc_output/out_${{ matrix.specs }}.json
fi
- name: Creating a log file from "cvat" container logs
if: failure()
run: |
docker logs cvat > ${{ github.workspace }}/tests/cvat.log
- 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: Uploading "cvat" container logs as an artifact
if: failure()
uses: actions/upload-artifact@v2
with:
name: cvat_container_logs
path: ${{ github.workspace }}/tests/cvat.log
- name: Uploading code coverage results as an artifact
uses: actions/upload-artifact@v2
with:
name: coverage_results
path: ${{ github.workspace }}/tests/.nyc_output
coveralls:
runs-on: ubuntu-latest
needs: [unit_testing, e2e_testing]
steps:
- uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: CVAT server. Extract metadata (tags, labels) for Docker
id: meta-server
uses: docker/metadata-action@master
with:
images: ${{ secrets.DOCKERHUB_CI_WORKSPACE }}/${{ env.SERVER_IMAGE_TEST_REPO }}
tags:
type=raw,value=nightly
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_CI_USERNAME }}
password: ${{ secrets.DOCKERHUB_CI_TOKEN }}
- name: Pull CVAT server image
run: |
docker pull ${{ steps.meta-server.outputs.tags }}
docker tag ${{ steps.meta-server.outputs.tags }} cvat/server
- name: Downloading coverage results
uses: actions/download-artifact@v2
with:
name: coverage_results
- name: Combining coverage results
run: |
mkdir -p ./nyc_output_tmp
mv ./out_*.json ./nyc_output_tmp
mkdir -p ./.nyc_output
yarn --frozen-lockfile
npx nyc merge ./nyc_output_tmp ./.nyc_output/out.json
- name: Sending results to Coveralls
env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
CONTAINER_COVERAGE_DATA_DIR: "/coverage_data"
COVERALLS_SERVICE_NAME: github
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npx nyc report --reporter=text-lcov >> ${HOST_COVERAGE_DATA_DIR}/lcov.info
docker-compose \
-f docker-compose.yml \
-f docker-compose.dev.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.dev.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'

@ -1,7 +1,7 @@
name: Linter
name: StyleLint
on: pull_request
jobs:
StyleLint:
Linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -26,7 +26,7 @@ jobs:
done
if [[ ! -z $CHANGED_FILES ]]; then
npm ci
yarn install --frozen-lockfile
mkdir -p stylelint_report
echo "StyleLint version: "$(npx stylelint --version)

3
.gitignore vendored

@ -4,7 +4,7 @@
/share/
/static/
/db.sqlite3
/.env
/.*env*
/keys
/logs
/profiles
@ -44,6 +44,7 @@ yarn-error.log*
/site/resources/
/site/node_modules/
/site/tech-doc-hugo
/site/.hugo_build.lock
# Ignore all the installed packages
node_modules

File diff suppressed because it is too large Load Diff

@ -0,0 +1,2 @@
cvat-sdk/docs/
cvat-sdk/README.md

@ -169,7 +169,82 @@
"--settings",
"cvat.settings.testing",
"cvat/apps",
"utils/cli"
"cvat-cli/"
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {},
"console": "internalConsole"
},
{
"name": "server: REST API tests",
"type": "python",
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"module": "pytest",
"args": [
"tests/python/rest_api/"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
},
{
"name": "sdk: tests",
"type": "python",
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"module": "pytest",
"args": [
"tests/python/sdk/"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
},
{
"name": "cli: tests",
"type": "python",
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"module": "pytest",
"args": [
"tests/python/cli/"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
},
{
"name": "api client: Postprocess generator output",
"type": "python",
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"program": "${workspaceFolder}/cvat-sdk/gen/postprocess.py",
"args": [
"--schema", "${workspaceFolder}/cvat-sdk/schema/schema.yml",
"--input-path", "${workspaceFolder}/cvat-sdk/cvat_sdk/"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
},
{
"name": "server: Generate REST API Schema",
"type": "python",
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"spectacular",
"--file",
"schema.yml"
],
"django": true,
"cwd": "${workspaceFolder}",
@ -215,4 +290,4 @@
]
}
]
}
}

@ -7,10 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## \[2.2.0] - Unreleased
### Added
- TDB
- Added ability to delete frames from a job based on (<https://github.com/openvinotoolkit/cvat/pull/4194>)
- Support of attributes returned by serverless functions based on (<https://github.com/openvinotoolkit/cvat/pull/4506>)
- Project/task backups uploading via chunk uploads
- Fixed UX bug when jobs pagination is reset after changing a job
- Progressbars in CLI for file uploading and downloading
- `utils/cli` changed to `cvat-cli` package
- Support custom file name for backup
- Possibility to display tags on frame
- Support source and target storages (server part)
- Tests for import/export annotation, dataset, backup from/to cloud storage
- Added Python SDK package (`cvat-sdk`)
- Previews for jobs
- Documentation for LDAP authentication (<https://github.com/cvat-ai/cvat/pull/39>)
- OpenCV.js caching and autoload (<https://github.com/cvat-ai/cvat/pull/30>)
- Publishing dev version of CVAT docker images (<https://github.com/cvat-ai/cvat/pull/53>)
- Support of Human Pose Estimation, Facial Landmarks (and similar) use-cases, new shape type: Skeleton (<https://github.com/cvat-ai/cvat/pull/1>)
- Added helm chart support for serverless functions and analytics (<https://github.com/cvat-ai/cvat/pull/110>)
### Changed
- TDB
- Bumped nuclio version to 1.8.14
- Simplified running REST API tests. Extended CI-nightly workflow
- REST API tests are partially moved to Python SDK (`users`, `projects`, `tasks`)
- cvat-ui: Improve UI/UX on label, create task and create project forms (<https://github.com/cvat-ai/cvat/pull/7>)
- Removed link to OpenVINO documentation (<https://github.com/cvat-ai/cvat/pull/35>)
- Clarified meaning of chunking for videos
### Deprecated
- TDB
@ -19,7 +40,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- TDB
### Fixed
- TDB
- Task creation progressbar bug
- Removed Python dependency ``open3d`` which brought different issues to the building process
- Analytics not accessible when https is enabled
- Dataset import in an organization
- Updated minimist npm package to v1.2.6
- Request Status Code 500 "StopIteration" when exporting dataset
- Generated OpenAPI schema for several endpoints
- Annotation window might have top offset if try to move a locked object
- Image search in cloud storage (<https://github.com/cvat-ai/cvat/pull/8>)
- Reset password functionality (<https://github.com/cvat-ai/cvat/pull/52>)
- Creating task with cloud storage data (<https://github.com/cvat-ai/cvat/pull/116>)
### Security
- TDB
@ -44,6 +75,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Unable to upload annotations (<https://github.com/openvinotoolkit/cvat/pull/4513>)
- Fix build dependencies for Siammask (<https://github.com/openvinotoolkit/cvat/pull/4486>)
- Bug: Exif orientation information handled incorrectly (<https://github.com/openvinotoolkit/cvat/pull/4529>)
- Fixed build of retinanet function image (<https://github.com/cvat-ai/cvat/pull/54>)
## \[2.0.0] - 2022-03-04
### Added

@ -139,15 +139,16 @@ COPY --from=build-image /tmp/openh264/openh264*.tar.gz /tmp/ffmpeg/ffmpeg*.tar.g
# Copy python virtual environment and FFmpeg binaries from build-image
COPY --from=build-image /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:${PATH}"
ENV NUMPROCS=1
COPY --from=build-image /opt/ffmpeg /usr
# Install and initialize CVAT, copy all necessary files
COPY --chown=${USER} components /tmp/components
COPY --chown=${USER} supervisord/ ${HOME}/supervisord
COPY --chown=${USER} ssh ${HOME}/.ssh
COPY --chown=${USER} supervisord.conf mod_wsgi.conf wait-for-it.sh manage.py ${HOME}/
COPY --chown=${USER} cvat/ ${HOME}/cvat
COPY --chown=${USER} mod_wsgi.conf wait-for-it.sh manage.py ${HOME}/
COPY --chown=${USER} utils/ ${HOME}/utils
COPY --chown=${USER} tests/ ${HOME}/tests
COPY --chown=${USER} cvat/ ${HOME}/cvat
# RUN all commands below as 'django' user
USER ${USER}
@ -157,3 +158,4 @@ RUN mkdir data share media keys logs /tmp/supervisord
EXPOSE 8080
ENTRYPOINT ["/usr/bin/supervisord"]
CMD ["-c", "supervisord/all.conf"]

@ -1,4 +1,4 @@
FROM openvino/cvat_server
FROM cvat/server
ENV DJANGO_CONFIGURATION=testing
USER root
@ -19,17 +19,19 @@ RUN apt-get update && \
google-chrome-stable \
nodejs \
&& \
npm install --global yarn && \
rm -rf /var/lib/apt/lists/*;
COPY cvat/requirements/ /tmp/requirements/
COPY cvat/requirements/ /tmp/cvat/requirements/
RUN DATUMARO_HEADLESS=1 python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt && \
RUN DATUMARO_HEADLESS=1 python3 -m pip install --no-cache-dir -r /tmp/cvat/requirements/${DJANGO_CONFIGURATION}.txt && \
python3 -m pip install --no-cache-dir coveralls
RUN gem install coveralls-lcov
COPY utils ${HOME}/utils
COPY cvat-core ${HOME}/cvat-core
COPY cvat-data ${HOME}/cvat-data
COPY package.json ${HOME}/
COPY yarn.lock ${HOME}/
COPY tests ${HOME}/tests
COPY .coveragerc .

@ -16,16 +16,17 @@ ENV TERM=xterm \
LC_ALL='C.UTF-8'
# Install dependencies
COPY package*.json /tmp/
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/
COPY package.json /tmp/
COPY yarn.lock /tmp/
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/
# Install common dependencies
WORKDIR /tmp/
RUN npm ci --ignore-scripts
RUN yarn install --ignore-scripts --frozen-lockfile
# Build source code
COPY cvat-data/ /tmp/cvat-data/
@ -33,9 +34,9 @@ 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:cvat-ui
RUN yarn run build:cvat-ui
FROM nginx:mainline-alpine
FROM nginx:1.23.1-alpine
# Replace default.conf configuration to remove unnecessary rules
COPY cvat-ui/react_nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=cvat-ui /tmp/cvat-ui/dist /usr/share/nginx/html/

@ -1,31 +1,22 @@
MIT License
Copyright (C) 2018-2022 Intel Corporation
 
Copyright (c) 2018-2022 Intel Corporation
Copyright (c) 2022 CVAT.ai Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom
the Software is furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
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).
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES 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.

@ -1,34 +1,98 @@
![CVAT logo](site/content/en/images/cvat_poster_with_name.png)
# Computer Vision Annotation Tool (CVAT)
<a href="https://www.producthunt.com/posts/cvat-computer-vision-annotation-tool?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cvat&#0045;computer&#0045;vision&#0045;annotation&#0045;tool" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=353415&theme=light" alt="CVAT&#0032;&#0032;Computer&#0032;Vision&#0032;Annotation&#0032;Tool - The&#0032;open&#0032;data&#0032;annotation&#0032;platform&#0032;for&#0032;AI | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
[![CI][ci-img]][ci-url]
[![Gitter chat][gitter-img]][gitter-url]
[![Discord][discord-img]][discord-url]
[![Coverage Status][coverage-img]][coverage-url]
[![server pulls][docker-server-pulls-img]][docker-server-image-url]
[![ui pulls][docker-ui-pulls-img]][docker-ui-image-url]
[![DOI][doi-img]][doi-url]
CVAT is free, online, interactive video and image annotation
tool for computer vision. It is being used by our team to
annotate million of objects with different properties. Many UI
and UX decisions are based on feedbacks from professional data
annotation team. Try it online [cvat.org](https://cvat.org).
CVAT is an interactive video and image annotation
tool for computer vision. It is used by tens of thousands of users and
companies around the world. CVAT is free and open-source.
**A new repo**: CVAT core team moved the active development of the tool
to this new repository. Our mission is to help developers, companies and
organizations around the world to solve real problems using the Data-centric
AI approach.
Start using CVAT online for free: [cvat.ai](https://cvat.ai). Or set it up as a self-hosted solution:
[read here](https://cvat-ai.github.io/cvat/docs/administration/basics/installation/).
![CVAT screenshot](site/content/en/images/cvat.jpg)
![CVAT screencast](site/content/en/images/cvat-ai-screencast.gif)
## Documentation
## Quick start ⚡
- [Contributing](https://openvinotoolkit.github.io/cvat/docs/contributing/)
- [Installation guide](https://openvinotoolkit.github.io/cvat/docs/administration/basics/installation/)
- [Manual](https://openvinotoolkit.github.io/cvat/docs/manual/)
- [Django REST API documentation](https://openvinotoolkit.github.io/cvat/docs/administration/basics/rest_api_guide/)
- [Datumaro dataset framework](https://github.com/openvinotoolkit/datumaro/blob/develop/README.md)
- [Command line interface](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/cli/)
- [XML annotation format](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/xml_format/)
- [AWS Deployment Guide](https://openvinotoolkit.github.io/cvat/docs/administration/basics/aws-deployment-guide/)
- [Frequently asked questions](https://openvinotoolkit.github.io/cvat/docs/faq/)
- [Questions](#questions)
- [Installation guide](https://cvat-ai.github.io/cvat/docs/administration/basics/installation/)
- [Manual](https://cvat-ai.github.io/cvat/docs/manual/)
- [Contributing](https://cvat-ai.github.io/cvat/docs/contributing/)
- [Django REST API documentation](https://cvat-ai.github.io/cvat/docs/administration/basics/rest_api_guide/)
- [Datumaro dataset framework](https://github.com/cvat-ai/datumaro/blob/develop/README.md)
- [Command line interface](https://cvat-ai.github.io/cvat/docs/manual/advanced/cli/)
- [XML annotation format](https://cvat-ai.github.io/cvat/docs/manual/advanced/xml_format/)
- [AWS Deployment Guide](https://cvat-ai.github.io/cvat/docs/administration/basics/aws-deployment-guide/)
- [Frequently asked questions](https://cvat-ai.github.io/cvat/docs/faq/)
- [Where to ask questions](#where-to-ask-questions)
## Screencasts
## Partners ❤️
CVAT is used by teams all over the world. If you use us, please drop us a line at
[contact@cvat.ai](mailto:contact+github@cvat.ai) - and we'll add you to this list.
- [ATLANTIS](https://github.com/smhassanerfani/atlantis), an open-source dataset for semantic segmentation
of waterbody images, depeloped by [iWERS](http://ce.sc.edu/iwers/) group in the
Department of Civil and Environmental Engineering at University of South Carolina, is using CVAT.
For developing a semantic segmentation dataset using CVAT, please check
[ATLANTIS published article](https://www.sciencedirect.com/science/article/pii/S1364815222000391),
[ATLANTIS Development Kit](https://github.com/smhassanerfani/atlantis/tree/master/adk)
and [annotation tutorial videos](https://www.youtube.com/playlist?list=PLIfLGY-zZChS5trt7Lc3MfNhab7OWl2BR).
- [Onepanel](https://github.com/onepanelio/core) is an open-source
vision AI platform that fully integrates CVAT with scalable data processing
and parallelized training pipelines.
- [DataIsKey](https://dataiskey.eu/annotation-tool/) uses CVAT as their prime data labeling tool
to offer annotation services for projects of any size.
- [Human Protocol](https://hmt.ai) uses CVAT as a way of adding annotation service to the Human Protocol.
- [Cogito Tech LLC](https://bit.ly/3klT0h6), a Human-in-the-Loop Workforce Solutions Provider, used CVAT
in annotation of about 5,000 images for a brand operating in the fashion segment.
- [FiftyOne](https://fiftyone.ai) is an open-source dataset curation and model analysis
tool for visualizing, exploring, and improving computer vision datasets and models that is
[tightly integrated](https://voxel51.com/docs/fiftyone/integrations/cvat.html) with CVAT
for annotation and label refinement.
## CVAT online: [cvat.ai](https://cvat.ai)
This is an online version of CVAT. It's free, efficient, and easy to use.
[cvat.ai](https://cvat.ai) runs the latest version of the tool. You can create up
to 10 tasks there and upload up to 500Mb of data to annotate. It will only be
visible to you or people you assign to it.
For now, it does not have [analytics features](https://cvat-ai.github.io/cvat/docs/administration/advanced/analytics/)
like management and monitoring the data annotation team.
We plan to enhance [cvat.ai](https://cvat.ai) with new powerful features. Stay tuned!
## Prebuilt Docker images 🐳
Prebuilt docker images are the easiest way to start using CVAT locally. They are available on Docker Hub:
- [cvat/server](https://hub.docker.com/r/cvat/server)
- [cvat/ui](https://hub.docker.com/r/cvat/ui)
The images have been downloaded more than 1M times so far.
## REST API
CVAT has a REST API: [documentation](https://cvat-ai.github.io/cvat/docs/administration/basics/rest_api_guide/).
Its current version is `2.0-alpha`. We focus on its improvement, and the API may be changed in the next releases.
## Screencasts 🎦
Here are some screencasts showing how to use CVAT.
- [Introduction](https://youtu.be/JERohTFp-NI)
- [Annotation mode](https://youtu.be/vH_639N67HI)
@ -42,95 +106,73 @@ annotation team. Try it online [cvat.org](https://cvat.org).
## Supported annotation formats
Format selection is possible after clicking on the Upload annotation and Dump
annotation buttons. [Datumaro](https://github.com/openvinotoolkit/datumaro)
CVAT supports multiple annotation formats. You can select the format after clicking the "Upload annotation" and "Dump
annotation" buttons. [Datumaro](https://github.com/cvat-ai/datumaro)
dataset framework allows additional dataset transformations via its command
line tool and Python library.
For more information about supported formats look at the
[documentation](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/formats/).
For more information about the supported formats, look at the
[documentation](https://cvat-ai.github.io/cvat/docs/manual/advanced/formats/).
<!--lint disable maximum-line-length-->
| Annotation format | Import | Export |
| --------------------------------------------------------------------------------------------------------- | ------ | ------ |
| [CVAT for images](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/xml_format/#annotation) | X | X |
| [CVAT for a video](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/xml_format/#interpolation) | X | X |
| [Datumaro](https://github.com/openvinotoolkit/datumaro) | | X |
| [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| Segmentation masks from [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| [YOLO](https://pjreddie.com/darknet/yolo/) | X | X |
| [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X |
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tfrecord) | X | X |
| [MOT](https://motchallenge.net/) | X | X |
| [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | X | X |
| [ImageNet](http://www.image-net.org) | X | X |
| [CamVid](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) | X | X |
| [WIDER Face](http://shuoyang1213.me/WIDERFACE/) | X | X |
| [VGGFace2](https://github.com/ox-vgg/vgg_face2) | X | X |
| [Market-1501](https://www.aitribune.com/dataset/2018051063) | X | X |
| [ICDAR13/15](https://rrc.cvc.uab.es/?ch=2) | X | X |
| [Open Images V6](https://storage.googleapis.com/openimages/web/index.html) | X | X |
| [Cityscapes](https://www.cityscapes-dataset.com/login/) | X | X |
| [KITTI](http://www.cvlibs.net/datasets/kitti/) | X | X |
| [LFW](http://vis-www.cs.umass.edu/lfw/) | X | X |
| [CVAT for images](https://cvat-ai.github.io/cvat/docs/manual/advanced/xml_format/#annotation) | ✔️ | ✔️ |
| [CVAT for a video](https://cvat-ai.github.io/cvat/docs/manual/advanced/xml_format/#interpolation) | ✔️ | ✔️ |
| [Datumaro](https://github.com/cvat-ai/datumaro) | | ✔️ |
| [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | ✔️ | ✔️ |
| Segmentation masks from [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | ✔️ | ✔️ |
| [YOLO](https://pjreddie.com/darknet/yolo/) | ✔️ | ✔️ |
| [MS COCO Object Detection](http://cocodataset.org/#format-data) | ✔️ | ✔️ |
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tfrecord) | ✔️ | ✔️ |
| [MOT](https://motchallenge.net/) | ✔️ | ✔️ |
| [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | ✔️ | ✔️ |
| [ImageNet](http://www.image-net.org) | ✔️ | ✔️ |
| [CamVid](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) | ✔️ | ✔️ |
| [WIDER Face](http://shuoyang1213.me/WIDERFACE/) | ✔️ | ✔️ |
| [VGGFace2](https://github.com/ox-vgg/vgg_face2) | ✔️ | ✔️ |
| [Market-1501](https://www.aitribune.com/dataset/2018051063) | ✔️ | ✔️ |
| [ICDAR13/15](https://rrc.cvc.uab.es/?ch=2) | ✔️ | ✔️ |
| [Open Images V6](https://storage.googleapis.com/openimages/web/index.html) | ✔️ | ✔️ |
| [Cityscapes](https://www.cityscapes-dataset.com/login/) | ✔️ | ✔️ |
| [KITTI](http://www.cvlibs.net/datasets/kitti/) | ✔️ | ✔️ |
| [LFW](http://vis-www.cs.umass.edu/lfw/) | ✔️ | ✔️ |
<!--lint enable maximum-line-length-->
## Deep learning serverless functions for automatic labeling
CVAT supports automatic labelling. It can speed up the annotation process
up to 10x. Here is a list of the algorithms we support, and the platforms they
can be ran on:
<!--lint disable maximum-line-length-->
| Name | Type | Framework | CPU | GPU |
| ------------------------------------------------------------------------------------------------------- | ---------- | ---------- | --- | --- |
| [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 | |
| [YOLO v5](/serverless/pytorch/ultralytics/yolov5/nuclio) | detector | PyTorch | X | |
| [SiamMask](/serverless/pytorch/foolwood/siammask/nuclio) | tracker | PyTorch | X | X |
| [f-BRS](/serverless/pytorch/saic-vul/fbrs/nuclio) | interactor | PyTorch | X | |
| [HRNet](/serverless/pytorch/saic-vul/hrnet/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/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow | X | X |
| [RetinaNet](serverless/pytorch/facebookresearch/detectron2/retinanet/nuclio) | detector | PyTorch | X | X |
| [Face Detection](/serverless/openvino/omz/intel/face-detection-0205/nuclio) | detector | OpenVINO | X | |
| [Deep Extreme Cut](/serverless/openvino/dextr/nuclio) | interactor | OpenVINO | ✔️ | |
| [Faster RCNN](/serverless/openvino/omz/public/faster_rcnn_inception_v2_coco/nuclio) | detector | OpenVINO | ✔️ | |
| [Mask RCNN](/serverless/openvino/omz/public/mask_rcnn_inception_resnet_v2_atrous_coco/nuclio) | detector | OpenVINO | ✔️ | |
| [YOLO v3](/serverless/openvino/omz/public/yolo-v3-tf/nuclio) | detector | OpenVINO | ✔️ | |
| [Object reidentification](/serverless/openvino/omz/intel/person-reidentification-retail-300/nuclio) | reid | OpenVINO | ✔️ | |
| [Semantic segmentation for ADAS](/serverless/openvino/omz/intel/semantic-segmentation-adas-0001/nuclio) | detector | OpenVINO | ✔️ | |
| [Text detection v4](/serverless/openvino/omz/intel/text-detection-0004/nuclio) | detector | OpenVINO | ✔️ | |
| [YOLO v5](/serverless/pytorch/ultralytics/yolov5/nuclio) | detector | PyTorch | ✔️ | |
| [SiamMask](/serverless/pytorch/foolwood/siammask/nuclio) | tracker | PyTorch | ✔️ | ✔️ |
| [f-BRS](/serverless/pytorch/saic-vul/fbrs/nuclio) | interactor | PyTorch | ✔️ | |
| [HRNet](/serverless/pytorch/saic-vul/hrnet/nuclio) | interactor | PyTorch | | ✔️ |
| [Inside-Outside Guidance](/serverless/pytorch/shiyinzhang/iog/nuclio) | interactor | PyTorch | ✔️ | |
| [Faster RCNN](/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | detector | TensorFlow | ✔️ | ✔️ |
| [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow | ✔️ | ✔️ |
| [RetinaNet](serverless/pytorch/facebookresearch/detectron2/retinanet/nuclio) | detector | PyTorch | ✔️ | ✔️ |
| [Face Detection](/serverless/openvino/omz/intel/face-detection-0205/nuclio) | detector | OpenVINO | ✔️ | |
<!--lint enable maximum-line-length-->
## Online demo: [cvat.org](https://cvat.org)
## License
This is an online demo with the latest version of the annotation tool.
Try it online without local installation. Only own or assigned tasks
are visible to users.
Disabled features:
- [Analytics: management and monitoring of data annotation team](https://openvinotoolkit.github.io/cvat/docs/administration/advanced/analytics/)
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
The current REST API version is `2.0-alpha`. We focus on its improvement and therefore
REST API may be changed in the next release.
## LICENSE
Code released under the [MIT License](https://opensource.org/licenses/MIT).
The code is 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).
@ -138,49 +180,24 @@ The exact steps on how FFmpeg was configured and compiled can be found in the [D
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
additional licenses. CVAT.ai Corporation is not responsible for obtaining any
such licenses, nor liable for any licensing fees due in
connection with your use of FFmpeg.
## Partners
- [ATLANTIS](https://github.com/smhassanerfani/atlantis) is an open-source dataset for semantic segmentation
of waterbody images, depevoped by [iWERS](http://ce.sc.edu/iwers/) group in the
Department of Civil and Environmental Engineering at University of South Carolina, using CVAT.
For developing a semantic segmentation dataset using CVAT, please check
[ATLANTIS published article](https://www.sciencedirect.com/science/article/pii/S1364815222000391),
[ATLANTIS Development Kit](https://github.com/smhassanerfani/atlantis/tree/master/adk)
and [annotation tutorial videos](https://www.youtube.com/playlist?list=PLIfLGY-zZChS5trt7Lc3MfNhab7OWl2BR).
- [Onepanel](https://github.com/onepanelio/core) is an open source
vision AI platform that fully integrates CVAT with scalable data processing
and parallelized training pipelines.
- [DataIsKey](https://dataiskey.eu/annotation-tool/) uses CVAT as their prime data labeling tool
to offer annotation services for projects of any size.
- [Human Protocol](https://hmt.ai) uses CVAT as a way of adding annotation service to the human protocol.
- [Cogito Tech LLC](https://bit.ly/3klT0h6), a Human-in-the-Loop Workforce Solutions Provider, used CVAT
in annotation of about 5,000 images for a brand operating in the fashion segment.
- [FiftyOne](https://fiftyone.ai) is an open-source dataset curation and model analysis
tool for visualizing, exploring, and improving computer vision datasets and models that is
[tightly integrated](https://voxel51.com/docs/fiftyone/integrations/cvat.html) with CVAT
for annotation and label refinement.
## Questions
## Where to ask questions
CVAT usage related questions or unclear concepts can be posted in our
[Gitter chat](https://gitter.im/opencv-cvat) for **quick replies** from
contributors and other users.
[Gitter chat][gitter-url]: you can post CVAT usage related questions there.
Typically they get answered fast by the core team or community. There you can also browse other common questions.
However, if you have a feature request or a bug report that can reproduced,
feel free to open an issue (with steps to reproduce the bug if it's a bug
report) on [GitHub\* issues](https://github.com/opencv/cvat/issues).
[Discord][discord-url] is the place to also ask questions or discuss any other stuff related to CVAT.
If you are not sure or just want to browse other users common questions,
[Gitter chat](https://gitter.im/opencv-cvat) is the way to go.
[GitHub issues](https://github.com/cvat-ai/cvat/issues): please post them for feature requests or bug reports.
If it's a bug, please add the steps to reproduce it.
Other ways to ask questions and get our support:
[\#cvat](https://stackoverflow.com/search?q=%23cvat) tag on StackOverflow is one more way to ask
questions and get our support.
- [\#cvat](https://stackoverflow.com/search?q=%23cvat) tag on StackOverflow\*
- [Forum on Intel Developer Zone](https://software.intel.com/en-us/forums/computer-vision)
[contact@cvat.ai](mailto:contact+github@cvat.ai): reach out to us with feedback, comments, or inquiries.
## Links
@ -191,15 +208,23 @@ Other ways to ask questions and get our support:
<!-- prettier-ignore-start -->
<!-- Badges -->
[docker-server-pulls-img]: https://img.shields.io/docker/pulls/openvino/cvat_server.svg?style=flat-square&label=server%20pulls
[docker-server-image-url]: https://hub.docker.com/r/openvino/cvat_server
[docker-ui-pulls-img]: https://img.shields.io/docker/pulls/openvino/cvat_ui.svg?style=flat-square&label=UI%20pulls
[docker-ui-image-url]: https://hub.docker.com/r/openvino/cvat_ui
[ci-img]: https://github.com/openvinotoolkit/cvat/workflows/CI/badge.svg?branch=develop
[ci-url]: https://github.com/openvinotoolkit/cvat/actions
[gitter-img]: https://badges.gitter.im/opencv-cvat/gitter.png
[docker-server-pulls-img]: https://img.shields.io/docker/pulls/cvat/server.svg?style=flat-square&label=server%20pulls
[docker-server-image-url]: https://hub.docker.com/r/cvat/server
[docker-ui-pulls-img]: https://img.shields.io/docker/pulls/cvat/ui.svg?style=flat-square&label=UI%20pulls
[docker-ui-image-url]: https://hub.docker.com/r/cvat/ui
[ci-img]: https://github.com/cvat-ai/cvat/workflows/CI/badge.svg?branch=develop
[ci-url]: https://github.com/cvat-ai/cvat/actions
[gitter-img]: https://img.shields.io/gitter/room/opencv-cvat/public?style=flat
[gitter-url]: https://gitter.im/opencv-cvat
[coverage-img]: https://coveralls.io/repos/github/openvinotoolkit/cvat/badge.svg?branch=develop
[coverage-url]: https://coveralls.io/github/openvinotoolkit/cvat?branch=develop
[coverage-img]: https://coveralls.io/repos/github/cvat-ai/cvat/badge.svg?branch=develop
[coverage-url]: https://coveralls.io/github/cvat-ai/cvat?branch=develop
[doi-img]: https://zenodo.org/badge/139156354.svg
[doi-url]: https://zenodo.org/badge/latestdoi/139156354
[discord-img]: https://img.shields.io/discord/1000789942802337834?label=discord
[discord-url]: https://discord.gg/fNR3eXfk6C

@ -13,36 +13,12 @@ be sure it can be reproduced in the supported version.
## Reporting a Vulnerability
If you have information about a security issue or vulnerability in the product, please
send an e-mail to [secure@intel.com](mailto:secure@intel.com). Encrypt sensitive information
using our PGP public key.
send an e-mail to [secure@cvat.ai](mailto:secure+github@cvat.ai).
Please provide as much information as possible, including:
- The products and versions affected
- Detailed description of the vulnerability
- Information on known exploits
- A member of the Intel Product Security Team will review your e-mail and contact you to
- A member of the CVAT.ai Product Security Team will review your e-mail and contact you to
collaborate on resolving the issue.
For more information on how Intel works to resolve security issues, see:
[Vulnerability handling guidelines](<https://www.intel.com/content/www/us/en/security-center/vulnerability-handling-guidelines.html>)
## Intel® Bug Bounty Program
Intel Corporation believes that working with skilled security researchers across the globe
is a crucial part of identifying and mitigating security vulnerabilities in Intel products.
Like other major technology companies, Intel incentivizes security researchers to report
security vulnerabilities in Intel products to us to enable a coordinated response. To
encourage closer collaboration with the security research community on these kinds of issues,
Intel created its Bug Bounty Program.
If you believe you've found a security vulnerability in an Intel product or technology, we
encourage you to notify us through our program and work with us to mitigate and to coordinate
disclosure of the vulnerability.
[Intel® Bug Bounty Program Terms](<https://www.intel.com/content/www/us/en/security-center/bug-bounty-program.html>)
Watch this video, [So You Found a Vulnerability](<https://www.intel.com/content/www/us/en/security-center/so-you-found-a-vulnerability.html>),
to find out what you can expect when participating in the Intel® Bug Bounty Program.

@ -23,13 +23,15 @@ services:
args:
ELK_VERSION: 6.8.23
depends_on: ['elasticsearch']
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
restart: always
cvat_kibana_setup:
container_name: cvat_kibana_setup
image: openvino/cvat_server
image: cvat/server:${CVAT_VERSION:-latest}
volumes: ['./components/analytics/kibana:/home/django/kibana:ro']
depends_on: ['cvat']
depends_on: ['cvat_server']
working_dir: '/home/django'
networks:
- cvat
@ -72,7 +74,7 @@ services:
depends_on: ['elasticsearch']
restart: always
cvat:
cvat_server:
environment:
DJANGO_LOG_SERVER_HOST: logstash
DJANGO_LOG_SERVER_PORT: 8080

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

@ -1,4 +1,4 @@
# Copyright (C) 2021 Intel Corporation
# Copyright (C) 2021-2022 Intel Corporation
#
# SPDX-License-Identifier: MIT

@ -8,11 +8,20 @@ http:
- strip-prefix
service: kibana
rule: Host(`{{ env "CVAT_HOST" }}`) && PathPrefix(`/analytics`)
kibana_https:
entryPoints:
- websecure
middlewares:
- analytics-auth
- strip-prefix
service: kibana
tls: {}
rule: Host(`{{ env "CVAT_HOST" }}`) && PathPrefix(`/analytics`)
middlewares:
analytics-auth:
forwardauth:
address: http://cvat:8080/analytics
address: http://cvat_server:8080/analytics
authRequestHeaders:
- "Cookie"
- "Authorization"

@ -1,3 +1,4 @@
queue.type: persisted
queue.max_bytes: 1gb
queue.checkpoint.writes: 20
http.host: 0.0.0.0

@ -2,7 +2,7 @@ version: '3.3'
services:
nuclio:
container_name: nuclio
image: quay.io/nuclio/dashboard:1.5.16-amd64
image: quay.io/nuclio/dashboard:1.8.14-amd64
restart: always
networks:
- cvat
@ -18,7 +18,7 @@ services:
ports:
- '8070:8070'
cvat:
cvat_server:
environment:
CVAT_SERVERLESS: 1

@ -1,13 +1,8 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
const globalConfig = require('../.eslintrc.js');
module.exports = {
env: {
node: true,
},
ignorePatterns: [
'.eslintrc.js',
'webpack.config.js',
@ -15,31 +10,7 @@ module.exports = {
'dist/**',
],
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 6,
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint'],
extends: ['plugin:@typescript-eslint/recommended', 'airbnb-typescript/base'],
rules: {
...globalConfig.rules,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/indent': ['error', 4],
'@typescript-eslint/lines-between-class-members': 0,
'@typescript-eslint/no-explicit-any': [0],
'@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }],
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/ban-types': [
'error',
{
types: {
'{}': false, // TODO: try to fix with Record<string, unknown>
object: false, // TODO: try to fix with Record<string, unknown>
Function: false, // TODO: try to fix somehow
},
},
],
},
};

@ -9,17 +9,17 @@ It presents a canvas to viewing, drawing and editing of annotations.
If you make changes in this package, please do following:
- After not important changes (typos, backward compatible bug fixes, refactoring) do: `npm version patch`
- After changing API (backward compatible new features) do: `npm version minor`
- After changing API (changes that break backward compatibility) do: `npm version major`
- After not important changes (typos, backward compatible bug fixes, refactoring) do: `yarn version --patch`
- After changing API (backward compatible new features) do: `yarn version --minor`
- After changing API (changes that break backward compatibility) do: `yarn 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
yarn run build
yarn run build --mode=development # without a minification
```
## Using
@ -62,8 +62,19 @@ Canvas itself handles:
}
interface Configuration {
displayAllText?: boolean;
undefinedAttrValue?: string;
smoothImage?: boolean;
autoborders?: boolean;
displayAllText?: boolean;
textFontSize?: number;
textPosition?: 'auto' | 'center';
textContent?: string;
undefinedAttrValue?: string;
showProjections?: boolean;
forceDisableEditing?: boolean;
intelligentPolygonCrop?: boolean;
forceFrameUpdate?: boolean;
creationOpacity?: number;
CSSImageFilter?: string;
}
interface DrawData {

@ -1,169 +0,0 @@
{
"name": "cvat-canvas",
"version": "2.13.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-canvas",
"version": "2.13.2",
"license": "MIT",
"dependencies": {
"@types/polylabel": "^1.0.5",
"polylabel": "^1.1.0",
"svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.4",
"svg.js": "2.7.1",
"svg.resize.js": "1.4.3",
"svg.select.js": "3.0.1"
}
},
"node_modules/@types/polylabel": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.0.5.tgz",
"integrity": "sha512-gnaNmo1OJiYNBFAZMZdqLZ3hKx2ee4ksAzqhKWBxuQ61PmhINHMcvIqsGmyCD1WFKCkwRt9NFhMSmKE6AgYY+w=="
},
"node_modules/polylabel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz",
"integrity": "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==",
"dependencies": {
"tinyqueue": "^2.0.3"
}
},
"node_modules/svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
"dependencies": {
"svg.js": "^2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.draw.js": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/svg.draw.js/-/svg.draw.js-2.0.4.tgz",
"integrity": "sha512-NMbecB0vg11AP76B0aLfI2cX7g9WurPM8x3yKxuJ9feM1vkI1GVjWZZjWpo3mkEzB1UJ8pKngaPaUCIOGi8uUA==",
"dependencies": {
"svg.js": "2.x.x"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
},
"node_modules/svg.resize.js": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
"dependencies": {
"svg.js": "^2.6.5",
"svg.select.js": "^2.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.resize.js/node_modules/svg.select.js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
"dependencies": {
"svg.js": "^2.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.select.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
"dependencies": {
"svg.js": "^2.6.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/tinyqueue": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
}
},
"dependencies": {
"@types/polylabel": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.0.5.tgz",
"integrity": "sha512-gnaNmo1OJiYNBFAZMZdqLZ3hKx2ee4ksAzqhKWBxuQ61PmhINHMcvIqsGmyCD1WFKCkwRt9NFhMSmKE6AgYY+w=="
},
"polylabel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz",
"integrity": "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==",
"requires": {
"tinyqueue": "^2.0.3"
}
},
"svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
"requires": {
"svg.js": "^2.0.1"
}
},
"svg.draw.js": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/svg.draw.js/-/svg.draw.js-2.0.4.tgz",
"integrity": "sha512-NMbecB0vg11AP76B0aLfI2cX7g9WurPM8x3yKxuJ9feM1vkI1GVjWZZjWpo3mkEzB1UJ8pKngaPaUCIOGi8uUA==",
"requires": {
"svg.js": "2.x.x"
}
},
"svg.js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
},
"svg.resize.js": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
"requires": {
"svg.js": "^2.6.5",
"svg.select.js": "^2.1.2"
},
"dependencies": {
"svg.select.js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
"requires": {
"svg.js": "^2.2.5"
}
}
}
},
"svg.select.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
"requires": {
"svg.js": "^2.6.5"
}
},
"tinyqueue": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
}
}
}

@ -1,13 +1,13 @@
{
"name": "cvat-canvas",
"version": "2.13.2",
"version": "2.15.0",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.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",
"author": "CVAT.ai",
"license": "MIT",
"browserslist": [
"Chrome >= 63",

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -10,6 +10,12 @@
stroke-opacity: 1;
}
g.cvat_canvas_shape {
> circle {
fill-opacity: 1;
}
}
polyline.cvat_canvas_shape {
fill-opacity: 0;
}
@ -120,7 +126,6 @@ polyline.cvat_canvas_shape_splitting {
@extend .cvat_shape_drawing_opacity;
fill: white;
stroke: black;
}
.cvat_canvas_zoom_selection {
@ -134,6 +139,12 @@ polyline.cvat_canvas_shape_splitting {
stroke-dasharray: 5;
}
g.cvat_canvas_shape_occluded {
> rect {
stroke-dasharray: 5;
}
}
.svg_select_points_rot {
fill: white;
}
@ -226,6 +237,12 @@ polyline.cvat_canvas_shape_splitting {
}
}
.cvat_canvas_skeleton_wrapping_rect {
// wrapping rect must not apply transform attribute from selectize.js
// otherwise it rotated twice, because we apply the same rotation value to parent element (skeleton itself)
transform: none !important;
}
.cvat_canvas_pixelized {
image-rendering: optimizeSpeed; /* Legal fallback */
image-rendering: -moz-crisp-edges; /* Firefox */
@ -237,6 +254,10 @@ polyline.cvat_canvas_shape_splitting {
-ms-interpolation-mode: nearest-neighbor; /* IE8+ */
}
.cvat_canvas_removed_image {
filter: saturate(0) brightness(1.2) contrast(0.75) !important;
}
#cvat_canvas_wrapper {
width: calc(100% - 10px);
height: calc(100% - 10px);

@ -1,11 +1,11 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
import { Geometry } from './canvasModel';
import { Configuration, Geometry } from './canvasModel';
interface TransformedShape {
points: string;
@ -14,6 +14,7 @@ interface TransformedShape {
export interface AutoborderHandler {
autoborder(enabled: boolean, currentShape?: SVG.Shape, currentID?: number): void;
configurate(configuration: Configuration): void;
transform(geometry: Geometry): void;
updateObjects(): void;
}
@ -24,19 +25,14 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
private frameContent: SVGSVGElement;
private enabled: boolean;
private scale: number;
private controlPointsSize: number;
private groups: SVGGElement[];
private auxiliaryGroupID: number | null;
private auxiliaryClicks: number[];
private listeners: Record<
number,
Record<
number,
{
private listeners: Record<number, Record<number, {
click: (event: MouseEvent) => void;
dblclick: (event: MouseEvent) => void;
}
>
>;
}>>;
public constructor(frameContent: SVGSVGElement) {
this.frameContent = frameContent;
@ -45,6 +41,7 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
this.enabled = false;
this.scale = 1;
this.groups = [];
this.controlPointsSize = consts.BASE_POINT_SIZE;
this.auxiliaryGroupID = null;
this.auxiliaryClicks = [];
this.listeners = {};
@ -126,7 +123,7 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
circle.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.scale}`);
circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
circle.setAttribute('r', `${consts.BASE_POINT_SIZE / this.scale}`);
circle.setAttribute('r', `${this.controlPointsSize / this.scale}`);
const click = (event: MouseEvent): void => {
event.stopPropagation();
@ -303,9 +300,13 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
this.scale = geometry.scale;
this.groups.forEach((group: SVGGElement): void => {
Array.from(group.children).forEach((circle: SVGCircleElement): void => {
circle.setAttribute('r', `${consts.BASE_POINT_SIZE / this.scale}`);
circle.setAttribute('r', `${this.controlPointsSize / this.scale}`);
circle.setAttribute('stroke-width', `${consts.BASE_STROKE_WIDTH / this.scale}`);
});
});
}
public configurate(configuration: Configuration): void {
this.controlPointsSize = configuration.controlPointsSize || consts.BASE_POINT_SIZE;
}
}

@ -64,7 +64,12 @@ export interface Configuration {
forceDisableEditing?: boolean;
intelligentPolygonCrop?: boolean;
forceFrameUpdate?: boolean;
creationOpacity?: number;
CSSImageFilter?: string;
colorBy?: string;
selectedShapeOpacity?: number;
shapeOpacity?: number;
controlPointsSize?: number;
outlinedBorders?: string | false;
}
export interface DrawData {
@ -72,6 +77,7 @@ export interface DrawData {
shapeType?: string;
rectDrawingMethod?: RectDrawingMethod;
cuboidDrawingMethod?: CuboidDrawingMethod;
skeletonSVG?: string;
numberOfPoints?: number;
initialState?: any;
crosshair?: boolean;
@ -171,6 +177,7 @@ export enum Mode {
export interface CanvasModel {
readonly imageBitmap: boolean;
readonly imageIsDeleted: boolean;
readonly image: Image | null;
readonly issueRegions: Record<number, { hidden: boolean; points: number[] }>;
readonly objects: any[];
@ -230,6 +237,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
imageID: number | null;
imageOffset: number;
imageSize: Size;
imageIsDeleted: boolean;
focusData: FocusData;
gridSize: Size;
left: number;
@ -262,12 +270,23 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
width: 0,
},
configuration: {
displayAllText: false,
smoothImage: true,
autoborders: false,
undefinedAttrValue: '',
textContent: 'id,label,attributes,source,descriptions',
textPosition: 'auto',
displayAllText: false,
showProjections: false,
forceDisableEditing: false,
intelligentPolygonCrop: false,
forceFrameUpdate: false,
CSSImageFilter: '',
colorBy: 'Label',
selectedShapeOpacity: 0.5,
shapeOpacity: 0.2,
outlinedBorders: false,
textFontSize: consts.DEFAULT_SHAPE_TEXT_SIZE,
controlPointsSize: consts.BASE_POINT_SIZE,
textPosition: consts.DEFAULT_SHAPE_TEXT_POSITION,
textContent: consts.DEFAULT_SHAPE_TEXT_CONTENT,
undefinedAttrValue: consts.DEFAULT_UNDEFINED_ATTR_VALUE,
},
imageBitmap: false,
image: null,
@ -277,6 +296,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
height: 0,
width: 0,
},
imageIsDeleted: false,
focusData: {
clientID: 0,
padding: 0,
@ -406,7 +426,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
}
if (frameData.number === this.data.imageID && !this.data.configuration.forceFrameUpdate) {
if (frameData.number === this.data.imageID &&
frameData.deleted === this.data.imageIsDeleted &&
!this.data.configuration.forceFrameUpdate
) {
this.data.zLayer = zLayer;
this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED);
@ -431,6 +454,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
};
this.data.image = data;
this.data.imageIsDeleted = frameData.deleted;
if (this.data.imageIsDeleted) {
this.data.angle = 0;
}
this.notify(UpdateReasons.IMAGE_CHANGED);
this.data.zLayer = zLayer;
this.data.objects = objectStates;
@ -476,7 +503,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public rotate(rotationAngle: number): void {
if (this.data.angle !== rotationAngle) {
if (this.data.angle !== rotationAngle && !this.data.imageIsDeleted) {
this.data.angle = (360 + Math.floor(rotationAngle / 90) * 90) % 360;
this.fit();
}
@ -530,6 +557,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
if (drawData.enabled) {
if (drawData.shapeType === 'skeleton' && !drawData.skeletonSVG) {
throw new Error('Skeleton template must be specified when drawing a skeleton');
}
if (this.data.drawData.enabled) {
throw new Error('Drawing has been already started');
} else if (!drawData.shapeType && !drawData.initialState) {
@ -659,6 +690,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.configuration.textFontSize = configuration.textFontSize;
}
if (typeof configuration.controlPointsSize === 'number') {
this.data.configuration.controlPointsSize = configuration.controlPointsSize;
}
if (['auto', 'center'].includes(configuration.textPosition)) {
this.data.configuration.textPosition = configuration.textPosition;
}
@ -691,8 +726,21 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
if (typeof configuration.forceFrameUpdate === 'boolean') {
this.data.configuration.forceFrameUpdate = configuration.forceFrameUpdate;
}
if (typeof configuration.creationOpacity === 'number') {
this.data.configuration.creationOpacity = configuration.creationOpacity;
if (typeof configuration.selectedShapeOpacity === 'number') {
this.data.configuration.selectedShapeOpacity = configuration.selectedShapeOpacity;
}
if (typeof configuration.shapeOpacity === 'number') {
this.data.configuration.shapeOpacity = configuration.shapeOpacity;
}
if (['string', 'boolean'].includes(typeof configuration.outlinedBorders)) {
this.data.configuration.outlinedBorders = configuration.outlinedBorders;
}
if (['Instance', 'Group', 'Label'].includes(configuration.colorBy)) {
this.data.configuration.colorBy = configuration.colorBy;
}
if (typeof configuration.CSSImageFilter === 'string') {
this.data.configuration.CSSImageFilter = configuration.CSSImageFilter;
}
this.notify(UpdateReasons.CONFIG_UPDATED);
@ -753,6 +801,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return this.data.imageBitmap;
}
public get imageIsDeleted(): boolean {
return this.data.imageIsDeleted;
}
public get image(): Image | null {
return this.data.image;
}

File diff suppressed because it is too large Load Diff

@ -1,10 +1,10 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
const BASE_STROKE_WIDTH = 1.25;
const BASE_GRID_WIDTH = 2;
const BASE_POINT_SIZE = 5;
const BASE_POINT_SIZE = 4;
const TEXT_MARGIN = 10;
const AREA_THRESHOLD = 9;
const SIZE_THRESHOLD = 3;
@ -19,8 +19,13 @@ const ARROW_PATH = 'M13.162 6.284L.682.524a.483.483 0 0 0-.574.134.477.477 0 ' +
const BASE_PATTERN_SIZE = 5;
const SNAP_TO_ANGLE_RESIZE_DEFAULT = 0.1;
const SNAP_TO_ANGLE_RESIZE_SHIFT = 15;
const DEFAULT_SHAPE_TEXT_SIZE = 12;
const MINIMUM_TEXT_FONT_SIZE = 8;
const SKELETON_RECT_MARGIN = 20;
const DEFAULT_SHAPE_TEXT_SIZE = 12;
const DEFAULT_SHAPE_TEXT_CONTENT = 'id,label,attributes,source,descriptions';
const DEFAULT_SHAPE_TEXT_POSITION: 'auto' | 'center' = 'auto';
const DEFAULT_UNDEFINED_ATTR_VALUE = '__undefined__';
export default {
BASE_STROKE_WIDTH,
@ -40,5 +45,9 @@ export default {
SNAP_TO_ANGLE_RESIZE_DEFAULT,
SNAP_TO_ANGLE_RESIZE_SHIFT,
DEFAULT_SHAPE_TEXT_SIZE,
DEFAULT_SHAPE_TEXT_CONTENT,
DEFAULT_SHAPE_TEXT_POSITION,
DEFAULT_UNDEFINED_ATTR_VALUE,
MINIMUM_TEXT_FONT_SIZE,
SKELETON_RECT_MARGIN,
};

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

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

@ -17,6 +17,11 @@ import {
Point,
readPointsFromShape,
clamp,
translateToCanvas,
computeWrappingBox,
makeSVGFromTemplate,
setupSkeletonEdges,
translateFromCanvas,
} from './shared';
import Crosshair from './crosshair';
import consts from './consts';
@ -83,9 +88,11 @@ export class DrawHandlerImpl implements DrawHandler {
private crosshair: Crosshair;
private drawData: DrawData;
private geometry: Geometry;
private configuration: Configuration;
private autoborderHandler: AutoborderHandler;
private autobordersEnabled: boolean;
private controlPointsSize: number;
private selectedShapeOpacity: number;
private outlinedBorders: string;
// we should use any instead of SVG.Shape because svg plugins cannot change declared interface
// so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist
@ -348,26 +355,26 @@ export class DrawHandlerImpl implements DrawHandler {
this.canvas.off('mousedown.draw');
this.canvas.off('mousemove.draw');
if (this.pointsGroup) {
this.pointsGroup.remove();
this.pointsGroup = null;
}
// Draw plugin in some cases isn't activated
// For example when draw from initialState
// Or when no drawn points, but we call cancel() drawing
// We check if it is activated with remember function
if (this.drawInstance.remember('_paintHandler')) {
if (
['polygon', 'polyline', 'points'].includes(this.drawData.shapeType) ||
if (['polygon', 'polyline', 'points'].includes(this.drawData.shapeType) ||
(this.drawData.shapeType === 'cuboid' &&
this.drawData.cuboidDrawingMethod === CuboidDrawingMethod.CORNER_POINTS)
) {
this.drawData.cuboidDrawingMethod === CuboidDrawingMethod.CORNER_POINTS)) {
// Check for unsaved drawn shapes
this.drawInstance.draw('done');
}
// Clear drawing
this.drawInstance.draw('stop');
} else if (this.drawInstance && this.drawData.shapeType === 'ellipse' && !this.drawData.initialState) {
this.drawInstance.fire('drawstop');
}
if (this.pointsGroup) {
this.pointsGroup.remove();
this.pointsGroup = null;
}
this.drawInstance.off();
@ -417,7 +424,8 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
'fill-opacity': this.selectedShapeOpacity,
stroke: this.outlinedBorders,
});
}
@ -426,7 +434,8 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
'fill-opacity': this.selectedShapeOpacity,
stroke: this.outlinedBorders,
});
const initialPoint: {
@ -442,21 +451,7 @@ export class DrawHandlerImpl implements DrawHandler {
const translated = translateToSVG(this.canvas.node as any as SVGSVGElement, [e.clientX, e.clientY]);
[initialPoint.x, initialPoint.y] = translated;
} else {
const points = this.getFinalEllipseCoordinates(readPointsFromShape(this.drawInstance), false);
const { shapeType, redraw: clientID } = this.drawData;
this.release();
if (this.canceled) return;
if (checkConstraint('ellipse', points)) {
this.onDrawDone(
{
clientID,
shapeType,
points,
},
Date.now() - this.startTimestamp,
);
}
this.drawInstance.fire('drawstop');
}
});
@ -472,6 +467,25 @@ export class DrawHandlerImpl implements DrawHandler {
this.shapeSizeElement.update(this.drawInstance);
}
});
this.drawInstance.on('drawstop', () => {
this.drawInstance.off('drawstop');
const points = this.getFinalEllipseCoordinates(readPointsFromShape(this.drawInstance), false);
const { shapeType, redraw: clientID } = this.drawData;
this.release();
if (this.canceled) return;
if (checkConstraint('ellipse', points)) {
this.onDrawDone(
{
clientID,
shapeType,
points,
},
Date.now() - this.startTimestamp,
);
}
});
}
private drawBoxBy4Points(): void {
@ -612,7 +626,8 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
'fill-opacity': this.selectedShapeOpacity,
stroke: this.outlinedBorders,
});
this.drawPolyshape();
@ -628,6 +643,7 @@ export class DrawHandlerImpl implements DrawHandler {
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': 0,
stroke: this.outlinedBorders,
});
this.drawPolyshape();
@ -651,6 +667,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
stroke: this.outlinedBorders,
});
this.drawPolyshape();
}
@ -681,7 +698,131 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
'fill-opacity': this.selectedShapeOpacity,
stroke: this.outlinedBorders,
});
}
private drawSkeleton(): void {
this.drawInstance = this.canvas.rect().attr({
stroke: this.outlinedBorders,
});
this.pointsGroup = makeSVGFromTemplate(this.drawData.skeletonSVG);
this.canvas.add(this.pointsGroup);
this.pointsGroup.attr('stroke-width', consts.BASE_STROKE_WIDTH / this.geometry.scale);
this.pointsGroup.attr('stroke', this.outlinedBorders);
let minX = Number.MAX_SAFE_INTEGER;
let minY = Number.MAX_SAFE_INTEGER;
let maxX = 0;
let maxY = 0;
this.pointsGroup.children().forEach((child: SVG.Element): void => {
const cx = child.cx();
const cy = child.cy();
minX = Math.min(cx, minX);
minY = Math.min(cy, minY);
maxX = Math.max(cx, maxX);
maxY = Math.max(cy, maxY);
});
this.drawInstance
.on('drawstop', (e: Event): void => {
const points = readPointsFromShape((e.target as any as { instance: SVG.Rect }).instance);
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(points, true);
const elements: any[] = [];
Array.from(this.pointsGroup.node.children).forEach((child: Element) => {
if (child.tagName === 'circle') {
const cx = +(child.getAttribute('cx') as string) + xtl;
const cy = +(child.getAttribute('cy') as string) + ytl;
const label = +child.getAttribute('data-label-id');
elements.push({
shapeType: 'points',
points: [cx, cy],
labelID: label,
});
}
});
const { shapeType, redraw: clientID } = this.drawData;
this.release();
if (this.canceled) return;
if (checkConstraint('rectangle', [xtl, ytl, xbr, ybr])) {
this.onDrawDone({
clientID,
shapeType,
elements,
},
Date.now() - this.startTimestamp);
}
})
.on('drawupdate', (): void => {
const x = this.drawInstance.x();
const y = this.drawInstance.y();
const width = this.drawInstance.width();
const height = this.drawInstance.height();
this.pointsGroup.style({
transform: `translate(${x}px, ${y}px)`,
});
/* eslint-disable-next-line no-unsanitized/property */
this.pointsGroup.node.innerHTML = this.drawData.skeletonSVG;
Array.from(this.pointsGroup.node.children).forEach((child: Element) => {
const dataType = child.getAttribute('data-type');
if (child.tagName === 'circle' && dataType && dataType.includes('element')) {
child.setAttribute('r', `${this.controlPointsSize / this.geometry.scale}`);
let cx = +(child.getAttribute('cx') as string);
let cy = +(child.getAttribute('cy') as string);
const cxOffset = (cx - minX) / (maxX - minX);
const cyOffset = (cy - minY) / (maxY - minY);
cx = Number.isNaN(cxOffset) ? 0.5 * width : cxOffset * width;
cy = Number.isNaN(cyOffset) ? 0.5 * height : cyOffset * height;
child.setAttribute('cx', `${cx}`);
child.setAttribute('cy', `${cy}`);
}
});
Array.from(this.pointsGroup.node.children).forEach((child: Element) => {
const dataType = child.getAttribute('data-type');
if (child.tagName === 'line' && dataType && dataType.includes('edge')) {
child.setAttribute('stroke-width', 'inherit');
child.setAttribute('stroke', 'inherit');
const dataNodeFrom = child.getAttribute('data-node-from');
const dataNodeTo = child.getAttribute('data-node-to');
if (dataNodeFrom && dataNodeTo) {
const from = this.pointsGroup.node.querySelector(`[data-node-id="${dataNodeFrom}"]`);
const to = this.pointsGroup.node.querySelector(`[data-node-id="${dataNodeTo}"]`);
if (from && to) {
const x1 = from.getAttribute('cx');
const y1 = from.getAttribute('cy');
const x2 = to.getAttribute('cx');
const y2 = to.getAttribute('cy');
if (x1 && y1 && x2 && y2) {
child.setAttribute('x1', x1);
child.setAttribute('y1', y1);
child.setAttribute('x2', x2);
child.setAttribute('y2', y2);
}
}
}
let cx = +(child.getAttribute('cx') as string);
let cy = +(child.getAttribute('cy') as string);
const cxOffset = cx / 100;
const cyOffset = cy / 100;
cx = cxOffset * width;
cy = cyOffset * height;
child.setAttribute('cx', `${cx}`);
child.setAttribute('cy', `${cy}`);
}
});
})
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.selectedShapeOpacity,
});
}
@ -721,19 +862,18 @@ export class DrawHandlerImpl implements DrawHandler {
// Common settings for rectangle and polyshapes
private pasteShape(): void {
function moveShape(shape: SVG.Shape, x: number, y: number): void {
const bbox = shape.bbox();
const moveShape = (shape: SVG.Shape, x: number, y: number): void => {
const { rotation } = shape.transform();
shape.untransform();
shape.move(x - bbox.width / 2, y - bbox.height / 2);
shape.center(x, y);
shape.rotate(rotation);
}
};
const { x: initialX, y: initialY } = this.cursorPosition;
moveShape(this.drawInstance, initialX, initialY);
this.canvas.on('mousemove.draw', (): void => {
const { x, y } = this.cursorPosition; // was computer in another callback
const { x, y } = this.cursorPosition; // was computed in another callback
moveShape(this.drawInstance, x, y);
});
}
@ -741,11 +881,12 @@ export class DrawHandlerImpl implements DrawHandler {
private pasteBox(box: BBox, rotation: number): void {
this.drawInstance = (this.canvas as any)
.rect(box.width, box.height)
.move(box.x, box.y)
.center(box.x, box.y)
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
'fill-opacity': this.selectedShapeOpacity,
stroke: this.outlinedBorders,
}).rotate(rotation);
this.pasteShape();
@ -782,7 +923,8 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
'fill-opacity': this.selectedShapeOpacity,
stroke: this.outlinedBorders,
}).rotate(rotation);
this.pasteShape();
@ -820,7 +962,8 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
'fill-opacity': this.selectedShapeOpacity,
stroke: this.outlinedBorders,
});
this.pasteShape();
this.pastePolyshape();
@ -832,6 +975,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
stroke: this.outlinedBorders,
});
this.pasteShape();
this.pastePolyshape();
@ -843,26 +987,107 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'face-stroke': 'black',
'fill-opacity': this.configuration.creationOpacity,
'face-stroke': this.outlinedBorders,
'fill-opacity': this.selectedShapeOpacity,
stroke: this.outlinedBorders,
});
this.pasteShape();
this.pastePolyshape();
}
private pasteSkeleton(box: BBox, elements: any[]): void {
const { offset } = this.geometry;
let [xtl, ytl] = [box.x, box.y];
this.pasteBox(box, 0);
this.pointsGroup = makeSVGFromTemplate(this.drawData.skeletonSVG);
this.pointsGroup.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
stroke: this.outlinedBorders,
});
this.canvas.add(this.pointsGroup);
this.pointsGroup.children().forEach((child: SVG.Element): void => {
const dataType = child.attr('data-type');
if (child.node.tagName === 'circle' && dataType && dataType.includes('element')) {
child.attr('r', `${this.controlPointsSize / this.geometry.scale}`);
const labelID = +child.attr('data-label-id');
const element = elements.find((_element: any): boolean => _element.label.id === labelID);
if (element) {
const points = translateToCanvas(offset, element.points);
child.center(points[0], points[1]);
}
}
});
this.drawInstance.off('done').on('done', (e: CustomEvent) => {
const result = {
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
elements: this.drawData.initialState.elements.map((element: any) => ({
shapeType: element.shapeType,
outside: element.outside,
occluded: element.occluded,
label: element.label,
attributes: element.attributes,
points: (() => {
const circle = this.pointsGroup.children()
.find((child: SVG.Element) => child.attr('data-label-id') === element.label.id);
const points = translateFromCanvas(this.geometry.offset, [circle.cx(), circle.cy()]);
return points;
})(),
})),
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
rotation: this.drawData.initialState.rotation,
};
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
this.onDrawDone(
result,
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
);
});
this.canvas.on('mousemove.draw', (): void => {
const [newXtl, newYtl] = [
this.drawInstance.x(), this.drawInstance.y(),
this.drawInstance.width(), this.drawInstance.height(),
];
const [xDiff, yDiff] = [newXtl - xtl, newYtl - ytl];
xtl = newXtl;
ytl = newYtl;
this.pointsGroup.children().forEach((child: SVG.Element): void => {
const dataType = child.attr('data-type');
if (child.node.tagName === 'circle' && dataType && dataType.includes('element')) {
const [cx, cy] = [child.cx(), child.cy()];
child.center(cx + xDiff, cy + yDiff);
}
});
this.pointsGroup.untransform();
setupSkeletonEdges(this.pointsGroup, this.pointsGroup);
});
}
private pastePoints(initialPoints: string): void {
function moveShape(shape: SVG.PolyLine, group: SVG.G, x: number, y: number, scale: number): void {
const moveShape = (shape: SVG.PolyLine, group: SVG.G, x: number, y: number, scale: number): void => {
const bbox = shape.bbox();
shape.move(x - bbox.width / 2, y - bbox.height / 2);
const points = shape.attr('points').split(' ');
const radius = consts.BASE_POINT_SIZE / scale;
const radius = this.controlPointsSize / scale;
group.children().forEach((child: SVG.Element, idx: number): void => {
const [px, py] = points[idx].split(',');
child.move(px - radius / 2, py - radius / 2);
});
}
};
const { x: initialX, y: initialY } = this.cursorPosition;
this.pointsGroup = this.canvas.group();
@ -873,7 +1098,7 @@ export class DrawHandlerImpl implements DrawHandler {
let numOfPoints = initialPoints.split(' ').length;
while (numOfPoints) {
numOfPoints--;
const radius = consts.BASE_POINT_SIZE / this.geometry.scale;
const radius = this.controlPointsSize / this.geometry.scale;
const stroke = consts.POINTS_STROKE_WIDTH / this.geometry.scale;
this.pointsGroup.circle().fill('white').stroke('black').attr({
r: radius,
@ -919,10 +1144,7 @@ export class DrawHandlerImpl implements DrawHandler {
if (this.drawData.initialState) {
const { offset } = this.geometry;
if (this.drawData.shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = this.drawData.initialState.points.map(
(coord: number): number => coord + offset,
);
const [xtl, ytl, xbr, ybr] = translateToCanvas(offset, this.drawData.initialState.points);
this.pasteBox({
x: xtl,
y: ytl,
@ -930,13 +1152,15 @@ export class DrawHandlerImpl implements DrawHandler {
height: ybr - ytl,
}, this.drawData.initialState.rotation);
} else if (this.drawData.shapeType === 'ellipse') {
const [cx, cy, rightX, topY] = this.drawData.initialState.points.map(
(coord: number): number => coord + offset,
);
const [cx, cy, rightX, topY] = translateToCanvas(offset, this.drawData.initialState.points);
this.pasteEllipse([cx, cy, rightX - cx, cy - topY], this.drawData.initialState.rotation);
} else if (this.drawData.shapeType === 'skeleton') {
const box = computeWrappingBox(
translateToCanvas(offset, this.drawData.initialState.points), consts.SKELETON_RECT_MARGIN,
);
this.pasteSkeleton(box, this.drawData.initialState.elements);
} else {
const points = this.drawData.initialState.points.map((coord: number): number => coord + offset);
const points = translateToCanvas(offset, this.drawData.initialState.points);
const stringifiedPoints = stringifyPoints(points);
if (this.drawData.shapeType === 'polygon') {
@ -975,6 +1199,8 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawCuboid();
this.shapeSizeElement = displayShapeSize(this.canvas, this.text);
}
} else if (this.drawData.shapeType === 'skeleton') {
this.drawSkeleton();
}
if (this.drawData.shapeType !== 'ellipse') {
@ -995,6 +1221,9 @@ export class DrawHandlerImpl implements DrawHandler {
configuration: Configuration,
) {
this.autoborderHandler = autoborderHandler;
this.controlPointsSize = configuration.controlPointsSize;
this.selectedShapeOpacity = configuration.selectedShapeOpacity;
this.outlinedBorders = configuration.outlinedBorders || 'black';
this.autobordersEnabled = false;
this.startTimestamp = Date.now();
this.onDrawDone = onDrawDone;
@ -1004,7 +1233,6 @@ export class DrawHandlerImpl implements DrawHandler {
this.canceled = false;
this.drawData = null;
this.geometry = geometry;
this.configuration = configuration;
this.crosshair = new Crosshair();
this.drawInstance = null;
this.pointsGroup = null;
@ -1023,7 +1251,9 @@ export class DrawHandlerImpl implements DrawHandler {
}
public configurate(configuration: Configuration): void {
this.configuration = configuration;
this.controlPointsSize = configuration.controlPointsSize;
this.selectedShapeOpacity = configuration.selectedShapeOpacity;
this.outlinedBorders = configuration.outlinedBorders || 'black';
const isFillableRect = this.drawData &&
this.drawData.shapeType === 'rectangle' &&
@ -1034,17 +1264,23 @@ export class DrawHandlerImpl implements DrawHandler {
const isFilalblePolygon = this.drawData && this.drawData.shapeType === 'polygon';
if (this.drawInstance && (isFillableRect || isFillableCuboid || isFilalblePolygon)) {
this.drawInstance.fill({ opacity: configuration.creationOpacity });
this.drawInstance.fill({ opacity: configuration.selectedShapeOpacity });
}
if (typeof configuration.autoborders === 'boolean') {
this.autobordersEnabled = configuration.autoborders;
if (this.drawInstance && !this.drawData.initialState) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, this.drawData.redraw);
} else {
this.autoborderHandler.autoborder(false);
}
if (this.drawInstance && this.drawInstance.attr('stroke')) {
this.drawInstance.attr('stroke', this.outlinedBorders);
}
if (this.pointsGroup && this.pointsGroup.attr('stroke')) {
this.pointsGroup.attr('stroke', this.outlinedBorders);
}
this.autobordersEnabled = configuration.autoborders;
if (this.drawInstance && !this.drawData.initialState) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, this.drawData.redraw);
} else {
this.autoborderHandler.autoborder(false);
}
}
}
@ -1061,10 +1297,14 @@ export class DrawHandlerImpl implements DrawHandler {
}
if (this.pointsGroup) {
this.pointsGroup.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
for (const point of this.pointsGroup.children()) {
point.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / geometry.scale,
r: consts.BASE_POINT_SIZE / geometry.scale,
r: this.controlPointsSize / geometry.scale,
});
}
}
@ -1079,7 +1319,7 @@ export class DrawHandlerImpl implements DrawHandler {
for (const point of (paintHandler as any).set.members) {
point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`);
point.attr('r', `${consts.BASE_POINT_SIZE / geometry.scale}`);
point.attr('r', `${this.controlPointsSize / geometry.scale}`);
}
}
}

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -26,8 +26,10 @@ export class EditHandlerImpl implements EditHandler {
private editedShape: SVG.Shape;
private editLine: SVG.PolyLine;
private clones: SVG.Polygon[];
private controlPointsSize: number;
private autobordersEnabled: boolean;
private intelligentCutEnabled: boolean;
private outlinedBorders: string;
private setupTrailingPoint(circle: SVG.Circle): void {
const head = this.editedShape.attr('points').split(' ').slice(0, this.editData.pointID).join(' ');
@ -112,16 +114,15 @@ export class EditHandlerImpl implements EditHandler {
});
}
const strokeColor = this.editedShape.attr('stroke');
(this.editLine as any)
.addClass('cvat_canvas_shape_drawing')
.style({
'pointer-events': 'none',
'fill-opacity': 0,
stroke: strokeColor,
})
.attr({
'data-origin-client-id': this.editData.state.clientID,
stroke: this.editedShape.attr('stroke'),
})
.on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry);
@ -299,7 +300,7 @@ export class EditHandlerImpl implements EditHandler {
if (enabled) {
(this.editedShape as any).selectize(true, {
deepSelect: true,
pointSize: (2 * consts.BASE_POINT_SIZE) / getGeometry().scale,
pointSize: (2 * this.controlPointsSize) / getGeometry().scale,
rotationPoint: false,
pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested
@ -365,7 +366,9 @@ export class EditHandlerImpl implements EditHandler {
}
private initEditing(): void {
this.editedShape = this.canvas.select(`#cvat_canvas_shape_${this.editData.state.clientID}`).first().clone();
this.editedShape = this.canvas
.select(`#cvat_canvas_shape_${this.editData.state.clientID}`).first()
.clone().attr('stroke', this.outlinedBorders);
this.setupPoints(true);
this.startEdit();
// draw points for this with selected and start editing till another point is clicked
@ -387,6 +390,8 @@ export class EditHandlerImpl implements EditHandler {
this.autoborderHandler = autoborderHandler;
this.autobordersEnabled = false;
this.intelligentCutEnabled = false;
this.controlPointsSize = consts.BASE_POINT_SIZE;
this.outlinedBorders = 'black';
this.onEditDone = onEditDone;
this.canvas = canvas;
this.editData = null;
@ -416,20 +421,23 @@ export class EditHandlerImpl implements EditHandler {
}
public configurate(configuration: Configuration): void {
if (typeof configuration.autoborders === 'boolean') {
this.autobordersEnabled = configuration.autoborders;
if (this.editLine) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.editLine, this.editData.state.clientID);
} else {
this.autoborderHandler.autoborder(false);
}
}
this.autobordersEnabled = configuration.autoborders;
this.outlinedBorders = configuration.outlinedBorders || 'black';
if (this.editedShape) {
this.editedShape.attr('stroke', this.outlinedBorders);
}
if (typeof configuration.intelligentPolygonCrop === 'boolean') {
this.intelligentCutEnabled = configuration.intelligentPolygonCrop;
if (this.editLine) {
this.editLine.attr('stroke', this.outlinedBorders);
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.editLine, this.editData.state.clientID);
} else {
this.autoborderHandler.autoborder(false);
}
}
this.controlPointsSize = configuration.controlPointsSize || consts.BASE_POINT_SIZE;
this.intelligentCutEnabled = configuration.intelligentPolygonCrop;
}
public transform(geometry: Geometry): void {
@ -453,7 +461,7 @@ export class EditHandlerImpl implements EditHandler {
for (const point of (paintHandler as any).set.members) {
point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`);
point.attr('r', `${consts.BASE_POINT_SIZE / geometry.scale}`);
point.attr('r', `${this.controlPointsSize / geometry.scale}`);
}
}
}

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

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -23,7 +23,6 @@ export interface InteractionHandler {
export class InteractionHandlerImpl implements InteractionHandler {
private onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void;
private configuration: Configuration;
private geometry: Geometry;
private canvas: SVG.Container;
private interactionData: InteractionData;
@ -37,6 +36,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
private intermediateShape: PropType<InteractionData, 'intermediateShape'>;
private drawnIntermediateShape: SVG.Shape;
private thresholdWasModified: boolean;
private controlPointsSize: number;
private selectedShapeOpacity: number;
private prepareResult(): InteractionResult[] {
return this.interactionShapes.map(
@ -111,7 +112,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
if (!this.isWithinThreshold(cx, cy)) return;
this.currentInteractionShape = this.canvas
.circle((consts.BASE_POINT_SIZE * 2) / this.geometry.scale)
.circle((this.controlPointsSize * 2) / this.geometry.scale)
.center(cx, cy)
.fill('white')
.stroke(e.button === 0 ? 'green' : 'red')
@ -137,7 +138,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
self.addClass('cvat_canvas_removable_interaction_point');
self.attr({
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale,
r: (consts.BASE_POINT_SIZE * 1.5) / this.geometry.scale,
r: (this.controlPointsSize * 1.5) / this.geometry.scale,
});
self.on('mousedown', (_e: MouseEvent): void => {
@ -162,7 +163,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
self.removeClass('cvat_canvas_removable_interaction_point');
self.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
r: consts.BASE_POINT_SIZE / this.geometry.scale,
r: this.controlPointsSize / this.geometry.scale,
});
self.off('mousedown');
@ -205,7 +206,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
})
.fill({ opacity: this.configuration.creationOpacity, color: 'white' });
.fill({ opacity: this.selectedShapeOpacity, color: 'white' });
}
private initInteraction(): void {
@ -300,7 +301,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
stroke: erroredShape ? 'red' : 'black',
})
.fill({ opacity: this.configuration.creationOpacity, color: 'white' })
.fill({ opacity: this.selectedShapeOpacity, color: 'white' })
.addClass('cvat_canvas_interact_intermediate_shape');
this.selectize(true, this.drawnIntermediateShape, erroredShape);
} else {
@ -317,7 +318,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
if (value) {
(shape as any).selectize(value, {
deepSelect: true,
pointSize: consts.BASE_POINT_SIZE / self.geometry.scale,
pointSize: this.controlPointsSize / self.geometry.scale,
rotationPoint: false,
classPoints: 'cvat_canvas_interact_intermediate_shape_point',
pointType(cx: number, cy: number): SVG.Circle {
@ -399,7 +400,6 @@ export class InteractionHandlerImpl implements InteractionHandler {
onInteraction(shapes, shapesUpdated, isDone, this.threshold ? this.thresholdRectSize / 2 : null);
};
this.canvas = canvas;
this.configuration = configuration;
this.geometry = geometry;
this.shapesWereUpdated = false;
this.interactionShapes = [];
@ -410,6 +410,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.thresholdRectSize = 300;
this.intermediateShape = null;
this.drawnIntermediateShape = null;
this.controlPointsSize = configuration.controlPointsSize;
this.selectedShapeOpacity = configuration.selectedShapeOpacity;
this.cursorPosition = {
x: 0,
y: 0,
@ -477,10 +479,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
for (const shape of shapesToBeScaled) {
if (shape.type === 'circle') {
if (shape.hasClass('cvat_canvas_removable_interaction_point')) {
(shape as SVG.Circle).radius((consts.BASE_POINT_SIZE * 1.5) / this.geometry.scale);
(shape as SVG.Circle).radius((this.controlPointsSize * 1.5) / this.geometry.scale);
shape.attr('stroke-width', consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale);
} else {
(shape as SVG.Circle).radius(consts.BASE_POINT_SIZE / this.geometry.scale);
(shape as SVG.Circle).radius(this.controlPointsSize / this.geometry.scale);
shape.attr('stroke-width', consts.POINTS_STROKE_WIDTH / this.geometry.scale);
}
} else {
@ -490,7 +492,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
for (const element of window.document.getElementsByClassName('cvat_canvas_interact_intermediate_shape_point')) {
element.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / (2 * this.geometry.scale)}`);
element.setAttribute('r', `${consts.BASE_POINT_SIZE / this.geometry.scale}`);
element.setAttribute('r', `${this.controlPointsSize / this.geometry.scale}`);
}
if (this.drawnIntermediateShape) {
@ -520,21 +522,23 @@ export class InteractionHandlerImpl implements InteractionHandler {
}
public configurate(configuration: Configuration): void {
this.configuration = configuration;
this.controlPointsSize = configuration.controlPointsSize;
this.selectedShapeOpacity = configuration.selectedShapeOpacity;
if (this.drawnIntermediateShape) {
this.drawnIntermediateShape.fill({
opacity: configuration.creationOpacity,
opacity: configuration.selectedShapeOpacity,
});
}
// when interactRectangle
if (this.currentInteractionShape && this.currentInteractionShape.type === 'rect') {
this.currentInteractionShape.fill({ opacity: configuration.creationOpacity });
this.currentInteractionShape.fill({ opacity: configuration.selectedShapeOpacity });
}
// when interactPoints with startwithbbox
if (this.interactionShapes[0] && this.interactionShapes[0].type === 'rect') {
this.interactionShapes[0].fill({ opacity: configuration.creationOpacity });
this.interactionShapes[0].fill({ opacity: configuration.selectedShapeOpacity });
}
}

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

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

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

@ -52,6 +52,9 @@ export interface DrawnState {
updated: number;
frame: number;
label: any;
group: any;
color: string;
elements: DrawnState[] | null;
}
// Translate point array from the canvas coordinate system
@ -192,11 +195,13 @@ export function readPointsFromShape(shape: SVG.Shape): number[] {
let points = null;
if (shape.type === 'ellipse') {
const [rx, ry] = [+shape.attr('rx'), +shape.attr('ry')];
const [cx, cy] = [+shape.attr('cx'), +shape.attr('cy')];
const [cx, cy] = [shape.cx(), shape.cy()];
points = `${cx},${cy} ${cx + rx},${cy - ry}`;
} else if (shape.type === 'rect') {
points = `${shape.attr('x')},${shape.attr('y')} ` +
`${shape.attr('x') + shape.attr('width')},${shape.attr('y') + shape.attr('height')}`;
} else if (shape.type === 'circle') {
points = `${shape.cx()},${shape.cy()}`;
} else {
points = shape.attr('points');
}
@ -239,4 +244,121 @@ export function translateFromCanvas(offset: number, points: number[]): number[]
return points.map((coord: number): number => coord - offset);
}
export function computeWrappingBox(points: number[], margin = 0): Box & BBox {
let xtl = Number.MAX_SAFE_INTEGER;
let ytl = Number.MAX_SAFE_INTEGER;
let xbr = Number.MIN_SAFE_INTEGER;
let ybr = Number.MIN_SAFE_INTEGER;
for (let i = 0; i < points.length; i += 2) {
const [x, y] = [points[i], points[i + 1]];
xtl = Math.min(xtl, x);
ytl = Math.min(ytl, y);
xbr = Math.max(xbr, x);
ybr = Math.max(ybr, y);
}
const box = {
xtl: xtl - margin,
ytl: ytl - margin,
xbr: xbr + margin,
ybr: ybr + margin,
};
return {
...box,
x: box.xtl,
y: box.ytl,
width: box.xbr - box.xtl,
height: box.ybr - box.ytl,
};
}
export function getSkeletonEdgeCoordinates(edge: SVG.Line): {
x1: number, y1: number, x2: number, y2: number
} {
let x1 = 0;
let y1 = 0;
let x2 = 0;
let y2 = 0;
const parent = edge.parent() as any as SVG.G;
if (parent.type !== 'g') {
throw new Error('Edge parent must be a group');
}
const dataNodeFrom = edge.attr('data-node-from');
const dataNodeTo = edge.attr('data-node-to');
const nodeFrom = parent.children()
.find((element: SVG.Element): boolean => element.attr('data-node-id') === dataNodeFrom);
const nodeTo = parent.children()
.find((element: SVG.Element): boolean => element.attr('data-node-id') === dataNodeTo);
if (!nodeFrom || !nodeTo) {
throw new Error(`Edge's nodeFrom ${dataNodeFrom} or nodeTo ${dataNodeTo} do not to refer to any node`);
}
x1 = nodeFrom.cx();
y1 = nodeFrom.cy();
x2 = nodeTo.cx();
y2 = nodeTo.cy();
if (nodeFrom.hasClass('cvat_canvas_hidden') || nodeTo.hasClass('cvat_canvas_hidden')) {
edge.addClass('cvat_canvas_hidden');
} else {
edge.removeClass('cvat_canvas_hidden');
}
if (nodeFrom.hasClass('cvat_canvas_shape_occluded') || nodeTo.hasClass('cvat_canvas_shape_occluded')) {
edge.addClass('cvat_canvas_shape_occluded');
}
if ([x1, y1, x2, y2].some((coord: number): boolean => typeof coord !== 'number')) {
throw new Error(`Edge coordinates must be numbers, got [${x1}, ${y1}, ${x2}, ${y2}]`);
}
return {
x1, y1, x2, y2,
};
}
export function makeSVGFromTemplate(template: string): SVG.G {
const SVGElement = new SVG.G();
/* eslint-disable-next-line no-unsanitized/property */
SVGElement.node.innerHTML = template;
return SVGElement;
}
export function setupSkeletonEdges(skeleton: SVG.G, referenceSVG: SVG.G): void {
for (const child of referenceSVG.children()) {
// search for all edges on template
const dataType = child.attr('data-type');
if (child.type === 'line' && dataType === 'edge') {
const dataNodeFrom = child.attr('data-node-from');
const dataNodeTo = child.attr('data-node-to');
if (!Number.isInteger(dataNodeFrom) || !Number.isInteger(dataNodeTo)) {
throw new Error(`Edge nodeFrom and nodeTo must be numbers, got ${dataNodeFrom}, ${dataNodeTo}`);
}
// try to find the same edge on the skeleton
let edge = skeleton.children().find((_child: SVG.Element) => (
_child.attr('data-node-from') === dataNodeFrom && _child.attr('data-node-to') === dataNodeTo
)) as SVG.Line;
// if not found, lets create it
if (!edge) {
edge = skeleton.line(0, 0, 0, 0).attr({
'data-node-from': dataNodeFrom,
'data-node-to': dataNodeTo,
'stroke-width': 'inherit',
}).addClass('cvat_canvas_skeleton_edge') as SVG.Line;
}
skeleton.node.prepend(edge.node);
const points = getSkeletonEdgeCoordinates(edge);
edge.attr({ ...points, 'stroke-width': 'inherit' });
}
}
}
export type PropType<T, Prop extends keyof T> = T[Prop];

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

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

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

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -6,7 +6,7 @@
const path = require('path');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const DtsBundleWebpack = require('dts-bundle-webpack');
const BundleDeclarationsWebpackPlugin = require('bundle-declarations-webpack-plugin');
const styleLoaders = [
'style-loader',
@ -64,10 +64,8 @@ const nodeConfig = {
],
},
plugins: [
new DtsBundleWebpack({
name: 'cvat-canvas.node',
main: 'dist/declaration/src/typescript/canvas.d.ts',
out: '../cvat-canvas.node.d.ts',
new BundleDeclarationsWebpackPlugin({
outFile: "declaration/src/cvat-canvas.d.ts",
}),
],
};
@ -116,10 +114,8 @@ const webConfig = {
],
},
plugins: [
new DtsBundleWebpack({
name: 'cvat-canvas',
main: 'dist/declaration/src/typescript/canvas.d.ts',
out: '../cvat-canvas.d.ts',
new BundleDeclarationsWebpackPlugin({
outFile: "declaration/src/cvat-canvas.d.ts",
}),
],
};

@ -1,16 +1,9 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
const globalConfig = require('../.eslintrc.js');
module.exports = {
env: {
node: true,
},
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 6,
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
@ -20,26 +13,4 @@ module.exports = {
'node_modules/**',
'dist/**',
],
plugins: ['@typescript-eslint'],
extends: ['plugin:@typescript-eslint/recommended', 'airbnb-typescript/base'],
rules: {
...globalConfig.rules,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/indent': ['error', 4],
'@typescript-eslint/lines-between-class-members': 0,
'@typescript-eslint/no-explicit-any': [0],
'@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }],
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/ban-types': [
'error',
{
types: {
'{}': false, // TODO: try to fix with Record<string, unknown>
object: false, // TODO: try to fix with Record<string, unknown>
Function: false, // TODO: try to fix somehow
},
},
],
},
};

@ -9,17 +9,17 @@ It presents a canvas to viewing, drawing and editing of 3D annotations.
If you make changes in this package, please do following:
- After not important changes (typos, backward compatible bug fixes, refactoring) do: `npm version patch`
- After changing API (backward compatible new features) do: `npm version minor`
- After changing API (changes that break backward compatibility) do: `npm version major`
- After not important changes (typos, backward compatible bug fixes, refactoring) do: `yarn version --patch`
- After changing API (backward compatible new features) do: `yarn version --minor`
- After changing API (changes that break backward compatibility) do: `yarn 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
yarn run build
yarn run build --mode=development # without a minification
```
### API Methods

@ -1,55 +0,0 @@
{
"name": "cvat-canvas3d",
"version": "0.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-canvas3d",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@types/three": "^0.125.3",
"camera-controls": "^1.25.3",
"three": "^0.126.1"
},
"devDependencies": {}
},
"node_modules/@types/three": {
"version": "0.125.3",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.125.3.tgz",
"integrity": "sha512-tUPMzKooKDvMOhqcNVUPwkt+JNnF8ASgWSsrLgleVd0SjLj4boJhteSsF9f6YDjye0mmUjO+BDMWW83F97ehXA=="
},
"node_modules/camera-controls": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-1.33.0.tgz",
"integrity": "sha512-QTXwz/XbLCPGf7l6u9cWKfR3WwKulnNAahfg+RE+dFOAQ40KKvwTIvBs3Q29kqntJlKvY79ZVsmPUEUA6LoF2A==",
"peerDependencies": {
"three": ">=0.126.1"
}
},
"node_modules/three": {
"version": "0.126.1",
"resolved": "https://registry.npmjs.org/three/-/three-0.126.1.tgz",
"integrity": "sha512-eOEXnZeE1FDV0XgL1u08auIP13jxdN9LQBAEmlErYzMxtIIfuGIAZbijOyookALUhqVzVOx0Tywj6n192VM+nQ=="
}
},
"dependencies": {
"@types/three": {
"version": "0.125.3",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.125.3.tgz",
"integrity": "sha512-tUPMzKooKDvMOhqcNVUPwkt+JNnF8ASgWSsrLgleVd0SjLj4boJhteSsF9f6YDjye0mmUjO+BDMWW83F97ehXA=="
},
"camera-controls": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-1.33.0.tgz",
"integrity": "sha512-QTXwz/XbLCPGf7l6u9cWKfR3WwKulnNAahfg+RE+dFOAQ40KKvwTIvBs3Q29kqntJlKvY79ZVsmPUEUA6LoF2A==",
"requires": {}
},
"three": {
"version": "0.126.1",
"resolved": "https://registry.npmjs.org/three/-/three-0.126.1.tgz",
"integrity": "sha512-eOEXnZeE1FDV0XgL1u08auIP13jxdN9LQBAEmlErYzMxtIIfuGIAZbijOyookALUhqVzVOx0Tywj6n192VM+nQ=="
}
}
}

@ -7,7 +7,7 @@
"build": "tsc && webpack --config ./webpack.config.js",
"server": "nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'"
},
"author": "Intel",
"author": "CVAT.ai",
"license": "MIT",
"browserslist": [
"Chrome >= 63",

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -112,5 +112,5 @@ class Canvas3dImpl implements Canvas3d {
}
export {
Canvas3dImpl as Canvas3d, Canvas3dVersion, ViewType, MouseInteraction, CameraAction, ViewsDOM,
Canvas3dImpl as Canvas3d, Canvas3dVersion, ViewType, MouseInteraction, CameraAction, ViewsDOM, Mode as CanvasMode,
};

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -12,6 +12,7 @@ export interface Canvas3dController {
readonly selected: any;
readonly focused: FocusData;
readonly groupData: GroupData;
readonly imageIsDeleted: boolean;
mode: Mode;
group(groupData: GroupData): void;
}
@ -47,6 +48,10 @@ export class Canvas3dControllerImpl implements Canvas3dController {
return this.model.data.focusData;
}
public get imageIsDeleted(): any {
return this.model.imageIsDeleted;
}
public get groupData(): GroupData {
return this.model.groupData;
}

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -101,6 +101,7 @@ export interface Canvas3dDataModel {
imageID: number | null;
imageOffset: number;
imageSize: Size;
imageIsDeleted: boolean;
drawData: DrawData;
mode: Mode;
objectUpdating: boolean;
@ -116,6 +117,7 @@ export interface Canvas3dDataModel {
export interface Canvas3dModel {
mode: Mode;
data: Canvas3dDataModel;
readonly imageIsDeleted: boolean;
readonly groupData: GroupData;
setup(frameData: any, objectStates: any[]): void;
isAbleToChangeFrame(): boolean;
@ -153,6 +155,7 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
height: 0,
width: 0,
},
imageIsDeleted: false,
drawData: {
enabled: false,
initialState: null,
@ -187,7 +190,7 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
return;
}
if (frameData.number === this.data.imageID) {
if (frameData.number === this.data.imageID && frameData.deleted === this.data.imageIsDeleted) {
if (this.data.objectUpdating) {
return;
}
@ -213,7 +216,7 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
height: frameData.height as number,
width: frameData.width as number,
};
this.data.imageIsDeleted = frameData.deleted;
this.data.image = data;
this.notify(UpdateReasons.IMAGE_CHANGED);
this.data.objects = objectStates;
@ -342,5 +345,9 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
return { ...this.data.groupData };
}
public get imageIsDeleted(): boolean {
return this.data.imageIsDeleted;
}
public destroy(): void {}
}

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -287,7 +287,6 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
(_state: any): boolean => _state.clientID === Number(intersects[0].object.name),
);
if (item.length !== 0) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.model.data.groupData.grouped = this.model.data.groupData.grouped.filter(
(_state: any): boolean => _state.clientID !== Number(intersects[0].object.name),
@ -782,12 +781,43 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
this.model.data.drawData.enabled = false;
}
this.views.perspective.renderer.dispose();
this.model.mode = Mode.BUSY;
if (!this.controller.imageIsDeleted) {
this.model.mode = Mode.BUSY;
}
this.action.loading = true;
const loader = new PCDLoader();
const objectURL = URL.createObjectURL(model.data.image.imageData);
this.clearScene();
loader.load(objectURL, this.addScene.bind(this));
if (this.controller.imageIsDeleted) {
this.render();
const [container] = window.document.getElementsByClassName('cvat-canvas-container');
const overlay = window.document.createElement('canvas');
overlay.classList.add('cvat_3d_canvas_deleted_overlay');
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.position = 'absolute';
overlay.style.top = '0px';
overlay.style.left = '0px';
container.appendChild(overlay);
const { clientWidth: width, clientHeight: height } = overlay;
overlay.width = width;
overlay.height = height;
const canvasContext = overlay.getContext('2d');
const fontSize = width / 10;
canvasContext.font = `bold ${fontSize}px serif`;
canvasContext.textAlign = 'center';
canvasContext.lineWidth = fontSize / 20;
canvasContext.strokeStyle = 'white';
canvasContext.strokeText('IMAGE REMOVED', width / 2, height / 2);
canvasContext.fillStyle = 'black';
canvasContext.fillText('IMAGE REMOVED', width / 2, height / 2);
} else {
loader.load(objectURL, this.addScene.bind(this));
const [overlay] = window.document.getElementsByClassName('cvat_3d_canvas_deleted_overlay');
if (overlay) {
overlay.remove();
}
}
URL.revokeObjectURL(objectURL);
this.dispatchEvent(new CustomEvent('canvas.setup'));
} else if (reason === UpdateReasons.SHAPE_ACTIVATED) {

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

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as THREE from 'three';

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

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -6,7 +6,7 @@
const path = require('path');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const DtsBundleWebpack = require('dts-bundle-webpack');
const BundleDeclarationsWebpackPlugin = require('bundle-declarations-webpack-plugin');
const styleLoaders = [
'style-loader',
@ -64,10 +64,8 @@ const nodeConfig = {
],
},
plugins: [
new DtsBundleWebpack({
name: 'cvat-canvas3d.node',
main: 'dist/declaration/src/typescript/canvas3d.d.ts',
out: '../cvat-canvas3d.node.d.ts',
new BundleDeclarationsWebpackPlugin({
outFile: "declaration/src/cvat-canvas.d.ts",
}),
],
};
@ -116,10 +114,8 @@ const webConfig = {
],
},
plugins: [
new DtsBundleWebpack({
name: 'cvat-canvas3d',
main: 'dist/declaration/src/typescript/canvas3d.d.ts',
out: '../cvat-canvas3d.d.ts',
new BundleDeclarationsWebpackPlugin({
outFile: "declaration/src/cvat-canvas.d.ts",
}),
],
};

@ -0,0 +1,66 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
venv/
.venv/
.python-version
.pytest_cache
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
#Ipython Notebook
.ipynb_checkpoints

@ -0,0 +1,2 @@
include README.md
include requirements/base.txt

@ -0,0 +1,11 @@
# Command-line client for CVAT
## Installation
`pip install cvat-cli/`
## Usage
```bash
$ cvat-cli --help
```

@ -0,0 +1,19 @@
# Developer guide
Install testing requirements:
```bash
pip install -r requirements/testing.txt
```
Run unit tests:
```
cd cvat/
python manage.py test --settings cvat.settings.testing cvat-cli/
```
Install package in the editable mode:
```bash
pip install -e .
```

@ -0,0 +1,15 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[tool.isort]
profile = "black"
forced_separate = ["tests"]
line_length = 100
skip_gitignore = true # align tool behavior with Black
# Can't just use a pyproject in the root dir, so duplicate
# https://github.com/psf/black/issues/2863
[tool.black]
line-length = 100
target-version = ['py38']

@ -0,0 +1,2 @@
cvat-sdk==2.0
Pillow>=6.2.0

@ -0,0 +1,5 @@
-r base.txt
black>=22.1.0
isort>=5.10.1
pylint>=2.7.0

@ -0,0 +1,67 @@
# Copyright (C) 2022 Intel Corporation
#
# SPDX-License-Identifier: MIT
import os.path as osp
import re
from setuptools import find_packages, setup
def find_version(project_dir=None):
if not project_dir:
project_dir = osp.dirname(osp.abspath(__file__))
file_path = osp.join(project_dir, "version.py")
with open(file_path, "r") as version_file:
version_text = version_file.read()
# PEP440:
# https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
pep_regex = r"([1-9]\d*!)?(0|[1-9]\d*)(\.(0|[1-9]\d*))*((a|b|rc)(0|[1-9]\d*))?(\.post(0|[1-9]\d*))?(\.dev(0|[1-9]\d*))?"
version_regex = r"VERSION\s*=\s*.(" + pep_regex + ")."
match = re.match(version_regex, version_text)
if not match:
raise RuntimeError("Failed to find version string in '%s'" % file_path)
version = version_text[match.start(1) : match.end(1)]
return version
BASE_REQUIREMENTS_FILE = "requirements/base.txt"
def parse_requirements(filename=BASE_REQUIREMENTS_FILE):
with open(filename) as fh:
return fh.readlines()
BASE_REQUIREMENTS = parse_requirements(BASE_REQUIREMENTS_FILE)
with open("README.md", "r") as fh:
long_description = fh.read()
setup(
name="cvat-cli",
version=find_version(project_dir="src/cvat_cli"),
description="Command-line client for CVAT",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/cvat-ai/cvat/",
package_dir={"": "src"},
packages=find_packages(where="src"),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires=">=3.7",
install_requires=BASE_REQUIREMENTS,
entry_points={
"console_scripts": [
"cvat-cli=cvat_cli.__main__:main",
],
},
include_package_data=True,
)

@ -0,0 +1,61 @@
# Copyright (C) 2020-2022 Intel Corporation
# Copyright (C) 2022 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
import logging
import sys
from http.client import HTTPConnection
from typing import List
from cvat_sdk import exceptions, make_client
from cvat_cli.cli import CLI
from cvat_cli.parser import get_action_args, make_cmdline_parser
logger = logging.getLogger(__name__)
def configure_logger(level):
formatter = logging.Formatter(
"[%(asctime)s] %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", style="%"
)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(level)
if level <= logging.DEBUG:
HTTPConnection.debuglevel = 1
def main(args: List[str] = None):
actions = {
"create": CLI.tasks_create,
"delete": CLI.tasks_delete,
"ls": CLI.tasks_list,
"frames": CLI.tasks_frames,
"dump": CLI.tasks_dump,
"upload": CLI.tasks_upload,
"export": CLI.tasks_export,
"import": CLI.tasks_import,
}
parser = make_cmdline_parser()
parsed_args = parser.parse_args(args)
configure_logger(parsed_args.loglevel)
with make_client(parsed_args.server_host, port=parsed_args.server_port) as client:
client.logger = logger
action_args = get_action_args(parser, parsed_args)
try:
cli = CLI(client=client, credentials=parsed_args.auth)
actions[parsed_args.action](cli, **vars(action_args))
except exceptions.ApiException as e:
logger.critical(e)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

@ -0,0 +1,137 @@
# Copyright (C) 2020-2022 Intel Corporation
# Copyright (C) 2022 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
from __future__ import annotations
import json
from typing import Dict, List, Sequence, Tuple
import tqdm
from cvat_sdk import Client, models
from cvat_sdk.core.helpers import TqdmProgressReporter
from cvat_sdk.core.types import ResourceType
class CLI:
def __init__(self, client: Client, credentials: Tuple[str, str]):
self.client = client
# allow arbitrary kwargs in models
# TODO: will silently ignore invalid args, so remove this ASAP
self.client.api.configuration.discard_unknown_keys = True
self.client.login(credentials)
def tasks_list(self, *, use_json_output: bool = False, **kwargs):
"""List all tasks in either basic or JSON format."""
results = self.client.list_tasks(return_json=use_json_output, **kwargs)
if use_json_output:
print(json.dumps(json.loads(results), indent=2))
else:
for r in results:
print(r.id)
def tasks_create(
self,
name: str,
labels: List[Dict[str, str]],
resource_type: ResourceType,
resources: Sequence[str],
*,
annotation_path: str = "",
annotation_format: str = "CVAT XML 1.1",
status_check_period: int = 2,
dataset_repository_url: str = "",
lfs: bool = False,
**kwargs,
) -> None:
"""
Create a new task with the given name and labels JSON and add the files to it.
"""
task = self.client.create_task(
spec=models.TaskWriteRequest(name=name, labels=labels, **kwargs),
resource_type=resource_type,
resources=resources,
data_params=kwargs,
annotation_path=annotation_path,
annotation_format=annotation_format,
status_check_period=status_check_period,
dataset_repository_url=dataset_repository_url,
use_lfs=lfs,
pbar=self._make_pbar(),
)
print("Created task id", task.id)
def tasks_delete(self, task_ids: Sequence[int]) -> None:
"""Delete a list of tasks, ignoring those which don't exist."""
self.client.delete_tasks(task_ids=task_ids)
def tasks_frames(
self,
task_id: int,
frame_ids: Sequence[int],
*,
outdir: str = "",
quality: str = "original",
) -> None:
"""
Download the requested frame numbers for a task and save images as
task_<ID>_frame_<FRAME>.jpg.
"""
self.client.retrieve_task(task_id=task_id).download_frames(
frame_ids=frame_ids,
outdir=outdir,
quality=quality,
filename_pattern="task_{task_id}_frame_{frame_id:06d}{frame_ext}",
)
def tasks_dump(
self,
task_id: int,
fileformat: str,
filename: str,
*,
status_check_period: int = 2,
include_images: bool = False,
) -> None:
"""
Download annotations for a task in the specified format (e.g. 'YOLO ZIP 1.0').
"""
self.client.retrieve_task(task_id=task_id).export_dataset(
format_name=fileformat,
filename=filename,
pbar=self._make_pbar(),
status_check_period=status_check_period,
include_images=include_images,
)
def tasks_upload(
self, task_id: str, fileformat: str, filename: str, *, status_check_period: int = 2
) -> None:
"""Upload annotations for a task in the specified format
(e.g. 'YOLO ZIP 1.0')."""
self.client.retrieve_task(task_id=task_id).import_annotations(
format_name=fileformat,
filename=filename,
status_check_period=status_check_period,
pbar=self._make_pbar(),
)
def tasks_export(self, task_id: str, filename: str, *, status_check_period: int = 2) -> None:
"""Download a task backup"""
self.client.retrieve_task(task_id=task_id).download_backup(
filename=filename, status_check_period=status_check_period, pbar=self._make_pbar()
)
def tasks_import(self, filename: str, *, status_check_period: int = 2) -> None:
"""Import a task from a backup file"""
self.client.create_task_from_backup(
filename=filename, status_check_period=status_check_period, pbar=self._make_pbar()
)
def _make_pbar(self, title: str = None) -> TqdmProgressReporter:
return TqdmProgressReporter(
tqdm.tqdm(unit_scale=True, unit="B", unit_divisor=1024, desc=title)
)

@ -0,0 +1,334 @@
# Copyright (C) 2021-2022 Intel Corporation
# Copyright (C) 2022 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
import argparse
import getpass
import json
import logging
import os
from distutils.util import strtobool
from cvat_sdk.core.types import ResourceType
from .version import VERSION
def get_auth(s):
"""Parse USER[:PASS] strings and prompt for password if none was
supplied."""
user, _, password = s.partition(":")
password = password or os.environ.get("PASS") or getpass.getpass()
return user, password
def parse_label_arg(s):
"""If s is a file load it as JSON, otherwise parse s as JSON."""
if os.path.exists(s):
with open(s, "r") as fp:
return json.load(fp)
else:
return json.loads(s)
def parse_resource_type(s: str) -> ResourceType:
try:
return ResourceType[s.upper()]
except KeyError:
return s
def make_cmdline_parser() -> argparse.ArgumentParser:
#######################################################################
# Command line interface definition
#######################################################################
parser = argparse.ArgumentParser(
description="Perform common operations related to CVAT tasks.\n\n"
)
parser.add_argument("--version", action="version", version=VERSION)
task_subparser = parser.add_subparsers(dest="action")
#######################################################################
# Positional arguments
#######################################################################
parser.add_argument(
"--auth",
type=get_auth,
metavar="USER:[PASS]",
default=getpass.getuser(),
help="""defaults to the current user and supports the PASS
environment variable or password prompt
(default user: %(default)s).""",
)
parser.add_argument(
"--server-host", type=str, default="localhost", help="host (default: %(default)s)"
)
parser.add_argument(
"--server-port", type=int, default="8080", help="port (default: %(default)s)"
)
parser.add_argument(
"--https",
default=False,
action="store_true",
help="using https connection (default: %(default)s)",
)
parser.add_argument(
"--debug",
action="store_const",
dest="loglevel",
const=logging.DEBUG,
default=logging.INFO,
help="show debug output",
)
#######################################################################
# Create
#######################################################################
task_create_parser = task_subparser.add_parser(
"create",
description="""Create a new CVAT task. To create a task, you need
to specify labels using the --labels argument or
attach the task to an existing project using the
--project_id argument.""",
)
task_create_parser.add_argument("name", type=str, help="name of the task")
task_create_parser.add_argument(
"resource_type",
default="local",
choices=list(ResourceType),
type=parse_resource_type,
help="type of files specified",
)
task_create_parser.add_argument("resources", type=str, help="list of paths or URLs", nargs="+")
task_create_parser.add_argument(
"--annotation_path", default="", type=str, help="path to annotation file"
)
task_create_parser.add_argument(
"--annotation_format",
default="CVAT 1.1",
type=str,
help="format of the annotation file being uploaded, e.g. CVAT 1.1",
)
task_create_parser.add_argument(
"--bug_tracker", "--bug", default=None, type=str, help="bug tracker URL"
)
task_create_parser.add_argument(
"--chunk_size", default=None, type=int, help="the number of frames per chunk"
)
task_create_parser.add_argument(
"--completion_verification_period",
dest="status_check_period",
default=20,
type=float,
help="""number of seconds to wait until checking
if data compression finished (necessary before uploading annotations)""",
)
task_create_parser.add_argument(
"--copy_data",
default=False,
action="store_true",
help="""set the option to copy the data, only used when resource type is
share (default: %(default)s)""",
)
task_create_parser.add_argument(
"--dataset_repository_url",
default="",
type=str,
help=(
"git repository to store annotations e.g."
" https://github.com/user/repos [annotation/<anno_file_name.zip>]"
),
)
task_create_parser.add_argument(
"--frame_step",
default=None,
type=int,
help="""set the frame step option in the advanced configuration
when uploading image series or videos (default: %(default)s)""",
)
task_create_parser.add_argument(
"--image_quality",
default=70,
type=int,
help="""set the image quality option in the advanced configuration
when creating tasks.(default: %(default)s)""",
)
task_create_parser.add_argument(
"--labels",
default="[]",
type=parse_label_arg,
help="string or file containing JSON labels specification",
)
task_create_parser.add_argument(
"--lfs",
default=False,
action="store_true",
help="using lfs for dataset repository (default: %(default)s)",
)
task_create_parser.add_argument(
"--project_id", default=None, type=int, help="project ID if project exists"
)
task_create_parser.add_argument(
"--overlap",
default=None,
type=int,
help="the number of intersected frames between different segments",
)
task_create_parser.add_argument(
"--segment_size", default=None, type=int, help="the number of frames in a segment"
)
task_create_parser.add_argument(
"--sorting-method",
default="lexicographical",
choices=["lexicographical", "natural", "predefined", "random"],
help="""data soring method (default: %(default)s)""",
)
task_create_parser.add_argument(
"--start_frame", default=None, type=int, help="the start frame of the video"
)
task_create_parser.add_argument(
"--stop_frame", default=None, type=int, help="the stop frame of the video"
)
task_create_parser.add_argument(
"--use_cache", action="store_true", help="""use cache""" # automatically sets default=False
)
task_create_parser.add_argument(
"--use_zip_chunks",
action="store_true", # automatically sets default=False
help="""zip chunks before sending them to the server""",
)
#######################################################################
# Delete
#######################################################################
delete_parser = task_subparser.add_parser("delete", description="Delete a CVAT task.")
delete_parser.add_argument("task_ids", type=int, help="list of task IDs", nargs="+")
#######################################################################
# List
#######################################################################
ls_parser = task_subparser.add_parser(
"ls", description="List all CVAT tasks in simple or JSON format."
)
ls_parser.add_argument(
"--json",
dest="use_json_output",
default=False,
action="store_true",
help="output JSON data",
)
#######################################################################
# Frames
#######################################################################
frames_parser = task_subparser.add_parser(
"frames", description="Download all frame images for a CVAT task."
)
frames_parser.add_argument("task_id", type=int, help="task ID")
frames_parser.add_argument(
"frame_ids", type=int, help="list of frame IDs to download", nargs="+"
)
frames_parser.add_argument(
"--outdir", type=str, default="", help="directory to save images (default: CWD)"
)
frames_parser.add_argument(
"--quality",
type=str,
choices=("original", "compressed"),
default="original",
help="choose quality of images (default: %(default)s)",
)
#######################################################################
# Dump
#######################################################################
dump_parser = task_subparser.add_parser(
"dump", description="Download annotations for a CVAT task."
)
dump_parser.add_argument("task_id", type=int, help="task ID")
dump_parser.add_argument("filename", type=str, help="output file")
dump_parser.add_argument(
"--format",
dest="fileformat",
type=str,
default="CVAT for images 1.1",
help="annotation format (default: %(default)s)",
)
dump_parser.add_argument(
"--completion_verification_period",
dest="status_check_period",
default=3,
type=float,
help="number of seconds to wait until checking if dataset building finished",
)
dump_parser.add_argument(
"--with-images",
type=strtobool,
default=False,
dest="include_images",
help="Whether to include images or not (default: %(default)s)",
)
#######################################################################
# Upload Annotations
#######################################################################
upload_parser = task_subparser.add_parser(
"upload", description="Upload annotations for a CVAT task."
)
upload_parser.add_argument("task_id", type=int, help="task ID")
upload_parser.add_argument("filename", type=str, help="upload file")
upload_parser.add_argument(
"--format",
dest="fileformat",
type=str,
default="CVAT 1.1",
help="annotation format (default: %(default)s)",
)
#######################################################################
# Export task
#######################################################################
export_task_parser = task_subparser.add_parser("export", description="Export a CVAT task.")
export_task_parser.add_argument("task_id", type=int, help="task ID")
export_task_parser.add_argument("filename", type=str, help="output file")
export_task_parser.add_argument(
"--completion_verification_period",
dest="status_check_period",
default=3,
type=float,
help="time interval between checks if archive building has been finished, in seconds",
)
#######################################################################
# Import task
#######################################################################
import_task_parser = task_subparser.add_parser("import", description="Import a CVAT task.")
import_task_parser.add_argument("filename", type=str, help="upload file")
import_task_parser.add_argument(
"--completion_verification_period",
dest="status_check_period",
default=3,
type=float,
help="time interval between checks if archive proessing was finished, in seconds",
)
return parser
def get_action_args(
parser: argparse.ArgumentParser, parsed_args: argparse.Namespace
) -> argparse.Namespace:
# FIXME: a hacky way to remove unnecessary args
action_args = dict(vars(parsed_args))
for action in parser._actions:
action_args.pop(action.dest, None)
# remove default args
for k, v in dict(action_args).items():
if v is None:
action_args.pop(k, None)
return argparse.Namespace(**action_args)

@ -0,0 +1 @@
VERSION = "0.2-alpha"

@ -1,12 +1,9 @@
// Copyright (C) 2018-2021 Intel Corporation
// Copyright (C) 2018-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
module.exports = {
env: {
node: true,
browser: true,
es6: true,
'jest/globals': true,
},
ignorePatterns: [
@ -19,9 +16,8 @@ module.exports = {
'dist/**',
],
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module',
ecmaVersion: 2018,
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
plugins: ['jest'],
rules: {
@ -29,5 +25,5 @@ module.exports = {
'jest/no-focused-tests': 'error',
'jest/no-identical-title': 'error',
'jest/prefer-to-have-length': 'warn',
}
}
};

@ -9,43 +9,43 @@ It contains the core logic of the Computer Vision Annotation Tool.
If you make changes in this package, please do following:
- After not important changes (typos, backward compatible bug fixes, refactoring) do: `npm version patch`
- After changing API (backward compatible new features) do: `npm version minor`
- After changing API (changes that break backward compatibility) do: `npm version major`
- After not important changes (typos, backward compatible bug fixes, refactoring) do: `yarn version --patch`
- After changing API (backward compatible new features) do: `yarn version --minor`
- After changing API (changes that break backward compatibility) do: `yarn version --major`
### Commands
- Dependencies installation
```bash
npm ci
yarn ci --frozen-lockfile
```
- Building the module from sources in the `dist` directory:
```bash
npm run build
npm run build -- --mode=development # without a minification
yarn run build
yarn run build --mode=development # without a minification
```
- Building the documentation in the `docs` directory:
```bash
npm run-script docs
yarn run docs
```
- Running of tests:
```bash
npm run-script test
yarn run test
```
- Updating of a module version:
```bash
npm version patch # updated after minor fixes
npm version minor # updated after major changes which don't affect API compatibility with previous versions
npm version major # updated after major changes which affect API compatibility with previous versions
yarn version --patch # updated after minor fixes
yarn version --minor # updated after major changes which don't affect API compatibility with previous versions
yarn version --major # updated after major changes which affect API compatibility with previous versions
```
Visual studio code configurations:

@ -1,10 +1,11 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
const { defaults } = require('jest-config');
module.exports = {
preset: 'ts-jest',
coverageDirectory: 'reports/coverage',
coverageReporters: ['json', ['lcov', { projectRoot: '../' }]],
moduleFileExtensions: [...defaults.moduleFileExtensions, 'ts', 'tsx'],
@ -12,4 +13,9 @@ module.exports = {
testMatch: ['**/tests/**/*.js'],
testPathIgnorePatterns: ['/node_modules/', '/tests/mocks/*'],
automock: false,
globals: {
'ts-jest': {
diagnostics: false,
},
},
};

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

File diff suppressed because it is too large Load Diff

@ -1,15 +1,17 @@
{
"name": "cvat-core",
"version": "5.0.1",
"version": "6.0.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"main": "src/api.ts",
"scripts": {
"build": "webpack",
"test": "jest --config=jest.config.js --coverage",
"docs": "jsdoc --readme README.md src/*.js -p -c jsdoc.config.js -d docs",
"coveralls": "cat ./reports/coverage/lcov.info | coveralls"
"coveralls": "cat ./reports/coverage/lcov.info | coveralls",
"type-check": "tsc --noEmit",
"type-check:watch": "yarn run type-check -- --watch"
},
"author": "Intel",
"author": "CVAT.ai",
"license": "MIT",
"browserslist": [
"Chrome >= 63",
@ -21,17 +23,18 @@
"coveralls": "^3.0.5",
"jest": "^26.6.3",
"jest-junit": "^6.4.0",
"jsdoc": "^3.6.6"
"jsdoc": "^3.6.6",
"ts-jest": "26"
},
"dependencies": {
"axios": "^0.21.4",
"browser-or-node": "^1.2.1",
"cvat-data": "../cvat-data",
"axios": "^0.27.2",
"browser-or-node": "^2.0.0",
"cvat-data": "file:../cvat-data",
"detect-browser": "^5.2.1",
"error-stack-parser": "^2.0.2",
"form-data": "^2.5.0",
"jest-config": "^26.6.3",
"js-cookie": "^2.2.0",
"form-data": "^4.0.0",
"jest-config": "^28.1.2",
"js-cookie": "^3.0.1",
"json-logic-js": "^2.0.1",
"platform": "^1.3.5",
"quickhull": "^1.0.3",

@ -1,189 +0,0 @@
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
/**
* Class representing an annotation loader
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Loader {
constructor(initialData) {
const data = {
name: initialData.name,
format: initialData.ext,
version: initialData.version,
enabled: initialData.enabled,
dimension: initialData.dimension,
};
Object.defineProperties(this, {
name: {
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.name,
},
format: {
/**
* @name format
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.format,
},
version: {
/**
* @name version
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.version,
},
enabled: {
/**
* @name enabled
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.enabled,
},
dimension: {
/**
* @name dimension
* @type {string}
* @memberof module:API.cvat.enums.DimensionType
* @readonly
* @instance
*/
get: () => data.dimension,
},
});
}
}
/**
* Class representing an annotation dumper
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Dumper {
constructor(initialData) {
const data = {
name: initialData.name,
format: initialData.ext,
version: initialData.version,
enabled: initialData.enabled,
dimension: initialData.dimension,
};
Object.defineProperties(this, {
name: {
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.name,
},
format: {
/**
* @name format
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.format,
},
version: {
/**
* @name version
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.version,
},
enabled: {
/**
* @name enabled
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.enabled,
},
dimension: {
/**
* @name dimension
* @type {string}
* @memberof module:API.cvat.enums.DimensionType
* @readonly
* @instance
*/
get: () => data.dimension,
},
});
}
}
/**
* Class representing an annotation format
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class AnnotationFormats {
constructor(initialData) {
const data = {
exporters: initialData.exporters.map((el) => new Dumper(el)),
importers: initialData.importers.map((el) => new Loader(el)),
};
// Now all fields are readonly
Object.defineProperties(this, {
loaders: {
/**
* @name loaders
* @type {module:API.cvat.classes.Loader[]}
* @memberof module:API.cvat.classes.AnnotationFormats
* @readonly
* @instance
*/
get: () => [...data.importers],
},
dumpers: {
/**
* @name dumpers
* @type {module:API.cvat.classes.Dumper[]}
* @memberof module:API.cvat.classes.AnnotationFormats
* @readonly
* @instance
*/
get: () => [...data.exporters],
},
});
}
}
module.exports = {
AnnotationFormats,
Loader,
Dumper,
};
})();

@ -0,0 +1,210 @@
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
interface RawLoaderData {
name: string;
ext: string;
version: string;
enabled: boolean;
dimension: '2d' | '3d';
}
/**
* Class representing an annotation loader
* @memberof module:API.cvat.classes
* @hideconstructor
*/
export class Loader {
public name: string;
public format: string;
public version: string;
public enabled: boolean;
public dimension: '2d' | '3d';
constructor(initialData: RawLoaderData) {
const data = {
name: initialData.name,
format: initialData.ext,
version: initialData.version,
enabled: initialData.enabled,
dimension: initialData.dimension,
};
Object.defineProperties(this, {
name: {
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.name,
},
format: {
/**
* @name format
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.format,
},
version: {
/**
* @name version
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.version,
},
enabled: {
/**
* @name enabled
* @type {boolean}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.enabled,
},
dimension: {
/**
* @name dimension
* @type {module:API.cvat.enums.DimensionType}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.dimension,
},
});
}
}
type RawDumperData = RawLoaderData;
/**
* Class representing an annotation dumper
* @memberof module:API.cvat.classes
* @hideconstructor
*/
export class Dumper {
public name: string;
public format: string;
public version: string;
public enabled: boolean;
public dimension: '2d' | '3d';
constructor(initialData: RawDumperData) {
const data = {
name: initialData.name,
format: initialData.ext,
version: initialData.version,
enabled: initialData.enabled,
dimension: initialData.dimension,
};
Object.defineProperties(this, {
name: {
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.name,
},
format: {
/**
* @name format
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.format,
},
version: {
/**
* @name version
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.version,
},
enabled: {
/**
* @name enabled
* @type {boolean}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.enabled,
},
dimension: {
/**
* @name dimension
* @type {module:API.cvat.enums.DimensionType}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.dimension,
},
});
}
}
interface AnnotationFormatRawData {
importers: RawLoaderData[];
exporters: RawDumperData[];
}
/**
* Class representing an annotation format
* @memberof module:API.cvat.classes
* @hideconstructor
*/
export class AnnotationFormats {
public loaders: Loader[];
public dumpers: Dumper[];
constructor(initialData: AnnotationFormatRawData) {
const data = {
exporters: initialData.exporters.map((el) => new Dumper(el)),
importers: initialData.importers.map((el) => new Loader(el)),
};
Object.defineProperties(this, {
loaders: {
/**
* @name loaders
* @type {module:API.cvat.classes.Loader[]}
* @memberof module:API.cvat.classes.AnnotationFormats
* @readonly
* @instance
*/
get: () => [...data.importers],
},
dumpers: {
/**
* @name dumpers
* @type {module:API.cvat.classes.Dumper[]}
* @memberof module:API.cvat.classes.AnnotationFormats
* @readonly
* @instance
*/
get: () => [...data.exporters],
},
});
}
}

@ -4,100 +4,22 @@
(() => {
const {
RectangleShape,
PolygonShape,
PolylineShape,
PointsShape,
EllipseShape,
CuboidShape,
RectangleTrack,
PolygonTrack,
PolylineTrack,
PointsTrack,
EllipseTrack,
CuboidTrack,
shapeFactory,
trackFactory,
Track,
Shape,
Tag,
objectStateFactory,
} = require('./annotations-objects');
const AnnotationsFilter = require('./annotations-filter');
const AnnotationsFilter = require('./annotations-filter').default;
const { checkObjectType } = require('./common');
const Statistics = require('./statistics');
const { Label } = require('./labels');
const { DataError, ArgumentError, ScriptingError } = require('./exceptions');
const { ArgumentError, ScriptingError } = require('./exceptions');
const ObjectState = require('./object-state').default;
const {
HistoryActions, ObjectShape, ObjectType, colors,
HistoryActions, ShapeType, ObjectType, colors, Source,
} = require('./enums');
const ObjectState = require('./object-state');
function shapeFactory(shapeData, clientID, injection) {
const { type } = shapeData;
const color = colors[clientID % colors.length];
let shapeModel = null;
switch (type) {
case 'rectangle':
shapeModel = new RectangleShape(shapeData, clientID, color, injection);
break;
case 'polygon':
shapeModel = new PolygonShape(shapeData, clientID, color, injection);
break;
case 'polyline':
shapeModel = new PolylineShape(shapeData, clientID, color, injection);
break;
case 'points':
shapeModel = new PointsShape(shapeData, clientID, color, injection);
break;
case 'ellipse':
shapeModel = new EllipseShape(shapeData, clientID, color, injection);
break;
case 'cuboid':
shapeModel = new CuboidShape(shapeData, clientID, color, injection);
break;
default:
throw new DataError(`An unexpected type of shape "${type}"`);
}
return shapeModel;
}
function trackFactory(trackData, clientID, injection) {
if (trackData.shapes.length) {
const { type } = trackData.shapes[0];
const color = colors[clientID % colors.length];
let trackModel = null;
switch (type) {
case 'rectangle':
trackModel = new RectangleTrack(trackData, clientID, color, injection);
break;
case 'polygon':
trackModel = new PolygonTrack(trackData, clientID, color, injection);
break;
case 'polyline':
trackModel = new PolylineTrack(trackData, clientID, color, injection);
break;
case 'points':
trackModel = new PointsTrack(trackData, clientID, color, injection);
break;
case 'ellipse':
trackModel = new EllipseTrack(trackData, clientID, color, injection);
break;
case 'cuboid':
trackModel = new CuboidTrack(trackData, clientID, color, injection);
break;
default:
throw new DataError(`An unexpected type of track "${type}"`);
}
return trackModel;
}
console.warn('The track without any shapes had been found. It was ignored.');
return null;
}
class Collection {
constructor(data) {
@ -107,6 +29,10 @@
this.labels = data.labels.reduce((labelAccumulator, label) => {
labelAccumulator[label.id] = label;
(label?.structure?.sublabels || []).forEach((sublabel) => {
labelAccumulator[sublabel.id] = sublabel;
});
return labelAccumulator;
}, {});
@ -126,6 +52,7 @@
groups: this.groups,
frameMeta: this.frameMeta,
history: this.history,
nextClientID: () => ++this.count,
groupColors: {},
};
}
@ -202,10 +129,7 @@
const tags = this.tags[frame] || [];
const objects = [].concat(tracks, shapes, tags);
const visible = {
models: [],
data: [],
};
const visible = [];
for (const object of objects) {
if (object.removed) {
@ -213,21 +137,19 @@
}
const stateData = object.get(frame);
if (!allTracks && stateData.outside && !stateData.keyframe) {
if (stateData.outside && !stateData.keyframe && !allTracks && object instanceof Track) {
continue;
}
visible.models.push(object);
visible.data.push(stateData);
visible.push(stateData);
}
const objectStates = [];
const filtered = this.annotationsFilter.filter(visible.data, filters);
const filtered = this.annotationsFilter.filter(visible, filters);
visible.data.forEach((stateData, idx) => {
visible.forEach((stateData, idx) => {
if (!filters.length || filtered.includes(stateData.clientID)) {
const model = visible.models[idx];
const objectState = objectStateFactory.call(model, frame, stateData);
const objectState = new ObjectState(stateData);
objectStates.push(objectState);
}
});
@ -255,7 +177,7 @@
throw new ArgumentError(`Unknown label for the task: ${label.id}`);
}
if (!Object.values(ObjectShape).includes(shapeType)) {
if (!Object.values(ShapeType).includes(shapeType)) {
throw new ArgumentError(`Got unknown shapeType "${shapeType}"`);
}
@ -288,10 +210,14 @@
keyframes[object.frame] = {
type: shapeType,
frame: object.frame,
points: [...object.points],
points: object.shapeType === ShapeType.SKELETON ? undefined : [...object.points],
elements: object.shapeType === ShapeType.SKELETON ? object.elements.map((el) => {
const { id, clientID, ...rest } = el.toJSON();
return rest;
}) : undefined,
occluded: object.occluded,
rotation: object.rotation,
zOrder: object.zOrder,
z_order: object.zOrder,
outside: false,
attributes: Object.keys(object.attributes).reduce((accumulator, attrID) => {
// We save only mutable attributes inside a keyframe
@ -311,13 +237,24 @@
keyframes[object.frame + 1] = JSON.parse(JSON.stringify(keyframes[object.frame]));
keyframes[object.frame + 1].outside = true;
keyframes[object.frame + 1].frame++;
keyframes[object.frame + 1].attributes = [];
(keyframes[object.frame + 1].elements || []).forEach((el) => {
el.outside = keyframes[object.frame + 1].outside;
el.frame = keyframes[object.frame + 1].frame;
});
}
} else if (object instanceof Track) {
// If this object is track, iterate through all its
// keyframes and push copies to new keyframes
const attributes = {}; // id:value
for (const keyframe of Object.keys(object.shapes)) {
const shape = object.shapes[keyframe];
const trackShapes = object.shapes;
const exportedShapes = object.shapeType === ShapeType.SKELETON ?
object.prepareShapesForServer().reduce((acc, val) => {
acc[val.frame] = val;
return acc;
}, {}) : {};
for (const keyframe of Object.keys(trackShapes)) {
const shape = trackShapes[keyframe];
// Frame already saved and it is not outside
if (keyframe in keyframes && !keyframes[keyframe].outside) {
// This shape is outside and non-outside shape already exists
@ -341,11 +278,16 @@
keyframes[keyframe] = {
type: shapeType,
frame: +keyframe,
points: [...shape.points],
points: object.shapeType === ShapeType.SKELETON ? undefined : [...shape.points],
elements: object.shapeType === ShapeType.SKELETON ?
exportedShapes[keyframe].elements.map((el) => {
const { id, ...rest } = el;
return rest;
}) : undefined,
rotation: shape.rotation,
occluded: shape.occluded,
outside: shape.outside,
zOrder: shape.zOrder,
z_order: shape.zOrder,
attributes: updatedAttributes ? Object.keys(attributes).reduce((accumulator, attrID) => {
accumulator.push({
spec_id: +attrID,
@ -451,13 +393,30 @@
const exported = object.toJSON();
const position = {
type: objectState.shapeType,
points: [...objectState.points],
points: objectState.shapeType === ShapeType.SKELETON ? undefined : [...objectState.points],
elements: objectState.shapeType === ShapeType.SKELETON ? objectState.elements.map((el: ObjectState) => {
const elementAttributes = el.attributes;
return {
attributes: Object.keys(elementAttributes).reduce((acc, attrID) => {
acc.push({
spec_id: +attrID,
value: elementAttributes[attrID],
});
return acc;
}, []),
label_id: el.label.id,
occluded: el.occluded,
outside: el.outside,
points: [...el.points],
type: el.shapeType,
};
}) : undefined,
rotation: objectState.rotation,
occluded: objectState.occluded,
outside: objectState.outside,
zOrder: objectState.zOrder,
z_order: objectState.zOrder,
attributes: Object.keys(objectState.attributes).reduce((accumulator, attrID) => {
if (!labelAttributes[attrID].mutable) {
if (labelAttributes[attrID].mutable) {
accumulator.push({
spec_id: +attrID,
value: objectState.attributes[attrID],
@ -475,14 +434,19 @@
label_id: exported.label_id,
attributes: exported.attributes,
shapes: [],
source: Source.MANUAL,
};
const next = JSON.parse(JSON.stringify(prev));
next.frame = frame;
next.shapes.push(JSON.parse(JSON.stringify(position)));
exported.shapes.map((shape) => {
delete shape.id;
(shape.elements || []).forEach((element) => {
delete element.id;
});
if (shape.frame < frame) {
prev.shapes.push(JSON.parse(JSON.stringify(shape)));
} else if (shape.frame > frame) {
@ -499,6 +463,9 @@
prev.shapes[prev.shapes.length - 2].frame -= 1;
}
prev.shapes[prev.shapes.length - 1].outside = true;
(prev.shapes[prev.shapes.length - 1].elements || []).forEach((el) => {
el.outside = true;
});
let clientID = ++this.count;
const prevTrack = trackFactory(prev, clientID, this.injection);
@ -546,6 +513,7 @@
const undoGroups = objectsForGroup.map((object) => object.group);
for (const object of objectsForGroup) {
object.group = groupIdx;
object.updated = Date.now();
}
const redoGroups = objectsForGroup.map((object) => object.group);
@ -554,11 +522,13 @@
() => {
objectsForGroup.forEach((object, idx) => {
object.group = undoGroups[idx];
object.updated = Date.now();
});
},
() => {
objectsForGroup.forEach((object, idx) => {
object.group = redoGroups[idx];
object.updated = Date.now();
});
},
objectsForGroup.map((object) => object.clientID),
@ -579,8 +549,17 @@
tracks.forEach((track) => {
if (track.frame <= endframe) {
if (delTrackKeyframesOnly) {
for (const keyframe in track.shapes) {
if (keyframe >= startframe && keyframe <= endframe) { delete track.shapes[keyframe]; }
for (const keyframe of Object.keys(track.shapes)) {
if (+keyframe >= startframe && +keyframe <= endframe) {
delete track.shapes[keyframe];
(track.elements || []).forEach((element) => {
if (keyframe in element.shapes) {
delete element.shapes[keyframe];
element.updated = Date.now();
}
});
track.updated = Date.now();
}
}
} else if (track.frame >= startframe) {
const index = tracks.indexOf(track);
@ -593,7 +572,7 @@
this.shapes = {};
this.tags = {};
this.tracks = [];
this.objects = {}; // by id
this.objects = {};
this.count = 0;
this.flush = true;
@ -606,42 +585,74 @@
statistics() {
const labels = {};
const skeleton = {
rectangle: {
shape: 0,
track: 0,
},
polygon: {
shape: 0,
track: 0,
},
polyline: {
shape: 0,
track: 0,
},
points: {
shape: 0,
track: 0,
},
ellipse: {
shape: 0,
track: 0,
},
cuboid: {
shape: 0,
track: 0,
},
tags: 0,
const shapes = ['rectangle', 'polygon', 'polyline', 'points', 'ellipse', 'cuboid', 'skeleton'];
const body = {
...(shapes.reduce((acc, val) => ({
...acc,
[val]: { shape: 0, track: 0 },
}), {})),
tag: 0,
manually: 0,
interpolated: 0,
total: 0,
};
const total = JSON.parse(JSON.stringify(skeleton));
for (const label of Object.values(this.labels)) {
const { name } = label;
labels[name] = JSON.parse(JSON.stringify(skeleton));
}
const sep = '{{cvat.skeleton.lbl.sep}}';
const fillBody = (spec, prefix = ''): void => {
const pref = prefix ? `${prefix}${sep}` : '';
for (const label of spec) {
const { name } = label;
labels[`${pref}${name}`] = JSON.parse(JSON.stringify(body));
if (label?.structure?.sublabels) {
fillBody(label.structure.sublabels, `${pref}${name}`);
}
}
};
const total = JSON.parse(JSON.stringify(body));
fillBody(Object.values(this.labels).filter((label) => !label.hasParent));
const scanTrack = (track, prefix = ''): void => {
const pref = prefix ? `${prefix}${sep}` : '';
const label = `${pref}${track.label.name}`;
labels[label][track.shapeType].track++;
const keyframes = Object.keys(track.shapes)
.sort((a, b) => +a - +b)
.map((el) => +el);
let prevKeyframe = keyframes[0];
let visible = false;
for (const keyframe of keyframes) {
if (visible) {
const interpolated = keyframe - prevKeyframe - 1;
labels[label].interpolated += interpolated;
labels[label].total += interpolated;
}
visible = !track.shapes[keyframe].outside;
prevKeyframe = keyframe;
if (visible) {
labels[label].manually++;
labels[label].total++;
}
}
let lastKey = keyframes[keyframes.length - 1];
if (track.shapeType === ShapeType.SKELETON) {
track.elements.forEach((element) => {
scanTrack(element, label);
lastKey = Math.max(lastKey, ...Object.keys(element.shapes).map((key) => +key));
});
}
if (lastKey !== this.stopFrame && !track.get(lastKey).outside) {
const interpolated = this.stopFrame - lastKey;
labels[label].interpolated += interpolated;
labels[label].total += interpolated;
}
};
for (const object of Object.values(this.objects)) {
if (object.removed) {
@ -659,59 +670,37 @@
throw new ScriptingError(`Unexpected object type: "${objectType}"`);
}
const label = object.label.name;
const { name: label } = object.label;
if (objectType === 'tag') {
labels[label].tags++;
labels[label].tag++;
labels[label].manually++;
labels[label].total++;
} else if (objectType === 'track') {
scanTrack(object);
} else {
const { shapeType } = object;
labels[label][shapeType][objectType]++;
if (objectType === 'track') {
const keyframes = Object.keys(object.shapes)
.sort((a, b) => +a - +b)
.map((el) => +el);
let prevKeyframe = keyframes[0];
let visible = false;
for (const keyframe of keyframes) {
if (visible) {
const interpolated = keyframe - prevKeyframe - 1;
labels[label].interpolated += interpolated;
labels[label].total += interpolated;
}
visible = !object.shapes[keyframe].outside;
prevKeyframe = keyframe;
if (visible) {
labels[label].manually++;
labels[label].total++;
}
}
const lastKey = keyframes[keyframes.length - 1];
if (lastKey !== this.stopFrame && !object.shapes[lastKey].outside) {
const interpolated = this.stopFrame - lastKey;
labels[label].interpolated += interpolated;
labels[label].total += interpolated;
}
} else {
labels[label].manually++;
labels[label].total++;
labels[label][shapeType].shape++;
labels[label].manually++;
labels[label].total++;
if (shapeType === ShapeType.SKELETON) {
object.elements.forEach((element) => {
const combinedName = [label, element.label.name].join(sep);
labels[combinedName][element.shapeType].shape++;
labels[combinedName].manually++;
labels[combinedName].total++;
});
}
}
}
for (const label of Object.keys(labels)) {
for (const key of Object.keys(labels[label])) {
if (typeof labels[label][key] === 'object') {
for (const objectType of Object.keys(labels[label][key])) {
total[key][objectType] += labels[label][key][objectType];
for (const shapeType of Object.keys(labels[label])) {
if (typeof labels[label][shapeType] === 'object') {
for (const objectType of Object.keys(labels[label][shapeType])) {
total[shapeType][objectType] += labels[label][shapeType][objectType];
}
} else {
total[key] += labels[label][key];
total[shapeType] += labels[label][shapeType];
}
}
}
@ -744,7 +733,7 @@
for (const state of objectStates) {
checkObjectType('object state', state, null, ObjectState);
checkObjectType('state client ID', state.clientID, 'undefined', null);
checkObjectType('state client ID', state.clientID, null, null);
checkObjectType('state frame', state.frame, 'integer', null);
checkObjectType('state rotation', state.rotation || 0, 'number', null);
checkObjectType('state attributes', state.attributes, null, Object);
@ -775,9 +764,9 @@
checkObjectType('point coordinate', coord, 'number', null);
}
if (!Object.values(ObjectShape).includes(state.shapeType)) {
if (!Object.values(ShapeType).includes(state.shapeType)) {
throw new ArgumentError(
`Object shape must be one of: ${JSON.stringify(Object.values(ObjectShape))}`,
`Object shape must be one of: ${JSON.stringify(Object.values(ShapeType))}`,
);
}
@ -794,6 +783,18 @@
type: state.shapeType,
z_order: state.zOrder,
source: state.source,
elements: state.shapeType === 'skeleton' ? state.elements.map((element) => ({
attributes: [],
frame: element.frame,
group: 0,
label_id: element.label.id,
points: [...element.points],
rotation: 0,
type: element.shapeType,
z_order: 0,
outside: element.outside || false,
occluded: element.occluded || false,
})) : undefined,
});
} else if (state.objectType === 'track') {
constructed.tracks.push({
@ -807,7 +808,7 @@
{
attributes: attributes.filter((attr) => labelAttributes[attr.spec_id].mutable),
frame: state.frame,
occluded: state.occluded || false,
occluded: false,
outside: false,
points: [...state.points],
rotation: state.rotation || 0,
@ -815,6 +816,33 @@
z_order: state.zOrder,
},
],
elements: state.shapeType === 'skeleton' ? state.elements.map((element) => {
const elementAttrValues = Object.keys(state.attributes)
.reduce(convertAttributes.bind(state), []);
const elementAttributes = element.label.attributes.reduce((accumulator, attribute) => {
accumulator[attribute.id] = attribute;
return accumulator;
}, {});
return ({
attributes: elementAttrValues
.filter((attr) => !elementAttributes[attr.spec_id].mutable),
frame: state.frame,
group: 0,
label_id: element.label.id,
shapes: [{
frame: state.frame,
type: element.shapeType,
points: [...element.points],
zOrder: state.zOrder,
outside: element.outside || false,
occluded: element.occluded || false,
rotation: element.rotation || 0,
attributes: elementAttrValues
.filter((attr) => !elementAttributes[attr.spec_id].mutable),
}],
});
}) : undefined,
});
} else {
throw new ArgumentError(

@ -1,15 +1,15 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
const jsonLogic = require('json-logic-js');
const { AttributeType, ObjectType } = require('./enums');
import jsonLogic from 'json-logic-js';
import { AttributeType, ObjectType } from './enums';
function adjustName(name) {
function adjustName(name): string {
return name.replace(/\./g, '\u2219');
}
class AnnotationsFilter {
export default class AnnotationsFilter {
_convertObjects(statesData) {
const objects = statesData.map((state) => {
const labelAttributes = state.label.attributes.reduce((acc, attr) => {
@ -24,7 +24,11 @@ class AnnotationsFilter {
let [width, height] = [null, null];
if (state.objectType !== ObjectType.TAG) {
state.points.forEach((coord, idx) => {
const points = state.points || state.elements.reduce((acc, val) => {
acc.push(val.points);
return acc;
}, []).flat();
points.forEach((coord, idx) => {
if (idx % 2) {
// y
ytl = Math.min(ytl, coord);
@ -75,5 +79,3 @@ class AnnotationsFilter {
.filter((_, index) => jsonLogic.apply(filters[0], converted[index]));
}
}
module.exports = AnnotationsFilter;

@ -1,27 +1,41 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { HistoryActions } from './enums';
const MAX_HISTORY_LENGTH = 128;
class AnnotationHistory {
interface ActionItem {
action: HistoryActions;
undo: Function;
redo: Function;
clientIDs: number[];
frame: number;
}
export default class AnnotationHistory {
private frozen: boolean;
private _undo: ActionItem[];
private _redo: ActionItem[];
constructor() {
this.frozen = false;
this.clear();
}
freeze(frozen) {
public freeze(frozen: boolean): void {
this.frozen = frozen;
}
get() {
public get(): { undo: [HistoryActions, number][], redo: [HistoryActions, number][] } {
return {
undo: this._undo.map((undo) => [undo.action, undo.frame]),
redo: this._redo.map((redo) => [redo.action, redo.frame]),
};
}
do(action, undo, redo, clientIDs, frame) {
public do(action: HistoryActions, undo: Function, redo: Function, clientIDs: number[], frame: number): void {
if (this.frozen) return;
const actionItem = {
clientIDs,
@ -36,12 +50,12 @@ class AnnotationHistory {
this._redo = [];
}
undo(count) {
public async undo(count: number): Promise<number[]> {
const affectedObjects = [];
for (let i = 0; i < count; i++) {
const action = this._undo.pop();
if (action) {
action.undo();
await action.undo();
this._redo.push(action);
affectedObjects.push(...action.clientIDs);
} else {
@ -52,12 +66,12 @@ class AnnotationHistory {
return affectedObjects;
}
redo(count) {
public async redo(count: number): Promise<number[]> {
const affectedObjects = [];
for (let i = 0; i < count; i++) {
const action = this._redo.pop();
if (action) {
action.redo();
await action.redo();
this._undo.push(action);
affectedObjects.push(...action.clientIDs);
} else {
@ -68,10 +82,8 @@ class AnnotationHistory {
return affectedObjects;
}
clear() {
public clear(): void {
this._undo = [];
this._redo = [];
}
}
module.exports = AnnotationHistory;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -103,6 +103,7 @@
'rotation',
'type',
'shapes',
'elements',
'attributes',
'value',
'spec_id',

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

Loading…
Cancel
Save