Merge branch 'develop.cvat.ai' into develop

main
Nikita Manovich 4 years ago
commit e1c90477e7

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

@ -7,27 +7,27 @@ module.exports = {
env: { env: {
node: true, node: true,
browser: true, browser: true,
es6: true, es2020: true,
}, },
parserOptions: { parserOptions: {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 2018, parser: '@typescript-eslint/parser',
}, },
ignorePatterns: [ ignorePatterns: [
'.eslintrc.js', '.eslintrc.js',
'lint-staged.config.js', 'lint-staged.config.js',
], ],
plugins: ['security', 'no-unsanitized', 'eslint-plugin-header', 'import'], plugins: ['@typescript-eslint', 'security', 'no-unsanitized', 'eslint-plugin-header', 'import'],
extends: [ extends: [
'eslint:recommended', 'plugin:security/recommended', 'plugin:no-unsanitized/DOM', 'eslint:recommended', 'plugin:security/recommended', 'plugin:no-unsanitized/DOM',
'airbnb-base', 'plugin:import/errors', 'plugin:import/warnings', 'airbnb-base', 'plugin:import/errors', 'plugin:import/warnings',
'plugin:import/typescript', 'plugin:import/typescript', 'plugin:@typescript-eslint/recommended', 'airbnb-typescript/base',
], ],
rules: { rules: {
'header/header': [2, 'line', [{ // 'header/header': [2, 'line', [{
pattern: ' {1}Copyright \\(C\\) (?:20\\d{2}-)?2022 Intel Corporation', // pattern: ' {1}Copyright \\(C\\) (?:20\\d{2}-)?2022 Intel Corporation',
template: ' Copyright (C) 2022 Intel Corporation' // template: ' Copyright (C) 2022 Intel Corporation'
}, '', ' SPDX-License-Identifier: MIT']], // }, '', ' SPDX-License-Identifier: MIT']],
'no-plusplus': 0, 'no-plusplus': 0,
'no-continue': 0, 'no-continue': 0,
'no-console': 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 '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/order': ['error', {'groups': ['builtin', 'external', 'internal']}],
'import/prefer-default-export': 0, // works incorrect with interfaces '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 ### 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) - [ ] Searched [past issues](/issues)
<!--- Provide a general summary of the issue in the Title above --> <!--- Provide a general summary of the issue in the Title above -->
@ -49,5 +43,3 @@ the bug in -->
<summary>Logs from `cvat` container</summary> <summary>Logs from `cvat` container</summary>
</details> </details>
### Next steps
You may [join our Gitter](https://gitter.im/opencv-cvat/public) channel for community support.

@ -1,13 +1,7 @@
<!--- <!-- Raised an issue to propose your change (https://github.com/cvat-ai/cvat/issues).
Copyright (C) 2020-2022 Intel Corporation
SPDX-License-Identifier: MIT
-->
<!-- Raised an issue to propose your change (https://github.com/opencv/cvat/issues).
It helps to avoid duplication of efforts from multiple independent contributors. 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. 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. --> guide. -->
<!-- Provide a general summary of your changes in the Title above --> <!-- 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. 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! --> 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 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]( - [ ] 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 added tests to cover my changes
- [ ] I have linked related issues ([read github docs]( - [ ] 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)) 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), - [ ] 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/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)) [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 ### License
- [ ] I submit _my code changes_ under the same [MIT 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. 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 on: pull_request
jobs: jobs:
Bandit: Linter:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -17,7 +17,8 @@ jobs:
PR_FILES="$PR_FILES_AM $PR_FILES_RENAMED" PR_FILES="$PR_FILES_AM $PR_FILES_RENAMED"
for FILE in $PR_FILES; do for FILE in $PR_FILES; do
EXTENSION="${FILE##*.}" EXTENSION="${FILE##*.}"
if [[ $EXTENSION == 'py' ]]; then DIRECTORY="${FILE%%/*}"
if [[ "$EXTENSION" == 'py' && "$DIRECTORY" != 'cvat-sdk' ]]; then
CHANGED_FILES+=" $FILE" CHANGED_FILES+=" $FILE"
fi fi
done 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: jobs:
Caching_CVAT: Caching_CVAT:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - 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: with:
path: /tmp/cvat_cache_server path: /tmp/cvat_cache_server
key: ${{ runner.os }}-build-server-${{ github.sha }} key: ${{ runner.os }}-build-server-${{ github.sha }}
restore-keys: | restore-keys: |
${{ runner.os }}-build-server-${{ steps.get-sha.outputs.sha }}
${{ runner.os }}-build-server- ${{ runner.os }}-build-server-
- uses: actions/cache@v2 - uses: actions/cache@v3
id: ui-cache-action
with: with:
path: /tmp/cvat_cache_ui path: /tmp/cvat_cache_ui
key: ${{ runner.os }}-build-ui-${{ github.sha }} key: ${{ runner.os }}-build-ui-${{ github.sha }}
restore-keys: | restore-keys: |
${{ runner.os }}-build-ui-${{ steps.get-sha.outputs.sha }}
${{ runner.os }}-build-ui- ${{ 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 - 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 uses: docker/build-push-action@v2
with: with:
context: . context: .
@ -42,9 +89,34 @@ jobs:
cache-from: type=local,src=/tmp/cvat_cache_ui cache-from: type=local,src=/tmp/cvat_cache_ui
cache-to: type=local,dest=/tmp/cvat_cache_ui-new 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 - name: Moving cache
run: | run: |
rm -rf /tmp/cvat_cache_server rm -rf /tmp/cvat_cache_server
mv /tmp/cvat_cache_server-new /tmp/cvat_cache_server mv /tmp/cvat_cache_server-new /tmp/cvat_cache_server
rm -rf /tmp/cvat_cache_ui rm -rf /tmp/cvat_cache_ui
mv /tmp/cvat_cache_ui-new /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: on:
push: push:
branches: [ develop, hotfix-*, master, release-* ] branches: [ "develop", master, release-* ]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ develop ] branches: [ "develop" ]
schedule: schedule:
- cron: '25 19 * * 6' - cron: '27 19 * * 4'
jobs: jobs:
analyze: analyze:
@ -33,39 +33,40 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
language: [ 'javascript', 'python' ] language: [ 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more: # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
# 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
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # 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. # 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. # 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). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # 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 # 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 # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# uses a compiled language
#- run: | # - run: |
# make bootstrap # echo "Run, Build Application using script"
# make release # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - 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 on: pull_request
jobs: jobs:
ESLint: Linter:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -26,9 +26,8 @@ jobs:
done done
if [[ ! -z $CHANGED_FILES ]]; then if [[ ! -z $CHANGED_FILES ]]; then
npm ci yarn install --frozen-lockfile && cd tests && yarn install --frozen-lockfile && cd ..
cd tests && npm ci && cd .. yarn add eslint-detailed-reporter -D -W
npm install eslint-detailed-reporter --save-dev --legacy-peer-deps
mkdir -p eslint_report mkdir -p eslint_report
echo "ESLint version: "$(npx eslint --version) 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: jobs:
deploy: deploy:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:

@ -1,7 +1,7 @@
name: Linter name: HadoLint
on: pull_request on: pull_request
jobs: jobs:
HadoLint: Linter:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - 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' - 'develop'
pull_request: pull_request:
types: [edited, ready_for_review, opened, synchronize, reopened] types: [edited, ready_for_review, opened, synchronize, reopened]
paths-ignore:
- 'site/**'
- '**/*.md'
jobs: jobs:
Unit_testing: search_cache:
if: | if: |
github.event.pull_request.draft == false && github.event.pull_request.draft == false &&
!startsWith(github.event.pull_request.title, '[WIP]') && !startsWith(github.event.pull_request.title, '[WIP]') &&
!startsWith(github.event.pull_request.title, '[Dependent]') !startsWith(github.event.pull_request.title, '[Dependent]')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: outputs:
- uses: actions/checkout@v2 sha: ${{ steps.get-sha.outputs.sha}}
- uses: actions/setup-python@v2 env:
with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
python-version: '3.8' REPO: ${{ github.repository }}
- name: Getting SHA from the default branch steps:
- name: Getting SHA with cache from the default branch
id: get-sha id: get-sha
run: | run: |
URL_get_default_branch="https://api.github.com/repos/${{ github.repository }}" DEFAULT_BRANCH=$(gh api /repos/$REPO | jq -r '.default_branch')
DEFAULT_BRANCH=$(curl -s -X GET -G ${URL_get_default_branch} | jq -r '.default_branch') for sha in $(gh api "/repos/$REPO/commits?per_page=100&sha=$DEFAULT_BRANCH" | jq -r '.[].sha');
URL_get_sha_default_branch="https://api.github.com/repos/${{ github.repository }}/git/ref/heads/${DEFAULT_BRANCH}" do
SHA=$(curl -s -X GET -G ${URL_get_sha_default_branch} | jq .object.sha | tr -d '"') RUN_status=$(gh api /repos/${REPO}/actions/workflows/cache.yml/runs | \
echo ::set-output name=default_branch::${DEFAULT_BRANCH} jq -r ".workflow_runs[]? | select((.head_sha == \"${sha}\") and (.conclusion == \"success\")) | .status")
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 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 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 fi
done done
if [[ ${NUMBER_ATTEMPTS} -eq 0 ]]; then
echo "Number of attempts expired!" echo Default branch is ${DEFAULT_BRANCH}
echo "Probably the creation of the cache is not yet complete. Will continue working without the cache." echo Workflow will try to get cache from commit: ${SHA}
fi
- name: Getting CVAT server cache from the default branch echo ::set-output name=default_branch::${DEFAULT_BRANCH}
uses: actions/cache@v2 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: with:
path: /tmp/cvat_cache_server 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 - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.1.2 uses: docker/setup-buildx-action@v2
- name: Building CVAT server image
uses: docker/build-push-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: with:
context: .
file: ./Dockerfile
cache-from: type=local,src=/tmp/cvat_cache_server cache-from: type=local,src=/tmp/cvat_cache_server
tags: openvino/cvat_server:latest context: .
load: true 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 - name: Running OPA tests
run: | run: |
curl -L -o opa https://openpolicyagent.org/downloads/v0.34.2/opa_linux_amd64_static curl -L -o opa https://openpolicyagent.org/downloads/v0.34.2/opa_linux_amd64_static
chmod +x ./opa chmod +x ./opa
./opa test cvat/apps/iam/rules ./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 - name: Running unit tests
env: env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }} HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
CONTAINER_COVERAGE_DATA_DIR: "/coverage_data" CONTAINER_COVERAGE_DATA_DIR: "/coverage_data"
run: | run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'coverage run -a manage.py test cvat/apps utils/cli && mv .coverage ${CONTAINER_COVERAGE_DATA_DIR}' docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d cvat_opa
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 max_tries=12
if: github.ref == 'refs/heads/develop' 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 uses: actions/upload-artifact@v2
env:
LOGS_DIR: "${{ github.workspace }}/unit_testing"
with: with:
name: coverage_results name: container_logs
path: | path: $LOGS_DIR
${{ github.workspace }}/.coverage
${{ github.workspace }}/lcov.info
E2E_testing: e2e_testing:
if: | needs: build
github.event.pull_request.draft == false &&
!startsWith(github.event.pull_request.title, '[WIP]') &&
!startsWith(github.event.pull_request.title, '[Dependent]')
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
specs: ['actions_tasks', 'actions_tasks2', 'actions_tasks3', 'actions_objects', 'actions_objects2', 'actions_users', 'actions_projects_models', 'actions_organizations', 'canvas3d_functionality', 'canvas3d_functionality_2', 'issues_prs', 'issues_prs2'] specs: ['canvas3d_functionality', 'actions']
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Getting SHA from the default branch
id: get-sha - name: Set up Docker Buildx
run: | uses: docker/setup-buildx-action@v2
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 }}
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '16.x' node-version: '16.x'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.1.2 - name: Download CVAT server images
- name: Building CVAT server image uses: actions/download-artifact@v3
uses: docker/build-push-action@v2
with: with:
context: . name: cvat_server
file: ./Dockerfile path: /tmp/cvat_server/
cache-from: type=local,src=/tmp/cvat_cache_server
tags: openvino/cvat_server:latest - name: Download CVAT UI images
load: true uses: actions/download-artifact@v3
- name: Building CVAT UI image
uses: docker/build-push-action@v2
with: with:
context: . name: cvat_ui
file: ./Dockerfile.ui path: /tmp/cvat_ui/
cache-from: type=local,src=/tmp/cvat_cache_ui
tags: openvino/cvat_ui:latest - name: Load Docker images
load: true run: |
- name: Instrumentation of the code then rebuilding the CVAT UI docker load --input /tmp/cvat_server/image.tar
if: github.ref == 'refs/heads/develop' docker load --input /tmp/cvat_ui/image.tar
docker image ls -a
- name: Run CVAT instance
run: | run: |
npm ci docker-compose \
npm run coverage -f docker-compose.yml \
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml build cvat_ui -f docker-compose.dev.yml \
- name: Running e2e tests -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: env:
DJANGO_SU_NAME: 'admin' DJANGO_SU_NAME: 'admin'
DJANGO_SU_EMAIL: 'admin@localhost.company' DJANGO_SU_EMAIL: 'admin@localhost.company'
DJANGO_SU_PASSWORD: '12qwaszx' DJANGO_SU_PASSWORD: '12qwaszx'
API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: | 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 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"
/bin/bash -c 'while [[ $(curl -s -o /dev/null -w "%{http_code}" ${API_ABOUT_PAGE}) != "401" ]]; do sleep 5; done'
docker exec -i cvat /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
cd ./tests cd ./tests
npm ci yarn --frozen-lockfile
if [[ ${{ github.ref }} == 'refs/heads/develop' ]]; then
if [ ${{ matrix.specs }} == 'canvas3d_functionality' ] || [ ${{ matrix.specs }} == 'canvas3d_functionality_2' ]; then if [ ${{ matrix.specs }} == 'canvas3d_functionality' ]; 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' npx cypress run --headed --browser chrome --config-file pr_cypress_canvas3d.json
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
else else
if [ ${{ matrix.specs }} == 'canvas3d_functionality' ] || [ ${{ matrix.specs }} == 'canvas3d_functionality_2' ]; then npx cypress run --browser chrome --config-file pr_cypress.json
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
fi fi
- name: Creating a log file from "cvat" container logs - name: Creating a log file from "cvat" container logs
if: failure() if: failure()
run: | run: |
docker logs cvat > ${{ github.workspace }}/tests/cvat_${{ matrix.specs }}.log 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 - name: Uploading "cvat" container logs as an artifact
if: failure() if: failure()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: cvat_container_logs name: container_logs
path: ${{ github.workspace }}/tests/cvat_${{ matrix.specs }}.log 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 uses: actions/upload-artifact@v2
with: with:
name: coverage_results name: cypress_screenshots_${{ matrix.specs }}
path: ${{ github.workspace }}/tests/.nyc_output path: ${{ github.workspace }}/tests/cypress/screenshots
Coveralls: publish_dev_images:
if: github.ref == 'refs/heads/develop' if: github.ref == 'refs/heads/develop'
needs: [rest_api, unit_testing, e2e_testing]
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [Unit_testing, E2E_testing]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Getting SHA from the default branch
id: get-sha - name: Download CVAT server images
run: | uses: actions/download-artifact@v3
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
with: with:
context: . name: cvat_server
file: ./Dockerfile path: /tmp/cvat_server/
cache-from: type=local,src=/tmp/cvat_cache_server
tags: openvino/cvat_server:latest - name: Download CVAT UI images
load: true uses: actions/download-artifact@v3
- name: Downloading coverage results
uses: actions/download-artifact@v2
with: with:
name: coverage_results name: cvat_ui
- name: Combining coverage results path: /tmp/cvat_ui/
- name: Load Docker images
run: | run: |
mkdir -p ./nyc_output_tmp docker load --input /tmp/cvat_server/image.tar
mv ./out_*.json ./nyc_output_tmp docker load --input /tmp/cvat_ui/image.tar
mkdir -p ./.nyc_output
npm ci - name: Login to Docker Hub
npx nyc merge ./nyc_output_tmp ./.nyc_output/out.json uses: docker/login-action@v1
- name: Sending results to Coveralls with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push to Docker Hub
env: env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }} SERVER_IMAGE_REPO: ${{ secrets.DOCKERHUB_WORKSPACE }}/server
CONTAINER_COVERAGE_DATA_DIR: "/coverage_data" UI_IMAGE_REPO: ${{ secrets.DOCKERHUB_WORKSPACE }}/ui
COVERALLS_SERVICE_NAME: github
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
npx nyc report --reporter=text-lcov >> ${HOST_COVERAGE_DATA_DIR}/lcov.info docker tag cvat/server:latest "${SERVER_IMAGE_REPO}:dev"
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 push "${SERVER_IMAGE_REPO}:dev"
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/ui:latest "${UI_IMAGE_REPO}:dev"
docker push "${UI_IMAGE_REPO}:dev"

@ -1,53 +1,11 @@
name: Publish Docker images name: Publish Docker images
on: on:
release: release:
types: [published] types: [released]
jobs: 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: Push_to_registry:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [Unit_testing, E2E_testing]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Build images - name: Build images
@ -60,9 +18,9 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push to Docker Hub - name: Push to Docker Hub
env: env:
DOCKERHUB_WORKSPACE: 'openvino' DOCKERHUB_WORKSPACE: ${{ secrets.DOCKERHUB_WORKSPACE }}
SERVER_IMAGE_REPO: 'cvat_server' SERVER_IMAGE_REPO: 'server'
UI_IMAGE_REPO: 'cvat_ui' UI_IMAGE_REPO: 'ui'
run: | run: |
docker tag "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:latest" "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:${{ github.event.release.tag_name }}" docker tag "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:latest" "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:${{ github.event.release.tag_name }}"
docker push "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:${{ github.event.release.tag_name }}" docker push "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:${{ github.event.release.tag_name }}"

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

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

@ -3,32 +3,469 @@ on:
schedule: schedule:
- cron: '0 22 * * *' - cron: '0 22 * * *'
workflow_dispatch: workflow_dispatch:
env:
SERVER_IMAGE_TEST_REPO: cvat_server
UI_IMAGE_TEST_REPO: instrumentation_cvat_ui
jobs: 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: 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 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - 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 - uses: actions/setup-node@v2
with: with:
node-version: '16.x' 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: env:
DJANGO_SU_NAME: "admin" DJANGO_SU_NAME: "admin"
DJANGO_SU_EMAIL: "admin@localhost.company" DJANGO_SU_EMAIL: "admin@localhost.company"
DJANGO_SU_PASSWORD: "12qwaszx" DJANGO_SU_PASSWORD: "12qwaszx"
API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: | 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" 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: | run: |
cd ./tests cd ./tests
npm ci yarn --frozen-lockfile
npm run cypress:run:firefox
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 - name: Uploading cypress screenshots as an artifact
if: failure() if: failure()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: cypress_screenshots name: cypress_screenshots
path: ${{ github.workspace }}/tests/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 on: pull_request
jobs: jobs:
StyleLint: Linter:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -26,7 +26,7 @@ jobs:
done done
if [[ ! -z $CHANGED_FILES ]]; then if [[ ! -z $CHANGED_FILES ]]; then
npm ci yarn install --frozen-lockfile
mkdir -p stylelint_report mkdir -p stylelint_report
echo "StyleLint version: "$(npx stylelint --version) echo "StyleLint version: "$(npx stylelint --version)

3
.gitignore vendored

@ -4,7 +4,7 @@
/share/ /share/
/static/ /static/
/db.sqlite3 /db.sqlite3
/.env /.*env*
/keys /keys
/logs /logs
/profiles /profiles
@ -44,6 +44,7 @@ yarn-error.log*
/site/resources/ /site/resources/
/site/node_modules/ /site/node_modules/
/site/tech-doc-hugo /site/tech-doc-hugo
/site/.hugo_build.lock
# Ignore all the installed packages # Ignore all the installed packages
node_modules 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", "--settings",
"cvat.settings.testing", "cvat.settings.testing",
"cvat/apps", "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, "django": true,
"cwd": "${workspaceFolder}", "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 ## \[2.2.0] - Unreleased
### Added ### 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 ### 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 ### Deprecated
- TDB - TDB
@ -19,7 +40,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- TDB - TDB
### Fixed ### 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 ### Security
- TDB - 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>) - Unable to upload annotations (<https://github.com/openvinotoolkit/cvat/pull/4513>)
- Fix build dependencies for Siammask (<https://github.com/openvinotoolkit/cvat/pull/4486>) - 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>) - 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 ## \[2.0.0] - 2022-03-04
### Added ### 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 python virtual environment and FFmpeg binaries from build-image
COPY --from=build-image /opt/venv /opt/venv COPY --from=build-image /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:${PATH}" ENV PATH="/opt/venv/bin:${PATH}"
ENV NUMPROCS=1
COPY --from=build-image /opt/ffmpeg /usr COPY --from=build-image /opt/ffmpeg /usr
# Install and initialize CVAT, copy all necessary files # Install and initialize CVAT, copy all necessary files
COPY --chown=${USER} components /tmp/components COPY --chown=${USER} components /tmp/components
COPY --chown=${USER} supervisord/ ${HOME}/supervisord
COPY --chown=${USER} ssh ${HOME}/.ssh COPY --chown=${USER} ssh ${HOME}/.ssh
COPY --chown=${USER} supervisord.conf mod_wsgi.conf wait-for-it.sh manage.py ${HOME}/ COPY --chown=${USER} mod_wsgi.conf wait-for-it.sh manage.py ${HOME}/
COPY --chown=${USER} cvat/ ${HOME}/cvat
COPY --chown=${USER} utils/ ${HOME}/utils COPY --chown=${USER} utils/ ${HOME}/utils
COPY --chown=${USER} tests/ ${HOME}/tests COPY --chown=${USER} cvat/ ${HOME}/cvat
# RUN all commands below as 'django' user # RUN all commands below as 'django' user
USER ${USER} USER ${USER}
@ -157,3 +158,4 @@ RUN mkdir data share media keys logs /tmp/supervisord
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["/usr/bin/supervisord"] ENTRYPOINT ["/usr/bin/supervisord"]
CMD ["-c", "supervisord/all.conf"]

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

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

@ -1,31 +1,22 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), of this software and associated documentation files (the "Software"), to deal
to deal in the Software without restriction, including without limitation in the Software without restriction, including without limitation the rights
the rights to use, copy, modify, merge, publish, distribute, sublicense, to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
and/or sell copies of the Software, and to permit persons to whom copies of the Software, and to permit persons to whom the Software is
the Software is furnished to do so, subject to the following conditions: furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included The above copyright notice and this permission notice shall be included in all
in all copies or substantial portions of the Software. 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).
FFmpeg is an open source framework licensed under LGPL and GPL. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
See https://www.ffmpeg.org/legal.html. You are solely responsible IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
for determining if your use of FFmpeg requires any FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
additional licenses. Intel is not responsible for obtaining any AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
such licenses, nor liable for any licensing fees due in LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
connection with your use of FFmpeg. 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) # 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] [![CI][ci-img]][ci-url]
[![Gitter chat][gitter-img]][gitter-url] [![Gitter chat][gitter-img]][gitter-url]
[![Discord][discord-img]][discord-url]
[![Coverage Status][coverage-img]][coverage-url] [![Coverage Status][coverage-img]][coverage-url]
[![server pulls][docker-server-pulls-img]][docker-server-image-url] [![server pulls][docker-server-pulls-img]][docker-server-image-url]
[![ui pulls][docker-ui-pulls-img]][docker-ui-image-url] [![ui pulls][docker-ui-pulls-img]][docker-ui-image-url]
[![DOI][doi-img]][doi-url] [![DOI][doi-img]][doi-url]
CVAT is free, online, interactive video and image annotation CVAT is an interactive video and image annotation
tool for computer vision. It is being used by our team to tool for computer vision. It is used by tens of thousands of users and
annotate million of objects with different properties. Many UI companies around the world. CVAT is free and open-source.
and UX decisions are based on feedbacks from professional data
annotation team. Try it online [cvat.org](https://cvat.org). **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://cvat-ai.github.io/cvat/docs/administration/basics/installation/)
- [Installation guide](https://openvinotoolkit.github.io/cvat/docs/administration/basics/installation/) - [Manual](https://cvat-ai.github.io/cvat/docs/manual/)
- [Manual](https://openvinotoolkit.github.io/cvat/docs/manual/) - [Contributing](https://cvat-ai.github.io/cvat/docs/contributing/)
- [Django REST API documentation](https://openvinotoolkit.github.io/cvat/docs/administration/basics/rest_api_guide/) - [Django REST API documentation](https://cvat-ai.github.io/cvat/docs/administration/basics/rest_api_guide/)
- [Datumaro dataset framework](https://github.com/openvinotoolkit/datumaro/blob/develop/README.md) - [Datumaro dataset framework](https://github.com/cvat-ai/datumaro/blob/develop/README.md)
- [Command line interface](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/cli/) - [Command line interface](https://cvat-ai.github.io/cvat/docs/manual/advanced/cli/)
- [XML annotation format](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/xml_format/) - [XML annotation format](https://cvat-ai.github.io/cvat/docs/manual/advanced/xml_format/)
- [AWS Deployment Guide](https://openvinotoolkit.github.io/cvat/docs/administration/basics/aws-deployment-guide/) - [AWS Deployment Guide](https://cvat-ai.github.io/cvat/docs/administration/basics/aws-deployment-guide/)
- [Frequently asked questions](https://openvinotoolkit.github.io/cvat/docs/faq/) - [Frequently asked questions](https://cvat-ai.github.io/cvat/docs/faq/)
- [Questions](#questions) - [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) - [Introduction](https://youtu.be/JERohTFp-NI)
- [Annotation mode](https://youtu.be/vH_639N67HI) - [Annotation mode](https://youtu.be/vH_639N67HI)
@ -42,95 +106,73 @@ annotation team. Try it online [cvat.org](https://cvat.org).
## Supported annotation formats ## Supported annotation formats
Format selection is possible after clicking on the Upload annotation and Dump CVAT supports multiple annotation formats. You can select the format after clicking the "Upload annotation" and "Dump
annotation buttons. [Datumaro](https://github.com/openvinotoolkit/datumaro) annotation" buttons. [Datumaro](https://github.com/cvat-ai/datumaro)
dataset framework allows additional dataset transformations via its command dataset framework allows additional dataset transformations via its command
line tool and Python library. line tool and Python library.
For more information about supported formats look at the For more information about the supported formats, look at the
[documentation](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/formats/). [documentation](https://cvat-ai.github.io/cvat/docs/manual/advanced/formats/).
<!--lint disable maximum-line-length--> <!--lint disable maximum-line-length-->
| Annotation format | Import | Export | | Annotation format | Import | Export |
| --------------------------------------------------------------------------------------------------------- | ------ | ------ | | --------------------------------------------------------------------------------------------------------- | ------ | ------ |
| [CVAT for images](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/xml_format/#annotation) | X | X | | [CVAT for images](https://cvat-ai.github.io/cvat/docs/manual/advanced/xml_format/#annotation) | ✔️ | ✔️ |
| [CVAT for a video](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/xml_format/#interpolation) | X | X | | [CVAT for a video](https://cvat-ai.github.io/cvat/docs/manual/advanced/xml_format/#interpolation) | ✔️ | ✔️ |
| [Datumaro](https://github.com/openvinotoolkit/datumaro) | | X | | [Datumaro](https://github.com/cvat-ai/datumaro) | | ✔️ |
| [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X | | [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | ✔️ | ✔️ |
| Segmentation masks from [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/) | ✔️ | ✔️ |
| [YOLO](https://pjreddie.com/darknet/yolo/) | X | X | | [YOLO](https://pjreddie.com/darknet/yolo/) | ✔️ | ✔️ |
| [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X | | [MS COCO Object Detection](http://cocodataset.org/#format-data) | ✔️ | ✔️ |
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tfrecord) | X | X | | [TFrecord](https://www.tensorflow.org/tutorials/load_data/tfrecord) | ✔️ | ✔️ |
| [MOT](https://motchallenge.net/) | X | X | | [MOT](https://motchallenge.net/) | ✔️ | ✔️ |
| [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | X | X | | [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | ✔️ | ✔️ |
| [ImageNet](http://www.image-net.org) | X | X | | [ImageNet](http://www.image-net.org) | ✔️ | ✔️ |
| [CamVid](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) | X | X | | [CamVid](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) | ✔️ | ✔️ |
| [WIDER Face](http://shuoyang1213.me/WIDERFACE/) | X | X | | [WIDER Face](http://shuoyang1213.me/WIDERFACE/) | ✔️ | ✔️ |
| [VGGFace2](https://github.com/ox-vgg/vgg_face2) | X | X | | [VGGFace2](https://github.com/ox-vgg/vgg_face2) | ✔️ | ✔️ |
| [Market-1501](https://www.aitribune.com/dataset/2018051063) | X | X | | [Market-1501](https://www.aitribune.com/dataset/2018051063) | ✔️ | ✔️ |
| [ICDAR13/15](https://rrc.cvc.uab.es/?ch=2) | X | X | | [ICDAR13/15](https://rrc.cvc.uab.es/?ch=2) | ✔️ | ✔️ |
| [Open Images V6](https://storage.googleapis.com/openimages/web/index.html) | X | X | | [Open Images V6](https://storage.googleapis.com/openimages/web/index.html) | ✔️ | ✔️ |
| [Cityscapes](https://www.cityscapes-dataset.com/login/) | X | X | | [Cityscapes](https://www.cityscapes-dataset.com/login/) | ✔️ | ✔️ |
| [KITTI](http://www.cvlibs.net/datasets/kitti/) | X | X | | [KITTI](http://www.cvlibs.net/datasets/kitti/) | ✔️ | ✔️ |
| [LFW](http://vis-www.cs.umass.edu/lfw/) | X | X | | [LFW](http://vis-www.cs.umass.edu/lfw/) | ✔️ | ✔️ |
<!--lint enable maximum-line-length--> <!--lint enable maximum-line-length-->
## Deep learning serverless functions for automatic labeling ## 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--> <!--lint disable maximum-line-length-->
| Name | Type | Framework | CPU | GPU | | Name | Type | Framework | CPU | GPU |
| ------------------------------------------------------------------------------------------------------- | ---------- | ---------- | --- | --- | | ------------------------------------------------------------------------------------------------------- | ---------- | ---------- | --- | --- |
| [Deep Extreme Cut](/serverless/openvino/dextr/nuclio) | interactor | 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 | X | | | [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 | X | | | [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 | X | | | [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 | X | | | [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 | X | | | [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 | X | | | [Text detection v4](/serverless/openvino/omz/intel/text-detection-0004/nuclio) | detector | OpenVINO | ✔️ | |
| [YOLO v5](/serverless/pytorch/ultralytics/yolov5/nuclio) | detector | PyTorch | X | | | [YOLO v5](/serverless/pytorch/ultralytics/yolov5/nuclio) | detector | PyTorch | ✔️ | |
| [SiamMask](/serverless/pytorch/foolwood/siammask/nuclio) | tracker | PyTorch | X | X | | [SiamMask](/serverless/pytorch/foolwood/siammask/nuclio) | tracker | PyTorch | ✔️ | ✔️ |
| [f-BRS](/serverless/pytorch/saic-vul/fbrs/nuclio) | interactor | PyTorch | X | | | [f-BRS](/serverless/pytorch/saic-vul/fbrs/nuclio) | interactor | PyTorch | ✔️ | |
| [HRNet](/serverless/pytorch/saic-vul/hrnet/nuclio) | interactor | PyTorch | | X | | [HRNet](/serverless/pytorch/saic-vul/hrnet/nuclio) | interactor | PyTorch | | ✔️ |
| [Inside-Outside Guidance](/serverless/pytorch/shiyinzhang/iog/nuclio) | interactor | PyTorch | X | | | [Inside-Outside Guidance](/serverless/pytorch/shiyinzhang/iog/nuclio) | interactor | PyTorch | ✔️ | |
| [Faster RCNN](/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | detector | TensorFlow | X | X | | [Faster RCNN](/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | detector | TensorFlow | ✔️ | ✔️ |
| [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow | X | X | | [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow | ✔️ | ✔️ |
| [RetinaNet](serverless/pytorch/facebookresearch/detectron2/retinanet/nuclio) | detector | PyTorch | X | X | | [RetinaNet](serverless/pytorch/facebookresearch/detectron2/retinanet/nuclio) | detector | PyTorch | ✔️ | ✔️ |
| [Face Detection](/serverless/openvino/omz/intel/face-detection-0205/nuclio) | detector | OpenVINO | X | | | [Face Detection](/serverless/openvino/omz/intel/face-detection-0205/nuclio) | detector | OpenVINO | ✔️ | |
<!--lint enable maximum-line-length--> <!--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. The code is released under the [MIT License](https://opensource.org/licenses/MIT).
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).
This software uses LGPL licensed libraries from the [FFmpeg](https://www.ffmpeg.org) project. 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). 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. 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 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 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 such licenses, nor liable for any licensing fees due in
connection with your use of FFmpeg. connection with your use of FFmpeg.
## Partners ## Where to ask questions
- [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
CVAT usage related questions or unclear concepts can be posted in our [Gitter chat][gitter-url]: you can post CVAT usage related questions there.
[Gitter chat](https://gitter.im/opencv-cvat) for **quick replies** from Typically they get answered fast by the core team or community. There you can also browse other common questions.
contributors and other users.
However, if you have a feature request or a bug report that can reproduced, [Discord][discord-url] is the place to also ask questions or discuss any other stuff related to CVAT.
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).
If you are not sure or just want to browse other users common questions, [GitHub issues](https://github.com/cvat-ai/cvat/issues): please post them for feature requests or bug reports.
[Gitter chat](https://gitter.im/opencv-cvat) is the way to go. 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\* [contact@cvat.ai](mailto:contact+github@cvat.ai): reach out to us with feedback, comments, or inquiries.
- [Forum on Intel Developer Zone](https://software.intel.com/en-us/forums/computer-vision)
## Links ## Links
@ -191,15 +208,23 @@ Other ways to ask questions and get our support:
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<!-- Badges --> <!-- Badges -->
[docker-server-pulls-img]: https://img.shields.io/docker/pulls/openvino/cvat_server.svg?style=flat-square&label=server%20pulls [docker-server-pulls-img]: https://img.shields.io/docker/pulls/cvat/server.svg?style=flat-square&label=server%20pulls
[docker-server-image-url]: https://hub.docker.com/r/openvino/cvat_server [docker-server-image-url]: https://hub.docker.com/r/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 [docker-ui-pulls-img]: https://img.shields.io/docker/pulls/cvat/ui.svg?style=flat-square&label=UI%20pulls
[ci-img]: https://github.com/openvinotoolkit/cvat/workflows/CI/badge.svg?branch=develop [docker-ui-image-url]: https://hub.docker.com/r/cvat/ui
[ci-url]: https://github.com/openvinotoolkit/cvat/actions
[gitter-img]: https://badges.gitter.im/opencv-cvat/gitter.png [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 [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-img]: https://zenodo.org/badge/139156354.svg
[doi-url]: https://zenodo.org/badge/latestdoi/139156354 [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 ## Reporting a Vulnerability
If you have information about a security issue or vulnerability in the product, please 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 send an e-mail to [secure@cvat.ai](mailto:secure+github@cvat.ai).
using our PGP public key.
Please provide as much information as possible, including: Please provide as much information as possible, including:
- The products and versions affected - The products and versions affected
- Detailed description of the vulnerability - Detailed description of the vulnerability
- Information on known exploits - 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. 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: args:
ELK_VERSION: 6.8.23 ELK_VERSION: 6.8.23
depends_on: ['elasticsearch'] depends_on: ['elasticsearch']
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
restart: always restart: always
cvat_kibana_setup: cvat_kibana_setup:
container_name: 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'] volumes: ['./components/analytics/kibana:/home/django/kibana:ro']
depends_on: ['cvat'] depends_on: ['cvat_server']
working_dir: '/home/django' working_dir: '/home/django'
networks: networks:
- cvat - cvat
@ -72,7 +74,7 @@ services:
depends_on: ['elasticsearch'] depends_on: ['elasticsearch']
restart: always restart: always
cvat: cvat_server:
environment: environment:
DJANGO_LOG_SERVER_HOST: logstash DJANGO_LOG_SERVER_HOST: logstash
DJANGO_LOG_SERVER_PORT: 8080 DJANGO_LOG_SERVER_PORT: 8080

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

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

@ -8,11 +8,20 @@ http:
- strip-prefix - strip-prefix
service: kibana service: kibana
rule: Host(`{{ env "CVAT_HOST" }}`) && PathPrefix(`/analytics`) 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: middlewares:
analytics-auth: analytics-auth:
forwardauth: forwardauth:
address: http://cvat:8080/analytics address: http://cvat_server:8080/analytics
authRequestHeaders: authRequestHeaders:
- "Cookie" - "Cookie"
- "Authorization" - "Authorization"

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

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

@ -1,13 +1,8 @@
// Copyright (C) 2019-2021 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const globalConfig = require('../.eslintrc.js');
module.exports = { module.exports = {
env: {
node: true,
},
ignorePatterns: [ ignorePatterns: [
'.eslintrc.js', '.eslintrc.js',
'webpack.config.js', 'webpack.config.js',
@ -15,31 +10,7 @@ module.exports = {
'dist/**', 'dist/**',
], ],
parserOptions: { parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 6,
project: './tsconfig.json', project: './tsconfig.json',
tsconfigRootDir: __dirname, 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: 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 not important changes (typos, backward compatible bug fixes, refactoring) do: `yarn version --patch`
- After changing API (backward compatible new features) do: `npm version minor` - After changing API (backward compatible new features) do: `yarn version --minor`
- After changing API (changes that break backward compatibility) do: `npm version major` - After changing API (changes that break backward compatibility) do: `yarn version --major`
## Commands ## Commands
- Building of the module from sources in the `dist` directory: - Building of the module from sources in the `dist` directory:
```bash ```bash
npm run build yarn run build
npm run build -- --mode=development # without a minification yarn run build --mode=development # without a minification
``` ```
## Using ## Using
@ -62,8 +62,19 @@ Canvas itself handles:
} }
interface Configuration { interface Configuration {
displayAllText?: boolean; smoothImage?: boolean;
undefinedAttrValue?: string; 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 { 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", "name": "cvat-canvas",
"version": "2.13.2", "version": "2.15.0",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library", "description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts", "main": "src/canvas.ts",
"scripts": { "scripts": {
"build": "tsc && webpack --config ./webpack.config.js", "build": "tsc && webpack --config ./webpack.config.js",
"server": "nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'" "server": "nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'"
}, },
"author": "Intel", "author": "CVAT.ai",
"license": "MIT", "license": "MIT",
"browserslist": [ "browserslist": [
"Chrome >= 63", "Chrome >= 63",

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -10,6 +10,12 @@
stroke-opacity: 1; stroke-opacity: 1;
} }
g.cvat_canvas_shape {
> circle {
fill-opacity: 1;
}
}
polyline.cvat_canvas_shape { polyline.cvat_canvas_shape {
fill-opacity: 0; fill-opacity: 0;
} }
@ -120,7 +126,6 @@ polyline.cvat_canvas_shape_splitting {
@extend .cvat_shape_drawing_opacity; @extend .cvat_shape_drawing_opacity;
fill: white; fill: white;
stroke: black;
} }
.cvat_canvas_zoom_selection { .cvat_canvas_zoom_selection {
@ -134,6 +139,12 @@ polyline.cvat_canvas_shape_splitting {
stroke-dasharray: 5; stroke-dasharray: 5;
} }
g.cvat_canvas_shape_occluded {
> rect {
stroke-dasharray: 5;
}
}
.svg_select_points_rot { .svg_select_points_rot {
fill: white; 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 { .cvat_canvas_pixelized {
image-rendering: optimizeSpeed; /* Legal fallback */ image-rendering: optimizeSpeed; /* Legal fallback */
image-rendering: -moz-crisp-edges; /* Firefox */ image-rendering: -moz-crisp-edges; /* Firefox */
@ -237,6 +254,10 @@ polyline.cvat_canvas_shape_splitting {
-ms-interpolation-mode: nearest-neighbor; /* IE8+ */ -ms-interpolation-mode: nearest-neighbor; /* IE8+ */
} }
.cvat_canvas_removed_image {
filter: saturate(0) brightness(1.2) contrast(0.75) !important;
}
#cvat_canvas_wrapper { #cvat_canvas_wrapper {
width: calc(100% - 10px); width: calc(100% - 10px);
height: 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 // SPDX-License-Identifier: MIT
import * as SVG from 'svg.js'; import * as SVG from 'svg.js';
import consts from './consts'; import consts from './consts';
import { Geometry } from './canvasModel'; import { Configuration, Geometry } from './canvasModel';
interface TransformedShape { interface TransformedShape {
points: string; points: string;
@ -14,6 +14,7 @@ interface TransformedShape {
export interface AutoborderHandler { export interface AutoborderHandler {
autoborder(enabled: boolean, currentShape?: SVG.Shape, currentID?: number): void; autoborder(enabled: boolean, currentShape?: SVG.Shape, currentID?: number): void;
configurate(configuration: Configuration): void;
transform(geometry: Geometry): void; transform(geometry: Geometry): void;
updateObjects(): void; updateObjects(): void;
} }
@ -24,19 +25,14 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
private frameContent: SVGSVGElement; private frameContent: SVGSVGElement;
private enabled: boolean; private enabled: boolean;
private scale: number; private scale: number;
private controlPointsSize: number;
private groups: SVGGElement[]; private groups: SVGGElement[];
private auxiliaryGroupID: number | null; private auxiliaryGroupID: number | null;
private auxiliaryClicks: number[]; private auxiliaryClicks: number[];
private listeners: Record< private listeners: Record<number, Record<number, {
number,
Record<
number,
{
click: (event: MouseEvent) => void; click: (event: MouseEvent) => void;
dblclick: (event: MouseEvent) => void; dblclick: (event: MouseEvent) => void;
} }>>;
>
>;
public constructor(frameContent: SVGSVGElement) { public constructor(frameContent: SVGSVGElement) {
this.frameContent = frameContent; this.frameContent = frameContent;
@ -45,6 +41,7 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
this.enabled = false; this.enabled = false;
this.scale = 1; this.scale = 1;
this.groups = []; this.groups = [];
this.controlPointsSize = consts.BASE_POINT_SIZE;
this.auxiliaryGroupID = null; this.auxiliaryGroupID = null;
this.auxiliaryClicks = []; this.auxiliaryClicks = [];
this.listeners = {}; this.listeners = {};
@ -126,7 +123,7 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
circle.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.scale}`); circle.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.scale}`);
circle.setAttribute('cx', x); circle.setAttribute('cx', x);
circle.setAttribute('cy', y); 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 => { const click = (event: MouseEvent): void => {
event.stopPropagation(); event.stopPropagation();
@ -303,9 +300,13 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
this.scale = geometry.scale; this.scale = geometry.scale;
this.groups.forEach((group: SVGGElement): void => { this.groups.forEach((group: SVGGElement): void => {
Array.from(group.children).forEach((circle: SVGCircleElement): 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}`); 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; forceDisableEditing?: boolean;
intelligentPolygonCrop?: boolean; intelligentPolygonCrop?: boolean;
forceFrameUpdate?: boolean; forceFrameUpdate?: boolean;
creationOpacity?: number; CSSImageFilter?: string;
colorBy?: string;
selectedShapeOpacity?: number;
shapeOpacity?: number;
controlPointsSize?: number;
outlinedBorders?: string | false;
} }
export interface DrawData { export interface DrawData {
@ -72,6 +77,7 @@ export interface DrawData {
shapeType?: string; shapeType?: string;
rectDrawingMethod?: RectDrawingMethod; rectDrawingMethod?: RectDrawingMethod;
cuboidDrawingMethod?: CuboidDrawingMethod; cuboidDrawingMethod?: CuboidDrawingMethod;
skeletonSVG?: string;
numberOfPoints?: number; numberOfPoints?: number;
initialState?: any; initialState?: any;
crosshair?: boolean; crosshair?: boolean;
@ -171,6 +177,7 @@ export enum Mode {
export interface CanvasModel { export interface CanvasModel {
readonly imageBitmap: boolean; readonly imageBitmap: boolean;
readonly imageIsDeleted: boolean;
readonly image: Image | null; readonly image: Image | null;
readonly issueRegions: Record<number, { hidden: boolean; points: number[] }>; readonly issueRegions: Record<number, { hidden: boolean; points: number[] }>;
readonly objects: any[]; readonly objects: any[];
@ -230,6 +237,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
imageID: number | null; imageID: number | null;
imageOffset: number; imageOffset: number;
imageSize: Size; imageSize: Size;
imageIsDeleted: boolean;
focusData: FocusData; focusData: FocusData;
gridSize: Size; gridSize: Size;
left: number; left: number;
@ -262,12 +270,23 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
width: 0, width: 0,
}, },
configuration: { configuration: {
displayAllText: false, smoothImage: true,
autoborders: false, autoborders: false,
undefinedAttrValue: '', displayAllText: false,
textContent: 'id,label,attributes,source,descriptions', showProjections: false,
textPosition: 'auto', forceDisableEditing: false,
intelligentPolygonCrop: false,
forceFrameUpdate: false,
CSSImageFilter: '',
colorBy: 'Label',
selectedShapeOpacity: 0.5,
shapeOpacity: 0.2,
outlinedBorders: false,
textFontSize: consts.DEFAULT_SHAPE_TEXT_SIZE, 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, imageBitmap: false,
image: null, image: null,
@ -277,6 +296,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
height: 0, height: 0,
width: 0, width: 0,
}, },
imageIsDeleted: false,
focusData: { focusData: {
clientID: 0, clientID: 0,
padding: 0, padding: 0,
@ -406,7 +426,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
throw Error(`Canvas is busy. Action: ${this.data.mode}`); throw Error(`Canvas is busy. Action: ${this.data.mode}`);
} }
} }
if (frameData.number === this.data.imageID && !this.data.configuration.forceFrameUpdate) { if (frameData.number === this.data.imageID &&
frameData.deleted === this.data.imageIsDeleted &&
!this.data.configuration.forceFrameUpdate
) {
this.data.zLayer = zLayer; this.data.zLayer = zLayer;
this.data.objects = objectStates; this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED); this.notify(UpdateReasons.OBJECTS_UPDATED);
@ -431,6 +454,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}; };
this.data.image = data; this.data.image = data;
this.data.imageIsDeleted = frameData.deleted;
if (this.data.imageIsDeleted) {
this.data.angle = 0;
}
this.notify(UpdateReasons.IMAGE_CHANGED); this.notify(UpdateReasons.IMAGE_CHANGED);
this.data.zLayer = zLayer; this.data.zLayer = zLayer;
this.data.objects = objectStates; this.data.objects = objectStates;
@ -476,7 +503,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
} }
public rotate(rotationAngle: number): void { 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.data.angle = (360 + Math.floor(rotationAngle / 90) * 90) % 360;
this.fit(); this.fit();
} }
@ -530,6 +557,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
} }
if (drawData.enabled) { 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) { if (this.data.drawData.enabled) {
throw new Error('Drawing has been already started'); throw new Error('Drawing has been already started');
} else if (!drawData.shapeType && !drawData.initialState) { } else if (!drawData.shapeType && !drawData.initialState) {
@ -659,6 +690,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.configuration.textFontSize = configuration.textFontSize; this.data.configuration.textFontSize = configuration.textFontSize;
} }
if (typeof configuration.controlPointsSize === 'number') {
this.data.configuration.controlPointsSize = configuration.controlPointsSize;
}
if (['auto', 'center'].includes(configuration.textPosition)) { if (['auto', 'center'].includes(configuration.textPosition)) {
this.data.configuration.textPosition = configuration.textPosition; this.data.configuration.textPosition = configuration.textPosition;
} }
@ -691,8 +726,21 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
if (typeof configuration.forceFrameUpdate === 'boolean') { if (typeof configuration.forceFrameUpdate === 'boolean') {
this.data.configuration.forceFrameUpdate = configuration.forceFrameUpdate; this.data.configuration.forceFrameUpdate = configuration.forceFrameUpdate;
} }
if (typeof configuration.creationOpacity === 'number') { if (typeof configuration.selectedShapeOpacity === 'number') {
this.data.configuration.creationOpacity = configuration.creationOpacity; 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); this.notify(UpdateReasons.CONFIG_UPDATED);
@ -753,6 +801,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return this.data.imageBitmap; return this.data.imageBitmap;
} }
public get imageIsDeleted(): boolean {
return this.data.imageIsDeleted;
}
public get image(): Image | null { public get image(): Image | null {
return this.data.image; 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 // SPDX-License-Identifier: MIT
const BASE_STROKE_WIDTH = 1.25; const BASE_STROKE_WIDTH = 1.25;
const BASE_GRID_WIDTH = 2; const BASE_GRID_WIDTH = 2;
const BASE_POINT_SIZE = 5; const BASE_POINT_SIZE = 4;
const TEXT_MARGIN = 10; const TEXT_MARGIN = 10;
const AREA_THRESHOLD = 9; const AREA_THRESHOLD = 9;
const SIZE_THRESHOLD = 3; 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 BASE_PATTERN_SIZE = 5;
const SNAP_TO_ANGLE_RESIZE_DEFAULT = 0.1; const SNAP_TO_ANGLE_RESIZE_DEFAULT = 0.1;
const SNAP_TO_ANGLE_RESIZE_SHIFT = 15; const SNAP_TO_ANGLE_RESIZE_SHIFT = 15;
const DEFAULT_SHAPE_TEXT_SIZE = 12;
const MINIMUM_TEXT_FONT_SIZE = 8; 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 { export default {
BASE_STROKE_WIDTH, BASE_STROKE_WIDTH,
@ -40,5 +45,9 @@ export default {
SNAP_TO_ANGLE_RESIZE_DEFAULT, SNAP_TO_ANGLE_RESIZE_DEFAULT,
SNAP_TO_ANGLE_RESIZE_SHIFT, SNAP_TO_ANGLE_RESIZE_SHIFT,
DEFAULT_SHAPE_TEXT_SIZE, DEFAULT_SHAPE_TEXT_SIZE,
DEFAULT_SHAPE_TEXT_CONTENT,
DEFAULT_SHAPE_TEXT_POSITION,
DEFAULT_UNDEFINED_ATTR_VALUE,
MINIMUM_TEXT_FONT_SIZE, 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 // SPDX-License-Identifier: MIT

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

@ -17,6 +17,11 @@ import {
Point, Point,
readPointsFromShape, readPointsFromShape,
clamp, clamp,
translateToCanvas,
computeWrappingBox,
makeSVGFromTemplate,
setupSkeletonEdges,
translateFromCanvas,
} from './shared'; } from './shared';
import Crosshair from './crosshair'; import Crosshair from './crosshair';
import consts from './consts'; import consts from './consts';
@ -83,9 +88,11 @@ export class DrawHandlerImpl implements DrawHandler {
private crosshair: Crosshair; private crosshair: Crosshair;
private drawData: DrawData; private drawData: DrawData;
private geometry: Geometry; private geometry: Geometry;
private configuration: Configuration;
private autoborderHandler: AutoborderHandler; private autoborderHandler: AutoborderHandler;
private autobordersEnabled: boolean; 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 // 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 // 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('mousedown.draw');
this.canvas.off('mousemove.draw'); this.canvas.off('mousemove.draw');
if (this.pointsGroup) {
this.pointsGroup.remove();
this.pointsGroup = null;
}
// Draw plugin in some cases isn't activated // Draw plugin in some cases isn't activated
// For example when draw from initialState // For example when draw from initialState
// Or when no drawn points, but we call cancel() drawing // Or when no drawn points, but we call cancel() drawing
// We check if it is activated with remember function // We check if it is activated with remember function
if (this.drawInstance.remember('_paintHandler')) { if (this.drawInstance.remember('_paintHandler')) {
if ( if (['polygon', 'polyline', 'points'].includes(this.drawData.shapeType) ||
['polygon', 'polyline', 'points'].includes(this.drawData.shapeType) ||
(this.drawData.shapeType === 'cuboid' && (this.drawData.shapeType === 'cuboid' &&
this.drawData.cuboidDrawingMethod === CuboidDrawingMethod.CORNER_POINTS) this.drawData.cuboidDrawingMethod === CuboidDrawingMethod.CORNER_POINTS)) {
) {
// Check for unsaved drawn shapes // Check for unsaved drawn shapes
this.drawInstance.draw('done'); this.drawInstance.draw('done');
} }
// Clear drawing // Clear drawing
this.drawInstance.draw('stop'); 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(); this.drawInstance.off();
@ -417,7 +424,8 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, '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') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity, 'fill-opacity': this.selectedShapeOpacity,
stroke: this.outlinedBorders,
}); });
const initialPoint: { 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]); const translated = translateToSVG(this.canvas.node as any as SVGSVGElement, [e.clientX, e.clientY]);
[initialPoint.x, initialPoint.y] = translated; [initialPoint.x, initialPoint.y] = translated;
} else { } else {
const points = this.getFinalEllipseCoordinates(readPointsFromShape(this.drawInstance), false); this.drawInstance.fire('drawstop');
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,
);
}
} }
}); });
@ -472,6 +467,25 @@ export class DrawHandlerImpl implements DrawHandler {
this.shapeSizeElement.update(this.drawInstance); 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 { private drawBoxBy4Points(): void {
@ -612,7 +626,8 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity, 'fill-opacity': this.selectedShapeOpacity,
stroke: this.outlinedBorders,
}); });
this.drawPolyshape(); this.drawPolyshape();
@ -628,6 +643,7 @@ export class DrawHandlerImpl implements DrawHandler {
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': 0, 'fill-opacity': 0,
stroke: this.outlinedBorders,
}); });
this.drawPolyshape(); this.drawPolyshape();
@ -651,6 +667,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
stroke: this.outlinedBorders,
}); });
this.drawPolyshape(); this.drawPolyshape();
} }
@ -681,7 +698,131 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, '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 // Common settings for rectangle and polyshapes
private pasteShape(): void { private pasteShape(): void {
function moveShape(shape: SVG.Shape, x: number, y: number): void { const moveShape = (shape: SVG.Shape, x: number, y: number): void => {
const bbox = shape.bbox();
const { rotation } = shape.transform(); const { rotation } = shape.transform();
shape.untransform(); shape.untransform();
shape.move(x - bbox.width / 2, y - bbox.height / 2); shape.center(x, y);
shape.rotate(rotation); shape.rotate(rotation);
} };
const { x: initialX, y: initialY } = this.cursorPosition; const { x: initialX, y: initialY } = this.cursorPosition;
moveShape(this.drawInstance, initialX, initialY); moveShape(this.drawInstance, initialX, initialY);
this.canvas.on('mousemove.draw', (): void => { 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); moveShape(this.drawInstance, x, y);
}); });
} }
@ -741,11 +881,12 @@ export class DrawHandlerImpl implements DrawHandler {
private pasteBox(box: BBox, rotation: number): void { private pasteBox(box: BBox, rotation: number): void {
this.drawInstance = (this.canvas as any) this.drawInstance = (this.canvas as any)
.rect(box.width, box.height) .rect(box.width, box.height)
.move(box.x, box.y) .center(box.x, box.y)
.addClass('cvat_canvas_shape_drawing') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity, 'fill-opacity': this.selectedShapeOpacity,
stroke: this.outlinedBorders,
}).rotate(rotation); }).rotate(rotation);
this.pasteShape(); this.pasteShape();
@ -782,7 +923,8 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity, 'fill-opacity': this.selectedShapeOpacity,
stroke: this.outlinedBorders,
}).rotate(rotation); }).rotate(rotation);
this.pasteShape(); this.pasteShape();
@ -820,7 +962,8 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity, 'fill-opacity': this.selectedShapeOpacity,
stroke: this.outlinedBorders,
}); });
this.pasteShape(); this.pasteShape();
this.pastePolyshape(); this.pastePolyshape();
@ -832,6 +975,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
stroke: this.outlinedBorders,
}); });
this.pasteShape(); this.pasteShape();
this.pastePolyshape(); this.pastePolyshape();
@ -843,26 +987,107 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'face-stroke': 'black', 'face-stroke': this.outlinedBorders,
'fill-opacity': this.configuration.creationOpacity, 'fill-opacity': this.selectedShapeOpacity,
stroke: this.outlinedBorders,
}); });
this.pasteShape(); this.pasteShape();
this.pastePolyshape(); 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 { 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(); const bbox = shape.bbox();
shape.move(x - bbox.width / 2, y - bbox.height / 2); shape.move(x - bbox.width / 2, y - bbox.height / 2);
const points = shape.attr('points').split(' '); 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 => { group.children().forEach((child: SVG.Element, idx: number): void => {
const [px, py] = points[idx].split(','); const [px, py] = points[idx].split(',');
child.move(px - radius / 2, py - radius / 2); child.move(px - radius / 2, py - radius / 2);
}); });
} };
const { x: initialX, y: initialY } = this.cursorPosition; const { x: initialX, y: initialY } = this.cursorPosition;
this.pointsGroup = this.canvas.group(); this.pointsGroup = this.canvas.group();
@ -873,7 +1098,7 @@ export class DrawHandlerImpl implements DrawHandler {
let numOfPoints = initialPoints.split(' ').length; let numOfPoints = initialPoints.split(' ').length;
while (numOfPoints) { while (numOfPoints) {
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; const stroke = consts.POINTS_STROKE_WIDTH / this.geometry.scale;
this.pointsGroup.circle().fill('white').stroke('black').attr({ this.pointsGroup.circle().fill('white').stroke('black').attr({
r: radius, r: radius,
@ -919,10 +1144,7 @@ export class DrawHandlerImpl implements DrawHandler {
if (this.drawData.initialState) { if (this.drawData.initialState) {
const { offset } = this.geometry; const { offset } = this.geometry;
if (this.drawData.shapeType === 'rectangle') { if (this.drawData.shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = this.drawData.initialState.points.map( const [xtl, ytl, xbr, ybr] = translateToCanvas(offset, this.drawData.initialState.points);
(coord: number): number => coord + offset,
);
this.pasteBox({ this.pasteBox({
x: xtl, x: xtl,
y: ytl, y: ytl,
@ -930,13 +1152,15 @@ export class DrawHandlerImpl implements DrawHandler {
height: ybr - ytl, height: ybr - ytl,
}, this.drawData.initialState.rotation); }, this.drawData.initialState.rotation);
} else if (this.drawData.shapeType === 'ellipse') { } else if (this.drawData.shapeType === 'ellipse') {
const [cx, cy, rightX, topY] = this.drawData.initialState.points.map( const [cx, cy, rightX, topY] = translateToCanvas(offset, this.drawData.initialState.points);
(coord: number): number => coord + offset,
);
this.pasteEllipse([cx, cy, rightX - cx, cy - topY], this.drawData.initialState.rotation); 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 { } 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); const stringifiedPoints = stringifyPoints(points);
if (this.drawData.shapeType === 'polygon') { if (this.drawData.shapeType === 'polygon') {
@ -975,6 +1199,8 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawCuboid(); this.drawCuboid();
this.shapeSizeElement = displayShapeSize(this.canvas, this.text); this.shapeSizeElement = displayShapeSize(this.canvas, this.text);
} }
} else if (this.drawData.shapeType === 'skeleton') {
this.drawSkeleton();
} }
if (this.drawData.shapeType !== 'ellipse') { if (this.drawData.shapeType !== 'ellipse') {
@ -995,6 +1221,9 @@ export class DrawHandlerImpl implements DrawHandler {
configuration: Configuration, configuration: Configuration,
) { ) {
this.autoborderHandler = autoborderHandler; this.autoborderHandler = autoborderHandler;
this.controlPointsSize = configuration.controlPointsSize;
this.selectedShapeOpacity = configuration.selectedShapeOpacity;
this.outlinedBorders = configuration.outlinedBorders || 'black';
this.autobordersEnabled = false; this.autobordersEnabled = false;
this.startTimestamp = Date.now(); this.startTimestamp = Date.now();
this.onDrawDone = onDrawDone; this.onDrawDone = onDrawDone;
@ -1004,7 +1233,6 @@ export class DrawHandlerImpl implements DrawHandler {
this.canceled = false; this.canceled = false;
this.drawData = null; this.drawData = null;
this.geometry = geometry; this.geometry = geometry;
this.configuration = configuration;
this.crosshair = new Crosshair(); this.crosshair = new Crosshair();
this.drawInstance = null; this.drawInstance = null;
this.pointsGroup = null; this.pointsGroup = null;
@ -1023,7 +1251,9 @@ export class DrawHandlerImpl implements DrawHandler {
} }
public configurate(configuration: Configuration): void { 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 && const isFillableRect = this.drawData &&
this.drawData.shapeType === 'rectangle' && this.drawData.shapeType === 'rectangle' &&
@ -1034,17 +1264,23 @@ export class DrawHandlerImpl implements DrawHandler {
const isFilalblePolygon = this.drawData && this.drawData.shapeType === 'polygon'; const isFilalblePolygon = this.drawData && this.drawData.shapeType === 'polygon';
if (this.drawInstance && (isFillableRect || isFillableCuboid || isFilalblePolygon)) { if (this.drawInstance && (isFillableRect || isFillableCuboid || isFilalblePolygon)) {
this.drawInstance.fill({ opacity: configuration.creationOpacity }); this.drawInstance.fill({ opacity: configuration.selectedShapeOpacity });
} }
if (typeof configuration.autoborders === 'boolean') { if (this.drawInstance && this.drawInstance.attr('stroke')) {
this.autobordersEnabled = configuration.autoborders; this.drawInstance.attr('stroke', this.outlinedBorders);
if (this.drawInstance && !this.drawData.initialState) { }
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, this.drawData.redraw); if (this.pointsGroup && this.pointsGroup.attr('stroke')) {
} else { this.pointsGroup.attr('stroke', this.outlinedBorders);
this.autoborderHandler.autoborder(false); }
}
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) { if (this.pointsGroup) {
this.pointsGroup.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
for (const point of this.pointsGroup.children()) { for (const point of this.pointsGroup.children()) {
point.attr({ point.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / geometry.scale, '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) { for (const point of (paintHandler as any).set.members) {
point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`); 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 // SPDX-License-Identifier: MIT
@ -26,8 +26,10 @@ export class EditHandlerImpl implements EditHandler {
private editedShape: SVG.Shape; private editedShape: SVG.Shape;
private editLine: SVG.PolyLine; private editLine: SVG.PolyLine;
private clones: SVG.Polygon[]; private clones: SVG.Polygon[];
private controlPointsSize: number;
private autobordersEnabled: boolean; private autobordersEnabled: boolean;
private intelligentCutEnabled: boolean; private intelligentCutEnabled: boolean;
private outlinedBorders: string;
private setupTrailingPoint(circle: SVG.Circle): void { private setupTrailingPoint(circle: SVG.Circle): void {
const head = this.editedShape.attr('points').split(' ').slice(0, this.editData.pointID).join(' '); 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) (this.editLine as any)
.addClass('cvat_canvas_shape_drawing') .addClass('cvat_canvas_shape_drawing')
.style({ .style({
'pointer-events': 'none', 'pointer-events': 'none',
'fill-opacity': 0, 'fill-opacity': 0,
stroke: strokeColor,
}) })
.attr({ .attr({
'data-origin-client-id': this.editData.state.clientID, 'data-origin-client-id': this.editData.state.clientID,
stroke: this.editedShape.attr('stroke'),
}) })
.on('drawstart drawpoint', (e: CustomEvent): void => { .on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry); this.transform(this.geometry);
@ -299,7 +300,7 @@ export class EditHandlerImpl implements EditHandler {
if (enabled) { if (enabled) {
(this.editedShape as any).selectize(true, { (this.editedShape as any).selectize(true, {
deepSelect: true, deepSelect: true,
pointSize: (2 * consts.BASE_POINT_SIZE) / getGeometry().scale, pointSize: (2 * this.controlPointsSize) / getGeometry().scale,
rotationPoint: false, rotationPoint: false,
pointType(cx: number, cy: number): SVG.Circle { pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested const circle: SVG.Circle = this.nested
@ -365,7 +366,9 @@ export class EditHandlerImpl implements EditHandler {
} }
private initEditing(): void { 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.setupPoints(true);
this.startEdit(); this.startEdit();
// draw points for this with selected and start editing till another point is clicked // 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.autoborderHandler = autoborderHandler;
this.autobordersEnabled = false; this.autobordersEnabled = false;
this.intelligentCutEnabled = false; this.intelligentCutEnabled = false;
this.controlPointsSize = consts.BASE_POINT_SIZE;
this.outlinedBorders = 'black';
this.onEditDone = onEditDone; this.onEditDone = onEditDone;
this.canvas = canvas; this.canvas = canvas;
this.editData = null; this.editData = null;
@ -416,20 +421,23 @@ export class EditHandlerImpl implements EditHandler {
} }
public configurate(configuration: Configuration): void { public configurate(configuration: Configuration): void {
if (typeof configuration.autoborders === 'boolean') { this.autobordersEnabled = configuration.autoborders;
this.autobordersEnabled = configuration.autoborders; this.outlinedBorders = configuration.outlinedBorders || 'black';
if (this.editLine) {
if (this.autobordersEnabled) { if (this.editedShape) {
this.autoborderHandler.autoborder(true, this.editLine, this.editData.state.clientID); this.editedShape.attr('stroke', this.outlinedBorders);
} else {
this.autoborderHandler.autoborder(false);
}
}
} }
if (typeof configuration.intelligentPolygonCrop === 'boolean') { if (this.editLine) {
this.intelligentCutEnabled = configuration.intelligentPolygonCrop; 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 { public transform(geometry: Geometry): void {
@ -453,7 +461,7 @@ export class EditHandlerImpl implements EditHandler {
for (const point of (paintHandler as any).set.members) { for (const point of (paintHandler as any).set.members) {
point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`); 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 // SPDX-License-Identifier: MIT

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

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

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

@ -52,6 +52,9 @@ export interface DrawnState {
updated: number; updated: number;
frame: number; frame: number;
label: any; label: any;
group: any;
color: string;
elements: DrawnState[] | null;
} }
// Translate point array from the canvas coordinate system // Translate point array from the canvas coordinate system
@ -192,11 +195,13 @@ export function readPointsFromShape(shape: SVG.Shape): number[] {
let points = null; let points = null;
if (shape.type === 'ellipse') { if (shape.type === 'ellipse') {
const [rx, ry] = [+shape.attr('rx'), +shape.attr('ry')]; 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}`; points = `${cx},${cy} ${cx + rx},${cy - ry}`;
} else if (shape.type === 'rect') { } else if (shape.type === 'rect') {
points = `${shape.attr('x')},${shape.attr('y')} ` + points = `${shape.attr('x')},${shape.attr('y')} ` +
`${shape.attr('x') + shape.attr('width')},${shape.attr('y') + shape.attr('height')}`; `${shape.attr('x') + shape.attr('width')},${shape.attr('y') + shape.attr('height')}`;
} else if (shape.type === 'circle') {
points = `${shape.cx()},${shape.cy()}`;
} else { } else {
points = shape.attr('points'); points = shape.attr('points');
} }
@ -239,4 +244,121 @@ export function translateFromCanvas(offset: number, points: number[]): number[]
return points.map((coord: number): number => coord - offset); 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]; 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 // SPDX-License-Identifier: MIT

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

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

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

@ -1,16 +1,9 @@
// Copyright (C) 2021 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const globalConfig = require('../.eslintrc.js');
module.exports = { module.exports = {
env: {
node: true,
},
parserOptions: { parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 6,
project: './tsconfig.json', project: './tsconfig.json',
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
}, },
@ -20,26 +13,4 @@ module.exports = {
'node_modules/**', 'node_modules/**',
'dist/**', '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: 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 not important changes (typos, backward compatible bug fixes, refactoring) do: `yarn version --patch`
- After changing API (backward compatible new features) do: `npm version minor` - After changing API (backward compatible new features) do: `yarn version --minor`
- After changing API (changes that break backward compatibility) do: `npm version major` - After changing API (changes that break backward compatibility) do: `yarn version --major`
## Commands ## Commands
- Building of the module from sources in the `dist` directory: - Building of the module from sources in the `dist` directory:
```bash ```bash
npm run build yarn run build
npm run build -- --mode=development # without a minification yarn run build --mode=development # without a minification
``` ```
### API Methods ### 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", "build": "tsc && webpack --config ./webpack.config.js",
"server": "nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'" "server": "nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'"
}, },
"author": "Intel", "author": "CVAT.ai",
"license": "MIT", "license": "MIT",
"browserslist": [ "browserslist": [
"Chrome >= 63", "Chrome >= 63",

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

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

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -287,7 +287,6 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
(_state: any): boolean => _state.clientID === Number(intersects[0].object.name), (_state: any): boolean => _state.clientID === Number(intersects[0].object.name),
); );
if (item.length !== 0) { if (item.length !== 0) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
this.model.data.groupData.grouped = this.model.data.groupData.grouped.filter( this.model.data.groupData.grouped = this.model.data.groupData.grouped.filter(
(_state: any): boolean => _state.clientID !== Number(intersects[0].object.name), (_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.model.data.drawData.enabled = false;
} }
this.views.perspective.renderer.dispose(); this.views.perspective.renderer.dispose();
this.model.mode = Mode.BUSY; if (!this.controller.imageIsDeleted) {
this.model.mode = Mode.BUSY;
}
this.action.loading = true; this.action.loading = true;
const loader = new PCDLoader(); const loader = new PCDLoader();
const objectURL = URL.createObjectURL(model.data.image.imageData); const objectURL = URL.createObjectURL(model.data.image.imageData);
this.clearScene(); 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); URL.revokeObjectURL(objectURL);
this.dispatchEvent(new CustomEvent('canvas.setup')); this.dispatchEvent(new CustomEvent('canvas.setup'));
} else if (reason === UpdateReasons.SHAPE_ACTIVATED) { } 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 // SPDX-License-Identifier: MIT

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

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

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -6,7 +6,7 @@
const path = require('path'); const path = require('path');
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const DtsBundleWebpack = require('dts-bundle-webpack'); const BundleDeclarationsWebpackPlugin = require('bundle-declarations-webpack-plugin');
const styleLoaders = [ const styleLoaders = [
'style-loader', 'style-loader',
@ -64,10 +64,8 @@ const nodeConfig = {
], ],
}, },
plugins: [ plugins: [
new DtsBundleWebpack({ new BundleDeclarationsWebpackPlugin({
name: 'cvat-canvas3d.node', outFile: "declaration/src/cvat-canvas.d.ts",
main: 'dist/declaration/src/typescript/canvas3d.d.ts',
out: '../cvat-canvas3d.node.d.ts',
}), }),
], ],
}; };
@ -116,10 +114,8 @@ const webConfig = {
], ],
}, },
plugins: [ plugins: [
new DtsBundleWebpack({ new BundleDeclarationsWebpackPlugin({
name: 'cvat-canvas3d', outFile: "declaration/src/cvat-canvas.d.ts",
main: 'dist/declaration/src/typescript/canvas3d.d.ts',
out: '../cvat-canvas3d.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 // SPDX-License-Identifier: MIT
module.exports = { module.exports = {
env: { env: {
node: true,
browser: true,
es6: true,
'jest/globals': true, 'jest/globals': true,
}, },
ignorePatterns: [ ignorePatterns: [
@ -19,9 +16,8 @@ module.exports = {
'dist/**', 'dist/**',
], ],
parserOptions: { parserOptions: {
parser: 'babel-eslint', project: './tsconfig.json',
sourceType: 'module', tsconfigRootDir: __dirname,
ecmaVersion: 2018,
}, },
plugins: ['jest'], plugins: ['jest'],
rules: { rules: {
@ -29,5 +25,5 @@ module.exports = {
'jest/no-focused-tests': 'error', 'jest/no-focused-tests': 'error',
'jest/no-identical-title': 'error', 'jest/no-identical-title': 'error',
'jest/prefer-to-have-length': 'warn', '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: 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 not important changes (typos, backward compatible bug fixes, refactoring) do: `yarn version --patch`
- After changing API (backward compatible new features) do: `npm version minor` - After changing API (backward compatible new features) do: `yarn version --minor`
- After changing API (changes that break backward compatibility) do: `npm version major` - After changing API (changes that break backward compatibility) do: `yarn version --major`
### Commands ### Commands
- Dependencies installation - Dependencies installation
```bash ```bash
npm ci yarn ci --frozen-lockfile
``` ```
- Building the module from sources in the `dist` directory: - Building the module from sources in the `dist` directory:
```bash ```bash
npm run build yarn run build
npm run build -- --mode=development # without a minification yarn run build --mode=development # without a minification
``` ```
- Building the documentation in the `docs` directory: - Building the documentation in the `docs` directory:
```bash ```bash
npm run-script docs yarn run docs
``` ```
- Running of tests: - Running of tests:
```bash ```bash
npm run-script test yarn run test
``` ```
- Updating of a module version: - Updating of a module version:
```bash ```bash
npm version patch # updated after minor fixes yarn version --patch # updated after minor fixes
npm version minor # updated after major changes which don't affect API compatibility with previous versions yarn 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 --major # updated after major changes which affect API compatibility with previous versions
``` ```
Visual studio code configurations: Visual studio code configurations:

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

File diff suppressed because it is too large Load Diff

@ -1,15 +1,17 @@
{ {
"name": "cvat-core", "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", "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js", "main": "src/api.ts",
"scripts": { "scripts": {
"build": "webpack", "build": "webpack",
"test": "jest --config=jest.config.js --coverage", "test": "jest --config=jest.config.js --coverage",
"docs": "jsdoc --readme README.md src/*.js -p -c jsdoc.config.js -d docs", "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", "license": "MIT",
"browserslist": [ "browserslist": [
"Chrome >= 63", "Chrome >= 63",
@ -21,17 +23,18 @@
"coveralls": "^3.0.5", "coveralls": "^3.0.5",
"jest": "^26.6.3", "jest": "^26.6.3",
"jest-junit": "^6.4.0", "jest-junit": "^6.4.0",
"jsdoc": "^3.6.6" "jsdoc": "^3.6.6",
"ts-jest": "26"
}, },
"dependencies": { "dependencies": {
"axios": "^0.21.4", "axios": "^0.27.2",
"browser-or-node": "^1.2.1", "browser-or-node": "^2.0.0",
"cvat-data": "../cvat-data", "cvat-data": "file:../cvat-data",
"detect-browser": "^5.2.1", "detect-browser": "^5.2.1",
"error-stack-parser": "^2.0.2", "error-stack-parser": "^2.0.2",
"form-data": "^2.5.0", "form-data": "^4.0.0",
"jest-config": "^26.6.3", "jest-config": "^28.1.2",
"js-cookie": "^2.2.0", "js-cookie": "^3.0.1",
"json-logic-js": "^2.0.1", "json-logic-js": "^2.0.1",
"platform": "^1.3.5", "platform": "^1.3.5",
"quickhull": "^1.0.3", "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 { const {
RectangleShape, shapeFactory,
PolygonShape, trackFactory,
PolylineShape,
PointsShape,
EllipseShape,
CuboidShape,
RectangleTrack,
PolygonTrack,
PolylineTrack,
PointsTrack,
EllipseTrack,
CuboidTrack,
Track, Track,
Shape, Shape,
Tag, Tag,
objectStateFactory,
} = require('./annotations-objects'); } = require('./annotations-objects');
const AnnotationsFilter = require('./annotations-filter'); const AnnotationsFilter = require('./annotations-filter').default;
const { checkObjectType } = require('./common'); const { checkObjectType } = require('./common');
const Statistics = require('./statistics'); const Statistics = require('./statistics');
const { Label } = require('./labels'); const { Label } = require('./labels');
const { DataError, ArgumentError, ScriptingError } = require('./exceptions'); const { ArgumentError, ScriptingError } = require('./exceptions');
const ObjectState = require('./object-state').default;
const { const {
HistoryActions, ObjectShape, ObjectType, colors, HistoryActions, ShapeType, ObjectType, colors, Source,
} = require('./enums'); } = 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 { class Collection {
constructor(data) { constructor(data) {
@ -107,6 +29,10 @@
this.labels = data.labels.reduce((labelAccumulator, label) => { this.labels = data.labels.reduce((labelAccumulator, label) => {
labelAccumulator[label.id] = label; labelAccumulator[label.id] = label;
(label?.structure?.sublabels || []).forEach((sublabel) => {
labelAccumulator[sublabel.id] = sublabel;
});
return labelAccumulator; return labelAccumulator;
}, {}); }, {});
@ -126,6 +52,7 @@
groups: this.groups, groups: this.groups,
frameMeta: this.frameMeta, frameMeta: this.frameMeta,
history: this.history, history: this.history,
nextClientID: () => ++this.count,
groupColors: {}, groupColors: {},
}; };
} }
@ -202,10 +129,7 @@
const tags = this.tags[frame] || []; const tags = this.tags[frame] || [];
const objects = [].concat(tracks, shapes, tags); const objects = [].concat(tracks, shapes, tags);
const visible = { const visible = [];
models: [],
data: [],
};
for (const object of objects) { for (const object of objects) {
if (object.removed) { if (object.removed) {
@ -213,21 +137,19 @@
} }
const stateData = object.get(frame); const stateData = object.get(frame);
if (!allTracks && stateData.outside && !stateData.keyframe) { if (stateData.outside && !stateData.keyframe && !allTracks && object instanceof Track) {
continue; continue;
} }
visible.models.push(object); visible.push(stateData);
visible.data.push(stateData);
} }
const objectStates = []; 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)) { if (!filters.length || filtered.includes(stateData.clientID)) {
const model = visible.models[idx]; const objectState = new ObjectState(stateData);
const objectState = objectStateFactory.call(model, frame, stateData);
objectStates.push(objectState); objectStates.push(objectState);
} }
}); });
@ -255,7 +177,7 @@
throw new ArgumentError(`Unknown label for the task: ${label.id}`); throw new ArgumentError(`Unknown label for the task: ${label.id}`);
} }
if (!Object.values(ObjectShape).includes(shapeType)) { if (!Object.values(ShapeType).includes(shapeType)) {
throw new ArgumentError(`Got unknown shapeType "${shapeType}"`); throw new ArgumentError(`Got unknown shapeType "${shapeType}"`);
} }
@ -288,10 +210,14 @@
keyframes[object.frame] = { keyframes[object.frame] = {
type: shapeType, type: shapeType,
frame: object.frame, 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, occluded: object.occluded,
rotation: object.rotation, rotation: object.rotation,
zOrder: object.zOrder, z_order: object.zOrder,
outside: false, outside: false,
attributes: Object.keys(object.attributes).reduce((accumulator, attrID) => { attributes: Object.keys(object.attributes).reduce((accumulator, attrID) => {
// We save only mutable attributes inside a keyframe // 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] = JSON.parse(JSON.stringify(keyframes[object.frame]));
keyframes[object.frame + 1].outside = true; keyframes[object.frame + 1].outside = true;
keyframes[object.frame + 1].frame++; 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) { } else if (object instanceof Track) {
// If this object is track, iterate through all its // If this object is track, iterate through all its
// keyframes and push copies to new keyframes // keyframes and push copies to new keyframes
const attributes = {}; // id:value const attributes = {}; // id:value
for (const keyframe of Object.keys(object.shapes)) { const trackShapes = object.shapes;
const shape = object.shapes[keyframe]; 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 // Frame already saved and it is not outside
if (keyframe in keyframes && !keyframes[keyframe].outside) { if (keyframe in keyframes && !keyframes[keyframe].outside) {
// This shape is outside and non-outside shape already exists // This shape is outside and non-outside shape already exists
@ -341,11 +278,16 @@
keyframes[keyframe] = { keyframes[keyframe] = {
type: shapeType, type: shapeType,
frame: +keyframe, 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, rotation: shape.rotation,
occluded: shape.occluded, occluded: shape.occluded,
outside: shape.outside, outside: shape.outside,
zOrder: shape.zOrder, z_order: shape.zOrder,
attributes: updatedAttributes ? Object.keys(attributes).reduce((accumulator, attrID) => { attributes: updatedAttributes ? Object.keys(attributes).reduce((accumulator, attrID) => {
accumulator.push({ accumulator.push({
spec_id: +attrID, spec_id: +attrID,
@ -451,13 +393,30 @@
const exported = object.toJSON(); const exported = object.toJSON();
const position = { const position = {
type: objectState.shapeType, 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, rotation: objectState.rotation,
occluded: objectState.occluded, occluded: objectState.occluded,
outside: objectState.outside, outside: objectState.outside,
zOrder: objectState.zOrder, z_order: objectState.zOrder,
attributes: Object.keys(objectState.attributes).reduce((accumulator, attrID) => { attributes: Object.keys(objectState.attributes).reduce((accumulator, attrID) => {
if (!labelAttributes[attrID].mutable) { if (labelAttributes[attrID].mutable) {
accumulator.push({ accumulator.push({
spec_id: +attrID, spec_id: +attrID,
value: objectState.attributes[attrID], value: objectState.attributes[attrID],
@ -475,14 +434,19 @@
label_id: exported.label_id, label_id: exported.label_id,
attributes: exported.attributes, attributes: exported.attributes,
shapes: [], shapes: [],
source: Source.MANUAL,
}; };
const next = JSON.parse(JSON.stringify(prev)); const next = JSON.parse(JSON.stringify(prev));
next.frame = frame; next.frame = frame;
next.shapes.push(JSON.parse(JSON.stringify(position))); next.shapes.push(JSON.parse(JSON.stringify(position)));
exported.shapes.map((shape) => { exported.shapes.map((shape) => {
delete shape.id; delete shape.id;
(shape.elements || []).forEach((element) => {
delete element.id;
});
if (shape.frame < frame) { if (shape.frame < frame) {
prev.shapes.push(JSON.parse(JSON.stringify(shape))); prev.shapes.push(JSON.parse(JSON.stringify(shape)));
} else if (shape.frame > frame) { } else if (shape.frame > frame) {
@ -499,6 +463,9 @@
prev.shapes[prev.shapes.length - 2].frame -= 1; prev.shapes[prev.shapes.length - 2].frame -= 1;
} }
prev.shapes[prev.shapes.length - 1].outside = true; prev.shapes[prev.shapes.length - 1].outside = true;
(prev.shapes[prev.shapes.length - 1].elements || []).forEach((el) => {
el.outside = true;
});
let clientID = ++this.count; let clientID = ++this.count;
const prevTrack = trackFactory(prev, clientID, this.injection); const prevTrack = trackFactory(prev, clientID, this.injection);
@ -546,6 +513,7 @@
const undoGroups = objectsForGroup.map((object) => object.group); const undoGroups = objectsForGroup.map((object) => object.group);
for (const object of objectsForGroup) { for (const object of objectsForGroup) {
object.group = groupIdx; object.group = groupIdx;
object.updated = Date.now();
} }
const redoGroups = objectsForGroup.map((object) => object.group); const redoGroups = objectsForGroup.map((object) => object.group);
@ -554,11 +522,13 @@
() => { () => {
objectsForGroup.forEach((object, idx) => { objectsForGroup.forEach((object, idx) => {
object.group = undoGroups[idx]; object.group = undoGroups[idx];
object.updated = Date.now();
}); });
}, },
() => { () => {
objectsForGroup.forEach((object, idx) => { objectsForGroup.forEach((object, idx) => {
object.group = redoGroups[idx]; object.group = redoGroups[idx];
object.updated = Date.now();
}); });
}, },
objectsForGroup.map((object) => object.clientID), objectsForGroup.map((object) => object.clientID),
@ -579,8 +549,17 @@
tracks.forEach((track) => { tracks.forEach((track) => {
if (track.frame <= endframe) { if (track.frame <= endframe) {
if (delTrackKeyframesOnly) { if (delTrackKeyframesOnly) {
for (const keyframe in track.shapes) { for (const keyframe of Object.keys(track.shapes)) {
if (keyframe >= startframe && keyframe <= endframe) { delete track.shapes[keyframe]; } 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) { } else if (track.frame >= startframe) {
const index = tracks.indexOf(track); const index = tracks.indexOf(track);
@ -593,7 +572,7 @@
this.shapes = {}; this.shapes = {};
this.tags = {}; this.tags = {};
this.tracks = []; this.tracks = [];
this.objects = {}; // by id this.objects = {};
this.count = 0; this.count = 0;
this.flush = true; this.flush = true;
@ -606,42 +585,74 @@
statistics() { statistics() {
const labels = {}; const labels = {};
const skeleton = { const shapes = ['rectangle', 'polygon', 'polyline', 'points', 'ellipse', 'cuboid', 'skeleton'];
rectangle: { const body = {
shape: 0, ...(shapes.reduce((acc, val) => ({
track: 0, ...acc,
}, [val]: { shape: 0, track: 0 },
polygon: { }), {})),
shape: 0,
track: 0, tag: 0,
},
polyline: {
shape: 0,
track: 0,
},
points: {
shape: 0,
track: 0,
},
ellipse: {
shape: 0,
track: 0,
},
cuboid: {
shape: 0,
track: 0,
},
tags: 0,
manually: 0, manually: 0,
interpolated: 0, interpolated: 0,
total: 0, total: 0,
}; };
const total = JSON.parse(JSON.stringify(skeleton)); const sep = '{{cvat.skeleton.lbl.sep}}';
for (const label of Object.values(this.labels)) { const fillBody = (spec, prefix = ''): void => {
const { name } = label; const pref = prefix ? `${prefix}${sep}` : '';
labels[name] = JSON.parse(JSON.stringify(skeleton)); 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)) { for (const object of Object.values(this.objects)) {
if (object.removed) { if (object.removed) {
@ -659,59 +670,37 @@
throw new ScriptingError(`Unexpected object type: "${objectType}"`); throw new ScriptingError(`Unexpected object type: "${objectType}"`);
} }
const label = object.label.name; const { name: label } = object.label;
if (objectType === 'tag') { if (objectType === 'tag') {
labels[label].tags++; labels[label].tag++;
labels[label].manually++; labels[label].manually++;
labels[label].total++; labels[label].total++;
} else if (objectType === 'track') {
scanTrack(object);
} else { } else {
const { shapeType } = object; const { shapeType } = object;
labels[label][shapeType][objectType]++; labels[label][shapeType].shape++;
labels[label].manually++;
if (objectType === 'track') { labels[label].total++;
const keyframes = Object.keys(object.shapes) if (shapeType === ShapeType.SKELETON) {
.sort((a, b) => +a - +b) object.elements.forEach((element) => {
.map((el) => +el); const combinedName = [label, element.label.name].join(sep);
labels[combinedName][element.shapeType].shape++;
let prevKeyframe = keyframes[0]; labels[combinedName].manually++;
let visible = false; labels[combinedName].total++;
});
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++;
} }
} }
} }
for (const label of Object.keys(labels)) { for (const label of Object.keys(labels)) {
for (const key of Object.keys(labels[label])) { for (const shapeType of Object.keys(labels[label])) {
if (typeof labels[label][key] === 'object') { if (typeof labels[label][shapeType] === 'object') {
for (const objectType of Object.keys(labels[label][key])) { for (const objectType of Object.keys(labels[label][shapeType])) {
total[key][objectType] += labels[label][key][objectType]; total[shapeType][objectType] += labels[label][shapeType][objectType];
} }
} else { } else {
total[key] += labels[label][key]; total[shapeType] += labels[label][shapeType];
} }
} }
} }
@ -744,7 +733,7 @@
for (const state of objectStates) { for (const state of objectStates) {
checkObjectType('object state', state, null, ObjectState); 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 frame', state.frame, 'integer', null);
checkObjectType('state rotation', state.rotation || 0, 'number', null); checkObjectType('state rotation', state.rotation || 0, 'number', null);
checkObjectType('state attributes', state.attributes, null, Object); checkObjectType('state attributes', state.attributes, null, Object);
@ -775,9 +764,9 @@
checkObjectType('point coordinate', coord, 'number', null); checkObjectType('point coordinate', coord, 'number', null);
} }
if (!Object.values(ObjectShape).includes(state.shapeType)) { if (!Object.values(ShapeType).includes(state.shapeType)) {
throw new ArgumentError( 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, type: state.shapeType,
z_order: state.zOrder, z_order: state.zOrder,
source: state.source, 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') { } else if (state.objectType === 'track') {
constructed.tracks.push({ constructed.tracks.push({
@ -807,7 +808,7 @@
{ {
attributes: attributes.filter((attr) => labelAttributes[attr.spec_id].mutable), attributes: attributes.filter((attr) => labelAttributes[attr.spec_id].mutable),
frame: state.frame, frame: state.frame,
occluded: state.occluded || false, occluded: false,
outside: false, outside: false,
points: [...state.points], points: [...state.points],
rotation: state.rotation || 0, rotation: state.rotation || 0,
@ -815,6 +816,33 @@
z_order: state.zOrder, 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 { } else {
throw new ArgumentError( throw new ArgumentError(

@ -1,15 +1,15 @@
// Copyright (C) 2020-2021 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const jsonLogic = require('json-logic-js'); import jsonLogic from 'json-logic-js';
const { AttributeType, ObjectType } = require('./enums'); import { AttributeType, ObjectType } from './enums';
function adjustName(name) { function adjustName(name): string {
return name.replace(/\./g, '\u2219'); return name.replace(/\./g, '\u2219');
} }
class AnnotationsFilter { export default class AnnotationsFilter {
_convertObjects(statesData) { _convertObjects(statesData) {
const objects = statesData.map((state) => { const objects = statesData.map((state) => {
const labelAttributes = state.label.attributes.reduce((acc, attr) => { const labelAttributes = state.label.attributes.reduce((acc, attr) => {
@ -24,7 +24,11 @@ class AnnotationsFilter {
let [width, height] = [null, null]; let [width, height] = [null, null];
if (state.objectType !== ObjectType.TAG) { 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) { if (idx % 2) {
// y // y
ytl = Math.min(ytl, coord); ytl = Math.min(ytl, coord);
@ -75,5 +79,3 @@ class AnnotationsFilter {
.filter((_, index) => jsonLogic.apply(filters[0], converted[index])); .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 // SPDX-License-Identifier: MIT
import { HistoryActions } from './enums';
const MAX_HISTORY_LENGTH = 128; 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() { constructor() {
this.frozen = false; this.frozen = false;
this.clear(); this.clear();
} }
freeze(frozen) { public freeze(frozen: boolean): void {
this.frozen = frozen; this.frozen = frozen;
} }
get() { public get(): { undo: [HistoryActions, number][], redo: [HistoryActions, number][] } {
return { return {
undo: this._undo.map((undo) => [undo.action, undo.frame]), undo: this._undo.map((undo) => [undo.action, undo.frame]),
redo: this._redo.map((redo) => [redo.action, redo.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; if (this.frozen) return;
const actionItem = { const actionItem = {
clientIDs, clientIDs,
@ -36,12 +50,12 @@ class AnnotationHistory {
this._redo = []; this._redo = [];
} }
undo(count) { public async undo(count: number): Promise<number[]> {
const affectedObjects = []; const affectedObjects = [];
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const action = this._undo.pop(); const action = this._undo.pop();
if (action) { if (action) {
action.undo(); await action.undo();
this._redo.push(action); this._redo.push(action);
affectedObjects.push(...action.clientIDs); affectedObjects.push(...action.clientIDs);
} else { } else {
@ -52,12 +66,12 @@ class AnnotationHistory {
return affectedObjects; return affectedObjects;
} }
redo(count) { public async redo(count: number): Promise<number[]> {
const affectedObjects = []; const affectedObjects = [];
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const action = this._redo.pop(); const action = this._redo.pop();
if (action) { if (action) {
action.redo(); await action.redo();
this._undo.push(action); this._undo.push(action);
affectedObjects.push(...action.clientIDs); affectedObjects.push(...action.clientIDs);
} else { } else {
@ -68,10 +82,8 @@ class AnnotationHistory {
return affectedObjects; return affectedObjects;
} }
clear() { public clear(): void {
this._undo = []; this._undo = [];
this._redo = []; 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 // SPDX-License-Identifier: MIT
@ -103,6 +103,7 @@
'rotation', 'rotation',
'type', 'type',
'shapes', 'shapes',
'elements',
'attributes', 'attributes',
'value', 'value',
'spec_id', 'spec_id',

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

Loading…
Cancel
Save