chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:23 +08:00
commit 10df941303
2913 changed files with 455804 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
[bandit]
# B101 : assert_used
# B102 : exec_used
# B404 : import_subprocess
# B406 : import_xml_sax
skips: B101,B102,B404,B406
exclude: **/tests/**,tests
+38
View File
@@ -0,0 +1,38 @@
[run]
branch = true
source =
cvat/apps/
cvat-sdk/
cvat-cli/
utils/dataset_manifest
omit =
cvat/settings/*
*/tests/*
*/test_*
*/_test_*
*/migrations/*
[report]
# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover
# Don't complain about missing debug-only code:
def __repr__
if\s+[\w\.()]+\.isEnabledFor\(log\.DEBUG\):
# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
# Don't complain if non-runnable code isn't run:
if 0:
if __name__ == .__main__.:
# don't fail on the code that can be found
ignore_errors = true
skip_empty = true
+18
View File
@@ -0,0 +1,18 @@
/.git
/.env
/.vscode
/.github
/.husky
/share
/data
/media
/keys
**/node_modules
/static
/serverless
/site
/helm-chart
/dev
/changelog.d
/cvat-cli
/profiles
+21
View File
@@ -0,0 +1,21 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# Change these settings to your own preference
indent_style = space
indent_size = 4
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{yml,yaml}]
indent_size = 2
+88
View File
@@ -0,0 +1,88 @@
// Copyright (C) 2018-2022 Intel Corporation
// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
module.exports = {
root: true,
env: {
node: true,
browser: true,
es2020: true,
},
parserOptions: {
sourceType: 'module',
parser: '@typescript-eslint/parser',
},
ignorePatterns: [
'.eslintrc.cjs',
'lint-staged.config.js',
'site/**',
'webpack.config.cjs',
],
plugins: ['@typescript-eslint', '@stylistic', 'security', 'no-unsanitized', 'import'],
extends: [
'eslint:recommended', 'plugin:security/recommended', 'plugin:no-unsanitized/DOM',
'airbnb-base', 'plugin:import/errors', 'plugin:import/warnings',
'plugin:import/typescript', 'plugin:@typescript-eslint/recommended', 'airbnb-typescript/base',
],
rules: {
// 'header/header': [2, 'line', [{
// pattern: ' {1}Copyright \\(C\\) (?:20\\d{2}-)?2022 Intel Corporation',
// template: ' Copyright (C) 2022 Intel Corporation'
// }, '', ' SPDX-License-Identifier: MIT']],
'no-plusplus': 0,
'no-continue': 0,
'no-console': 0,
'no-restricted-syntax': [0, { selector: 'ForOfStatement' }],
'no-await-in-loop': 0,
'@stylistic/indent': ['error', 4, { 'SwitchCase': 1 }],
'max-len': ['error', { code: 120, ignoreStrings: true }],
'func-names': 0,
'valid-typeof': 0,
'quotes': ['error', 'single', { "avoidEscape": true }],
'lines-between-class-members': 'off',
'@stylistic/lines-between-class-members': 0,
'@typescript-eslint/lines-between-class-members': 'off',
'class-methods-use-this': 0,
'no-underscore-dangle': ['error', { allowAfterThis: true }],
'max-classes-per-file': 0,
'operator-linebreak': ['error', 'after'],
'newline-per-chained-call': 0,
'global-require': 0,
'arrow-parens': ['error', 'always'],
'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/no-unresolved': 'off',
'import/prefer-default-export': 0, // works incorrect with interfaces
'no-useless-assignment': 'off',
'preserve-caught-error': 'off',
'react/jsx-indent-props': 0, // new rule, breaks current styling
'react/jsx-indent': 0, // new rule, conflicts with eslint@typescript-eslint/indent eslint@indent, breaks current styling
'function-paren-newline': 0, // new rule, breaks current styling
'@typescript-eslint/default-param-last': 0, // does not really work with redux reducers
'@typescript-eslint/ban-ts-comment': 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/no-empty-object-type': [
'error',
{
allowInterfaces: 'always',
allowObjectTypes: 'never',
},
],
'@typescript-eslint/no-unsafe-function-type': 'error',
'@typescript-eslint/no-restricted-types': [
'error',
{
types: {
object: {
message: 'Use a more specific object shape, Record<string, unknown>, or unknown instead of object.',
},
},
},
],
},
};
+29
View File
@@ -0,0 +1,29 @@
* text=auto whitespace=trailing-space,space-before-tab,-indent-with-non-tab,tab-in-indent,tabwidth=4
.git* text export-ignore
*.txt text
*.htm text
*.html text
*.js text
*.py text
*.css text
*.md text
*.yml text
Dockerfile text
LICENSE text
*.conf text
*.mimetypes text
*.sh text eol=lf
*.avi binary
*.bmp binary
*.exr binary
*.ico binary
*.jpeg binary
*.jpg binary
*.png binary
*.gif binary
*.ttf binary
*.pdf binary
+59
View File
@@ -0,0 +1,59 @@
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence, they will
# be requested for review when someone opens a pull request.
* @nmanovic
# Order is important; the last matching pattern takes the most
# precedence. When someone opens a pull request that only
# modifies components below, only the list of owners and not
# the global owner(s) will be requested for a review.
# Component: Server
/cvat/ @SpecLad
/cvat/apps/dataset_manager/ @zhiltsov-max
/cvat/apps/consensus/ @zhiltsov-max
/cvat/apps/quality_control/ @zhiltsov-max
# Component: CVAT SDK/CLI
/cvat-sdk/ @SpecLad
/cvat/schema.yml @SpecLad
/cvat-cli/ @SpecLad
# Component: Documentation
/site/ @bsekachev
/CHANGELOG.md @bsekachev
/README.md @bsekachev
# Component: CVAT UI
/cvat-ui/ @bsekachev
/cvat-data/ @bsekachev
/cvat-canvas/ @bsekachev
/cvat-canvas3d/ @bsekachev
/cvat-core/ @bsekachev
# Advanced components (e.g. analytics)
/components/ @azhavoro
# Component: Tests
/tests/ @bsekachev
/tests/python/ @zhiltsov-max
# Component: Serverless functions
/serverless/ @bsekachev
# Infrastructure
Dockerfile* @azhavoro
docker-compose* @azhavoro
.* @azhavoro
*.js @bsekachev
*.conf @azhavoro
*.sh @azhavoro
/supervisord/ @azhavoro
/helm-chart/ @azhavoro
/utils/ @SpecLad
/.github/ @SpecLad
/dev/ @SpecLad
.regal/ @SpecLad
/LICENSE @nmanovic
+65
View File
@@ -0,0 +1,65 @@
name: "\U0001F41E Bug Report"
description: Create a bug report to help us fix it
labels: ["bug"]
body:
- type: checkboxes
attributes:
label: Actions before raising this issue
options:
- label: I searched the existing issues and did not find anything similar.
required: true
- label: I read/searched [the docs](https://docs.cvat.ai/docs/)
required: true
- type: textarea
attributes:
label: Steps to Reproduce
description: Provide a link to a live example or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: false
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: false
- type: textarea
attributes:
label: Possible Solution
description: |
Not obligatory, but suggest a fix/reason for the bug, or ideas on how to implement the addition or change
validations:
required: false
- type: textarea
attributes:
label: Context
description: |
How has this issue affected you? What are you trying to accomplish?
Providing context helps us come up with a solution that is most useful in the real world!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
- type: textarea
attributes:
label: Environment
description: |
Include relevant details about the environment you experienced
placeholder: |
- Git hash commit (`git log -1`):
- Docker version `docker version` (e.g. Docker 17.0.05):
- Are you using Docker Swarm or Kubernetes?
- Operating System and version (e.g. Linux, Windows, MacOS):
- Code example or link to GitHub repo or gist to reproduce problem:
- Other diagnostic information / logs:
<details>
<summary>Logs from `cvat` container</summary>
</details>
render: Markdown
validations:
required: false
@@ -0,0 +1,41 @@
name: "\U0001F680 Feature Request"
description: Suggest an idea for this project
labels: ["enhancement"]
body:
- type: checkboxes
attributes:
label: Actions before raising this issue
options:
- label: I searched the existing issues and did not find anything similar.
required: true
- label: I read/searched [the docs](https://docs.cvat.ai/docs/)
required: true
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is.
placeholder: I'm always frustrated when I have to press this small button.
validations:
required: false
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
placeholder: Make this button bigger
validations:
required: false
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
placeholder: I wanted to buy bigger display, but it would be too expensive.
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: |
Add any other context or screenshots about the feature request here.
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
+34
View File
@@ -0,0 +1,34 @@
<!-- Raise an issue to propose your change (https://github.com/cvat-ai/cvat/issues).
It helps to avoid duplication of efforts from multiple independent contributors.
Discuss your ideas with maintainers to be sure that changes will be approved and merged.
Read the [Contribution guide](https://docs.cvat.ai/docs/contributing/). -->
<!-- Provide a general summary of your changes in the Title above -->
### Motivation and context
<!-- Why is this change required? What problem does it solve? If it fixes an open
issue, please link to the issue here. Describe your changes in detail, add
screenshots. -->
### How has this been tested?
<!-- Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc. -->
### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes that apply.
If an item isn't applicable for some reason, then ~~explicitly strikethrough~~ the whole
line. If you don't do that, GitHub will show incorrect progress for the pull request.
If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] I submit my changes into the `develop` branch
- [ ] I have created a changelog fragment <!-- see top comment in CHANGELOG.md -->
- [ ] I have updated the documentation accordingly
- [ ] I have added tests to cover my changes
- [ ] I have linked related issues (see [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))
### License
- [ ] I submit _my code changes_ under the same [MIT License](
https://github.com/cvat-ai/cvat/blob/develop/LICENSE) that covers the project.
Feel free to contact the maintainers if that's a concern.
+62
View File
@@ -0,0 +1,62 @@
name: Setup Yarn
description: Set up Node.js, enable Yarn via Corepack, cache dependencies, and install packages
inputs:
node-version:
description: Node.js version to install
required: false
default: 22.x
node-modules-path:
description: Paths to cache for node_modules
required: false
default: node_modules
install-command:
description: Command used to install dependencies
required: false
default: yarn install --immutable
runs:
using: composite
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Enable corepack
shell: bash
run: corepack enable yarn
- name: Get yarn cache directory path
id: yarn-cache-dir-path
shell: bash
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- name: Cache yarn cache
uses: actions/cache@v4
id: cache-yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Get yarn version
id: yarn-version
shell: bash
run: echo "version=$(node --version)" >> $GITHUB_OUTPUT
- name: Cache node_modules
uses: actions/cache@v4
id: cache-node-modules
with:
path: ${{ inputs.node-modules-path }}
key: ${{ runner.os }}-${{ steps.yarn-version.outputs.version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ steps.yarn-version.outputs.version }}-nodemodules-
- name: Install dependencies
if: |
steps.cache-yarn-cache.outputs.cache-hit != 'true' ||
steps.cache-node-modules.outputs.cache-hit != 'true'
shell: bash
run: ${{ inputs.install-command }}
+33
View File
@@ -0,0 +1,33 @@
comment:
layout: "header, diff, components"
component_management:
individual_components:
- component_id: cvat-ui
name: cvat-ui
paths:
- cvat-canvas/**
- cvat-canvas3d/**
- cvat-core/**
- cvat-data/**
- cvat-ui/**
- component_id: cvat-server
name: cvat-server
paths:
- cvat/**
- cvat-cli/**
- cvat-sdk/**
- utils/**
codecov:
require_ci_to_pass: yes
notify:
wait_for_ci: yes
coverage:
status:
patch: false
project:
default:
target: auto
threshold: 5%
+35
View File
@@ -0,0 +1,35 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
# Python dependencies in cvat/requirements are managed by pip-compile-multi.
# Dependabot should update source *.in files; generated *.txt files are
# regenerated by a separate workflow.
- package-ecosystem: "pip"
directory: "/utils/dataset_manifest"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
exclude-paths:
- "*.txt"
# Dependabot resolves the full CVAT requirements graph for each candidate
# update, while generated lock files are refreshed by the workflow below.
- package-ecosystem: "pip"
directory: "/cvat/requirements"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
exclude-paths:
- "*.txt"
- "all.in"
+63
View File
@@ -0,0 +1,63 @@
name: Cache
on:
push:
branches:
- 'develop'
jobs:
get-sha:
uses: ./.github/workflows/search-cache.yml
Caching_CVAT:
needs: get-sha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/cache@v6
id: server-cache-action
with:
path: /tmp/cvat_cache_server
key: ${{ runner.os }}-build-server-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-server-${{ needs.get-sha.outputs.sha }}
${{ runner.os }}-build-server-
- uses: actions/cache@v6
id: ui-cache-action
with:
path: /tmp/cvat_cache_ui
key: ${{ runner.os }}-build-ui-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-ui-${{ needs.get-sha.outputs.sha }}
${{ runner.os }}-build-ui-
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Caching CVAT Server
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
cache-from: type=local,src=/tmp/cvat_cache_server
cache-to: type=local,dest=/tmp/cvat_cache_server-new
- name: Caching CVAT UI
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile.ui
cache-from: type=local,src=/tmp/cvat_cache_ui
cache-to: type=local,dest=/tmp/cvat_cache_ui-new
- name: Moving cache
run: |
rm -rf /tmp/cvat_cache_server
mv /tmp/cvat_cache_server-new /tmp/cvat_cache_server
rm -rf /tmp/cvat_cache_ui
mv /tmp/cvat_cache_ui-new /tmp/cvat_cache_ui
+72
View File
@@ -0,0 +1,72 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "develop", master, release-* ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "develop" ]
schedule:
- cron: '27 19 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v4
# ️ Command-line programs to run using the OS shell.
# 📚 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.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
+89
View File
@@ -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 "allow=${ALLOW}" >> $GITHUB_OUTPUT
- name: Verify that author of comment is collaborator
if: steps.check-author.outputs.allow == ''
uses: actions/github-script@v9
with:
script: |
core.setFailed('User that send comment with /check command is not collaborator')
- name: Get branch name
id: get-ref
run: |
SHA=$(gh api /repos/${{ github.repository }}/pulls/${{ github.event.issue.number }} | jq -r '.head.sha')
echo "ref=${SHA}" >> $GITHUB_OUTPUT
- 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 "cid=${COMMENT_ID}" >> $GITHUB_OUTPUT
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}"
+65
View File
@@ -0,0 +1,65 @@
name: Docs
on:
push:
branches:
- 'master'
- 'develop'
pull_request:
types: [opened, synchronize, reopened]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
generate_github_pages:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Generate CVAT SDK
run: |
pip3 install --user -r cvat-sdk/gen/requirements.txt
./cvat-sdk/gen/generate.sh
- name: Setup Hugo
run: |
wget https://github.com/gohugoio/hugo/releases/download/v0.110.0/hugo_extended_0.110.0_Linux-64bit.tar.gz
(mkdir hugo_extended_0.110.0_Linux-64bit && tar -xf hugo_extended_0.110.0_Linux-64bit.tar.gz -C hugo_extended_0.110.0_Linux-64bit)
mkdir hugo
cp hugo_extended_0.110.0_Linux-64bit/hugo hugo/hugo-0.110
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '22.x'
- name: Install npm packages
working-directory: ./site
run: |
npm ci
- name: Build docs
run: |
pip install -r site/requirements.txt
python site/process_sdk_docs.py
PATH="$PWD/hugo:$PATH" python site/build_docs.py
env:
BASE_URL: https://docs.cvat.ai
HUGO_ENV: production
GOOGLE_TAG_ID: G-GVSBK1DNK5
- name: Deploy
if: github.ref == 'refs/heads/develop'
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
force_orphan: true
cname: docs.cvat.ai
+109
View File
@@ -0,0 +1,109 @@
name: Finalize release
on:
workflow_dispatch:
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Discover pending release
id: discover
env:
GH_TOKEN: "${{ github.token }}"
run: |
gh --repo="${{ github.repository }}" \
pr list --base master --state open --json number,headRefName \
--jq 'map(select(.headRefName | startswith("release-")))' \
> /tmp/release-prs.json
if jq -e 'length < 1' /tmp/release-prs.json > /dev/null; then
echo "No open release pull requests found."
exit 1
elif jq -e 'length > 1' /tmp/release-prs.json > /dev/null; then
echo "Multiple open release pull requests found:"
jq -r '.[] | "https://github.com/${{ github.repository }}/pull/\(.number)"' /tmp/release-prs.json
exit 1
fi
jq -r '.[] | "prNumber=\(.number)", "version=\(.headRefName | ltrimstr("release-"))"' \
/tmp/release-prs.json >> "$GITHUB_OUTPUT"
# When you use the default github.token to make changes in the repository,
# it does not trigger further GitHub pipelines. We want to trigger CI for
# the pull request and artifact building for the release, so we have to use
# an app token.
- name: Generate authentication token
id: gen-token
uses: actions/create-github-app-token@v3
with:
client-id: "${{ secrets.CVAT_BOT_CLIENT_ID }}"
private-key: "${{ secrets.CVAT_BOT_PRIVATE_KEY }}"
- name: Install dependencies
run:
sudo apt-get install -y pandoc
- uses: actions/checkout@v7
with:
ref: "release-${{ steps.discover.outputs.version }}"
- name: Verify that the release is new
env:
NEW_VERSION: "${{ steps.discover.outputs.version }}"
run: |
if git ls-remote --exit-code origin "refs/tags/v$NEW_VERSION" > /dev/null; then
echo "Release v$NEW_VERSION already exists"
exit 1
fi
# Do post-release tasks before publishing the release. If anything goes wrong,
# the dev-release-* branch can be deleted, and the whole process restarted again;
# whereas we can't unmerge the release PR.
- name: Create post-release branch
run:
git checkout -b "dev-release-${{ steps.discover.outputs.version }}"
- name: Bump version
run:
./dev/update_version.py --patch
- name: Commit post-release changes
run: |
git -c user.name='cvat-bot[bot]' -c user.email='147643061+cvat-bot[bot]@users.noreply.github.com' \
commit -a -m "Update ${{ github.ref_name }} after v${{ steps.discover.outputs.version }}"
- name: Push post-release branch
run:
git push -u origin "dev-release-${{ steps.discover.outputs.version }}"
- name: Create post-release pull request
env:
GH_TOKEN: "${{ steps.gen-token.outputs.token }}"
run: |
gh pr create \
--base="${{ github.ref_name }}" \
--title="Update ${{ github.ref_name }} after v${{ steps.discover.outputs.version }}" \
--body=""
# Now publish the release.
- name: Merge release pull request
env:
GH_TOKEN: "${{ steps.gen-token.outputs.token }}"
run:
gh pr merge --merge "${{ steps.discover.outputs.prNumber }}" --delete-branch
- name: Create release
env:
GH_TOKEN: "${{ steps.gen-token.outputs.token }}"
NEW_VERSION: "${{ steps.discover.outputs.version }}"
run: |
# We could grab the release notes from the PR description, but it could
# be outdated if any changes were made on the release branch. So instead,
# just re-extract them from the changelog again.
./dev/gh_release_notes.sh \
| gh release create "v$NEW_VERSION" \
--target=master \
--title="v$NEW_VERSION" \
--notes-file=-
+354
View File
@@ -0,0 +1,354 @@
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 }}
CYPRESS_VERIFY_TIMEOUT: 180000 # https://docs.cypress.io/guides/guides/command-line#cypress-verify
CVAT_VERSION: "local"
jobs:
search_cache:
uses: ./.github/workflows/search-cache.yml
build:
needs: search_cache
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref }}
- name: CVAT server. Getting cache from the default branch
uses: actions/cache@v6
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@v6
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@v4
- name: Create artifact directories
run: |
mkdir /tmp/cvat_server
mkdir /tmp/cvat_ui
mkdir /tmp/cvat_sdk
- name: CVAT server. Build and push
uses: docker/build-push-action@v7
with:
cache-from: type=local,src=/tmp/cvat_cache_server
context: .
file: Dockerfile
tags: cvat/server:${{ env.CVAT_VERSION }}
outputs: type=docker,dest=/tmp/cvat_server/image.tar
- name: CVAT UI. Build and push
uses: docker/build-push-action@v7
with:
cache-from: type=local,src=/tmp/cvat_cache_ui
context: .
file: Dockerfile.ui
tags: cvat/ui:${{ env.CVAT_VERSION }}
outputs: type=docker,dest=/tmp/cvat_ui/image.tar
- name: CVAT SDK. Build
run: |
pip3 install --user -r cvat-sdk/gen/requirements.txt
./cvat-sdk/gen/generate.sh
cp -r cvat-sdk/* /tmp/cvat_sdk/
- name: Upload CVAT server artifact
uses: actions/upload-artifact@v7
with:
name: cvat_server
path: /tmp/cvat_server/image.tar
- name: Upload CVAT UI artifact
uses: actions/upload-artifact@v7
with:
name: cvat_ui
path: /tmp/cvat_ui/image.tar
- name: Upload CVAT SDK artifact
uses: actions/upload-artifact@v7
with:
name: cvat_sdk
path: /tmp/cvat_sdk/
rest_api_testing:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref }}
- uses: actions/setup-python@v6
with:
python-version: '3.10'
- name: Download CVAT server image
uses: actions/download-artifact@v8
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Download CVAT UI images
uses: actions/download-artifact@v8
with:
name: cvat_ui
path: /tmp/cvat_ui/
- name: Download CVAT SDK package
uses: actions/download-artifact@v8
with:
name: cvat_sdk
path: /tmp/cvat_sdk/
- 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: Verify API schema
id: verify_schema
run: |
docker run --rm cvat/server:${CVAT_VERSION} bash \
-c 'python manage.py spectacular' > cvat/schema-expected.yml
if ! git diff --no-index cvat/schema.yml cvat/schema-expected.yml; then
echo
echo 'API schema has changed! Please update cvat/schema.yml:'
echo
echo ' docker run --rm cvat/server:dev bash \'
echo " -c 'python manage.py spectacular' > cvat/schema.yml"
exit 1
fi
- name: Verify migrations
run: |
docker run --rm cvat/server:${CVAT_VERSION} bash \
-c 'python manage.py makemigrations --check'
- name: Generate SDK
run: |
pip3 install -r cvat-sdk/gen/requirements.txt
./cvat-sdk/gen/generate.sh
- name: Install SDK
run: |
pip3 install -r ./tests/python/requirements.txt \
-e './cvat-sdk[masks,pytorch]' -e ./cvat-cli \
--extra-index-url https://download.pytorch.org/whl/cpu
- name: Running REST API and SDK tests
id: run_tests
run: |
pytest tests/python/
CVAT_ALLOW_STATIC_CACHE="true" pytest -k "TestTaskData" tests/python
- name: Creating a log file from cvat containers
if: failure() && steps.run_tests.conclusion == 'failure'
env:
LOGS_DIR: "${{ github.workspace }}/rest_api_testing"
run: |
mkdir $LOGS_DIR
docker logs test_cvat_server_1 > $LOGS_DIR/cvat_server.log
docker logs test_cvat_worker_export_1 > $LOGS_DIR/cvat_worker_export.log
docker logs test_cvat_worker_import_1 > $LOGS_DIR/cvat_worker_import.log
docker logs test_cvat_opa_1 2> $LOGS_DIR/cvat_opa.log
- name: Uploading "cvat" container logs as an artifact
if: failure() && steps.run_tests.conclusion == 'failure'
uses: actions/upload-artifact@v7
with:
name: rest_api_container_logs
path: "${{ github.workspace }}/rest_api_testing"
unit_testing:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref }}
- name: Download CVAT server image
uses: actions/download-artifact@v8
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Load Docker server image
run: |
docker load --input /tmp/cvat_server/image.tar
docker image ls -a
- name: Running OPA tests
run: |
python cvat/apps/iam/rules/tests/generate_tests.py
docker compose run --rm -v "$PWD:/mnt/src:ro" -w /mnt/src \
cvat_opa test cvat/apps/*/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 cvat_server cvat_db
max_tries=12
while [[ $(curl -s -o /dev/null -w "%{http_code}" localhost:8181/health?bundles) != "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 --env PYTHONDEVMODE=1 cvat_ci /bin/bash \
-c 'python manage.py test cvat/apps -v 2'
- name: Creating a log file from cvat containers
if: failure()
env:
LOGS_DIR: "${{ github.workspace }}/unit_testing"
run: |
mkdir $LOGS_DIR
docker logs cvat_server > $LOGS_DIR/cvat_server.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@v7
with:
name: unit_tests_container_logs
path: "${{ github.workspace }}/unit_testing"
e2e_testing:
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
specs: ['actions_tasks', 'actions_tasks2', 'actions_tasks3', 'actions_tasks4',
'actions_objects', 'actions_objects2', 'actions_users',
'actions_projects_models', 'canvas3d_functionality',
'issues_prs', 'issues_prs2', 'features', 'features2',
'audio_workspace']
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref }}
- uses: actions/setup-node@v6
with:
node-version: '22.x'
- name: Download CVAT server image
uses: actions/download-artifact@v8
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Download CVAT UI image
uses: actions/download-artifact@v8
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.minio.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 != "200" && 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_server /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
cd ./tests
corepack enable yarn
yarn --immutable
if [[ ${{ matrix.specs }} == canvas3d_* ]]; then
npx cypress run \
--headed \
--browser chrome \
--env coverage=false \
--config-file cypress_canvas3d.config.js \
--spec 'cypress/e2e/setup/setup_canvas3d.js,cypress/e2e/canvas3d_*/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
elif [[ ${{ matrix.specs }} == audio_* ]]; then
npx cypress run \
--headed \
--browser chrome \
--env coverage=false \
--config-file cypress_audio.config.js \
--spec 'cypress/e2e/setup/setup_audio.js,cypress/e2e/audio_workspace/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
else
npx cypress run \
--browser chrome \
--env coverage=false \
--spec 'cypress/e2e/setup/setup.js,cypress/e2e/setup/setup_project.js,cypress/e2e/${{ matrix.specs }}/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
fi
- name: Creating a log file from "cvat" container logs
if: failure()
run: |
docker logs cvat_server > ${{ github.workspace }}/tests/cvat_${{ matrix.specs }}.log
- name: Uploading "cvat" container logs as an artifact
if: failure()
uses: actions/upload-artifact@v7
with:
name: e2e_container_logs_${{ matrix.specs }}
path: ${{ github.workspace }}/tests/cvat_${{ matrix.specs }}.log
- name: Uploading cypress screenshots as an artifact
if: failure()
uses: actions/upload-artifact@v7
with:
name: cypress_screenshots_${{ matrix.specs }}
path: ${{ github.workspace }}/tests/cypress/screenshots
@@ -0,0 +1,68 @@
name: Generate Allure Report
on:
workflow_call:
secrets:
AWS_ALLURE_REPORTS_ROLE:
required: true
env:
S3_BUCKET: cvat-allure-reports
jobs:
generate_report:
name: Generate Allure Report and Upload to S3
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Install Allure CLI (.deb)
run: |
sudo apt-get update
sudo apt-get install -y default-jre-headless
wget https://github.com/allure-framework/allure2/releases/download/2.34.0/allure_2.34.0-1_all.deb
sudo dpkg -i allure_2.34.0-1_all.deb
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_ALLURE_REPORTS_ROLE }}
aws-region: eu-west-1
- name: Set a timestamp
id: timestampid
run: |
echo "timestamp=$(date --utc +%Y%m%d_%H%M%SZ)" >> "$GITHUB_OUTPUT"
- name: Download Allure results from artifacts
uses: actions/download-artifact@v8
with:
pattern: allure-results*
merge-multiple: true
path: merged-allure-results
- name: Download previous history from S3
run: aws s3 cp s3://${{ env.S3_BUCKET }}/history/ ./merged-allure-results/history --recursive || echo "No history found"
- name: Generate Allure Report
run: allure generate ./merged-allure-results --clean -o allure-report
- name: Backup updated history to S3
run: aws s3 cp ./allure-report/history s3://${{ env.S3_BUCKET }}/history/ --recursive
- name: Deploy report to S3 (timestamped folder)
run: aws s3 cp ./allure-report s3://${{ env.S3_BUCKET }}/report/${{ steps.timestampid.outputs.timestamp }}/ --recursive
- name: Update latest report alias
run: |
aws s3 rm s3://${{ env.S3_BUCKET }}/report/latest/ --recursive || true
aws s3 cp ./allure-report s3://${{ env.S3_BUCKET }}/report/latest/ --recursive
- name: Write report URL to summary
run: |
echo "### 🧪 [Allure Report](http://${{ env.S3_BUCKET }}.s3-website.eu-west-1.amazonaws.com/report/${{ steps.timestampid.outputs.timestamp }}/index.html)" >> $GITHUB_STEP_SUMMARY
+189
View File
@@ -0,0 +1,189 @@
name: linters
on: pull_request
jobs:
bandit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Run checks
run: |
pipx install $(grep "^bandit" ./dev/requirements.txt)
echo "Bandit version: "$(bandit --version | head -1)
bandit -a file --ini .bandit --recursive .
black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Run checks
run: |
pipx install $(grep "^black" ./dev/requirements.txt)
echo "Black version: $(black --version)"
black --check --diff .
hadolint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Run checks
env:
HADOLINT: '${{ github.workspace }}/hadolint'
HADOLINT_VER: 2.12.0
VERIFICATION_LEVEL: error
run: |
curl -sLo "$HADOLINT" "https://github.com/hadolint/hadolint/releases/download/v$HADOLINT_VER/hadolint-Linux-x86_64"
chmod +x "$HADOLINT"
echo "hadolint version: $("$HADOLINT" --version)"
git ls-files -z 'Dockerfile*' '*/Dockerfile*' | xargs -0 "$HADOLINT" -t "$VERIFICATION_LEVEL"
isort:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Run checks
run: |
pipx install $(grep "^isort" ./dev/requirements.txt)
echo "isort version: $(isort --version-number)"
isort --check --diff --resolve-all-configs .
pylint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- id: setup-pipx
shell: bash
run: |
# custom cached dir
cache_home_dir="$HOME/.local/pipx"
cache_bin_dir="$HOME/.local/pipx_bin"
# add cached dir to PATH
echo "$cache_bin_dir" >> $GITHUB_PATH
echo "cache_home_dir=$cache_home_dir" >> $GITHUB_OUTPUT
echo "cache_bin_dir=$cache_bin_dir" >> $GITHUB_OUTPUT
echo "version=$(pipx --version)" >> $GITHUB_OUTPUT
echo "python_version=$(python3 --version)" >> $GITHUB_OUTPUT
- id: cache-pipx
uses: actions/cache@v6
with:
path: |
${{ steps.setup-pipx.outputs.cache_home_dir }}
${{ steps.setup-pipx.outputs.cache_bin_dir }}
# cache based on pipx version, python version, and both requirements files
# nb! we need to keep python version in the key because packages might be not compatible between python versions
key: pipx-pylint-${{ steps.setup-pipx.outputs.version}}-${{ steps.setup-pipx.outputs.python_version}}-${{ hashFiles('./dev/requirements.txt', './cvat/requirements/base.txt') }}
- name: Install pylint
if: steps.cache-pipx.outputs.cache-hit != 'true'
shell: bash
env:
# nb! install in our custom cached dir, using /opt will not work because of permission issues
PIPX_HOME: ${{ steps.setup-pipx.outputs.cache_home_dir }}
PIPX_BIN_DIR: ${{ steps.setup-pipx.outputs.cache_bin_dir }}
run: |
pipx install $(grep "^pylint==" ./dev/requirements.txt)
pipx inject pylint \
$(grep "^pylint-.\+==" ./dev/requirements.txt) \
$(grep "^django==" ./cvat/requirements/base.txt)
- name: Run checks
run: |
echo "Pylint version: "$(pylint --version | head -1)
pylint -j0 .
regal:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup Regal
uses: open-policy-agent/setup-regal@761188c3b435761fa254beca508a44875619648f # v2.0.0
with:
version: v0.21.3
- run: regal lint --format=github cvat/apps/*/rules
spellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Run checks
run: |
pipx install $(grep "^typos==" ./dev/requirements.txt)
echo "Typos version: $(typos --version )"
typos
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-yarn
with:
node-version: 22.x
node-modules-path: |
node_modules
tests/node_modules
install-command: |
yarn install --immutable
(cd tests && yarn install --immutable)
- name: Run checks
run: |
echo "ESLint version: "$(yarn run eslint --version)
yarn run eslint .
typescript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-yarn
with:
node-version: 22.x
- name: Run TypeScript checks for cvat frontend code
run: yarn run type-check
remark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-yarn
with:
node-version: 22.x
- name: Run checks
run: |
echo "Remark version: "`npx remark --version`
npx remark --quiet --frail -i .remarkignore .
stylelint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-yarn
with:
node-version: 22.x
- name: Run checks
run: |
echo "StyleLint version: "$(yarn run stylelint --version)
yarn run stylelint '**/*.css' '**/*.scss'
+625
View File
@@ -0,0 +1,625 @@
name: CI
on:
push:
branches:
- 'master'
- 'develop'
pull_request:
types: [ready_for_review, opened, synchronize, reopened]
paths-ignore:
- 'site/**'
- '**/*.md'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CYPRESS_VERIFY_TIMEOUT: 180000 # https://docs.cypress.io/guides/guides/command-line#cypress-verify
CVAT_VERSION: "local"
jobs:
search_cache:
if: |
github.event.pull_request.draft == false &&
!startsWith(github.event.pull_request.title, '[WIP]') &&
!startsWith(github.event.pull_request.title, '[Dependent]')
uses: ./.github/workflows/search-cache.yml
build:
needs: search_cache
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Verify version consistency
run: ./dev/update_version.py --verify-current
- name: Check changelog fragments
run: ./dev/check_changelog_fragments.py
- name: CVAT server. Getting cache from the default branch
uses: actions/cache@v6
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@v6
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@v4
- name: Create artifact directories
run: |
mkdir /tmp/cvat_server
mkdir /tmp/cvat_ui
mkdir /tmp/cvat_sdk
- name: CVAT server. Build and push
uses: docker/build-push-action@v7
with:
build-args: |
"COVERAGE_PROCESS_START=.coveragerc"
cache-from: type=local,src=/tmp/cvat_cache_server
context: .
file: Dockerfile
tags: cvat/server:${{ env.CVAT_VERSION }}
outputs: type=docker,dest=/tmp/cvat_server/image.tar
- name: Instrumentation of the code then rebuilding the CVAT UI
run: |
corepack enable yarn
yarn --immutable
yarn run coverage
- name: CVAT UI. Build and push
uses: docker/build-push-action@v7
with:
cache-from: type=local,src=/tmp/cvat_cache_ui
context: .
file: Dockerfile.ui
tags: cvat/ui:${{ env.CVAT_VERSION }}
outputs: type=docker,dest=/tmp/cvat_ui/image.tar
- name: CVAT SDK. Build
run: |
pip3 install --user -r cvat-sdk/gen/requirements.txt
./cvat-sdk/gen/generate.sh
cp -r cvat-sdk/* /tmp/cvat_sdk/
- name: Verify API schema
id: verify_schema
run: |
docker load --input /tmp/cvat_server/image.tar
docker run --rm "cvat/server:${CVAT_VERSION}" bash \
-c 'python manage.py spectacular' > cvat/schema-expected.yml
if ! git diff --no-index cvat/schema.yml cvat/schema-expected.yml; then
echo
echo 'API schema has changed! Please update cvat/schema.yml:'
echo
echo ' docker run --rm cvat/server:dev bash \'
echo " -c 'python manage.py spectacular' > cvat/schema.yml"
exit 1
fi
- name: Verify migrations
run: |
docker run --rm "cvat/server:${CVAT_VERSION}" bash \
-c 'python manage.py makemigrations --check'
- name: Verify redis migration filenames
run: |
docker run --rm "cvat/server:${CVAT_VERSION}" bash \
-c 'python manage.py checkredismigrations'
- name: Upload CVAT server artifact
uses: actions/upload-artifact@v7
with:
name: cvat_server
path: /tmp/cvat_server/image.tar
- name: Upload CVAT UI artifact
uses: actions/upload-artifact@v7
with:
name: cvat_ui
path: /tmp/cvat_ui/image.tar
- name: Upload CVAT SDK artifact
uses: actions/upload-artifact@v7
with:
name: cvat_sdk
path: /tmp/cvat_sdk/
rest_api_testing:
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- pattern: "test_task_data"
allow_cache: 'true'
- pattern: "test_task_data"
allow_cache: 'no'
- pattern: "not test_task_data"
allow_cache: 'no'
- pattern: "not test_task_data"
allow_cache: 'true'
name: rest_api_testing [${{ matrix.pattern }}, cache-${{ matrix.allow_cache }}]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: '3.10'
- name: Download CVAT server image
uses: actions/download-artifact@v8
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Download CVAT UI images
uses: actions/download-artifact@v8
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: Generate SDK
run: |
pip3 install -r cvat-sdk/gen/requirements.txt
./cvat-sdk/gen/generate.sh
- name: Install SDK
run: |
pip3 install -r ./tests/python/requirements.txt \
-e './cvat-sdk[masks,pytorch]' -e ./cvat-cli \
--extra-index-url https://download.pytorch.org/whl/cpu
- name: Run REST API and SDK tests
id: run_tests
env:
COVERAGE_PROCESS_START: ".coveragerc"
CVAT_ALLOW_STATIC_CACHE: ${{ matrix.allow_cache }}
run: |
pytest tests/python/ -k "${{ matrix.pattern }}" --cov --cov-report=json --alluredir=tests/python/allure-results
for COVERAGE_FILE in `find -name "coverage*.json" -type f -printf "%f\n"`; do mv ${COVERAGE_FILE} "${COVERAGE_FILE%%.*}_0.json"; done
- name: Uploading code coverage results as an artifact
uses: actions/upload-artifact@v7
with:
name: ${{format(
'coverage_results_rest_api-{0}-cache-{1}',
matrix.pattern, matrix.allow_cache
)}}
path: |
coverage*.json
- name: Creating a log file from cvat containers
if: failure() && steps.run_tests.conclusion == 'failure'
env:
LOGS_DIR: "${{ github.workspace }}/rest_api_testing"
run: |
mkdir $LOGS_DIR
docker logs test_cvat_server_1 > $LOGS_DIR/cvat_server.log
docker logs test_cvat_worker_export_1 > $LOGS_DIR/cvat_worker_export.log
docker logs test_cvat_worker_import_1 > $LOGS_DIR/cvat_worker_import.log
docker logs test_cvat_worker_annotation_1 > $LOGS_DIR/cvat_worker_annotation.log
docker logs test_cvat_opa_1 2> $LOGS_DIR/cvat_opa.log
# all remaining workers (any one can fail)
docker logs test_cvat_worker_utils_1 > $LOGS_DIR/cvat_worker_utils.log
docker logs test_cvat_worker_annotation_1 > $LOGS_DIR/cvat_worker_annotation.log
docker logs test_cvat_worker_quality_reports_1 > $LOGS_DIR/cvat_worker_quality_reports.log
docker logs test_cvat_worker_consensus_1 > $LOGS_DIR/cvat_worker_consensus.log
docker logs test_cvat_worker_webhooks_1 > $LOGS_DIR/cvat_worker_webhooks.log
docker logs test_cvat_worker_export_1 > $LOGS_DIR/cvat_worker_export.log
docker logs test_cvat_redis_inmem_1 > $LOGS_DIR/cvat_redis_inmem.log
docker logs test_cvat_redis_ondisk_1 > $LOGS_DIR/cvat_redis_ondisk.log
- name: Creating a log file from cvat containers
if: failure() && steps.run_tests.conclusion == 'failure'
uses: actions/upload-artifact@v7
with:
name: ${{format(
'rest_api_container_logs-{0}-cache-{1}',
matrix.pattern, matrix.allow_cache
)}}
path: "${{ github.workspace }}/rest_api_testing"
- name: Upload allure results
if: always()
uses: actions/upload-artifact@v7
with:
name: ${{format(
'allure-results-tests-api-{0}-cache-{1}',
matrix.pattern,matrix.allow_cache
)}}
path: tests/python/allure-results
unit_testing:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Download CVAT server image
uses: actions/download-artifact@v8
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Load Docker server image
run: |
docker load --input /tmp/cvat_server/image.tar
docker image ls -a
- name: Running OPA tests
run: |
python cvat/apps/iam/rules/tests/generate_tests.py
docker compose run --rm -v "$PWD:/mnt/src:ro" -w /mnt/src \
cvat_opa test cvat/apps/*/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 cvat_server cvat_db
max_tries=12
while [[ $(curl -s -o /dev/null -w "%{http_code}" localhost:8181/health?bundles) != "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 --env PYTHONDEVMODE=1 cvat_ci /bin/bash \
-c 'coverage run -a manage.py test -v 2 cvat/apps && coverage json && mv coverage.json ${CONTAINER_COVERAGE_DATA_DIR}/unit_tests_coverage.json'
- name: Uploading code coverage results as an artifact
uses: actions/upload-artifact@v7
with:
name: coverage_results_unit_tests
path: |
${{ github.workspace }}/coverage-final.json
${{ github.workspace }}/unit_tests_coverage.json
- name: Creating a log file from cvat containers
if: failure()
env:
LOGS_DIR: "${{ github.workspace }}/unit_testing"
run: |
mkdir $LOGS_DIR
docker logs cvat_server > $LOGS_DIR/cvat_server.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@v7
with:
name: unit_tests_container_logs
path: "${{ github.workspace }}/unit_testing"
e2e_testing:
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
specs: ['actions_tasks', 'actions_tasks2', 'actions_tasks3', 'actions_tasks4',
'actions_objects', 'actions_objects2', 'actions_users',
'actions_projects_models', 'canvas3d_functionality',
'issues_prs', 'issues_prs2', 'features', 'features2',
'audio_workspace']
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: '22.x'
# NOTE: corepack is not bundled after Node 25
# https://nodejs.org/docs/v22.18.0/api/corepack.html#corepack
- name: Download CVAT server image
uses: actions/download-artifact@v8
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Download CVAT UI image
uses: actions/download-artifact@v8
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.minio.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 != "200" && 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_server /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
cd ./tests
corepack enable yarn
yarn --immutable
if [[ ${{ matrix.specs }} == canvas3d_* ]]; then
npx cypress run \
--headed \
--browser chrome \
--config-file cypress_canvas3d.config.js \
--spec 'cypress/e2e/setup/setup_canvas3d.js,cypress/e2e/canvas3d_*/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
elif [[ ${{ matrix.specs }} == audio_* ]]; then
npx cypress run \
--headed \
--browser chrome \
--config-file cypress_audio.config.js \
--spec 'cypress/e2e/setup/setup_audio.js,cypress/e2e/audio_workspace/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
else
npx cypress run \
--browser chrome \
--spec 'cypress/e2e/setup/setup.js,cypress/e2e/setup/setup_project.js,cypress/e2e/${{ matrix.specs }}/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
fi
mv coverage/coverage-final.json coverage/${{ matrix.specs }}_coverage.json
- name: Uploading code coverage results as an artifact
uses: actions/upload-artifact@v7
with:
name: coverage_results_e2e_${{ matrix.specs }}
path: |
tests/coverage/${{ matrix.specs }}_coverage.json
- name: Creating a log file from "cvat" container logs
if: failure()
run: |
docker logs cvat_server > ${{ github.workspace }}/tests/cvat_${{ matrix.specs }}.log
docker logs cvat_worker_export > ${{ github.workspace }}/tests/cvat_worker_export_${{ matrix.specs }}.log
docker logs cvat_worker_import > ${{ github.workspace }}/tests/cvat_worker_import_${{ matrix.specs }}.log
- name: Uploading "cvat" container logs as an artifact
if: failure()
uses: actions/upload-artifact@v7
with:
name: e2e_container_logs_${{ matrix.specs }}
path: ${{ github.workspace }}/tests/cvat_${{ matrix.specs }}.log
- name: Uploading cypress screenshots as an artifact
if: failure()
uses: actions/upload-artifact@v7
with:
name: cypress_screenshots_${{ matrix.specs }}
path: ${{ github.workspace }}/tests/cypress/screenshots
- name: Uploading cypress videos as an artifact
if: failure()
uses: actions/upload-artifact@v7
with:
name: cypress_videos_${{ matrix.specs }}
path: ${{ github.workspace }}/tests/cypress/videos
- name: Upload Allure results as an artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: allure-results-e2e-${{ matrix.specs }}
path: tests/allure-results-e2e
helm_rest_api_testing:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Start minikube
uses: medyagh/setup-minikube@latest
with:
cpus: max
memory: max
- name: Try the cluster
run: kubectl get pods -A
- name: Download CVAT server image
uses: actions/download-artifact@v8
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Download CVAT UI images
uses: actions/download-artifact@v8
with:
name: cvat_ui
path: /tmp/cvat_ui/
- name: Load images
run: |
eval $(minikube -p minikube docker-env)
docker load --input /tmp/cvat_server/image.tar
docker load --input /tmp/cvat_ui/image.tar
docker image ls -a
- uses: azure/setup-helm@v5
- name: Update Helm chart dependencies
working-directory: helm-chart
run: |
helm dependency update
- name: Deploy to minikube
run: |
printf " service:\n externalIPs:\n - $(minikube ip)\n" >> helm-chart/test.values.yaml
helm upgrade release-${{ github.run_id }}-${{ github.run_attempt }} --install helm-chart \
-f helm-chart/cvat.values.yaml \
-f helm-chart/test.values.yaml \
--set cvat.backend.tag=${{ env.CVAT_VERSION }} \
--set cvat.frontend.tag=${{ env.CVAT_VERSION }}
- name: Update test config
run: |
sed -i -e 's$http://localhost:8080$http://cvat.local:80$g' tests/python/shared/utils/config.py
find tests/python/shared/assets/ -type f -name '*.json' | xargs sed -i -e 's$http://localhost:8080$http://cvat.local$g'
echo "$(minikube ip) cvat.local" | sudo tee -a /etc/hosts
- name: Generate SDK
run: |
pip3 install --user -r cvat-sdk/gen/requirements.txt
./cvat-sdk/gen/generate.sh
- name: Install test requirements
run: |
pip3 install --user cvat-sdk/ cvat-cli/ -r tests/python/requirements.txt
- name: Wait for CVAT to be ready
run: |
max_tries=60
while [[ $(kubectl get pods -l component=server -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}') != "True" && max_tries -gt 0 ]]; do echo "waiting for CVAT pod" && (( max_tries-- )) && sleep 5; done
while [[ $(kubectl get pods -l app.kubernetes.io/name=postgresql -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}') != "True" && max_tries -gt 0 ]]; do echo "waiting for DB pod" && (( max_tries-- )) && sleep 5; done
while [[ $(curl -s -o /tmp/server_response -w "%{http_code}" cvat.local/api/server/about) != "200" && max_tries -gt 0 ]]; do echo "waiting for CVAT" && (( max_tries-- )) && sleep 5; done
kubectl get pods
kubectl logs $(kubectl get pods -l component=server -o jsonpath='{.items[0].metadata.name}')
- name: REST API and SDK tests
# We don't have external services in Helm tests, so we ignore corresponding cases
# They are still tested without Helm
run: |
kubectl cp tests/mounted_file_share $(kubectl get pods -l component=server -o jsonpath='{.items[0].metadata.name}'):/home/django/share/
kubectl exec $(kubectl get pods -l component=server -o jsonpath='{.items[0].metadata.name}') -- bash -c "mv /home/django/share/mounted_file_share/* /home/django/share/ && rm -r /home/django/share/mounted_file_share"
pytest --timeout 30 --platform=kube -m "not with_external_services" tests/python --log-level INFO
- name: Creating a log file from "cvat" container logs
if: failure()
env:
LOGS_DIR: "${{ github.workspace }}/rest_api_testing"
run: |
mkdir ${LOGS_DIR}
for backend_pod in $(kubectl get pods -l tier=backend -o 'jsonpath={.items[*].metadata.name}'); do
kubectl logs ${backend_pod} >${LOGS_DIR}/${backend_pod}.log
done
kubectl logs $(kubectl get pods -l app.kubernetes.io/name=traefik -o 'jsonpath={.items[0].metadata.name}') >${LOGS_DIR}/traefik.log
- name: Uploading "cvat" container logs as an artifact
if: failure()
uses: actions/upload-artifact@v7
with:
name: helm_rest_api_container_logs
path: "${{ github.workspace }}/rest_api_testing"
publish_dev_images:
if: github.ref == 'refs/heads/develop'
needs: [rest_api_testing, unit_testing, e2e_testing]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Download CVAT server images
uses: actions/download-artifact@v8
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Download CVAT UI images
uses: actions/download-artifact@v8
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
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push to Docker Hub
env:
SERVER_IMAGE_REPO: ${{ secrets.DOCKERHUB_WORKSPACE }}/server
UI_IMAGE_REPO: ${{ secrets.DOCKERHUB_WORKSPACE }}/ui
run: |
docker tag "cvat/server:${CVAT_VERSION}" "${SERVER_IMAGE_REPO}:dev"
docker push "${SERVER_IMAGE_REPO}:dev"
docker tag "cvat/ui:${CVAT_VERSION}" "${UI_IMAGE_REPO}:dev"
docker push "${UI_IMAGE_REPO}:dev"
generate_report:
name: Generate Allure Report and Upload to S3
needs: [rest_api_testing, e2e_testing]
if: github.event_name == 'push' && github.ref == 'refs/heads/develop' && github.repository == 'cvat.ai/cvat'
permissions:
id-token: write
contents: read
uses: ./.github/workflows/generate-allure-report.yml
secrets:
AWS_ALLURE_REPORTS_ROLE: ${{ secrets.AWS_ALLURE_REPORTS_ROLE }}
codecov:
runs-on: ubuntu-latest
needs: [unit_testing, e2e_testing, rest_api_testing]
steps:
- uses: actions/checkout@v7
- name: Merge coverage artifacts
uses: actions/upload-artifact/merge@v7
with:
name: coverage_results
pattern: coverage_results_*
delete-merged: true
- name: Downloading coverage results
uses: actions/download-artifact@v8
with:
name: coverage_results
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
+76
View File
@@ -0,0 +1,76 @@
name: Prepare release
on:
workflow_dispatch:
inputs:
newVersion:
description: "Version number for the new release"
required: true
default: X.Y.Z
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Validate version number
env:
NEW_VERSION: "${{ inputs.newVersion }}"
run: |
if ! [[ "$NEW_VERSION" =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
echo "Invalid version number"
exit 1
fi
# When you use the default github.token to make changes in the repository,
# it does not trigger further GitHub pipelines. We want to trigger CI for
# the pull request, so we have to use an app token.
- name: Generate authentication token
id: gen-token
uses: actions/create-github-app-token@v3
with:
client-id: "${{ secrets.CVAT_BOT_CLIENT_ID }}"
private-key: "${{ secrets.CVAT_BOT_PRIVATE_KEY }}"
- name: Install dependencies
run:
sudo apt-get install -y pandoc
- uses: actions/checkout@v7
- name: Verify that the release is new
run: |
if git ls-remote --exit-code origin refs/tags/v${{ inputs.newVersion }} > /dev/null; then
echo "Release v${{ inputs.newVersion }} already exists"
exit 1
fi
- name: Create release branch
run:
git checkout -b "release-${{ inputs.newVersion }}"
- name: Collect changelog
run: |
if ls changelog.d/*.md >/dev/null 2>&1; then
pipx run scriv collect --version='${{ inputs.newVersion }}'
fi
- name: Set the new version
run:
./dev/update_version.py --set="${{ inputs.newVersion }}"
- name: Commit release preparation changes
run: |
git -c user.name='cvat-bot[bot]' -c user.email='147643061+cvat-bot[bot]@users.noreply.github.com' \
commit -a -m "Prepare release v${{ inputs.newVersion }}"
- name: Push release branch
run:
git push -u origin "release-${{ inputs.newVersion }}"
- name: Create release pull request
env:
GH_TOKEN: "${{ steps.gen-token.outputs.token }}"
run: |
./dev/gh_release_notes.sh \
| gh pr create \
--base=master \
--title="Release v${{ inputs.newVersion }}" \
--body-file=-
+84
View File
@@ -0,0 +1,84 @@
name: Publish artifacts
on:
release:
types: [released]
jobs:
docker-images:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Build images
run: |
CVAT_VERSION=latest CLAM_AV=yes docker compose -f docker-compose.yml -f docker-compose.dev.yml build
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push to Docker Hub
env:
DOCKERHUB_WORKSPACE: ${{ secrets.DOCKERHUB_WORKSPACE }}
SERVER_IMAGE_REPO: 'server'
UI_IMAGE_REPO: 'ui'
run: |
docker tag "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:latest" "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:${{ github.event.release.tag_name }}"
docker push "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:${{ github.event.release.tag_name }}"
docker push "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:latest"
docker tag "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:latest" "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:${{ github.event.release.tag_name }}"
docker push "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:${{ github.event.release.tag_name }}"
docker push "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:latest"
python-packages:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v7
- name: Generate SDK
run: |
pip3 install --user -r cvat-sdk/gen/requirements.txt
./cvat-sdk/gen/generate.sh
- name: Build packages
run: |
for d in cvat-sdk cvat-cli; do
pipx run --spec=build pyproject-build --outdir=dist "$d"
done
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
helm:
runs-on: ubuntu-latest
needs: docker-images
env:
DOCKERHUB_WORKSPACE: ${{ secrets.DOCKERHUB_WORKSPACE }}
steps:
- uses: actions/checkout@v7
- name: Set up Helm
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
with:
version: v3.19.0
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Package Helm chart
working-directory: helm-chart
run: |
helm package . --dependency-update --destination ../
- name: Push Helm chart to Docker Hub
run: |
helm push cvat-*.tgz oci://registry-1.docker.io/${DOCKERHUB_WORKSPACE}
+317
View File
@@ -0,0 +1,317 @@
name: CI-nightly
on:
schedule:
- cron: '0 22 * * *'
workflow_dispatch:
env:
CYPRESS_VERIFY_TIMEOUT: 180000 # https://docs.cypress.io/guides/guides/command-line#cypress-verify
CVAT_VERSION: "local"
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 "last_commit_time=${last_commit_time}" >> $GITHUB_OUTPUT
echo "last_night_time=${last_night_time}" >> $GITHUB_OUTPUT
search_cache:
needs: check_updates
uses: ./.github/workflows/search-cache.yml
build:
needs: search_cache
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: CVAT server. Getting cache from the default branch
uses: actions/cache@v6
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@v6
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@v4
- name: Create artifact directories
run: |
mkdir /tmp/cvat_server
mkdir /tmp/cvat_ui
mkdir /tmp/cvat_sdk
- name: CVAT server. Build and push
uses: docker/build-push-action@v7
with:
cache-from: type=local,src=/tmp/cvat_cache_server
context: .
file: Dockerfile
tags: cvat/server:${{ env.CVAT_VERSION }}
outputs: type=docker,dest=/tmp/cvat_server/image.tar
- name: CVAT UI. Build and push
uses: docker/build-push-action@v7
with:
cache-from: type=local,src=/tmp/cvat_cache_ui
context: .
file: Dockerfile.ui
tags: cvat/ui:${{ env.CVAT_VERSION }}
outputs: type=docker,dest=/tmp/cvat_ui/image.tar
- name: Upload CVAT server artifact
uses: actions/upload-artifact@v7
with:
name: cvat_server
path: /tmp/cvat_server/image.tar
- name: Upload CVAT UI artifact
uses: actions/upload-artifact@v7
with:
name: cvat_ui
path: /tmp/cvat_ui/image.tar
unit_testing:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: '3.10'
- name: Download CVAT server image
uses: actions/download-artifact@v8
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Download CVAT UI images
uses: actions/download-artifact@v8
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: OPA tests
run: |
python cvat/apps/iam/rules/tests/generate_tests.py
docker compose run --rm -v "$PWD:/mnt/src:ro" -w /mnt/src \
cvat_opa test cvat/apps/*/rules
- name: Generate SDK
run: |
pip3 install -r cvat-sdk/gen/requirements.txt
./cvat-sdk/gen/generate.sh
- name: Install test requirements
run: |
pip3 install -e ./cvat-sdk -e ./cvat-cli -r ./tests/python/requirements.txt
- name: REST API and SDK tests
run: |
echo 'Run 1: without cache'
pytest tests/python/ --alluredir=tests/python/allure-results
pytest tests/python/ --stop-services
echo 'Run 2: with cache'
CVAT_ALLOW_STATIC_CACHE="true" pytest tests/python
pytest tests/python/ --stop-services
- name: Upload allure results for REST API tests
if: always()
uses: actions/upload-artifact@v7
with:
name: allure-results-rest-api
path: tests/python/allure-results
- 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 cvat_server cvat_db
max_tries=12
while [[ $(curl -s -o /dev/null -w "%{http_code}" localhost:8181/health?bundles) != "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 --env PYTHONDEVMODE=1 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 down -v
e2e_testing:
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
specs: ['actions_tasks', 'actions_tasks2', 'actions_tasks3', 'actions_tasks4',
'actions_objects', 'actions_objects2', 'actions_users',
'actions_projects_models', 'canvas3d_functionality',
'issues_prs', 'issues_prs2', 'features']
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: '22.x'
- name: Download CVAT server image
uses: actions/download-artifact@v8
with:
name: cvat_server
path: /tmp/cvat_server/
- name: Download CVAT UI image
uses: actions/download-artifact@v8
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 tests/docker-compose.file_share.yml \
-f tests/docker-compose.minio.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 != "200" && 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 != "200" ]]; then
echo Response from server is incorrect, output:
cat /tmp/server_response
fi
echo "status_code=${status_code}" >> $GITHUB_OUTPUT
- name: Fail on bad response from server
if: steps.wait-server.outputs.status_code != '200'
uses: actions/github-script@v9
with:
script: |
core.setFailed('Workflow failed: incorrect response from server. See logs artifact to get more info')
- name: Add user for tests
env:
DJANGO_SU_NAME: "admin"
DJANGO_SU_EMAIL: "admin@localhost.company"
DJANGO_SU_PASSWORD: "12qwaszx"
run: |
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"
- name: Run tests
run: |
cd ./tests
corepack enable yarn
yarn --immutable
shopt -s extglob
if [[ ${{ matrix.specs }} == canvas3d_* ]]; then
npx cypress run \
--headed \
--browser chrome \
--config-file cypress_canvas3d.config.js \
--spec 'cypress/e2e/setup/setup_canvas3d.js,cypress/e2e/canvas3d_*/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
else
npx cypress run \
--browser chrome \
--spec 'cypress/e2e/setup/setup.js,cypress/e2e/setup/setup_project.js,cypress/e2e/${{ matrix.specs }}/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
fi
- name: Creating a log file from "cvat" container logs
if: failure()
run: |
docker logs cvat_server > ${{ github.workspace }}/tests/cvat.log
- name: Uploading cypress screenshots as an artifact
if: failure()
uses: actions/upload-artifact@v7
with:
name: cypress_screenshots_${{ matrix.specs }}
path: ${{ github.workspace }}/tests/cypress/screenshots
- name: Uploading cypress videos as an artifact
if: failure()
uses: actions/upload-artifact@v7
with:
name: cypress_videos_${{ matrix.specs }}
path: ${{ github.workspace }}/tests/cypress/videos
- name: Uploading "cvat" container logs as an artifact
if: failure()
uses: actions/upload-artifact@v7
with:
name: cvat_container_logs_${{ matrix.specs }}
path: ${{ github.workspace }}/tests/cvat.log
- name: Upload Allure results as an artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: allure-results-e2e-${{ matrix.specs }}
path: tests/allure-results-e2e
generate_report:
name: Generate Allure Report and Upload to S3
needs: [unit_testing, e2e_testing]
if: always()
uses: ./.github/workflows/generate-allure-report.yml
secrets:
AWS_ALLURE_REPORTS_ROLE: ${{ secrets.AWS_ALLURE_REPORTS_ROLE }}
+40
View File
@@ -0,0 +1,40 @@
name: Search Cache
on:
workflow_call:
outputs:
sha:
value: ${{ jobs.search_cache.outputs.sha }}
# if: |
# github.event.pull_request.draft == false &&
# !startsWith(github.event.pull_request.title, '[WIP]') &&
# !startsWith(github.event.pull_request.title, '[Dependent]')
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 "sha=${SHA}" >> $GITHUB_OUTPUT
@@ -0,0 +1,30 @@
name: Trigger Agent Image Builds
on:
workflow_dispatch:
release:
types: [released]
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Generate authentication token
id: gen-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ secrets.CVAT_BOT_CLIENT_ID }}
private-key: ${{ secrets.CVAT_BOT_PRIVATE_KEY }}
repositories: "cvat-models"
- name: Trigger build in cvat-models
env:
GH_TOKEN: "${{ steps.gen-token.outputs.token }}"
run: |
curl -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/cvat-ai/cvat-models/dispatches \
-d '{"event_type":"build-agent-images","client_payload":{"tag_name":"${{ github.event.release.tag_name }}"}}'
@@ -0,0 +1,30 @@
name: Trigger dependent repo update workflow
on:
workflow_dispatch:
push:
branches: [ develop ]
jobs:
trigger:
runs-on: ubuntu-latest
steps:
- name: Generate authentication token
id: gen-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ secrets.CVAT_BOT_CLIENT_ID }}
private-key: ${{ secrets.CVAT_BOT_PRIVATE_KEY }}
repositories: ${{ secrets.CVAT_DEPENDENT_REPO }}
- name: Trigger private repository workflow
env:
GH_TOKEN: "${{ steps.gen-token.outputs.token }}"
run: |
gh api \
--method POST \
/repos/cvat-ai/${{ secrets.CVAT_DEPENDENT_REPO }}/dispatches \
-f 'event_type=upstream_updated' \
-F "client_payload[triggeredBy]=${{ github.repository }}" \
-F "client_payload[commitSha]=${{ github.sha }}" \
-F "client_payload[branch]=${{ github.ref }}"
@@ -0,0 +1,91 @@
name: Update Python requirements
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "cvat/requirements/*.in"
- "utils/dataset_manifest/requirements.in"
permissions:
contents: read
pull-requests: read
jobs:
update:
# Only run on Dependabot branches from this repository. Regenerating
# requirements can execute package build code, so fork PRs must not run this
# workflow with access to the CVAT bot token.
if: >
github.event.pull_request.user.login == 'dependabot[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository &&
startsWith(github.event.pull_request.head.ref, 'dependabot/pip/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y \
cargo \
g++ \
gcc \
libhdf5-dev \
libldap2-dev \
libmp3lame-dev \
libsasl2-dev \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
make \
nasm \
pkg-config \
python3-dev
- name: Install pip-compile-multi
run: python -m pip install pip-compile-multi
- name: Regenerate Python requirements
run: cvat/requirements/regenerate.sh --no-upgrade
- name: Check generated requirements changes
id: changes
run: |
if git diff --quiet -- cvat/requirements/*.txt utils/dataset_manifest/requirements.txt; then
echo "No generated requirements changes"
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
# The default github.token does not trigger follow-up CI on push. Use the
# same app token as the release workflow so the updated Dependabot PR runs
# the normal validation pipeline.
- name: Generate authentication token
if: steps.changes.outputs.changed == 'true'
id: gen-token
uses: actions/create-github-app-token@v3
with:
client-id: "${{ secrets.CVAT_BOT_CLIENT_ID }}"
private-key: "${{ secrets.CVAT_BOT_PRIVATE_KEY }}"
- name: Commit regenerated requirements
if: steps.changes.outputs.changed == 'true'
env:
GH_TOKEN: "${{ steps.gen-token.outputs.token }}"
run: |
git config user.name "cvat-bot[bot]"
git config user.email "147643061+cvat-bot[bot]@users.noreply.github.com"
git add cvat/requirements/*.txt utils/dataset_manifest/requirements.txt
git commit -m "Regenerate Python requirements"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
git push origin HEAD:${{ github.event.pull_request.head.ref }}
+79
View File
@@ -0,0 +1,79 @@
# Project Specific
/data/
/events/
/share/
/static/
/db.sqlite3
/keys
/logs
/profiles
# Ignore temporary files
docker-compose.override.yml
__pycache__
*.pyc
._*
.coverage
.husky/
.python-version
/tmp*cvat/
/temp*/
# Ignore generated test files
docker-compose.tests.yml
# Ignore npm logs file
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
#Ignore Cypress tests temp files
/tests/cypress/fixtures
/tests/cypress/screenshots
.idea/
#Ignore helm-related files
/helm-chart/Chart.lock
/helm-chart/values.*.yaml
/helm-chart/charts/*
#Ignore website temp files
/site/public/
/site/resources/
/site/node_modules/
/site/tech-doc-hugo
/site/.hugo_build.lock
# Ignore all the installed packages
node_modules
/*env*/
/.*env*
perfkit.egg-info/
# Ignore all js dists
cvat-data/dist
cvat-core/dist
cvat-canvas/dist
cvat-canvas3d/dist
cvat-ui/dist
# produced by prepare in the root package.json script
.husky
# produced by cvat/apps/iam/rules/tests/generate_tests.py
/cvat/apps/*/rules/*_test.gen.rego
# produced by allure report
/**/allure-report*
/**/allure-results*
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
**/.pnp.*
**/.yarn/*
**/!.yarn/patches
**/!.yarn/plugins
**/!.yarn/releases
**/!.yarn/sdks
**/!.yarn/versions
+3
View File
@@ -0,0 +1,3 @@
[submodule "site/themes/docsy"]
path = site/themes/docsy
url = https://github.com/google/docsy
+19
View File
@@ -0,0 +1,19 @@
{
"all": true,
"compact": false,
"extension": [
".js",
".jsx",
".ts",
".tsx"
],
"exclude": [
"**/3rdparty/*",
"**/tests/*",
"cvat-ui/src/actions/boundaries-actions.ts",
"cvat-ui/src/utils/platform-checker.ts"
],
"parser-plugins": [
"typescript"
]
}
+10
View File
@@ -0,0 +1,10 @@
.*/
3rdparty/
node_modules/
dist/
data/
datumaro/
keys/
logs/
static/
templates/
+27
View File
@@ -0,0 +1,27 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": true,
"printWidth": 120,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false,
"overrides": [
{
"files": ["*.json", "*.yml", "*.yaml", "*.md"],
"options": {
"tabWidth": 2
}
}
]
}
+986
View File
@@ -0,0 +1,986 @@
[MAIN]
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Load and enable all available extensions. Use --list-extensions to see a list
# all available extensions.
#enable-all-extensions=
# In error mode, messages with a category besides ERROR or FATAL are
# suppressed, and no reports are done by default. Error mode is compatible with
# disabling specific errors.
#errors-only=
# Always return a 0 (non-error) status code, even if lint errors are found.
# This is primarily useful in continuous integration scripts.
#exit-zero=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
extension-pkg-whitelist=
# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
# specified are enabled, while categories only check already-enabled messages.
fail-on=
# Specify a score threshold to be exceeded before program exits with error.
fail-under=10
# Interpret the stdin as a python script, whose filename needs to be passed as
# the module_or_package argument.
#from-stdin=
# Files or directories to be skipped. They should be base names, not paths.
ignore=CVS
# Add files or directories matching the regex patterns to the ignore-list. The
# regex matches against paths and can be in Posix or Windows format.
ignore-paths=
cvat-sdk/cvat_sdk/api_client|cvat-sdk/build|node_modules
# Files or directories matching the regex patterns are skipped. The regex
# matches against base names, not paths. The default value ignores Emacs file
# locks
ignore-patterns=
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
# avoid hangs.
jobs=0
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=pylint_django
# Pickle collected data for later comparisons.
persistent=yes
# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.10
# Discover python modules and packages in the file system subtree.
recursive=no
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# In verbose mode, extra non-checker-related info will be displayed.
#verbose=
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
# 'convention', and 'info' which contain the number of messages in each
# category, as well as 'statement' which is the total number of statements
# analyzed. This score is used by the global evaluation report (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
#output-format=
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
# UNDEFINED.
confidence=HIGH,
CONTROL_FLOW,
INFERENCE,
INFERENCE_FAILURE,
UNDEFINED
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then re-enable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=abstract-method,
arguments-out-of-order,
arguments-renamed,
assert-on-string-literal,
assigning-non-slot,
assignment-from-none,
astroid-error,
attribute-defined-outside-init,
await-outside-async,
bad-classmethod-argument,
bad-configuration-section,
bad-file-encoding,
bad-inline-option,
bad-mcs-classmethod-argument,
bad-mcs-method-argument,
bad-open-mode,
bad-plugin-value,
bad-reversed-sequence,
bad-staticmethod-argument,
bad-str-strip-call,
bad-string-format-type,
bad-thread-instantiation,
bidirectional-unicode,
boolean-datetime,
broad-exception-caught,
broad-exception-raised,
c-extension-no-member,
cell-var-from-loop,
chained-comparison,
class-variable-slots-conflict,
comparison-of-constants,
comparison-with-callable,
comparison-with-itself,
condition-evals-to-constant,
config-parse-error,
consider-iterating-dictionary,
consider-math-not-float,
consider-merging-isinstance,
consider-swap-variables,
consider-using-dict-comprehension,
consider-using-dict-items,
consider-using-f-string,
consider-using-from-import,
consider-using-generator,
consider-using-get,
consider-using-in,
consider-using-join,
consider-using-max-builtin,
consider-using-min-builtin,
consider-using-set-comprehension,
consider-using-sys-exit,
consider-using-ternary,
consider-using-with,
cyclic-import,
deprecated-argument,
deprecated-class,
deprecated-decorator,
deprecated-method,
deprecated-module,
deprecated-pragma,
dict-iter-missing-items,
disallowed-name,
django-not-available-placeholder,
django-not-available,
django-not-configured,
django-settings-module-not-found,
duplicate-code,
duplicate-string-formatting-argument,
duplicate-value,
empty-docstring,
eval-used,
f-string-without-interpolation,
fatal,
file-ignored,
fixme,
forgotten-debug-statement,
format-string-without-interpolation,
global-statement,
hard-coded-auth-user,
http-response-with-content-type-json,
http-response-with-json-dumps,
implicit-str-concat,
import-error,
import-outside-toplevel,
import-self,
imported-auth-user,
inconsistent-quotes,
inconsistent-return-statements,
invalid-all-format,
invalid-bool-returned,
invalid-bytes-returned,
invalid-character-backspace,
invalid-character-carriage-return,
invalid-character-esc,
invalid-character-nul,
invalid-character-sub,
invalid-character-zero-width-space,
invalid-characters-in-docstring,
invalid-class-object,
invalid-enum-extension,
invalid-envvar-default,
invalid-envvar-value,
invalid-format-returned,
invalid-getnewargs-ex-returned,
invalid-getnewargs-returned,
invalid-hash-returned,
invalid-index-returned,
invalid-length-hint-returned,
invalid-length-returned,
invalid-metaclass,
invalid-name,
invalid-overridden-method,
invalid-repr-returned,
invalid-str-returned,
invalid-unary-operand-type,
invalid-unicode-codec,
isinstance-second-argument-not-valid-type,
keyword-arg-before-vararg,
line-too-long,
literal-comparison,
locally-disabled,
logging-format-interpolation,
logging-fstring-interpolation,
logging-not-lazy,
method-cache-max-size-none,
method-check-failed,
misplaced-format-function,
missing-class-docstring,
missing-function-docstring,
missing-module-docstring,
missing-parentheses-for-call-in-test,
mixed-line-endings,
model-has-unicode,
model-missing-unicode,
model-no-explicit-unicode,
model-unicode-not-callable,
modelform-uses-exclude,
modified-iterating-dict,
modified-iterating-list,
modified-iterating-set,
multiple-imports,
multiple-statements,
nan-comparison,
no-else-break,
no-else-continue,
no-else-raise,
no-else-return,
no-member,
no-name-in-module,
no-self-argument,
non-ascii-file-name,
non-ascii-module-import,
non-ascii-name,
non-str-assignment-to-dunder-name,
not-a-mapping,
not-an-iterable,
not-async-context-manager,
not-context-manager,
overridden-final-method,
parse-error,
possibly-unused-variable,
possibly-used-before-assignment,
potential-index-error,
preferred-module,
property-with-parameters,
protected-access,
raise-missing-from,
raising-format-tuple,
raw-checker-failed,
redeclared-assigned-name,
redefined-argument-from-local,
redefined-outer-name,
redefined-slots-in-subclass,
redundant-content-type-for-json-response,
redundant-u-string-prefix,
redundant-unittest-assert,
relative-beyond-top-level,
return-arg-in-generator,
self-assigning-variable,
self-cls-assignment,
shallow-copy-environ,
simplifiable-condition,
simplifiable-if-expression,
simplify-boolean-expression,
single-string-used-for-slots,
stop-iteration-return,
subclassed-final-class,
subprocess-popen-preexec-fn,
subprocess-run-check,
super-init-not-called,
super-with-arguments,
super-without-brackets,
suppressed-message,
too-few-public-methods,
too-many-ancestors,
too-many-arguments,
too-many-boolean-expressions,
too-many-branches,
too-many-instance-attributes,
too-many-lines,
too-many-locals,
too-many-nested-blocks,
too-many-positional-arguments,
too-many-public-methods,
too-many-return-statements,
too-many-statements,
trailing-comma-tuple,
trailing-newlines,
try-except-raise,
typevar-double-variance,
typevar-name-incorrect-variance,
typevar-name-mismatch,
unbalanced-tuple-unpacking,
undefined-loop-variable,
unexpected-line-ending-format,
ungrouped-imports,
unhashable-member,
unknown-option-value,
unnecessary-comprehension,
unnecessary-dict-index-lookup,
unnecessary-direct-lambda-call,
unnecessary-dunder-call,
unnecessary-ellipsis,
unnecessary-lambda-assignment,
unnecessary-list-index-lookup,
unnecessary-negation,
unpacking-non-sequence,
unrecognized-inline-option,
unrecognized-option,
unspecified-encoding,
unsubscriptable-object,
unsupported-assignment-operation,
unsupported-binary-operation,
unsupported-delete-operation,
unsupported-membership-test,
unused-argument,
unused-format-string-argument,
unused-private-member,
unused-wildcard-import,
use-a-generator,
use-dict-literal,
use-implicit-booleaness-not-comparison,
use-implicit-booleaness-not-len,
use-list-literal,
use-maxsplit-arg,
use-sequence-for-iteration,
use-symbolic-message-instead,
used-prior-global-declaration,
useless-import-alias,
useless-option-value,
useless-parent-delegation,
useless-return,
useless-suppression,
useless-with-lock,
using-constant-test,
using-f-string-in-unsupported-version,
using-final-decorator-in-unsupported-version,
wrong-exception-operation,
wrong-import-order,
wrong-import-position,
wrong-spelling-in-comment,
wrong-spelling-in-docstring,
yield-inside-async-function,
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=abstract-class-instantiated,
access-member-before-definition,
anomalous-backslash-in-string,
anomalous-unicode-escape-in-string,
arguments-differ,
assert-on-tuple,
assignment-from-no-return,
async-context-manager-with-regular-with,
bad-except-order,
bad-exception-cause,
bad-format-character,
bad-format-string-key,
bad-format-string,
bad-indentation,
bad-super-call,
bare-except,
bare-name-capture-pattern,
binary-op-exception,
break-in-finally,
catching-non-exception,
confusing-with-statement,
consider-using-enumerate,
continue-in-finally,
dangerous-default-value,
duplicate-argument-name,
duplicate-bases,
duplicate-except,
duplicate-key,
exec-used,
expression-not-assigned,
format-combined-specification,
format-needs-mapping,
function-redefined,
global-at-module-level,
global-variable-not-assigned,
global-variable-undefined,
inconsistent-mro,
inherit-non-class,
init-is-generator,
invalid-all-object,
invalid-format-index,
invalid-match-args-definition,
invalid-sequence-index,
invalid-slice-index,
invalid-slots-object,
invalid-slots,
invalid-star-assignment-target,
logging-format-truncated,
logging-too-few-args,
logging-too-many-args,
logging-unsupported-format,
lost-exception,
match-class-bind-self,
match-class-positional-attributes,
method-hidden,
misplaced-bare-raise,
misplaced-future,
missing-final-newline,
missing-format-argument-key,
missing-format-attribute,
missing-format-string-key,
missing-kwoa,
mixed-format-string,
multiple-class-sub-patterns,
no-classmethod-decorator,
no-method-argument,
no-staticmethod-decorator,
no-value-for-parameter,
non-iterator-returned,
non-parent-init-called,
nonexistent-operator,
nonlocal-and-global,
nonlocal-without-binding,
not-callable,
not-in-loop,
notimplemented-raised,
pointless-statement,
pointless-string-statement,
raising-bad-type,
raising-non-exception,
redefined-builtin,
redundant-keyword-arg,
reimported,
repeated-keyword,
return-in-init,
return-outside-function,
signature-differs,
simplifiable-if-statement,
singleton-comparison,
star-needs-assignment-target,
superfluous-parens,
syntax-error,
too-few-format-args,
too-many-format-args,
too-many-function-args,
too-many-positional-sub-patterns,
too-many-star-expressions,
trailing-whitespace,
truncated-format-string,
undefined-all-variable,
undefined-variable,
unexpected-keyword-arg,
unexpected-special-method-signature,
unidiomatic-typecheck,
unnecessary-lambda,
unnecessary-pass,
unnecessary-semicolon,
unreachable,
unused-format-string-key,
unused-import,
unused-variable,
used-before-assignment,
useless-else-on-loop,
useless-object-inheritance,
wildcard-import,
yield-outside-function,
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style. If left empty, argument names will be checked with the set
# naming style.
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style. If left empty, attribute names will be checked with the set naming
# style.
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style. If left empty, class attribute names will be checked
# with the set naming style.
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE
# Regular expression matching correct class constant names. Overrides class-
# const-naming-style. If left empty, class constant names will be checked with
# the set naming style.
#class-const-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style. If left empty, class names will be checked with the set naming style.
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style. If left empty, constant names will be checked with the set naming
# style.
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style. If left empty, function names will be checked with the set
# naming style.
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style. If left empty, inline iteration names will be checked
# with the set naming style.
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style. If left empty, method names will be checked with the set naming style.
method-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style. If left empty, module names will be checked with the set naming style.
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Regular expression matching correct type variable names. If left empty, type
# variable names will be checked with the set naming style.
#typevar-rgx=
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style. If left empty, variable names will be checked with the set
# naming style.
variable-rgx=[a-z_][a-z0-9_]{2,30}$
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of names allowed to shadow builtins
allowed-redefined-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy
# Argument names that match this expression will be ignored. Default to name
# with leading underscore.
ignored-argument-names=_.*
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,future.builtins
[DESIGN]
# List of regular expressions of class ancestor names to ignore when counting
# public methods (see R0903)
exclude-too-few-public-methods=
# List of qualified class names to ignore when counting class parents (see
# R0901)
ignored-parents=
# Maximum number of arguments for function / method.
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
# Maximum number of lines in a module.
max-module-lines=1000
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
# Regular expression of note tags to take in consideration.
notes-rgx=
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. Available dictionaries: none. To make it work,
# install the 'python-enchant' package.
spelling-dict=
# List of comma separated words that should be considered directives if they
# appear at the beginning of a comment and should not be checked.
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no
[SIMILARITIES]
# Comments are removed from the similarity computation
ignore-comments=yes
# Docstrings are removed from the similarity computation
ignore-docstrings=yes
# Imports are removed from the similarity computation
ignore-imports=no
# Signatures are removed from the similarity computation
ignore-signatures=yes
# Minimum lines number of a similarity.
min-similarity-lines=4
[EXCEPTIONS]
# Exceptions that will emit a warning when caught.
overgeneral-exceptions=builtins.Exception
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=optparse
# Output a graph (.gv or any supported image format) of external dependencies
# to the given file (report RP0402 must not be disabled).
ext-import-graph=
# Output a graph (.gv or any supported image format) of all (i.e. internal and
# external) dependencies to the given file (report RP0402 must not be
# disabled).
import-graph=
# Output a graph (.gv or any supported image format) of internal dependencies
# to the given file (report RP0402 must not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Couples of modules and preferred modules, separated by a comma.
preferred-modules=
[CLASSES]
# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of symbolic message names to ignore for Mixin members.
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
# Regex pattern to define which classes are considered mixins.
mixin-class-rgx=.*[Mm]ixin
# List of decorators that change the signature of a decorated function.
signature-mutators=
[LOGGING]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[DJANGO FOREIGN KEYS REFERENCED BY STRINGS]
# A module containing Django settings to be used while linting.
#django-settings-module=
+40
View File
@@ -0,0 +1,40 @@
ignore:
files:
- "*_test.gen.rego"
rules:
custom:
naming-convention:
conventions:
- pattern: '^[a-z_]+$|^[A-Z_]+$'
targets:
- rule
- function
- variable
level: error
idiomatic:
no-defined-entrypoint:
# This would likely be the allow rule in each package
# Not critical though, so ignoring for the time being
level: ignore
style:
avoid-get-and-list-prefix:
# Mainly a style preference
# https://docs.styra.com/regal/rules/style/avoid-get-and-list-prefix
level: ignore
external-reference:
# https://docs.styra.com/regal/rules/style/external-reference
level: ignore
opa-fmt:
# https://docs.styra.com/regal/rules/style/opa-fmt
level: ignore
prefer-snake-case:
# Disabled in favor of custom naming-convention rule above
level: ignore
rule-length:
# Many rules longer than the default limit of 30 lines
level: error
max-rule-length: 60
imports:
unresolved-import:
# one of "error", "warning", "ignore"
level: error
+4
View File
@@ -0,0 +1,4 @@
cvat-sdk/docs/
cvat-sdk/README.md
.env/
site/themes/
+18
View File
@@ -0,0 +1,18 @@
exports.settings = { bullet: '*', paddedTable: false };
exports.plugins = [
'remark-frontmatter',
'remark-gfm',
'remark-preset-lint-recommended',
'remark-preset-lint-consistent',
['remark-lint-list-item-indent', 'one'],
['remark-lint-no-dead-urls', false], // Does not work because of github protection system
['remark-lint-maximum-line-length', 120],
['remark-lint-maximum-heading-length', 120],
['remark-lint-strong-marker', '*'],
['remark-lint-emphasis-marker', '_'],
['remark-lint-unordered-list-marker-style', '-'],
['remark-lint-ordered-list-marker-style', '.'],
['remark-lint-no-file-name-irregular-characters', false],
['remark-lint-list-item-spacing', false],
];
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "stylelint-config-standard-scss",
"rules": {
"scss/comment-no-empty": null,
"value-keyword-case": null,
"color-function-notation": ["legacy"],
"scss/at-extend-no-missing-placeholder": null,
"no-descending-specificity": null
},
"ignoreFiles": ["**/*.js", "**/*.ts", "**/*.py"]
}
+427
View File
@@ -0,0 +1,427 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "REST API tests: Attach to server",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "127.0.0.1",
"port": 9090
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/home/django/"
},
{
"localRoot": "${workspaceFolder}/.env",
"remoteRoot": "/opt/venv",
}
],
"justMyCode": false,
},
{
"name": "REST API tests: Attach to RQ annotation worker",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "127.0.0.1",
"port": 9091
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/home/django/"
},
{
"localRoot": "${workspaceFolder}/.env",
"remoteRoot": "/opt/venv",
}
],
"justMyCode": false,
},
{
"name": "REST API tests: Attach to RQ export worker",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "127.0.0.1",
"port": 9092
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/home/django/"
},
{
"localRoot": "${workspaceFolder}/.env",
"remoteRoot": "/opt/venv",
}
],
"justMyCode": false,
},
{
"name": "REST API tests: Attach to RQ import worker",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "127.0.0.1",
"port": 9093
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/home/django/"
},
{
"localRoot": "${workspaceFolder}/.env",
"remoteRoot": "/opt/venv",
}
],
"justMyCode": false,
},
{
"name": "REST API tests: Attach to RQ quality reports worker",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "127.0.0.1",
"port": 9094
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/home/django/"
},
{
"localRoot": "${workspaceFolder}/.env",
"remoteRoot": "/opt/venv",
}
],
"justMyCode": false,
},
{
"name": "REST API tests: Attach to RQ consensus worker",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "127.0.0.1",
"port": 9096
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/home/django/"
},
{
"localRoot": "${workspaceFolder}/.env",
"remoteRoot": "/opt/venv",
}
],
"justMyCode": false,
},
{
"type": "pwa-chrome",
"request": "launch",
"preLaunchTask": "npm: start - cvat-ui",
"name": "ui.js: debug",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/cvat-ui",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack://cvat/./*": "${workspaceFolder}/cvat-core/*",
"webpack:///./*": "${webRoot}/*",
"webpack:///src/*": "${webRoot}/*",
"webpack:///*": "*",
"webpack:///./~/*": "${webRoot}/node_modules/*"
},
"smartStep": true,
},
{
"type": "node",
"request": "launch",
"name": "ui.js: test",
"cwd": "${workspaceFolder}/tests",
"runtimeExecutable": "${workspaceFolder}/tests/node_modules/.bin/cypress",
"args": [
"run",
"--headless",
"--browser",
"chrome"
],
"outputCapture": "std",
"console": "internalConsole"
},
{
"name": "server: django",
"type": "debugpy",
"request": "launch",
"stopOnEntry": false,
"justMyCode": false,
"python": "${command:python.interpreterPath}",
"program": "${workspaceFolder}/manage.py",
"env": {
"CVAT_SERVERLESS": "1",
"ALLOWED_HOSTS": "*",
"DJANGO_LOG_SERVER_HOST": "localhost",
"DJANGO_LOG_SERVER_PORT": "8282",
},
"args": [
"runserver",
"--noreload",
"--insecure",
"127.0.0.1:7000"
],
"django": true,
"cwd": "${workspaceFolder}",
"console": "internalConsole",
},
{
"name": "server: chrome",
"type": "pwa-chrome",
"request": "launch",
"url": "http://localhost:7000/",
"disableNetworkCache": true,
"trace": true,
"showAsyncStacks": true,
"pathMapping": {
"/static/engine/": "${workspaceFolder}/cvat/apps/engine/static/engine/",
"/static/dashboard/": "${workspaceFolder}/cvat/apps/dashboard/static/dashboard/",
}
},
{
"name": "server: RQ - worker",
"type": "debugpy",
"request": "launch",
"stopOnEntry": false,
"justMyCode": false,
"python": "${command:python.interpreterPath}",
"program": "${workspaceFolder}/manage.py",
"args": [
"rqworker",
"--worker-class=cvat.rqworker.SimpleWorker",
// high priority
"chunks",
// normal priority
"annotation",
"consensus",
"export",
"import",
"quality_reports",
"webhooks",
// low priority
"cleaning",
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {
"DJANGO_LOG_SERVER_HOST": "localhost",
"DJANGO_LOG_SERVER_PORT": "8282"
},
"console": "internalConsole"
},
{
"name": "server: RQ - scheduler",
"type": "debugpy",
"request": "launch",
"stopOnEntry": false,
"justMyCode": false,
"python": "${command:python.interpreterPath}",
"program": "${workspaceFolder}/rqscheduler.py",
"django": true,
"cwd": "${workspaceFolder}",
"args": [
"-i", "1"
],
"env": {
"DJANGO_LOG_SERVER_HOST": "localhost",
"DJANGO_LOG_SERVER_PORT": "8282"
},
"console": "internalConsole"
},
{
"name": "server: migrate",
"type": "debugpy",
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"program": "${workspaceFolder}/manage.py",
"args": [
"migrate"
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {},
"console": "internalConsole"
},
{
"name": "server: sync periodic jobs",
"type": "debugpy",
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"program": "${workspaceFolder}/manage.py",
"args": [
"syncperiodicjobs"
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {},
"console": "internalConsole"
},
{
"name": "server: tests",
"type": "debugpy",
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"program": "${workspaceFolder}/manage.py",
"args": [
"test",
"--settings",
"cvat.settings.testing",
"cvat/apps",
"cvat-cli/"
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {},
"console": "internalConsole"
},
{
"name": "server: REST API tests",
"type": "debugpy",
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"module": "pytest",
"args": [
"--verbose",
"--no-cov", // vscode debugger might not work otherwise
"tests/python/rest_api/"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
},
{
"name": "sdk: tests",
"type": "debugpy",
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"module": "pytest",
"args": [
"tests/python/sdk/"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
},
{
"name": "cli: tests",
"type": "debugpy",
"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": "debugpy",
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"program": "${workspaceFolder}/cvat-sdk/gen/postprocess.py",
"args": [
"--schema", "${workspaceFolder}/cvat/schema.yml",
"--input-path", "${workspaceFolder}/cvat-sdk/cvat_sdk/"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
},
{
"name": "docs: Postprocess SDK docs",
"type": "debugpy",
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"program": "${workspaceFolder}/site/process_sdk_docs.py",
"args": [
"--input-dir", "${workspaceFolder}/cvat-sdk/docs/",
"--site-root", "${workspaceFolder}/site/",
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
},
{
"name": "docs: Build docs",
"type": "debugpy",
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"program": "${workspaceFolder}/site/build_docs.py",
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
},
{
"name": "server: Generate REST API Schema",
"type": "debugpy",
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"program": "${workspaceFolder}/manage.py",
"args": [
"spectacular",
"--file",
"${workspaceFolder}/cvat/schema.yml"
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {},
"console": "internalConsole"
},
{
"name": "core.js: debug",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/cvat-core",
"runtimeExecutable": "node",
"runtimeArgs": [
"--nolazy",
"--inspect-brk=9230",
"src/api.js"
],
"port": 9230
}
],
"compounds": [
{
"name": "server: debug",
"configurations": [
"server: django",
"server: RQ - worker",
"server: RQ - scheduler",
],
"stopAll": true,
}
]
}
+42
View File
@@ -0,0 +1,42 @@
{
"eslint.probe": [
"javascript",
"typescript",
"typescriptreact"
],
"eslint.onIgnoredFiles": "warn",
"eslint.workingDirectories": [
{
"directory": "${cwd}",
},
{
"pattern": "cvat-*"
},
{
"directory": "tests",
"!cwd": true
}
],
"npm.exclude": "**/.env/**",
"files.trimTrailingWhitespace": true,
"python.analysis.exclude": [
// VS Code defaults
"**/node_modules",
"**/__pycache__",
".git",
"cvat-cli/build",
"cvat-sdk/build",
],
"python.defaultInterpreterPath": "${workspaceFolder}/.env/",
"python.testing.pytestArgs": [
"--rootdir","${workspaceFolder}/tests/"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.testing.pytestPath": "${workspaceFolder}/.env/bin/pytest",
"python.testing.cwd": "${workspaceFolder}/tests",
"cSpell.words": [
"crowdsourcing"
]
}
+38
View File
@@ -0,0 +1,38 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"path": "cvat-ui/",
"label": "npm: start - cvat-ui",
"detail": "webpack-dev-server --env.API_URL=http://localhost:7000 --config ./webpack.config.js --mode=development",
"promptOnClose": true,
"isBackground": true,
"problemMatcher": {
"owner": "webpack",
"severity": "error",
"fileLocation": "absolute",
"pattern": [
{
"regexp": "ERROR in (.*)",
"file": 1
},
{
"regexp": "\\((\\d+),(\\d+)\\):(.*)",
"line": 1,
"column": 2,
"message": 3
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "webpack-dev-server",
"endsPattern": "compiled"
}
}
}
]
}
+1
View File
@@ -0,0 +1 @@
nodeLinker: node-modules
+5760
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
# This CITATION.cff file was generated with cffinit.
# Visit https://bit.ly/cffinit to generate yours today!
cff-version: 1.2.0
title: Computer Vision Annotation Tool (CVAT)
message: >-
If you use this software, please cite it using the
metadata from this file.
type: software
authors:
- email: support+github@cvat.ai
name: CVAT.ai Corporation
identifiers:
- type: doi
value: 10.5281/zenodo.4009388
repository-code: 'https://github.com/cvat-ai/cvat'
url: 'https://cvat.ai/'
abstract: >-
Annotate better with CVAT, the industry-leading
data engine for machine learning. Used and trusted
by teams at any scale, for data of any scale.
keywords:
- image-labeling-tool
- computer-vision-annotation
- labeling-tool
- image-labeling
- semantic-segmentation
- annotation-tool
- object-detection
- image-classification
- video-annotation
- computer-vision
- deep-learning
- annotation
license: MIT
version: 2.25.0
date-released: '2023-11-06'
+209
View File
@@ -0,0 +1,209 @@
ARG BASE_IMAGE=ubuntu:24.04
FROM ${BASE_IMAGE} AS build-image-base
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
cargo-1.85 \
curl \
g++ \
gcc \
git \
libhdf5-dev \
libldap2-dev \
libmp3lame-dev \
libsasl2-dev \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
make \
nasm \
pkg-config \
python3-dev \
python3-pip \
&& update-alternatives \
--install /usr/bin/rustc rustc /usr/bin/rustc-1.85 185 \
--slave /usr/bin/cargo cargo /usr/bin/cargo-1.85 \
&& rm -rf /var/lib/apt/lists/*
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
# We build OpenH264, FFmpeg and PyAV in a separate build stage,
# because this way Docker can do it in parallel to all the other packages.
FROM build-image-base AS build-image-av
# Compile Openh264 and FFmpeg
ARG PREFIX=/opt/ffmpeg
ARG PKG_CONFIG_PATH=${PREFIX}/lib/pkgconfig
ENV FFMPEG_VERSION=8.0 \
OPENH264_VERSION=2.6.0
WORKDIR /tmp/openh264
RUN curl -sL https://github.com/cisco/openh264/archive/v${OPENH264_VERSION}.tar.gz --output - | \
tar -zx --strip-components=1 && \
make -j5 && make install-shared PREFIX=${PREFIX} && make clean
WORKDIR /tmp/ffmpeg
RUN curl -sL https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz --output - | \
tar -zx --strip-components=1 && \
./configure --disable-nonfree --disable-gpl --enable-libopenh264 --enable-libmp3lame \
--enable-shared --disable-static --disable-doc --disable-programs --prefix="${PREFIX}" && \
make -j5 && make install && make clean
COPY utils/dataset_manifest/requirements.txt /tmp/utils/dataset_manifest/requirements.txt
# Since we're using pip-compile-multi, each dependency can only be listed in
# one requirements file. In the case of PyAV, that should be
# `dataset_manifest/requirements.txt`. Make sure it's actually there,
# and then remove everything else.
RUN grep -q '^av==' /tmp/utils/dataset_manifest/requirements.txt
RUN sed -i '/^av==/!d' /tmp/utils/dataset_manifest/requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip/http-v2 \
python3 -m pip wheel --no-binary=av \
-r /tmp/utils/dataset_manifest/requirements.txt \
-w /tmp/wheelhouse
# This stage builds wheels for all dependencies (except PyAV)
FROM build-image-base AS build-image
COPY cvat/requirements/ /tmp/cvat/requirements/
COPY utils/dataset_manifest/requirements.txt /tmp/utils/dataset_manifest/requirements.txt
# Exclude av from the requirements file
RUN sed -i '/^av==/d' /tmp/utils/dataset_manifest/requirements.txt
ARG CVAT_CONFIGURATION="production"
# https://github.com/SAML-Toolkits/python3-saml#note
# Building from source is recommended for lxml and xmlsec to avoid libxml2 version conflicts
RUN --mount=type=cache,target=/root/.cache/pip/http-v2 \
DATUMARO_HEADLESS=1 python3 -m pip wheel --no-deps --no-binary lxml,xmlsec \
-r /tmp/cvat/requirements/${CVAT_CONFIGURATION}.txt \
-w /tmp/wheelhouse
FROM golang:1.26.4 AS build-smokescreen
RUN git clone --filter=blob:none --no-checkout https://github.com/stripe/smokescreen.git
RUN cd smokescreen && git checkout eb1ac09 && go build -o /tmp/smokescreen
FROM ${BASE_IMAGE}
ARG http_proxy
ARG https_proxy
ARG no_proxy
ARG socks_proxy
ARG TZ="Etc/UTC"
ENV TERM=xterm \
http_proxy=${http_proxy} \
https_proxy=${https_proxy} \
no_proxy=${no_proxy} \
socks_proxy=${socks_proxy} \
LANG='C.UTF-8' \
LC_ALL='C.UTF-8' \
TZ=${TZ}
ARG USER="django"
ARG CVAT_CONFIGURATION="production"
ENV DJANGO_SETTINGS_MODULE="cvat.settings.${CVAT_CONFIGURATION}"
# Install necessary apt packages
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
adduser \
bzip2 \
ca-certificates \
curl \
git \
libgl1 \
libgomp1 \
libldap2 \
libmp3lame0 \
libpython3.12t64 \
libsasl2-2 \
libxml2 \
libxmlsec1 \
libxmlsec1-openssl \
nginx \
p7zip-full \
poppler-utils \
python3 \
python3-venv \
supervisor \
tzdata \
unrar \
wait-for-it \
&& ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \
rm -rf /var/lib/apt/lists/*
# Install smokescreen
COPY --from=build-smokescreen /tmp/smokescreen /usr/local/bin/smokescreen
# Add a non-root user
ENV USER=${USER}
ENV HOME /home/${USER}
RUN deluser --remove-home ubuntu && \
adduser --uid=1000 --shell /bin/bash --disabled-password --gecos "" ${USER}
ARG CLAM_AV="no"
RUN if [ "$CLAM_AV" = "yes" ]; then \
apt-get update && \
apt-get --no-install-recommends install -yq \
clamav \
libclamunrar && \
sed -i 's/ReceiveTimeout 30/ReceiveTimeout 300/g' /etc/clamav/freshclam.conf && \
freshclam && \
chown -R ${USER}:${USER} /var/lib/clamav && \
rm -rf /var/lib/apt/lists/*; \
fi
# Install wheels from the build image
RUN python3 -m venv /opt/venv
ENV PATH="/opt/venv/bin:${PATH}"
ARG PIP_DISABLE_PIP_VERSION_CHECK=1
RUN --mount=type=bind,from=build-image,source=/tmp/wheelhouse,target=/mnt/wheelhouse \
--mount=type=bind,from=build-image-av,source=/tmp/wheelhouse,target=/mnt/wheelhouse-av \
python -m pip install --no-index /mnt/wheelhouse/*.whl /mnt/wheelhouse-av/*.whl
ENV NUMPROCS=1
COPY --from=build-image-av /opt/ffmpeg/lib /usr/lib
# These variables are required for supervisord substitutions in files
# This library allows remote python debugging with VS Code
ARG CVAT_DEBUG_ENABLED
RUN if [ "${CVAT_DEBUG_ENABLED}" = 'yes' ]; then \
python3 -m pip install --no-cache-dir debugpy; \
fi
# Removing pip due to security reasons. See: https://scout.docker.com/vulnerabilities/id/CVE-2018-20225
# The vulnerability is dubious and we don't use pip at runtime, but some vulnerability scanners mark it as a high vulnerability,
# and it was decided to remove pip from the final image
RUN python -m pip uninstall -y pip
# Install and initialize CVAT, copy all necessary files
COPY cvat/nginx.conf /etc/nginx/nginx.conf
COPY --chown=${USER} supervisord/ ${HOME}/supervisord
COPY --chown=${USER} backend_entrypoint.d/ ${HOME}/backend_entrypoint.d
COPY --chown=${USER} manage.py rqscheduler.py backend_entrypoint.sh wait_for_deps.sh ${HOME}/
COPY --chown=${USER} utils/ ${HOME}/utils
COPY --chown=${USER} cvat/ ${HOME}/cvat
COPY --chown=${USER} components/analytics/clickhouse/init.py ${HOME}/components/analytics/clickhouse/init.py
ARG COVERAGE_PROCESS_START
RUN if [ "${COVERAGE_PROCESS_START}" ]; then \
echo "import coverage; coverage.process_startup()" > /opt/venv/lib/python3.12/site-packages/coverage_subprocess.pth; \
fi
# RUN all commands below as 'django' user.
# Use numeric UID/GID so that the image is compatible with the Kubernetes runAsNonRoot setting.
USER 1000:1000
WORKDIR ${HOME}
RUN mkdir -p data share keys logs /tmp/supervisord /tmp/cvat static
EXPOSE 8080
ENTRYPOINT ["./backend_entrypoint.sh"]
+21
View File
@@ -0,0 +1,21 @@
FROM cvat/server:local
ENV DJANGO_SETTINGS_MODULE=cvat.settings.testing
USER root
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
build-essential \
python3-dev \
&& \
rm -rf /var/lib/apt/lists/*;
COPY cvat/requirements/ /tmp/cvat/requirements/
COPY utils/dataset_manifest/requirements.txt /tmp/utils/dataset_manifest/requirements.txt
RUN python3 -m ensurepip
RUN DATUMARO_HEADLESS=1 python3 -m pip install --no-cache-dir -r /tmp/cvat/requirements/testing.txt
COPY .coveragerc .
ENTRYPOINT []
+40
View File
@@ -0,0 +1,40 @@
FROM node:lts-slim AS cvat-ui
ENV TERM=xterm \
LANG='C.UTF-8' \
LC_ALL='C.UTF-8'
# Install dependencies
RUN npm install -g corepack
COPY .yarnrc.yml /tmp/
COPY package.json /tmp/
COPY yarn.lock /tmp/
COPY cvat-core/package.json /tmp/cvat-core/
COPY cvat-canvas/package.json /tmp/cvat-canvas/
COPY cvat-canvas3d/package.json /tmp/cvat-canvas3d/
COPY cvat-ui/package.json /tmp/cvat-ui/
COPY cvat-data/package.json /tmp/cvat-data/
# Install common dependencies
WORKDIR /tmp/
RUN DISABLE_HUSKY=1 yarn install --immutable
# Build source code
COPY cvat-data/ /tmp/cvat-data/
COPY cvat-core/ /tmp/cvat-core/
COPY cvat-canvas3d/ /tmp/cvat-canvas3d/
COPY cvat-canvas/ /tmp/cvat-canvas/
COPY cvat-ui/ /tmp/cvat-ui/
ARG CLIENT_PLUGINS
ARG SOURCE_MAPS_ENABLED=false
RUN CLIENT_PLUGINS="${CLIENT_PLUGINS}" \
SOURCE_MAPS_ENABLED="${SOURCE_MAPS_ENABLED}" yarn run build:cvat-ui
FROM nginxinc/nginx-unprivileged:1.31.2-alpine3.23-slim
# Replace default.conf configuration to remove unnecessary rules
COPY cvat-ui/react_nginx.conf /etc/nginx/conf.d/default.conf
COPY cvat-ui/robots.txt /usr/share/nginx/html/
COPY --from=cvat-ui /tmp/cvat-ui/dist /usr/share/nginx/html/
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (C) 2018-2022 Intel Corporation
Copyright (C) 2022-2025 CVAT.ai Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+265
View File
@@ -0,0 +1,265 @@
[![CVAT Community header](site/content/en/images/cvat_github_header.webp)](https://app.cvat.ai)
# CVAT: Computer Vision Annotation Tool
[![Release][release-img]][release-url]
[![GitHub stars][stars-img]][stars-url]
[![License][license-img]][license-url]
[![CI][ci-img]][ci-url]
[![server pulls][docker-server-pulls-img]][docker-server-image-url]
[![ui pulls][docker-ui-pulls-img]][docker-ui-image-url]
[![CVAT Online][online-img]][online-url]
[![CVAT Enterprise][enterprise-img]][enterprise-url]
[![Status][status-img]][status-url]
[![Discord][discord-img]][discord-url]
[![Docs][docs-img]][docs-url]
[Website](https://www.cvat.ai/) ·
[Docs](https://docs.cvat.ai/docs/) ·
[Changelog](https://www.cvat.ai/resources/changelog) ·
[Tutorials](https://www.cvat.ai/resources/videos) ·
[Academy](https://www.cvat.ai/resources/academy) ·
[Blog](https://www.cvat.ai/resources/blog)
## What is CVAT Community?
**CVAT Community** is the free, self-hosted open-source edition of [CVAT](https://www.cvat.ai/) — one of
the most widely used data annotation platforms for building high-quality visual datasets for
computer vision and visual AI.
Since 2018, CVAT has become one of the best-known data annotation tools in computer vision, with a
large open-source community, millions of Docker pulls, and broad adoption across research and
production AI teams.
CVAT Community supports image, video, and 3D annotation, dataset management, team collaboration, cloud storage
integration, developer-friendly SDKs and APIs, and gives your team full control over your data
and annotation infrastructure.
The platform serves as the foundation of
[CVAT Online](https://www.cvat.ai/pricing/cvat-online) and
[CVAT Enterprise](https://www.cvat.ai/enterprise), and is actively maintained by the CVAT engineering team.
Why teams choose CVAT Community:
- **Own your data:** Run entirely within your own infrastructure. No data leaves your environment.
- **AI-powered annotation:** Connect your own ML models for detection, segmentation, and tracking to speed up labeling.
- **Team collaboration:** Multi-user and multi-organization support with roles, task assignments,
and review workflows.
- **MIT-licensed core:** Use, modify, and distribute CVAT Community under the permissive MIT License. Some serverless
assets and dependencies may have separate licenses.
- **Production-grade:** The foundation of all CVAT commercial products — battle-tested at scale.
- **True open-source:** Transparent development, active community, on GitHub since 2018.
This repository contains the source code and deployment assets for CVAT Community.
For a fully managed setup, annotation services, or enterprise features, see
[CVAT Online](https://www.cvat.ai/pricing/cvat-online),
[CVAT Enterprise](https://www.cvat.ai/enterprise) and
[CVAT Labeling Services](https://www.cvat.ai/annotation-services).
## Getting Started
> 💡 Want to explore CVAT before deploying anything?
> **[Try CVAT Online (Free plan)](https://app.cvat.ai)** directly in your browser.
> Feature availability and usage limits vary by plan; see
> [CVAT Online pricing](https://www.cvat.ai/pricing/cvat-online) for details.
### Installation
**Prerequisites:**
- [Docker Engine](https://docs.docker.com/engine/install/)
- [Docker Compose](https://docs.docker.com/compose/install/)
- [Git](https://git-scm.com/)
> 💡 CVAT is primarily tested with Chromium-based browsers (Google Chrome, Microsoft Edge).
> Firefox may work with some caveats; Safari/WebKit is not supported.
**1. Start the default stack**
Clone the repository and launch the services.
```bash
git clone https://github.com/cvat-ai/cvat
cd cvat
# Optional: set your IP or domain
# export CVAT_HOST=your-ip-or-domain
docker compose up -d
```
**2. Create an admin account**
```bash
docker exec -it cvat_server bash -ic 'python3 ~/manage.py createsuperuser'
```
See the [Installation Guide](https://docs.cvat.ai/docs/administration/community/basics/installation/) for full
instructions and OS-specific setup.
**3. Sign in and start labeling**
- Open [http://localhost:8080](http://localhost:8080) (or your `CVAT_HOST`) in your browser.
- Log in with your superuser account.
- Create a project or task, upload your data (images, videos, or point clouds), and define labels to start annotating.
Learn more about annotation tools and workflows in the [CVAT Documentation](https://docs.cvat.ai/docs/) or
take our free course [CVAT Academy](https://www.cvat.ai/resources/academy).
_For alternative deployments (AWS, Kubernetes, external PostgreSQL, backups, upgrades), see the [Deployment Guides](https://docs.cvat.ai/docs/administration/community/advanced/)._
## Key Capabilities
- **[Manual & Auto-labeling](https://docs.cvat.ai/docs/annotation/manual-annotation/):** Annotate images, videos, and
3D point clouds with bounding boxes, polygons, masks, keypoints, cuboids, tags, and more. Speed up labeling
by connecting your own models for automatic annotation.
- **[Task Management](https://docs.cvat.ai/docs/workspace/):** Organize datasets into projects, split them into tasks
and jobs, assign work to annotators, and track progress in real time.
- **[Collaboration](https://docs.cvat.ai/docs/account_management/user-roles/):** Create organizations, invite teammates,
assign roles, and collaborate on annotations with comments and issues.
- **[Quality Control](https://docs.cvat.ai/docs/qa-analytics/manual-qa/):** Review annotations, flag issues, compare
results across annotators with consensus, and run Ground Truth and Honeypot checks through the server API.
- **[Analytics](https://docs.cvat.ai/docs/administration/community/advanced/analytics/):** Monitor user activity,
working time by job, events, and server logs with Grafana dashboards.
- **[Data Ops & Integrations](https://docs.cvat.ai/docs/dataset_management/export-datasets/):** Export/import in 20+
formats (COCO, YOLO, Pascal VOC, KITTI, etc.), connect to cloud storage (S3, Azure, Google Cloud), and automate
via REST API and Python SDK.
Advanced capabilities such as advanced project analytics, quality control UI, built-in auto-labeling with SAM 2
and SAM 3, AI agents, SSO, and more are available in [CVAT Online](https://www.cvat.ai/pricing/cvat-online)
paid plans (Solo, Team) and [CVAT Enterprise](https://www.cvat.ai/enterprise).
## Developer Tools
CVAT is designed for automation. Beyond the Web UI, you can integrate it into your pipelines using:
- [Python SDK](https://docs.cvat.ai/docs/api_sdk/sdk/): install with `pip install cvat-sdk` and automate task creation,
uploads, and exports from Python.
- [Command line tool](https://docs.cvat.ai/docs/api_sdk/cli/): install with `pip install cvat-cli`
and script common CVAT workflows from the terminal.
- [REST API](https://docs.cvat.ai/docs/api_sdk/api/): full programmatic control over CVAT.
## Data and Formats
CVAT Community supports image, video, and 3D (point cloud) annotation workflows. You can move data in and out using 20+
industry-standard formats: CVAT (XML), COCO (JSON), YOLO (TXT), Ultralytics YOLO (TXT/YAML), Pascal VOC (XML),
KITTI (TXT), MOT (TXT), and more.
[Full list of supported formats.](https://docs.cvat.ai/docs/dataset_management/formats/)
## ML and AI Models
CVAT Community supports automatic annotation via pre-built serverless models powered by Nuclio,
covering detection, segmentation, pose estimation, and tracking:
| Model | Framework | Type |
| --- | --- | --- |
| [Segment Anything (SAM)](https://github.com/cvat-ai/cvat/tree/develop/serverless/pytorch/facebookresearch/sam/nuclio) | PyTorch | Interactor |
| [Inside-Outside Guidance (IOG)](https://github.com/cvat-ai/cvat/tree/develop/serverless/pytorch/shiyinzhang/iog/nuclio) | PyTorch | Interactor |
| [RetinaNet R101](https://github.com/cvat-ai/cvat/tree/develop/serverless/pytorch/facebookresearch/detectron2/retinanet_r101/nuclio) | PyTorch | Detector |
| [HRNet32 Whole Body Pose](https://github.com/cvat-ai/cvat/tree/develop/serverless/pytorch/mmpose/hrnet32/nuclio) | PyTorch | Pose Estimation |
| [TransT](https://github.com/cvat-ai/cvat/tree/develop/serverless/pytorch/dschoerk/transt/nuclio) | PyTorch | Tracker |
| [YOLO v7](https://github.com/cvat-ai/cvat/tree/develop/serverless/onnx/WongKinYiu/yolov7/nuclio) | ONNX | Detector |
| [Mask RCNN Inception ResNet v2](https://github.com/cvat-ai/cvat/tree/develop/serverless/openvino/omz/public/mask_rcnn_inception_resnet_v2_atrous_coco/nuclio) | OpenVINO | Detector |
| [Face Detection 0205](https://github.com/cvat-ai/cvat/tree/develop/serverless/openvino/omz/intel/face-detection-0205/nuclio) | OpenVINO | Detector |
| [Faster RCNN Inception v2](https://github.com/cvat-ai/cvat/tree/develop/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | TensorFlow | Detector |
To enable automatic annotation, add the serverless component to your deployment:
```bash
docker compose -f docker-compose.yml -f components/serverless/docker-compose.serverless.yml up -d
```
This starts the serverless infrastructure. To make models available in CVAT, install `nuctl` and deploy
the functions you need, for example SAM or YOLO, as described in the [Automatic Annotation Guide](https://docs.cvat.ai/docs/annotation/auto-annotation/automatic-annotation/).
## Which CVAT edition should I choose?
- **CVAT Online**: the fastest way to try CVAT and start labeling without deployment. Use it to evaluate CVAT in
the browser, explore managed features, and move to cost-efficient paid plans when you need more capacity or team
workflows.
- **CVAT Community**: the MIT-licensed self-hosted edition for teams that want to run CVAT themselves, customize the
stack, and control their infrastructure.
- **CVAT Enterprise**: for organizations that need CVAT in their own cloud or internal environment, enterprise support,
security controls such as SSO, paid platform features, and SLAs.
- **Labeling Services**: for teams that want to outsource annotation work to CVAT.ais experienced labeling team instead
of building an internal labeling operation. Customers get trial access to CVAT Online during the project.
For detailed plan limits and feature availability, see [CVAT Online pricing](https://www.cvat.ai/pricing/cvat-online),
[CVAT Enterprise](https://www.cvat.ai/enterprise), and [Labeling Services](https://www.cvat.ai/annotation-services).
## Support
- **Usage questions:** ask the community on [Discord](https://discord.com/invite/fNR3eXfk6C) or
Stack Overflow with the `cvat` tag.
- **Bugs and feature requests:** use [GitHub Issues](https://github.com/cvat-ai/cvat/issues).
- **FAQ:** [Installation, upgrades, troubleshooting](https://docs.cvat.ai/docs/faq/).
For dedicated support, SLAs, or advanced deployments, consider [CVAT Enterprise](https://www.cvat.ai/enterprise).
## Contributing
We welcome all contributions: bug reports, documentation fixes, integrations, and code.
- If you'd like to contribute to CVAT, please refer to our
[contribution documentation](https://docs.cvat.ai/docs/contributing/).
- For bug reports or feature requests, please use the [GitHub Issues](https://github.com/cvat-ai/cvat/issues) tracker.
## Security
- Please review our [Security Policy](https://github.com/cvat-ai/cvat/security/policy) before reporting vulnerabilities.
- For sensitive issues, contact: [secure@cvat.ai](mailto:secure@cvat.ai).
## License
CVAT Community is released under the MIT License.
- Code in `/serverless` is also MIT-licensed, but may use third-party assets under separate licenses (including
non-commercial). Review those licenses before use.
- This software uses FFmpeg libraries under LGPL/GPL. See the Dockerfile and
[FFmpeg legal info](https://www.ffmpeg.org/legal.html) for details.
## Additional Resources
For the latest product releases, feature walkthroughs, and all things CVAT see:
<table cellspacing="10" border="0"><tr>
<td><a href="https://www.cvat.ai/resources/blog"><img src="site/content/en/images/badge-blog.png" alt="CVAT Blog" height="120"/></a></td>
<td><a href="https://www.cvat.ai/resources/academy"><img src="site/content/en/images/badge-academy.png" alt="CVAT Academy" height="120"/></a></td>
<td><a href="https://www.cvat.ai/resources/case-studies"><img src="site/content/en/images/badge-case-studies.png" alt="Case Studies" height="120"/></a></td>
<td><a href="https://www.youtube.com/@cvat-ai"><img src="site/content/en/images/badge-youtube.png" alt="YouTube" height="120"/></a></td>
<td><a href="https://www.linkedin.com/company/cvat-ai"><img src="site/content/en/images/badge-linkedin.png" alt="LinkedIn" height="120"/></a></td>
</tr></table>
<!-- Badges -->
[ci-img]: https://github.com/cvat-ai/cvat/actions/workflows/main.yml/badge.svg?branch=develop
[ci-url]: https://github.com/cvat-ai/cvat/actions
[docs-img]: https://img.shields.io/badge/docs-docs.cvat.ai-blue?style=flat-square
[docs-url]: https://docs.cvat.ai
[online-img]: https://img.shields.io/badge/CVAT%20Online-app.cvat.ai-success?style=flat-square
[online-url]: https://app.cvat.ai
[release-img]: https://img.shields.io/github/v/release/cvat-ai/cvat?style=flat-square
[release-url]: https://github.com/cvat-ai/cvat/releases
[license-img]: https://img.shields.io/github/license/cvat-ai/cvat?style=flat-square
[license-url]: https://github.com/cvat-ai/cvat/blob/develop/LICENSE
[stars-img]: https://img.shields.io/github/stars/cvat-ai/cvat?style=flat-square
[stars-url]: https://github.com/cvat-ai/cvat/stargazers
[status-img]: https://uptime.betterstack.com/status-badges/v2/monitor/1yl3h.svg
[status-url]: https://status.cvat.ai
[enterprise-img]: https://img.shields.io/badge/CVAT%20Enterprise-cvat.ai-orange?style=flat-square
[enterprise-url]: https://www.cvat.ai/enterprise
[docker-server-pulls-img]: https://img.shields.io/docker/pulls/cvat/server.svg?style=flat-square&label=server%20pulls
[docker-server-image-url]: https://hub.docker.com/r/cvat/server
[docker-ui-pulls-img]: https://img.shields.io/docker/pulls/cvat/ui.svg?style=flat-square&label=UI%20pulls
[docker-ui-image-url]: https://hub.docker.com/r/cvat/ui
[discord-img]: https://img.shields.io/discord/1000789942802337834?label=discord
[discord-url]: https://discord.gg/fNR3eXfk6C
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`cvat-ai/cvat`
- 原始仓库:https://github.com/cvat-ai/cvat
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+19
View File
@@ -0,0 +1,19 @@
# Security Policy
## Supported Versions
At the moment only the latest release is supported. When you report a security issue,
be sure it can be reproduced in the supported version.
## Reporting a Vulnerability
If you have information about a security issue or vulnerability in the product, please
send an e-mail to [secure@cvat.ai](mailto:secure+github@cvat.ai).
Please provide as much information as possible, including:
- The products and versions affected
- Detailed description of the vulnerability
- Information on known exploits
- A member of the CVAT.ai Product Security Team will review your e-mail and contact you to
collaborate on resolving the issue.
+47
View File
@@ -0,0 +1,47 @@
files.extend-exclude = [
"ai-models/agents_deployment/transformers/supported_models",
"cvat-ui/src/assets/opencv_4.8.0.js",
"cvat-data/src/ts/3rdparty/*.js",
"site/themes/docsy/",
"*.log",
"*.min.js",
"*.patch",
]
default.extend-ignore-re = [
# test passwords
"\"md5.*?\"",
# Line ignore with trailing "# spellchecker:disable-line"
# or trailing "// spellchecker:disable-line"
"(?Rm)^.*(#|//)\\s*spellchecker:disable-line$",
# Line block with "# spellchecker:<on|off>"
# or "// spellchecker:<on|off>"
"(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on"
# Taken from https://github.com/crate-ci/typos/blob/master/docs/reference.md#example-configurations
]
[default.extend-words]
AVOD = "AVOD"
NAM = "NAM"
blok = "block"
cheeck = "check"
configurate = "configure"
convertation = "conversion"
convertor = "converter"
cubiod = "cuboid"
cuboiud = "cuboid"
cuccessfully = "successfully"
firts = "first"
leafs = "leafs"
nd = "nd"
onject = ""
perameters = "parameters"
segway = "segway"
splitted = "splitted"
succefull = "successful"
tesk = "task"
tpic = "tpic"
trak = "trak"
typ = "type"
+3
View File
@@ -0,0 +1,3 @@
### This folder was moved to a separate repository:
#### <https://github.com/cvat-ai/cvat-models.git>
+12
View File
@@ -0,0 +1,12 @@
(
["annotation"]="smokescreen"
["chunks"]="smokescreen"
["consensus"]=""
["export"]="smokescreen"
["import"]="smokescreen clamav"
["quality_reports"]=""
["cleaning"]="rqscheduler"
["webhooks"]="smokescreen"
["notifications"]=""
["server"]="smokescreen clamav"
)
+209
View File
@@ -0,0 +1,209 @@
#!/usr/bin/env bash
set -eu
fail() {
printf >&2 "%s: %s\n" "$0" "$1"
exit 1
}
wait_for_db() {
wait-for-it "${CVAT_POSTGRES_HOST}:${CVAT_POSTGRES_PORT:-5432}" -t 0
}
wait_for_redis_inmem() {
wait-for-it "${CVAT_REDIS_INMEM_HOST}:${CVAT_REDIS_INMEM_PORT:-6379}" -t 0
}
wait_for_clickhouse() {
wait-for-it "${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT:-8123}" -t 0
}
account_for_internal_proxy() {
CVAT_NUM_PROXIES="${CVAT_NUM_PROXIES:-0}"
if ! [[ "$CVAT_NUM_PROXIES" =~ ^[0-9]+$ ]]; then
fail "CVAT_NUM_PROXIES must be a non-negative integer"
fi
# The user-facing setting counts proxies before the backend container.
# Add the internal nginx hop that proxies requests to uvicorn.
export CVAT_NUM_PROXIES=$((CVAT_NUM_PROXIES + 1))
}
cmd_bash() {
exec bash "$@"
}
cmd_init() {
wait_for_db
~/manage.py migrate
wait_for_redis_inmem
~/manage.py migrateredis
~/manage.py syncperiodicjobs
if [[ "${CVAT_ANALYTICS:-0}" == "1" ]]; then
wait_for_clickhouse
python components/analytics/clickhouse/init.py
fi
}
_load_component_config() {
declare -gA merged_config=()
for config_file in ~/backend_entrypoint.d/*.conf; do
declare -A config=$(cat $config_file)
for key in "${!config[@]}"; do
if [[ -n ${merged_config[$key]+_} ]]; then
fail "Duplicated component definition: $key"
fi
merged_config[$key]=${config[$key]}
done
unset config
done
}
_get_includes() {
extra_configs=()
for key in "$@"; do
if [[ -z ${merged_config[$key]+_} ]]; then
fail "Unexpected component: $key"
fi
for include in ${merged_config["$key"]}; do
if ! [[ ${extra_configs[@]} =~ $include ]] && \
( ! [[ "$include" == "clamav" ]] || [[ "${CLAM_AV:-}" == "yes" ]] ); then
extra_configs+=("$include")
fi
done
done
if [ ${#extra_configs[@]} -gt 0 ]; then
printf 'reusable/%s.conf ' "${extra_configs[@]}"
fi
}
_get_reusable_includes() {
extra_configs=()
for include in "$@"; do
if ! [ -r "$HOME/supervisord/reusable/$include.conf" ]; then
fail "Unexpected supervisor include: $include"
fi
if ! [[ ${extra_configs[@]} =~ $include ]]; then
extra_configs+=("$include")
fi
done
if [ ${#extra_configs[@]} -gt 0 ]; then
printf 'reusable/%s.conf ' "${extra_configs[@]}"
fi
}
cmd_run() {
if [ "$#" -eq 0 ]; then
fail "run: at least 1 argument is expected"
fi
component="$1"
_load_component_config
case "$component" in
server|worker|worker-pool|nginx) ;;
*) fail "Unexpected run component: $component" ;;
esac
if [ "$component" = "nginx" ]; then
exec supervisord -c "supervisord/nginx.conf"
fi
if [ "$component" = "server" ]; then
account_for_internal_proxy
~/manage.py collectstatic --no-input
fi
wait_for_db
echo "waiting for migrations to complete..."
while ! ~/manage.py migrate --check; do
sleep 10
done
wait_for_redis_inmem
echo "waiting for Redis migrations to complete..."
while ! ~/manage.py migrateredis --check; do
sleep 10
done
supervisord_includes=""
postgres_app_name="cvat:$component"
if [ "$component" = "server" ]; then
supervisord_includes="$(_get_includes "server")$(_get_reusable_includes "${@:2}")"
elif [ "$component" = "worker" ] || [ "$component" = "worker-pool" ]; then
if [ "$#" -eq 1 ]; then
fail "run worker: expected at least 1 queue name"
fi
queues=()
extra_flags=()
for arg in "${@:2}"; do
if [[ "$arg" == --* ]]; then
extra_flags+=("$arg")
else
queues+=("$arg")
fi
done
if [ ${#queues[@]} -eq 0 ]; then
fail "run worker: expected at least 1 queue name"
fi
queue_list="${queues[*]}"
echo "Workers to run: $queue_list"
if [ ${#extra_flags[@]} -gt 0 ]; then
echo "Extra rqworker flags: ${extra_flags[*]}"
fi
export CVAT_QUEUES=$queue_list
export CVAT_RQWORKER_EXTRA_FLAGS="${extra_flags[*]:-}"
postgres_app_name+=":${queue_list// /+}"
supervisord_includes=$(_get_includes "${queues[@]}")
fi
echo "Additional supervisor configs that will be included: $supervisord_includes"
export CVAT_POSTGRES_APPLICATION_NAME=$postgres_app_name
export CVAT_SUPERVISORD_INCLUDES=$supervisord_includes
exec supervisord -c "supervisord/$component.conf"
}
if [ $# -eq 0 ]; then
echo >&2 "$0: at least one subcommand required"
echo >&2 ""
echo >&2 "available subcommands:"
echo >&2 " bash <bash args...>"
echo >&2 " init"
echo >&2 " run server [additional components]"
echo >&2 " run nginx"
echo >&2 " run worker <list of queues>"
exit 1
fi
for init_script in /etc/cvat/init.d/*; do
if [ -r "$init_script" ]; then
. "$init_script"
fi
done
while [ $# -ne 0 ]; do
if [ "$(type -t "cmd_$1")" != "function" ]; then
fail "unknown subcommand: $1"
fi
cmd_name="$1"
shift
"cmd_$cmd_name" "$@"
done
@@ -0,0 +1,5 @@
### Removed
- \[SDK, CLI\] Removed server schema autodetection, previously deprecated
in v2.36.0
(<https://github.com/cvat-ai/cvat/pull/10673>)
@@ -0,0 +1,4 @@
### Added
- Added region of interest support for automatic annotation detector and interactor functions
(<https://github.com/cvat-ai/cvat/pull/10794>)
@@ -0,0 +1,6 @@
### Added
- \[SDK\] `list()` methods on high-level proxy classes now accept filter parameters
via a composable `F` field DSL and keyword lookups,
which are compiled into the server-side `filter` query parameter automatically
(<https://github.com/cvat-ai/cvat/pull/10778>)
@@ -0,0 +1,8 @@
### Added
- \[SDK\] Persistent authentication support with saved profiles:
`make_client_from_profile`, `make_client_from_cli`, and
`resolve_server_host` helpers let SDK users save server + credential profiles
to disk and instantiate a `Client` from a profile name without re-entering
credentials
(<https://github.com/cvat-ai/cvat/pull/10824>)
@@ -0,0 +1,4 @@
### Fixed
- Requests API no longer returns a `null` status when a job's status cannot be
read from Redis (e.g. the job expired) (<https://github.com/cvat-ai/cvat/pull/10849>)
@@ -0,0 +1,4 @@
### Fixed
- Fix snap to contour crashes when drawing stops (drawCircles of undefined / array of null)
(<https://github.com/cvat-ai/cvat/pull/10848>)
+5
View File
@@ -0,0 +1,5 @@
### Fixed
- Fixed task creation from images hosted on Azure Blob Storage when
at least one image's header doesn't fit into 2047 bytes
(<https://github.com/cvat-ai/cvat/pull/10851>)
@@ -0,0 +1,5 @@
### Fixed
- Requesting task metadata (`GET /api/tasks/<id>/data/meta`) before any
data was uploaded to the task now returns 400 with a clear message.
(<https://github.com/cvat-ai/cvat/pull/10855>)
@@ -0,0 +1,4 @@
### Fixed
- Client error `Cannot read properties of undefined (reading 'occluded')` while copying masks
(<https://github.com/cvat-ai/cvat/pull/10853>)
@@ -0,0 +1,6 @@
### Fixed
- \[Server API\] Added missing input validation for the `source` field of annotations
in the `/api/tasks/<id>/annotations` and `/api/jobs/<id>/annotations` endpoints.
The existing invalid values are returned unmodified in the server responses.
(<https://github.com/cvat-ai/cvat/pull/10521>)
@@ -0,0 +1,4 @@
### Removed
- Removed the unused `PATCH /api/invitations/{key}` Server API operation.
(<https://github.com/cvat-ai/cvat/pull/10850>)
@@ -0,0 +1,4 @@
### Fixed
- Fixed AI tools tracking when starting tracking from multiple drawn rectangles
(<https://github.com/cvat-ai/cvat/pull/10860>)
@@ -0,0 +1,5 @@
### Fixed
- Client error thrown
when listing lambda functions failed (e.g. network
error or timeout) (<https://github.com/cvat-ai/cvat/pull/10857>)
@@ -0,0 +1,4 @@
### Fixed
- Fix pagination being reset when refreshing or going "back" to a resources page
(<https://github.com/cvat-ai/cvat/pull/10858>)
@@ -0,0 +1,4 @@
### Fixed
- Make loading previews on project/job cards clickable to allow opening the resource regardless of the preview state
(<https://github.com/cvat-ai/cvat/pull/10859>)
@@ -0,0 +1,4 @@
### Fixed
- Prevent requests cards content overflow
(<https://github.com/cvat-ai/cvat/pull/10862>)
@@ -0,0 +1,4 @@
### Fixed
- Improved performance of annotation responses.
(<https://github.com/cvat-ai/cvat/pull/10867>)
+4
View File
@@ -0,0 +1,4 @@
### {{ config.categories | join('|') }} <!-- pick one -->
- Describe your change here...
(<https://github.com/cvat-ai/cvat/pull/XXXX>)
+6
View File
@@ -0,0 +1,6 @@
[scriv]
categories = Added, Changed, Deprecated, Removed, Fixed, Security
entry_title_template = \[{{ version }}\] - {{ date.strftime('%%Y-%%m-%%d') }}
format = md
md_header_level = 2
new_fragment_template = file: fragment.j2
+37
View File
@@ -0,0 +1,37 @@
<!--
Copyright (C) CVAT.ai Corporation
SPDX-License-Identifier: MIT
-->
# Clickhouse DB migrations
This directory contains migrations for the Clickhouse DB used by CVAT.
## Implementation details
The Clickhouse documentation explains options to customize DB loading here:
<https://clickhouse.com/docs/install/docker#how-to-extend-image>
In the default Docker image, Clickhouse only runs these files if the DB is not initialized.
This is not the desired behavior for us, because it doesn't allow us to add new migrations
added after the DB is initialized. Instead, we run the migrations on each start using a custom
script. Therefore **make sure the migrations can be safely called multiple times**
on an existing DB.
The directory contains scripts for the Clickhouse instance:
- `init.py`: initializes the DB and applies required migrations
## Adding a migration
To add a migration, create a new `migration_NNN_<custom name>` function in the `init.py` script
and add it to the list of migrations.
Recommendations on the migrations:
- **Make sure the migrations can be safely called multiple times**
and do not fail on repeated calls on the initialized DB.
Typically, this can be achieved by using "IF NOT EXISTS" at some point.
- Avoid heavy data migrations as much as possible. In most cases, changing only the table
columns should be enough and you won't need a data migration.
As follows from the recommendations above, the current implementation is limited
to simple migrations only.
+117
View File
@@ -0,0 +1,117 @@
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
import os
import sys
import textwrap
from argparse import ArgumentParser
import clickhouse_connect
import clickhouse_connect.driver.client
CLICKHOUSE_HOST_ENV_VAR = "CLICKHOUSE_HOST"
CLICKHOUSE_PORT_ENV_VAR = "CLICKHOUSE_PORT"
CLICKHOUSE_DB_ENV_VAR = "CLICKHOUSE_DB"
CLICKHOUSE_USER_ENV_VAR = "CLICKHOUSE_USER" # nosec
CLICKHOUSE_PASSWORD_ENV_VAR = "CLICKHOUSE_PASSWORD" # nosec
def clear_db(client: clickhouse_connect.driver.client.Client):
client.query(f"TRUNCATE DATABASE IF EXISTS {client.database};")
def create_db(client: clickhouse_connect.driver.client.Client, db_name: str):
client.query(f"CREATE DATABASE IF NOT EXISTS {db_name};")
def migration_000_initial(client: clickhouse_connect.driver.client.Client):
client.query(textwrap.dedent("""\
CREATE TABLE IF NOT EXISTS events
(
`scope` String NOT NULL,
`obj_name` String NULL,
`obj_id` UInt64 NULL,
`obj_val` String NULL,
`source` String NOT NULL,
`timestamp` DateTime64(3, 'Etc/UTC') NOT NULL,
`count` UInt16 NULL,
`duration` UInt32 DEFAULT toUInt32(0),
`project_id` UInt64 NULL,
`task_id` UInt64 NULL,
`job_id` UInt64 NULL,
`user_id` UInt64 NULL,
`user_name` String NULL,
`user_email` String NULL,
`org_id` UInt64 NULL,
`org_slug` String NULL,
`payload` String NULL
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(timestamp)
ORDER BY (timestamp)
SETTINGS index_granularity = 8192;
"""))
def migration_001_add_access_token(client: clickhouse_connect.driver.client.Client):
client.query(textwrap.dedent("""\
ALTER TABLE events
ADD COLUMN IF NOT EXISTS
`access_token_id` Nullable(UInt64) DEFAULT NULL
"""))
def migration_002_add_remote_addr(client: clickhouse_connect.driver.client.Client):
client.query(textwrap.dedent("""\
ALTER TABLE events
ADD COLUMN IF NOT EXISTS
`remote_addr` Nullable(String) DEFAULT NULL
"""))
migrations = [
migration_000_initial,
migration_001_add_access_token,
migration_002_add_remote_addr,
]
def main(args: list[str] | None = None) -> int:
parser = ArgumentParser(description="Initializes the DB and applies migrations")
parser.add_argument("--host", help=f"Server host (env: {CLICKHOUSE_HOST_ENV_VAR})")
parser.add_argument("--port", help=f"Server port (env: {CLICKHOUSE_PORT_ENV_VAR})")
parser.add_argument("--db", help=f"Database name (env: {CLICKHOUSE_DB_ENV_VAR})")
parser.add_argument("--user", help=f"Username (env: {CLICKHOUSE_USER_ENV_VAR})")
parser.add_argument("--password", help=f"Password (env: {CLICKHOUSE_PASSWORD_ENV_VAR})")
parser.add_argument(
"--clear", action="store_true", help="Clear the existing DB and initialize a new one"
)
parsed_args = parser.parse_args(args)
db_name = parsed_args.db or os.getenv(CLICKHOUSE_DB_ENV_VAR)
with clickhouse_connect.get_client(
host=parsed_args.host or os.getenv(CLICKHOUSE_HOST_ENV_VAR),
port=parsed_args.port or os.getenv(CLICKHOUSE_PORT_ENV_VAR),
username=parsed_args.user or os.getenv(CLICKHOUSE_USER_ENV_VAR),
password=parsed_args.password or os.getenv(CLICKHOUSE_PASSWORD_ENV_VAR),
) as client:
create_db(client, db_name)
client.database = db_name # client can't be created with non existing DB
if parsed_args.clear:
print("Clearing the DB")
clear_db(client)
print("done")
for migration_func in migrations:
print("Applying migration", migration_func.__name__)
migration_func(client)
print("done")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,760 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "bars",
"fillOpacity": 90,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 24,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"builderOptions": {
"fields": [],
"filters": [],
"metrics": [
{
"aggregation": "count",
"field": "*"
}
],
"mode": "trend",
"orderBy": [],
"table": "events",
"timeField": "timestamp",
"timeFieldType": "DateTime64(3, 'Etc/UTC')"
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"format": 0,
"meta": {
"builderOptions": {
"fields": [],
"filters": [],
"metrics": [
{
"aggregation": "count",
"field": "*"
}
],
"mode": "trend",
"orderBy": [],
"table": "events",
"timeField": "timestamp",
"timeFieldType": "DateTime64(3, 'Etc/UTC')"
}
},
"queryType": "sql",
"rawSql": "SELECT $__timeInterval(timestamp) as time, count(*)\r\nFROM events\r\nWHERE $__timeFilter(timestamp)\r\nAND scope IN (${scopes})\r\nAND source IN (${sources})\r\nAND (-1 IN (${users}) OR user_id IN (${users}))\r\nAND (-1 IN (${organizations}) OR org_id IN (${organizations}))\r\nAND (-1 IN (${projects}) OR project_id IN (${projects}))\r\nAND (-1 IN (${tasks}) OR task_id IN (${tasks}))\r\nAND (-1 IN (${jobs}) OR job_id IN (${jobs}))\r\nGROUP BY time ORDER BY time ASC",
"refId": "A"
}
],
"title": "Overall Activity",
"type": "timeseries"
},
{
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"displayMode": "auto",
"inspect": true
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "timestamp"
},
"properties": [
{
"id": "custom.width",
"value": 158
}
]
}
]
},
"gridPos": {
"h": 23,
"w": 24,
"x": 0,
"y": 7
},
"id": 4,
"options": {
"footer": {
"enablePagination": true,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": []
},
"pluginVersion": "9.3.6",
"targets": [
{
"builderOptions": {
"fields": [
"*"
],
"filters": [
{
"condition": "AND",
"filterType": "custom",
"key": "timestamp",
"operator": "WITH IN DASHBOARD TIME RANGE",
"type": "DateTime64(3, 'Etc/UTC')",
"value": "TODAY"
},
{
"condition": "AND",
"filterType": "custom",
"key": "scope",
"operator": "IN",
"type": "String",
"value": [
""
]
}
],
"mode": "list",
"orderBy": [
{
"dir": "ASC",
"name": "timestamp"
}
],
"table": "events"
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"format": 1,
"meta": {
"builderOptions": {
"fields": [
"*"
],
"filters": [
{
"condition": "AND",
"filterType": "custom",
"key": "timestamp",
"operator": "WITH IN DASHBOARD TIME RANGE",
"type": "DateTime64(3, 'Etc/UTC')",
"value": "TODAY"
},
{
"condition": "AND",
"filterType": "custom",
"key": "scope",
"operator": "IN",
"type": "String",
"value": [
""
]
}
],
"mode": "list",
"orderBy": [
{
"dir": "ASC",
"name": "timestamp"
}
],
"table": "events"
}
},
"queryType": "sql",
"rawSql": "SELECT * \r\nFROM events \r\nWHERE $__timeFilter(timestamp)\r\n AND scope IN (${scopes})\r\n AND source IN (${sources})\r\n AND (-1 IN (${users}) OR user_id IN (${users}))\r\n AND (' ' IN (${usernames}) OR user_name IN (${usernames}))\r\n AND (-1 IN (${organizations}) OR org_id IN (${organizations}))\r\n AND (-1 IN (${projects}) OR project_id IN (${projects}))\r\n AND (-1 IN (${tasks}) OR task_id IN (${tasks}))\r\n AND (-1 IN (${jobs}) OR job_id IN (${jobs}))\r\nORDER BY timestamp DESC\r\nLIMIT 1000",
"refId": "A"
}
],
"title": "All events",
"type": "table"
},
{
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisGridShow": false,
"axisLabel": "",
"axisPlacement": "auto",
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 0,
"scaleDistribution": {
"type": "linear"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "percentage",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 12,
"x": 0,
"y": 30
},
"id": 6,
"options": {
"barRadius": 0,
"barWidth": 0.51,
"groupWidth": 0.7,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": false
},
"orientation": "horizontal",
"showValue": "always",
"stacking": "none",
"tooltip": {
"mode": "single",
"sort": "none"
},
"xField": "browser",
"xTickLabelRotation": 0,
"xTickLabelSpacing": 0
},
"pluginVersion": "9.3.6",
"targets": [
{
"builderOptions": {
"fields": [],
"filters": [],
"limit": 100,
"mode": "list",
"orderBy": [],
"table": "events"
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"format": 1,
"meta": {
"builderOptions": {
"fields": [],
"filters": [],
"limit": 100,
"mode": "list",
"orderBy": [],
"table": "events"
}
},
"queryType": "sql",
"rawSql": "SELECT\r\n browser,\r\n count()\r\nFROM\r\n(\r\n SELECT\r\n concat(JSON_VALUE(payload, '$.platform.name'), ' ', JSON_VALUE(payload, '$.platform.version')) AS browser,\r\n user_id\r\n FROM cvat.events\r\n WHERE $__timeFilter(timestamp) AND (scope = 'load:cvat') AND (browser != ' ')\r\n GROUP BY\r\n user_id,\r\n browser\r\n)\r\nGROUP BY browser\r\nORDER BY count() DESC",
"refId": "A"
}
],
"title": "Browser",
"type": "barchart"
},
{
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisGridShow": false,
"axisLabel": "",
"axisPlacement": "auto",
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 0,
"scaleDistribution": {
"type": "linear"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "percentage",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 12,
"x": 12,
"y": 30
},
"id": 8,
"options": {
"barRadius": 0,
"barWidth": 0.51,
"groupWidth": 0.7,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": false
},
"orientation": "horizontal",
"showValue": "always",
"stacking": "none",
"tooltip": {
"mode": "single",
"sort": "none"
},
"xField": "os",
"xTickLabelRotation": 0,
"xTickLabelSpacing": 0
},
"pluginVersion": "9.3.6",
"targets": [
{
"builderOptions": {
"fields": [],
"filters": [],
"limit": 100,
"mode": "list",
"orderBy": [],
"table": "events"
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"format": 1,
"meta": {
"builderOptions": {
"fields": [],
"filters": [],
"limit": 100,
"mode": "list",
"orderBy": [],
"table": "events"
}
},
"queryType": "sql",
"rawSql": "SELECT\r\n os,\r\n count()\r\nFROM\r\n(\r\n SELECT\r\n JSON_VALUE(payload, '$.platform.os') AS os,\r\n user_id\r\n FROM cvat.events\r\n WHERE $__timeFilter(timestamp) AND (scope = 'load:cvat') AND (os != '')\r\n GROUP BY\r\n user_id,\r\n os\r\n)\r\nGROUP BY os\r\nORDER BY count() DESC",
"refId": "A"
}
],
"title": "OS",
"type": "barchart"
}
],
"refresh": false,
"schemaVersion": 38,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"allValue": "",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"definition": "SELECT DISTINCT scope\nFROM events\nWHERE $__timeFilter(timestamp)",
"hide": 0,
"includeAll": true,
"label": "Scope",
"multi": true,
"name": "scopes",
"options": [],
"query": "SELECT DISTINCT scope\nFROM events\nWHERE $__timeFilter(timestamp)",
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"type": "query"
},
{
"allValue": "",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"definition": "SELECT DISTINCT source\nFROM events\nWHERE $__timeFilter(timestamp)",
"hide": 0,
"includeAll": true,
"label": "Source",
"multi": true,
"name": "sources",
"options": [],
"query": "SELECT DISTINCT source\nFROM events\nWHERE $__timeFilter(timestamp)",
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
},
{
"allValue": "-1",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"definition": "SELECT DISTINCT user_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND user_id IS NOT NULL",
"hide": 0,
"includeAll": true,
"label": "User",
"multi": true,
"name": "users",
"options": [],
"query": "SELECT DISTINCT user_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND user_id IS NOT NULL",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
},
{
"allValue": "' '",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"definition": "SELECT DISTINCT user_name\nFROM events\nWHERE $__timeFilter(timestamp)",
"hide": 0,
"includeAll": true,
"label": "Username",
"multi": true,
"name": "usernames",
"options": [],
"query": "SELECT DISTINCT user_name\nFROM events\nWHERE $__timeFilter(timestamp)",
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"type": "query"
},
{
"allValue": "-1",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"definition": "SELECT DISTINCT project_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND project_id IS NOT NULL",
"hide": 0,
"includeAll": true,
"label": "Project",
"multi": true,
"name": "projects",
"options": [],
"query": "SELECT DISTINCT project_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND project_id IS NOT NULL",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
},
{
"allValue": "-1",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"definition": "SELECT DISTINCT task_id\nFROM events\nWHERE $__timeFilter(timestamp) \n AND task_id IS NOT NULL",
"hide": 0,
"includeAll": true,
"label": "Task",
"multi": true,
"name": "tasks",
"options": [],
"query": "SELECT DISTINCT task_id\nFROM events\nWHERE $__timeFilter(timestamp) \n AND task_id IS NOT NULL",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
},
{
"allValue": "-1",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"definition": "SELECT DISTINCT job_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND job_id IS NOT NULL",
"hide": 0,
"includeAll": true,
"label": "Job",
"multi": true,
"name": "jobs",
"options": [],
"query": "SELECT DISTINCT job_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND job_id IS NOT NULL",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
},
{
"allValue": "-1",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"definition": "SELECT DISTINCT org_id\nFROM events\nWHERE $__timeFilter(timestamp)\nAND org_id IS NOT NULL",
"hide": 0,
"includeAll": true,
"label": "Organization",
"multi": true,
"name": "organizations",
"options": [],
"query": "SELECT DISTINCT org_id\nFROM events\nWHERE $__timeFilter(timestamp)\nAND org_id IS NOT NULL",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-7d",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "All events",
"uid": "EIGSTDAVz",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,532 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 25,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"__systemRef": "hideSeriesFrom",
"matcher": {
"id": "byNames",
"options": {
"mode": "exclude",
"names": [
"User 1"
],
"prefix": "All except:",
"readOnly": true
}
},
"properties": [
{
"id": "custom.hideFrom",
"value": {
"legend": false,
"tooltip": false,
"viz": true
}
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 0
},
"id": 6,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "9.3.6",
"targets": [
{
"builderOptions": {
"fields": [],
"filters": [],
"groupBy": [
"user_id"
],
"metrics": [
{
"aggregation": "count",
"alias": "value",
"field": "*"
}
],
"mode": "trend",
"orderBy": [],
"table": "events",
"timeField": "timestamp",
"timeFieldType": "DateTime64(3, 'Etc/UTC')"
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"format": 0,
"hide": false,
"meta": {
"builderOptions": {
"fields": [],
"filters": [],
"groupBy": [
"user_id"
],
"metrics": [
{
"aggregation": "count",
"alias": "value",
"field": "*"
}
],
"mode": "trend",
"orderBy": [],
"table": "events",
"timeField": "timestamp",
"timeFieldType": "DateTime64(3, 'Etc/UTC')"
}
},
"queryType": "sql",
"rawSql": "SELECT $__timeInterval(timestamp) as time, toString(user_id), count() as User\r\nFROM events\r\nWHERE $__timeFilter(timestamp)\r\nGROUP BY time, user_id\r\nORDER BY time ASC, user_id ASC",
"refId": "A"
}
],
"title": "User Activity",
"transformations": [],
"type": "timeseries"
},
{
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "bars",
"fillOpacity": 90,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 24,
"x": 0,
"y": 9
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"builderOptions": {
"fields": [],
"filters": [],
"metrics": [
{
"aggregation": "count",
"alias": "Count",
"field": "*"
}
],
"mode": "trend",
"orderBy": [],
"table": "events",
"timeField": "timestamp",
"timeFieldType": "DateTime64(3, 'Etc/UTC')"
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"format": 0,
"queryType": "builder",
"rawSql": "SELECT $__timeInterval(timestamp) as time, count(*) Count FROM events WHERE $__timeFilter(timestamp) GROUP BY time ORDER BY time ASC",
"refId": "A"
}
],
"title": "Overall Activity",
"type": "timeseries"
},
{
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"displayMode": "auto",
"filterable": false,
"inspect": true
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 13,
"w": 24,
"x": 0,
"y": 16
},
"id": 2,
"options": {
"footer": {
"enablePagination": true,
"fields": [
"Working time(h)",
"Activity"
],
"reducer": [
"sum"
],
"show": true
},
"showHeader": true,
"sortBy": []
},
"pluginVersion": "9.3.6",
"targets": [
{
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"format": 1,
"meta": {
"builderOptions": {
"fields": [],
"limit": 100,
"mode": "list"
}
},
"queryType": "sql",
"rawSql": "SELECT\r\n user_id as User,\r\n user_name as Username,\r\n project_id as Project,\r\n task_id as Task,\r\n job_id as Job, sum(JSONExtractUInt(payload, 'working_time')) / 1000 / 3600 as \"Working time(h)\",\r\n count() as Activity\r\nFROM events\r\nWHERE JSONHas(payload, 'working_time')\r\n AND $__timeFilter(timestamp)\r\n AND(-1 IN (${users}) OR user_id IN (${users}))\r\n AND (' ' IN (${usernames}) OR user_name IN (${usernames}))\r\n AND (-1 IN (${projects}) OR project_id IN (${projects}))\r\n AND task_id IN (${tasks})\r\n AND job_id IN (${jobs})\r\nGROUP BY user_id, user_name, project_id, task_id, job_id",
"refId": "A"
}
],
"title": "Working time",
"type": "table"
}
],
"refresh": false,
"schemaVersion": 38,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"allValue": "-1",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"definition": "SELECT DISTINCT user_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND source = 'client'",
"hide": 0,
"includeAll": true,
"label": "User",
"multi": true,
"name": "users",
"options": [],
"query": "SELECT DISTINCT user_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND source = 'client'",
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"type": "query"
},
{
"allValue": "' '",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"definition": "SELECT DISTINCT user_name\nFROM events\nWHERE $__timeFilter(timestamp)",
"hide": 0,
"includeAll": true,
"label": "Username",
"multi": true,
"name": "usernames",
"options": [],
"query": "SELECT DISTINCT user_name\nFROM events\nWHERE $__timeFilter(timestamp)",
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"type": "query"
},
{
"allValue": "-1",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"definition": "SELECT DISTINCT project_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND project_id IS NOT NULL\n AND source = 'client'",
"hide": 0,
"includeAll": true,
"label": "Project",
"multi": true,
"name": "projects",
"options": [],
"query": "SELECT DISTINCT project_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND project_id IS NOT NULL\n AND source = 'client'",
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"type": "query"
},
{
"current": {
"selected": false,
"text": "All",
"value": "$__all"
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"definition": "SELECT DISTINCT task_id\nFROM events\nWHERE $__timeFilter(timestamp) \n AND task_id IS NOT NULL\n AND source = 'client'\n AND (-1 IN (${projects}) OR project_id IN (${projects}))",
"description": "",
"hide": 0,
"includeAll": true,
"label": "Task",
"multi": true,
"name": "tasks",
"options": [],
"query": "SELECT DISTINCT task_id\nFROM events\nWHERE $__timeFilter(timestamp) \n AND task_id IS NOT NULL\n AND source = 'client'\n AND (-1 IN (${projects}) OR project_id IN (${projects}))",
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"type": "query"
},
{
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "grafana-clickhouse-datasource",
"uid": "PDEE91DDB90597936"
},
"definition": "SELECT DISTINCT job_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND job_id IS NOT NULL\n AND source = 'client'\n AND task_id in (${tasks})",
"hide": 0,
"includeAll": true,
"label": "Job",
"multi": true,
"name": "jobs",
"options": [],
"query": "SELECT DISTINCT job_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND job_id IS NOT NULL\n AND source = 'client'\n AND task_id in (${tasks})",
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-7d",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Management",
"uid": "w0if6WAVz",
"version": 2,
"weekStart": ""
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+39
View File
@@ -0,0 +1,39 @@
http:
routers:
grafana:
entryPoints:
- web
middlewares:
- analytics-auth
- strip-prefix
service: grafana
rule: Host(`{{ env "CVAT_HOST" }}`) && PathPrefix(`/analytics`)
grafana_https:
entryPoints:
- websecure
middlewares:
- analytics-auth
- strip-prefix
service: grafana
tls: {}
rule: Host(`{{ env "CVAT_HOST" }}`) && PathPrefix(`/analytics`)
middlewares:
analytics-auth:
forwardauth:
address: http://cvat_server:8080/analytics
authRequestHeaders:
- "Cookie"
- "Authorization"
strip-prefix:
stripprefix:
prefixes:
- /analytics
services:
grafana:
loadBalancer:
servers:
- url: http://{{ env "DJANGO_LOG_VIEWER_HOST" }}:{{ env "DJANGO_LOG_VIEWER_PORT" }}
passHostHeader: false
+47
View File
@@ -0,0 +1,47 @@
data_dir = "/vector-data-dir"
[sources.http-events]
type = "http_server"
address = "0.0.0.0:8282"
encoding = "json"
# Uncomment for debug
# [sinks.console]
# type = "console"
# inputs = [ "http-events" ]
# target = "stdout"
# [sinks.console.encoding]
# codec = "json"
[sinks.clickhouse]
inputs = [ "http-events" ]
type = "clickhouse"
database = "${CLICKHOUSE_DB}"
table = "events"
auth.strategy = "basic"
auth.user = "${CLICKHOUSE_USER}"
auth.password = "${CLICKHOUSE_PASSWORD}"
endpoint = "http://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}"
request.concurrency = "adaptive"
encoding.only_fields = [
"scope",
"obj_name",
"obj_id",
"obj_val",
"source",
"timestamp",
"count",
"duration",
"project_id",
"task_id",
"job_id",
"user_id",
"user_name",
"user_email",
"org_id",
"org_slug",
"payload",
"access_token_id",
"remote_addr",
]
+8
View File
@@ -0,0 +1,8 @@
## Serverless for Computer Vision Annotation Tool (CVAT)
### Run docker container
```bash
# From project root directory
docker compose -f docker-compose.yml -f components/serverless/docker-compose.serverless.yml up -d
```
@@ -0,0 +1,30 @@
services:
nuclio:
container_name: nuclio
image: quay.io/nuclio/dashboard:1.16.3-amd64
restart: always
networks:
- cvat
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
http_proxy:
https_proxy:
no_proxy: ${no_proxy:-}
NUCLIO_CHECK_FUNCTION_CONTAINERS_HEALTHINESS: 'true'
NUCLIO_DASHBOARD_DEFAULT_FUNCTION_MOUNT_MODE: 'volume'
logging:
driver: "json-file"
options:
max-size: 100m
max-file: "3"
cvat_server:
environment:
CVAT_SERVERLESS: 1
extra_hosts:
- "host.docker.internal:host-gateway"
cvat_worker_annotation:
extra_hosts:
- "host.docker.internal:host-gateway"
+27
View File
@@ -0,0 +1,27 @@
// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
const { join } = require('path');
module.exports = {
ignorePatterns: [
'.eslintrc.cjs',
'webpack.config.js',
'node_modules/**',
'dist/**',
],
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
rules: {
'import/no-extraneous-dependencies': [
'error',
{
packageDir: [__dirname, join(__dirname, '../')]
},
],
}
};
+134
View File
@@ -0,0 +1,134 @@
# Module CVAT-CANVAS
## Description
The CVAT module written in TypeScript language.
It presents a canvas to viewing, drawing and editing of annotations.
## Commands
- Building of the module from sources in the `dist` directory:
```bash
yarn run build
yarn run build --mode=development # without a minification
```
### API Methods
For API methods, their arguments and return types, please look at ``canvas.ts``.
### API CSS
- All drawn objects (shapes, tracks) have an id `cvat_canvas_shape_{objectState.clientID}`
- Drawn shapes and tracks have classes `cvat_canvas_shape`,
`cvat_canvas_shape_activated`,
`cvat_canvas_shape_selection`,
`cvat_canvas_shape_merging`,
`cvat_canvas_shape_drawing`,
`cvat_canvas_shape_occluded`
- Drawn review ROIs have an id `cvat_canvas_issue_region_{issue.id}`
- Drawn review roi has the class `cvat_canvas_issue_region`
- Drawn texts have the class `cvat_canvas_text`
- Tags have the class `cvat_canvas_tag`
- Canvas image has ID `cvat_canvas_image`
- Grid on the canvas has ID `cvat_canvas_grid` and `cvat_canvas_grid_pattern`
- Crosshair during a draw has class `cvat_canvas_crosshair`
- To stick something to a specific position you can use an element with id `cvat_canvas_attachment_board`
### Events
Standard JS events are used.
```js
- canvas.setup
- canvas.activated => {state: ObjectState}
- canvas.clicked => {state: ObjectState}
- canvas.moved => {states: ObjectState[], x: number, y: number}
- canvas.find => {states: ObjectState[], x: number, y: number}
- canvas.drawn => {state: DrawnData}
- canvas.interacted => {shapes: InteractionResult[]}
- canvas.editstart
- canvas.edited => {state: ObjectState, points: number[], rotation?: number}
- canvas.splitted => {state: ObjectState, frame: number, duration: number}
- canvas.grouped => {states: ObjectState[], duration: number}
- canvas.joined => {states: ObjectState[], points: number[], duration: number}
- canvas.sliced => {state: ObjectState, results: number[][], duration: number}
- canvas.merged => {states: ObjectState[], duration: number}
- canvas.canceled
- canvas.dragstart
- canvas.dragstop
- canvas.zoomstart
- canvas.zoomstop
- canvas.zoom
- canvas.reshape
- canvas.fit
- canvas.regionselected => {points: number[]}
- canvas.dragshape => {duration: number, state: ObjectState}
- canvas.roiselected => {points: number[]}
- canvas.resizeshape => {duration: number, state: ObjectState}
- canvas.contextmenu => { mouseEvent: MouseEvent, objectState: ObjectState, pointID: number }
- canvas.message => { messages: { type: 'text' | 'list'; content: string | string[]; className?: string; icon?: 'info' | 'loading' }[] | null, topic: string }
- canvas.error => { exception: Error, domain?: string }
- canvas.destroy
```
### WEB
```js
// Create an instance of a canvas
const canvas = new window.canvas.Canvas();
console.log('Version ', window.canvas.CanvasVersion);
console.log('Current mode is ', window.canvas.mode());
// Put canvas to a html container
htmlContainer.appendChild(canvas.html());
canvas.fitCanvas();
// Next you can use its API methods. For example:
canvas.rotate(270);
canvas.draw({
enabled: true,
shapeType: 'rectangle',
crosshair: true,
rectDrawingMethod: window.Canvas.RectDrawingMethod.CLASSIC,
});
```
<!--lint disable maximum-line-length-->
## API Reaction
| | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS | INTERACT | JOIN | SLICE | SELECT_REGION |
| -------------- | ---- | ----- | ----- | ---- | ----- | ---- | ---- | ------ | ----------- | ----------- | -------- | ---- | ----- | ------------- |
| setup() | + | + | + | +/- | + | +/- | +/- | +/- | + | + | + | + | + | + |
| activate() | + | - | - | - | - | - | - | - | - | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
| draw() | + | - | - | + | - | - | - | - | - | - | - | - | - | - |
| interact() | + | - | - | - | - | - | - | - | - | - | + | - | - | - |
| split() | + | - | + | - | - | - | - | - | - | - | - | - | - | - |
| group() | + | + | - | - | - | - | - | - | - | - | - | - | - | - |
| merge() | + | - | - | - | + | - | - | - | - | - | - | - | - | - |
| edit() | + | - | - | - | - | + | - | - | - | - | - | - | - | - |
| join() | + | - | - | - | - | - | - | - | - | - | - | + | - | - |
| slice() | + | - | - | - | - | - | - | - | - | - | - | - | + | - |
| selectRegion() | + | - | - | - | - | - | - | - | - | - | - | - | - | + |
| fitCanvas() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
| dragCanvas() | + | - | - | - | - | - | + | - | - | + | - | - | - | - |
| zoomCanvas() | + | - | - | - | - | - | - | + | + | - | - | - | - | - |
| cancel() | - | + | + | + | + | + | + | + | + | + | + | + | + | + |
| configure() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
| bitmap() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
| destroy() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
<!--lint enable maximum-line-length-->
You can call setup() during editing, dragging, and resizing only to update objects, not to change a frame.
You can change frame during draw only when you do not redraw an existing object
Other methods do not change state and can be used at any time.
+28
View File
@@ -0,0 +1,28 @@
{
"name": "cvat-canvas",
"version": "2.20.10",
"type": "module",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts",
"scripts": {
"build": "tsc && webpack --config ./webpack.config.cjs"
},
"author": "CVAT.ai",
"license": "MIT",
"browserslist": [
"Chrome >= 99",
"Firefox >= 110",
"not IE 11",
"> 2%"
],
"dependencies": {
"@types/polylabel": "^1.0.5",
"martinez-polygon-clipping": "^0.8.1",
"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"
}
}
+484
View File
@@ -0,0 +1,484 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
/* stylelint-disable selector-class-pattern, selector-id-pattern */
.cvat_canvas_hidden {
display: none;
}
.cvat_canvas_shape {
stroke-opacity: 1;
}
g.cvat_canvas_shape {
> circle {
fill-opacity: 1;
}
}
polyline.cvat_canvas_shape {
fill-opacity: 0;
}
.cvat_shape_action_opacity {
fill-opacity: 0.5;
stroke-opacity: 1;
}
polyline.cvat_shape_action_opacity {
fill-opacity: 0;
}
.cvat_shape_drawing_opacity {
stroke-opacity: 1;
}
polyline.cvat_shape_drawing_opacity {
fill-opacity: 0;
}
.cvat_shape_action_dasharray {
stroke-dasharray: 4 1 2 3;
}
.cvat_canvas_text {
font-weight: bold;
fill: white;
cursor: default;
filter: drop-shadow(1px 1px 1px black) drop-shadow(-1px -1px 1px black);
user-select: none;
pointer-events: none;
}
.cvat_canvas_text_dimensions {
fill: lightskyblue;
}
.cvat_canvas_text_layer {
fill: #73d13d;
}
.cvat_canvas_text_description {
fill: yellow;
font-style: oblique 40deg;
}
.cvat_canvas_text_score {
fill: #FFB347;
font-style: oblique 10deg;
}
.cvat_canvas_crosshair {
stroke: red;
}
.cvat_canvas_shape_selection {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: #fcfbfc;
}
image.cvat_canvas_shape_selection {
visibility: hidden;
}
.cvat_canvas_selection_box {
fill: white;
fill-opacity: 0.1;
stroke: white;
}
.cvat_canvas_shape_region_selection {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: white;
stroke: white;
}
.cvat_canvas_issue_region {
pointer-events: none;
stroke-width: 0;
}
circle.cvat_canvas_issue_region {
opacity: 1 !important;
}
polyline.cvat_canvas_shape_selection {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: darkmagenta;
}
.cvat_canvas_shape_merging {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: blue;
> circle[data-node-id] {
fill: blue;
}
}
polyline.cvat_canvas_shape_merging {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: blue;
}
polyline.cvat_canvas_shape_splitting {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: dodgerblue;
}
.cvat_canvas_shape_splitting {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: dodgerblue;
}
.cvat_canvas_shape_drawing {
@extend .cvat_shape_drawing_opacity;
fill: white;
}
.cvat_canvas_zoom_selection {
@extend .cvat_shape_action_dasharray;
stroke: #096dd9;
fill-opacity: 0;
}
.cvat_canvas_shape_occluded {
stroke-dasharray: 5;
}
.cvat_canvas_ground_truth {
stroke-dasharray: 1;
}
.cvat_canvas_conflicted {
stroke: #ff4800;
fill: #ff4800;
rect,
ellipse,
polygon,
polyline,
line {
fill: #ff4800;
stroke: #ff4800;
}
circle {
fill: #ff4800;
}
}
.cvat_canvas_warned {
stroke: #ff7301;
fill: #ff7301;
rect,
ellipse,
polygon,
polyline,
line {
fill: #ff7301;
stroke: #ff7301;
}
circle {
fill: #ff7301;
}
}
.cvat_canvas_shape_occluded_point {
stroke-dasharray: 1 !important;
stroke: white;
}
circle.cvat_canvas_shape_occluded {
@extend .cvat_canvas_shape_occluded_point;
}
g.cvat_canvas_shape_occluded {
> rect {
stroke-dasharray: 5;
}
> circle {
@extend .cvat_canvas_shape_occluded_point;
}
}
.svg_select_points_rot {
fill: white;
}
.cvat_canvas_shape .svg_select_points,
.cvat_canvas_shape .cvat_canvas_cuboid_projections {
stroke-dasharray: none;
}
.cvat_canvas_autoborder_point {
transition: opacity 0.05s;
opacity: 1;
}
.cvat_canvas_autoborder_point:hover {
opacity: 0.5;
}
.cvat_canvas_autoborder_point:active {
opacity: 1;
}
.cvat_canvas_interact_intermediate_shape {
@extend .cvat_canvas_shape;
}
.cvat_interaction_delete_button {
opacity: 0.25;
&:hover {
opacity: 1;
}
}
.cvat_interaction_rectangle {
@extend .cvat_canvas_shape_drawing;
}
.cvat_interaction_delete_button, .cvat_interaction_point {
transition: stroke-width 0.2s ease-in-out, opacity 0.2s ease-in-out;
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 30%));
}
.cvat_canvas_removable_interaction_point {
cursor:
url(
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K'
) 10 10,
auto;
}
.svg_select_boundingRect {
opacity: 0;
pointer-events: none;
}
.svg_select_points_lb:hover,
.svg_select_points_rt:hover {
cursor: nesw-resize;
}
.svg_select_points_lt:hover,
.svg_select_points_rb:hover {
cursor: nwse-resize;
}
.svg_select_points_l:hover,
.svg_select_points_r:hover,
.svg_select_points_ew:hover {
cursor: ew-resize;
}
.svg_select_points_t:hover,
.svg_select_points_b:hover {
cursor: ns-resize;
}
.cvat_canvas_shape_draggable:hover {
cursor: move;
}
.cvat_canvas_first_poly_point {
fill: lightgray;
}
.cvat_canvas_poly_direction {
fill: lightgray;
stroke: black;
&:hover {
fill: black;
stroke: lightgray;
}
&:active {
fill: lightgray;
stroke: black;
}
}
.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_shape > .cvat_canvas_skeleton_wrapping_rect {
visibility: hidden;
}
.cvat_canvas_shape.cvat_canvas_shape_activated > .cvat_canvas_skeleton_wrapping_rect {
visibility: initial;
}
.cvat_canvas_pixelized {
image-rendering: optimizeSpeed;
image-rendering: optimize-contrast;
image-rendering: crisp-edges;
image-rendering: pixelated;
}
.cvat_canvas_removed_image {
filter: saturate(0) brightness(1.2) contrast(0.75) !important;
}
#cvat_canvas_wrapper {
width: calc(100% - 10px);
height: calc(100% - 10px);
margin: 5px;
border-radius: 5px;
background-color: inherit;
overflow: hidden;
position: relative;
}
.cvat-canvas-highlight-enabled {
svg {
>rect:not(.cvat_canvas_issue_region,.cvat_canvas_conflicted,.cvat_canvas_warned),
>ellipse:not(.cvat_canvas_issue_region,.cvat_canvas_conflicted,.cvat_canvas_warned),
>polygon:not(.cvat_canvas_issue_region,.cvat_canvas_conflicted,.cvat_canvas_warned),
>polyline:not(.cvat_canvas_issue_region,.cvat_canvas_conflicted,.cvat_canvas_warned),
>line:not(.cvat_canvas_issue_region,.cvat_canvas_conflicted,.cvat_canvas_warned) {
fill: gray;
stroke: gray;
}
>circle:not(.cvat_canvas_issue_region,.cvat_canvas_conflicted,.cvat_canvas_warned) {
fill: gray;
}
>g:not(.cvat_canvas_issue_region,.cvat_canvas_conflicted,.cvat_canvas_warned) {
rect,
ellipse,
polygon,
polyline,
line {
fill: gray;
stroke: gray;
}
circle {
fill: gray;
}
}
}
}
#cvat_canvas_text_content {
text-rendering: optimizeSpeed;
position: absolute;
z-index: 3;
pointer-events: none;
width: 100%;
height: 100%;
}
#cvat_canvas_background {
position: absolute;
z-index: 0;
background-repeat: no-repeat;
width: 100%;
height: 100%;
box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 75%);
}
#cvat_canvas_bitmap {
@extend .cvat_canvas_pixelized;
pointer-events: none;
position: absolute;
z-index: 4;
background: black;
width: 100%;
height: 100%;
box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 75%);
}
#cvat_canvas_grid {
position: absolute;
z-index: 2;
pointer-events: none;
width: 100%;
height: 100%;
}
#cvat_canvas_grid_pattern {
opacity: 1;
stroke: white;
}
#cvat_canvas_content {
@extend .cvat_canvas_pixelized;
filter: contrast(120%) saturate(150%);
position: absolute;
z-index: 2;
outline: 10px solid black;
width: 100%;
height: 100%;
}
.cvat_masks_canvas_wrapper {
@extend .cvat_canvas_pixelized;
z-index: 3;
display: none;
}
#cvat_canvas_attachment_board {
position: absolute;
z-index: 4;
pointer-events: none;
width: 100%;
height: 100%;
user-select: none;
}
.cvat_canvas_shape_darken {
fill: #838383;
stroke: #838383;
}
.cvat_canvas_sliced_contour {
fill-opacity: 0.01;
}
.cvat_canvas_slicing_line {
pointer-events: none;
fill-opacity: 0;
}
.cvat-canvas-notification-list-warning {
color: orange;
}
.cvat-canvas-notification-list-shortcuts {
color: yellow;
}
@@ -0,0 +1,505 @@
// Copyright (C) CVAT.ai Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
import { Configuration, Geometry } from './canvasModel';
import { translateToSVG } from './shared';
interface TransformedShape {
points: number[][];
color: string;
}
export interface AutoborderHandler {
autoborder(enabled: boolean, currentShape?: SVG.Shape, excludedClientId?: number): void;
configure(configuration: Configuration): void;
transform(geometry: Geometry): void;
updateObjects(): void;
}
function collectSegmentPoints(
points: number[][],
startPointID: number,
endPointID: number,
direction: -1 | 1 | null,
): number[][] {
if (startPointID === endPointID) {
return [];
}
if (
Math.abs(startPointID - endPointID) === 1 ||
Math.abs(startPointID - endPointID) === points.length - 1
) {
// adjacent points, no need to calculate anything
return [points[endPointID]];
}
const walk = (step: number): {
length: number;
points: number[][];
} => {
let length = 0;
let previousPoint = points[startPointID];
const curvePoints: number[][] = [];
for (let i = startPointID + step; ; i += step) {
if (i < 0) {
i = points.length - 1;
} else if (i === points.length) {
i = 0;
}
const currentPoint = points[i];
const dx = currentPoint[0] - previousPoint[0];
const dy = currentPoint[1] - previousPoint[1];
length += Math.hypot(dx, dy);
curvePoints.push([...currentPoint]);
if (i === endPointID) {
break;
}
previousPoint = currentPoint;
}
return {
length,
points: curvePoints,
};
};
if (direction === 1) {
return walk(1).points;
}
if (direction === -1) {
return walk(-1).points;
}
const forward = walk(1);
const backward = walk(-1);
return forward.length <= backward.length ? forward.points : backward.points;
}
type PointIndexesAndDirection = {
addedPointIndexes: Set<number>;
direction: -1 | 1 | null;
};
function collectAddedPointIndexesAndDirection(
points: number[][],
pointsToRevertPreview: number[][],
): PointIndexesAndDirection {
const pointKey = (point: number[]): string => `${point[0]},${point[1]}`;
const addedPointIndexes = new Set<number>();
const indexesByPointKey = new Map<string, number[]>();
for (let i = 0; i < points.length; i++) {
const key = pointKey(points[i]);
const indexes = indexesByPointKey.get(key);
if (indexes) {
indexes.push(i);
} else {
indexesByPointKey.set(key, [i]);
}
}
const originalPointKeys = new Set(pointsToRevertPreview.map(pointKey));
for (let i = 0; i < points.length; i++) {
if (originalPointKeys.has(pointKey(points[i]))) {
addedPointIndexes.add(i);
}
}
if (points.length < 2 || pointsToRevertPreview.length < 2) {
return {
addedPointIndexes,
direction: null,
};
}
const previousKey = pointKey(pointsToRevertPreview[pointsToRevertPreview.length - 2]);
const lastKey = pointKey(pointsToRevertPreview[pointsToRevertPreview.length - 1]);
const previousIndexes = indexesByPointKey.get(previousKey) || [];
const lastIndexes = new Set(indexesByPointKey.get(lastKey) || []);
for (const previousIndex of previousIndexes) {
const nextIndex = (previousIndex + 1) % points.length;
const prevIndex = (previousIndex - 1 + points.length) % points.length;
if (lastIndexes.has(nextIndex)) {
return {
addedPointIndexes,
direction: 1,
};
}
if (lastIndexes.has(prevIndex)) {
return {
addedPointIndexes,
direction: -1,
};
}
}
return {
addedPointIndexes,
direction: null,
};
}
export class AutoborderHandlerImpl implements AutoborderHandler {
private currentShape: SVG.Shape | null;
private excludedClientId?: number;
private container: SVGSVGElement;
private pointGroups: SVGGElement[];
private enabled: boolean;
private scale: number;
private controlPointsSize: number;
private visibleShapes: TransformedShape[];
private currentClick: { groupIdx: number; pointIdx: number; } | null;
private currentPreview: { pointIdx: number; shapePoints: number[][]; } | null;
private pointsToRevertPreview: number[][] | null;
private listeners: Map<SVGCircleElement, { mousedown: (event: MouseEvent) => void; }>;
private isCtrlKeyDown: (() => boolean) | null;
public constructor(container: SVGSVGElement, isCtrlKeyDown: () => boolean) {
this.currentShape = null;
this.excludedClientId = undefined;
this.container = container;
this.pointGroups = [];
this.enabled = false;
this.scale = 1;
this.controlPointsSize = consts.BASE_POINT_SIZE;
this.visibleShapes = [];
this.currentClick = null;
this.currentPreview = null;
this.pointsToRevertPreview = null;
this.listeners = new Map();
this.isCtrlKeyDown = isCtrlKeyDown;
}
private removeMarkers(): void {
this.pointGroups.forEach((group: SVGGElement): void => {
Array.from(group.children).forEach((circle: SVGCircleElement): void => {
const listeners = this.listeners.get(circle);
if (listeners) {
circle.removeEventListener('mousedown', listeners.mousedown);
}
circle.remove();
});
group.remove();
});
this.pointGroups = [];
this.currentClick = null;
this.listeners.clear();
}
private readCurrentShapePoints(): number[][] {
return (this.currentShape as any).array().valueOf().map(
(shapePoint: number[]): number[] => [...shapePoint],
);
}
private replaceCurrentShapePoints(points: number[][]): void {
const paintHandler = this.currentShape?.remember('_paintHandler');
if (!paintHandler) {
return;
}
const lastPoint = (this.currentShape as any).array().valueOf().slice(-1)[0];
(this.currentShape as any).plot([...points, lastPoint]);
paintHandler.drawCircles();
paintHandler.set.members.forEach((el: SVG.Circle): void => {
el.attr('stroke-width', 1 / this.scale).attr('r', 2.5 / this.scale);
el.attr('r', `${this.controlPointsSize / this.scale}`);
});
}
private raiseMarkers(): void {
for (let i = 0; i < this.pointGroups.length; i++) {
this.container.appendChild(this.pointGroups[i]);
}
if (this.currentClick !== null) {
this.container.appendChild(this.pointGroups[this.currentClick.groupIdx]);
}
}
private drawMarkers(): void {
this.removeMarkers();
const ns = 'http://www.w3.org/2000/svg';
this.pointGroups = this.visibleShapes.map(
(shape: TransformedShape, groupIdx: number): SVGGElement => {
const group = document.createElementNS(ns, 'g');
const circles = shape.points.map(
(point: number[], pointIdx: number): SVGCircleElement => {
const [x, y] = point;
const circle = document.createElementNS(ns, 'circle');
circle.classList.add('cvat_canvas_autoborder_point');
circle.setAttribute('fill', shape.color);
circle.setAttribute('stroke', 'black');
circle.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.scale}`);
circle.setAttribute('cx', `${x}`);
circle.setAttribute('cy', `${y}`);
circle.setAttribute('r', `${this.controlPointsSize / this.scale}`);
const mousedown = (event: MouseEvent): void => {
if (event.button !== 0) {
return;
}
if (this.isCtrlKeyDown && this.isCtrlKeyDown()) {
return;
}
event.stopPropagation();
if (this.currentClick?.groupIdx !== groupIdx) {
// first click on this group of points
// svg.draw.js initializes the internal paint handler lazily,
// so the first autoborder click needs to bootstrap it first.
const handler = this.currentShape.remember('_paintHandler');
if (!handler || !handler.startPoint) {
(this.currentShape as any).draw('point', event);
(this.currentShape as any).draw('undo');
}
this.container.appendChild(group); // raise in DOM over other groups
const originalPoints = this.readCurrentShapePoints().slice(0, -1);
this.replaceCurrentShapePoints([...originalPoints, [x, y]]);
this.currentClick = { groupIdx, pointIdx };
}
};
circle.addEventListener('mousedown', mousedown);
this.listeners.set(circle, { mousedown });
return circle;
},
);
group.append(...circles);
return group;
},
);
this.container.append(...this.pointGroups);
this.raiseMarkers();
}
private findClosestPointInClickedGroup(e: MouseEvent): number {
let closestPointIdx = -1;
if (this.currentClick === null) {
// group not selected, do nothing
return closestPointIdx;
}
const { points } = this.visibleShapes[this.currentClick.groupIdx];
const [x, y] = translateToSVG(this.container, [e.clientX, e.clientY]);
let distance = (consts.BASE_POINT_SIZE * 2) / this.scale;
for (let i = 0; i < points.length; i++) {
const point = points[i];
const dx = point[0] - x;
const dy = point[1] - y;
const currentDistance = Math.hypot(dx, dy);
if (currentDistance <= distance) {
distance = currentDistance;
closestPointIdx = i;
}
}
return closestPointIdx;
}
private onContainerMouseMove = (e: MouseEvent): void => {
if (this.isCtrlKeyDown && this.isCtrlKeyDown()) {
return;
}
const closestPointIdx = this.findClosestPointInClickedGroup(e);
if (this.pointsToRevertPreview) {
this.replaceCurrentShapePoints(this.pointsToRevertPreview);
this.pointsToRevertPreview = null;
this.currentPreview = null;
}
if (closestPointIdx === -1) {
return;
}
const { points } = this.visibleShapes[this.currentClick.groupIdx];
const {
addedPointIndexes,
direction,
} = collectAddedPointIndexesAndDirection(points, this.readCurrentShapePoints().slice(0, -1));
if (addedPointIndexes.has(closestPointIdx)) {
return;
}
const curvePoints = collectSegmentPoints(points, this.currentClick.pointIdx, closestPointIdx, direction);
this.pointsToRevertPreview = this.readCurrentShapePoints().slice(0, -1);
this.currentPreview = {
pointIdx: closestPointIdx,
shapePoints: [...this.pointsToRevertPreview, ...curvePoints],
};
this.replaceCurrentShapePoints(this.currentPreview.shapePoints);
};
private onContainerMouseDown = (e: MouseEvent): void => {
if (this.isCtrlKeyDown && this.isCtrlKeyDown()) {
return;
}
if (e.button !== 0 || this.currentPreview === null) {
return;
}
const closestPointIdx = this.findClosestPointInClickedGroup(e);
if (closestPointIdx === this.currentPreview.pointIdx) {
e.stopPropagation();
this.currentPreview = null;
this.pointsToRevertPreview = null;
this.currentClick.pointIdx = closestPointIdx;
}
};
private setObjectsFromContainer(): void {
const shapes = Array.from(this.container.getElementsByClassName('cvat_canvas_shape')).filter(
(shape: HTMLElement): boolean => !shape.classList.contains('cvat_canvas_hidden'),
);
this.visibleShapes = shapes.map((shape: HTMLElement): TransformedShape | null => {
const color = shape.getAttribute('fill');
const clientId = shape.getAttribute('clientID');
const isSupportedType =
shape.tagName === 'polyline' ||
shape.tagName === 'polygon' ||
shape.tagName === 'rect';
if (
color === null || clientId === null || !isSupportedType ||
(typeof this.excludedClientId === 'number' && +clientId === this.excludedClientId)
) {
return null;
}
let points = '';
if (shape.tagName === 'polyline' || shape.tagName === 'polygon') {
points = shape.getAttribute('points');
} else if (shape.tagName === 'rect') {
const x = +shape.getAttribute('x');
const y = +shape.getAttribute('y');
const width = +shape.getAttribute('width');
const height = +shape.getAttribute('height');
if (Number.isNaN(x) || Number.isNaN(y) || Number.isNaN(width) || Number.isNaN(height)) {
return null;
}
const svgElement = shape as unknown as SVGGraphicsElement;
const ctm = svgElement.getCTM();
const localCorners = [
{ x, y },
{ x: x + width, y },
{ x: x + width, y: y + height },
{ x, y: y + height },
];
if (ctm && (ctm.a !== 1 || ctm.b !== 0 || ctm.c !== 0 || ctm.d !== 1)) {
const transformedCorners = localCorners.map((corner) => {
const transformedX = ctm.a * corner.x + ctm.c * corner.y + ctm.e;
const transformedY = ctm.b * corner.x + ctm.d * corner.y + ctm.f;
return [transformedX, transformedY];
});
return {
color,
points: transformedCorners,
};
}
points = `${x},${y} ${x + width},${y} ${x + width},${y + height} ${x},${y + height}`;
}
return {
color,
points: points.trim().split(/\s/).map(
(shapePoint: string): number[] => shapePoint.split(',')
.map((coordinate: string): number => +coordinate),
),
};
}).filter((state: TransformedShape | null): boolean => state !== null);
}
public updateObjects(): void {
if (this.enabled) {
this.setObjectsFromContainer();
this.drawMarkers();
}
}
public autoborder(enabled: boolean, currentShape?: SVG.Shape, excludedClientId?: number): void {
if (enabled && !this.enabled && currentShape) {
this.enabled = true;
this.currentShape = currentShape;
this.excludedClientId = excludedClientId;
this.updateObjects();
this.container.addEventListener('mousemove', this.onContainerMouseMove);
this.container.addEventListener('mousedown', this.onContainerMouseDown, { capture: true });
this.currentShape.on('undopoint', (): void => {
this.currentClick = null;
this.currentPreview = null;
if (this.pointsToRevertPreview) {
this.replaceCurrentShapePoints(this.pointsToRevertPreview);
this.pointsToRevertPreview = null;
}
});
this.currentShape.on('drawpoint', (): void => {
this.currentClick = null;
this.currentPreview = null;
this.pointsToRevertPreview = null;
});
} else {
this.container.removeEventListener('mousemove', this.onContainerMouseMove);
this.container.removeEventListener('mousedown', this.onContainerMouseDown);
this.removeMarkers();
this.enabled = false;
this.currentShape = null;
}
}
public transform(geometry: Geometry): void {
this.scale = geometry.scale;
this.pointGroups.forEach((group: SVGGElement): void => {
Array.from(group.children).forEach((circle: SVGCircleElement): void => {
circle.setAttribute('r', `${this.controlPointsSize / this.scale}`);
circle.setAttribute('stroke-width', `${consts.BASE_STROKE_WIDTH / this.scale}`);
});
});
}
public configure(configuration: Configuration): void {
this.controlPointsSize = configuration.controlPointsSize ?? consts.BASE_POINT_SIZE;
}
}
+204
View File
@@ -0,0 +1,204 @@
// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import {
DrawData, MergeData, SplitData, GroupData,
JoinData, SliceData, MasksEditData,
InteractionData as _InteractionData,
InteractionResult as _InteractionResult,
CanvasModel, CanvasModelImpl, RectDrawingMethod,
CuboidDrawingMethod, Configuration, Geometry, Mode,
HighlightSeverity as _HighlightSeverity, CanvasHint as _CanvasHint,
PolyEditData, RenderData as _RenderData,
} from './canvasModel';
import { Master } from './master';
import { CanvasController, CanvasControllerImpl } from './canvasController';
import { CanvasView, CanvasViewImpl } from './canvasView';
import '../scss/canvas.scss';
interface Canvas {
html(): HTMLDivElement;
setup(frameData: any, objectStates: any[], renderData?: RenderData): void;
setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void;
translateFromSVG(points: number[]): number[];
setupConflictRegions(clientID: number): number[];
activate(clientID: number | null, attributeID?: number): void;
highlight(clientIDs: number[] | null, severity: HighlightSeverity | null): void;
rotate(rotationAngle: number): void;
focus(clientID: number, padding?: number): void;
fit(): void;
grid(stepX: number, stepY: number): void;
interact(interactionData: InteractionData): void;
draw(drawData: DrawData): void;
edit(editData: MasksEditData | PolyEditData): void;
group(groupData: GroupData): void;
join(joinData: JoinData): void;
slice(sliceData: SliceData): void;
split(splitData: SplitData): void;
merge(mergeData: MergeData): void;
select(objectState: any): void;
fitCanvas(): void;
bitmap(enable: boolean): void;
selectRegion(enable: boolean): void;
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
mode(): Mode;
cancel(): void;
configure(configuration: Configuration): void;
isAbleToChangeFrame(): boolean;
destroy(): void;
readonly geometry: Geometry;
}
class CanvasImpl implements Canvas {
private model: CanvasModel & Master;
private controller: CanvasController;
private view: CanvasView;
public constructor() {
this.model = new CanvasModelImpl();
this.controller = new CanvasControllerImpl(this.model);
this.view = new CanvasViewImpl(this.model, this.controller);
}
public html(): HTMLDivElement {
return this.view.html();
}
public setup(frameData: any, objectStates: any[], renderData?: RenderData): void {
this.model.setup(frameData, objectStates, renderData);
}
public setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void {
this.model.setupIssueRegions(issueRegions);
}
public translateFromSVG(points: number[]): number[] {
return this.view.translateFromSVG(points);
}
public setupConflictRegions(clientID: number): number[] {
return this.view.setupConflictRegions(clientID);
}
public fitCanvas(): void {
this.model.fitCanvas(this.view.html().clientWidth, this.view.html().clientHeight);
}
public bitmap(enable: boolean): void {
this.model.bitmap(enable);
}
public selectRegion(enable: boolean): void {
this.model.selectRegion(enable);
}
public dragCanvas(enable: boolean): void {
this.model.dragCanvas(enable);
}
public zoomCanvas(enable: boolean): void {
this.model.zoomCanvas(enable);
}
public activate(clientID: number | null, attributeID: number | null = null): void {
this.model.activate(clientID, attributeID);
}
public highlight(clientIDs: number[], severity: HighlightSeverity | null = null): void {
this.model.highlight(clientIDs, severity);
}
public rotate(rotationAngle: number): void {
this.model.rotate(rotationAngle);
}
public focus(clientID: number, padding = 0): void {
this.model.focus(clientID, padding);
}
public fit(): void {
this.model.fit();
}
public grid(stepX: number, stepY: number): void {
this.model.grid(stepX, stepY);
}
public interact(interactionData: InteractionData): void {
this.model.interact(interactionData);
}
public draw(drawData: DrawData): void {
this.model.draw(drawData);
}
public edit(editData: MasksEditData | PolyEditData): void {
this.model.edit(editData);
}
public split(splitData: SplitData): void {
this.model.split(splitData);
}
public group(groupData: GroupData): void {
this.model.group(groupData);
}
public join(joinData: JoinData): void {
this.model.join(joinData);
}
public slice(sliceData: SliceData): void {
this.model.slice(sliceData);
}
public merge(mergeData: MergeData): void {
this.model.merge(mergeData);
}
public select(objectState: any): void {
this.model.select(objectState);
}
public mode(): Mode {
return this.model.mode;
}
public cancel(): void {
this.model.cancel();
}
public configure(configuration: Configuration): void {
this.model.configure(configuration);
}
public isAbleToChangeFrame(): boolean {
return this.model.isAbleToChangeFrame();
}
public get geometry(): Geometry {
return this.model.geometry;
}
public destroy(): void {
this.model.destroy();
}
}
export type InteractionData = _InteractionData;
export type CanvasHint = _CanvasHint;
export type InteractionResult = _InteractionResult;
export type HighlightSeverity = _HighlightSeverity;
export type RenderData = _RenderData;
export {
CanvasImpl as Canvas, RectDrawingMethod, CuboidDrawingMethod, Mode as CanvasMode,
};
@@ -0,0 +1,189 @@
// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import {
CanvasModel,
Geometry,
Position,
FocusData,
ActiveElement,
DrawData,
MergeData,
SplitData,
GroupData,
JoinData,
SliceData,
Mode,
InteractionData,
Configuration,
MasksEditData,
HighlightedElements,
PolyEditData,
RenderData,
} from './canvasModel';
export interface CanvasController {
readonly objects: any[];
readonly renderData: RenderData;
readonly issueRegions: Record<number, { hidden: boolean; points: number[] }>;
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
readonly highlightedElements: HighlightedElements;
readonly drawData: DrawData;
readonly editData: MasksEditData | PolyEditData;
readonly interactionData: InteractionData;
readonly mergeData: MergeData;
readonly splitData: SplitData;
readonly groupData: GroupData;
readonly joinData: JoinData;
readonly sliceData: SliceData;
readonly selected: any;
readonly configuration: Configuration;
mode: Mode;
geometry: Geometry;
zoom(x: number, y: number, deltaY: number): void;
draw(drawData: DrawData): void;
edit(editData: MasksEditData | PolyEditData): void;
enableDrag(x: number, y: number): void;
drag(x: number, y: number): void;
disableDrag(): void;
fit(): void;
focus(clientId: number, padding: number): void;
}
export class CanvasControllerImpl implements CanvasController {
private model: CanvasModel;
private lastDragPosition: Position;
private isDragging: boolean;
public constructor(model: CanvasModel) {
this.model = model;
}
public zoom(x: number, y: number, deltaY: number): void {
this.model.zoom(x, y, deltaY);
}
public fit(): void {
this.model.fit();
}
public enableDrag(x: number, y: number): void {
this.lastDragPosition = {
x,
y,
};
this.isDragging = true;
}
public drag(x: number, y: number): void {
if (this.isDragging) {
const topOffset: number = y - this.lastDragPosition.y;
const leftOffset: number = x - this.lastDragPosition.x;
this.lastDragPosition = {
x,
y,
};
this.model.move(topOffset, leftOffset);
}
}
public disableDrag(): void {
this.isDragging = false;
}
public draw(drawData: DrawData): void {
this.model.draw(drawData);
}
public edit(editData: MasksEditData | PolyEditData): void {
this.model.edit(editData);
}
public focus(clientID: number, padding: number): void {
this.model.focus(clientID, padding);
}
public get geometry(): Geometry {
return this.model.geometry;
}
public set geometry(geometry: Geometry) {
this.model.geometry = geometry;
}
public get issueRegions(): Record<number, { hidden: boolean; points: number[] }> {
return this.model.issueRegions;
}
public get objects(): any[] {
return this.model.objects;
}
public get renderData(): RenderData {
return this.model.renderData;
}
public get focusData(): FocusData {
return this.model.focusData;
}
public get activeElement(): ActiveElement {
return this.model.activeElement;
}
public get highlightedElements(): HighlightedElements {
return this.model.highlightedElements;
}
public get drawData(): DrawData {
return this.model.drawData;
}
public get editData(): MasksEditData | PolyEditData {
return this.model.editData;
}
public get interactionData(): InteractionData {
return this.model.interactionData;
}
public get mergeData(): MergeData {
return this.model.mergeData;
}
public get splitData(): SplitData {
return this.model.splitData;
}
public get groupData(): GroupData {
return this.model.groupData;
}
public get joinData(): JoinData {
return this.model.joinData;
}
public get sliceData(): SliceData {
return this.model.sliceData;
}
public get selected(): any {
return this.model.selected;
}
public get configuration(): Configuration {
return this.model.configuration;
}
public set mode(value: Mode) {
this.model.mode = value;
}
public get mode(): Mode {
return this.model.mode;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+59
View File
@@ -0,0 +1,59 @@
// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
const BASE_STROKE_WIDTH = 1.25;
const BASE_GRID_WIDTH = 2;
const BASE_POINT_SIZE = 4;
const TEXT_MARGIN = 10;
const SIZE_THRESHOLD = 1;
const POINTS_STROKE_WIDTH = 1;
const POINTS_SELECTED_STROKE_WIDTH = 4;
const MIN_EDGE_LENGTH = 3;
const CUBOID_ACTIVE_EDGE_STROKE_WIDTH = 2.5;
const CUBOID_UNACTIVE_EDGE_STROKE_WIDTH = 1.75;
const UNDEFINED_ATTRIBUTE_VALUE = '__undefined__';
const ARROW_PATH = 'M13.162 6.284L.682.524a.483.483 0 0 0-.574.134.477.477 0 ' +
'0 0-.012.59L4.2 6.72.096 12.192a.479.479 0 0 0 .585.724l12.48-5.76a.48.48 0 0 0 0-.872z';
const BASE_PATTERN_SIZE = 5;
const SNAP_TO_ANGLE_RESIZE_DEFAULT = 0.1;
const SNAP_TO_ANGLE_RESIZE_SHIFT = 15;
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__';
const CONFLICT_COLOR = '#ff4800';
const WARNING_COLOR = '#ff7301';
const SHADED_COLOR = '#808080';
export default {
BASE_STROKE_WIDTH,
BASE_GRID_WIDTH,
BASE_POINT_SIZE,
TEXT_MARGIN,
SIZE_THRESHOLD,
POINTS_STROKE_WIDTH,
POINTS_SELECTED_STROKE_WIDTH,
MIN_EDGE_LENGTH,
CUBOID_ACTIVE_EDGE_STROKE_WIDTH,
CUBOID_UNACTIVE_EDGE_STROKE_WIDTH,
UNDEFINED_ATTRIBUTE_VALUE,
ARROW_PATH,
BASE_PATTERN_SIZE,
SNAP_TO_ANGLE_RESIZE_DEFAULT,
SNAP_TO_ANGLE_RESIZE_SHIFT,
DEFAULT_SHAPE_TEXT_SIZE,
DEFAULT_SHAPE_TEXT_CONTENT,
DEFAULT_SHAPE_TEXT_POSITION,
DEFAULT_UNDEFINED_ATTR_VALUE,
MINIMUM_TEXT_FONT_SIZE,
SKELETON_RECT_MARGIN,
CONFLICT_COLOR,
WARNING_COLOR,
SHADED_COLOR,
};
+81
View File
@@ -0,0 +1,81 @@
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
export default class Crosshair {
private x: SVG.Line | null;
private y: SVG.Line | null;
private canvas: SVG.Container | null;
public constructor() {
this.x = null;
this.y = null;
this.canvas = null;
}
public show(canvas: SVG.Container, x: number, y: number, scale: number): void {
if (this.canvas && this.canvas !== canvas) {
if (this.x) this.x.remove();
if (this.y) this.y.remove();
this.x = null;
this.y = null;
}
if (this.x && this.y) {
this.move(x, y);
return;
}
this.canvas = canvas;
this.x = this.canvas
.line(0, y, this.canvas.node.clientWidth, y)
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * scale),
})
.addClass('cvat_canvas_crosshair');
this.y = this.canvas
.line(x, 0, x, this.canvas.node.clientHeight)
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * scale),
})
.addClass('cvat_canvas_crosshair');
}
public hide(): void {
if (this.x) {
this.x.remove();
this.x = null;
}
if (this.y) {
this.y.remove();
this.y = null;
}
this.canvas = null;
}
public move(x: number, y: number): void {
if (this.x) {
this.x.attr({ y1: y, y2: y });
}
if (this.y) {
this.y.attr({ x1: x, x2: x });
}
}
public scale(scale: number): void {
if (this.x) {
this.x.attr('stroke-width', consts.BASE_STROKE_WIDTH / (2 * scale));
}
if (this.y) {
this.y.attr('stroke-width', consts.BASE_STROKE_WIDTH / (2 * scale));
}
}
}
+484
View File
@@ -0,0 +1,484 @@
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import consts from './consts';
import { Point } from './shared';
export enum Orientation {
LEFT = 'left',
RIGHT = 'right',
}
export function intersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null {
// Check if none of the lines are of length 0
const { x: x1, y: y1 } = p1;
const { x: x2, y: y2 } = p2;
const { x: x3, y: y3 } = p3;
const { x: x4, y: y4 } = p4;
if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {
return null;
}
const denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
// Lines are parallel
if (Math.abs(denominator) < Number.EPSILON) {
return null;
}
const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
// Return a object with the x and y coordinates of the intersection
return { x: x1 + ua * (x2 - x1), y: y1 + ua * (y2 - y1) };
}
export class Equation {
private a: number;
private b: number;
private c: number;
public constructor(p1: Point, p2: Point) {
this.a = p1.y - p2.y;
this.b = p2.x - p1.x;
this.c = this.b * p1.y + this.a * p1.x;
}
// get the line equation in actual coordinates
public getY(x: number): number {
return (this.c - this.a * x) / this.b;
}
}
export class Figure {
private indices: number[];
private allPoints: Point[];
public constructor(indices: number[], points: Point[]) {
this.indices = indices;
this.allPoints = points;
}
public get points(): Point[] {
const points = [];
for (const index of this.indices) {
points.push(this.allPoints[index]);
}
return points;
}
// sets the point for a given edge, points must be given in
// array form in the same ordering as the getter
// if you only need to update a subset of the points,
// simply put null for the points you want to keep
public set points(newPoints) {
const oldPoints = this.allPoints;
for (let i = 0; i < newPoints.length; i += 1) {
if (newPoints[i] !== null) {
oldPoints[this.indices[i]] = { x: newPoints[i].x, y: newPoints[i].y };
}
}
}
}
export class Edge extends Figure {
public getEquation(): Equation {
return new Equation(this.points[0], this.points[1]);
}
}
export class CuboidModel {
public points: Point[];
private fr: Edge;
private fl: Edge;
private dr: Edge;
private dl: Edge;
private ft: Edge;
private rt: Edge;
private lt: Edge;
private dt: Edge;
private fb: Edge;
private rb: Edge;
private lb: Edge;
private db: Edge;
public edgeList: Edge[];
private front: Figure;
private right: Figure;
private dorsal: Figure;
private left: Figure;
private top: Figure;
private bot: Figure;
public facesList: Figure[];
public vpl: Point | null;
public vpr: Point | null;
public orientation: Orientation;
public constructor(points?: Point[]) {
this.points = points;
this.initEdges();
this.initFaces();
this.updateVanishingPoints(false);
this.buildBackEdge(false);
this.updatePoints();
this.updateOrientation();
}
public getPoints(): Point[] {
return this.points;
}
public setPoints(points: (Point | null)[]): void {
points.forEach((point: Point | null, i: number): void => {
if (point !== null) {
this.points[i].x = point.x;
this.points[i].y = point.y;
}
});
}
public updateOrientation(): void {
if (this.dl.points[0].x > this.fl.points[0].x) {
this.orientation = Orientation.LEFT;
} else {
this.orientation = Orientation.RIGHT;
}
}
public updatePoints(): void {
// making sure that the edges are vertical
this.fr.points[0].x = this.fr.points[1].x;
this.fl.points[0].x = this.fl.points[1].x;
this.dr.points[0].x = this.dr.points[1].x;
this.dl.points[0].x = this.dl.points[1].x;
}
public computeSideEdgeConstraints(edge: any): any {
const midLength = this.fr.points[1].y - this.fr.points[0].y - 1;
const minY = edge.points[1].y - midLength;
const maxY = edge.points[0].y + midLength;
const y1 = edge.points[0].y;
const y2 = edge.points[1].y;
const miny1 = y2 - midLength;
const maxy1 = y2 - consts.MIN_EDGE_LENGTH;
const miny2 = y1 + consts.MIN_EDGE_LENGTH;
const maxy2 = y1 + midLength;
return {
constraint: {
minY,
maxY,
},
y1Range: {
max: maxy1,
min: miny1,
},
y2Range: {
max: maxy2,
min: miny2,
},
};
}
// boolean value parameter controls which edges should be used to recalculate vanishing points
private updateVanishingPoints(buildright: boolean): void {
let leftEdge = [];
let rightEdge = [];
let midEdge = [];
if (buildright) {
leftEdge = this.fr.points;
rightEdge = this.dl.points;
midEdge = this.fl.points;
} else {
leftEdge = this.fl.points;
rightEdge = this.dr.points;
midEdge = this.fr.points;
}
this.vpl = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
this.vpr = intersection(rightEdge[0], midEdge[0], rightEdge[1], midEdge[1]);
if (this.vpl === null) {
// shift the edge slightly to avoid edge case
leftEdge[0].y -= 0.001;
leftEdge[0].x += 0.001;
leftEdge[1].x += 0.001;
this.vpl = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
}
if (this.vpr === null) {
// shift the edge slightly to avoid edge case
rightEdge[0].y -= 0.001;
rightEdge[0].x -= 0.001;
rightEdge[1].x -= 0.001;
this.vpr = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
}
}
private initEdges(): void {
this.fl = new Edge([0, 1], this.points);
this.fr = new Edge([2, 3], this.points);
this.dr = new Edge([4, 5], this.points);
this.dl = new Edge([6, 7], this.points);
this.ft = new Edge([0, 2], this.points);
this.lt = new Edge([0, 6], this.points);
this.rt = new Edge([2, 4], this.points);
this.dt = new Edge([6, 4], this.points);
this.fb = new Edge([1, 3], this.points);
this.lb = new Edge([1, 7], this.points);
this.rb = new Edge([3, 5], this.points);
this.db = new Edge([7, 5], this.points);
this.edgeList = [
this.fl,
this.fr,
this.dl,
this.dr,
this.ft,
this.lt,
this.rt,
this.dt,
this.fb,
this.lb,
this.rb,
this.db,
];
}
private initFaces(): void {
this.front = new Figure([0, 1, 3, 2], this.points);
this.right = new Figure([2, 3, 5, 4], this.points);
this.dorsal = new Figure([4, 5, 7, 6], this.points);
this.left = new Figure([6, 7, 1, 0], this.points);
this.top = new Figure([0, 2, 4, 6], this.points);
this.bot = new Figure([1, 3, 5, 7], this.points);
this.facesList = [this.front, this.right, this.dorsal, this.left];
}
private buildBackEdge(buildright: boolean): void {
this.updateVanishingPoints(buildright);
let leftPoints = [];
let rightPoints = [];
let topIndex = 0;
let botIndex = 0;
if (buildright) {
leftPoints = this.dl.points;
rightPoints = this.fr.points;
topIndex = 4;
botIndex = 5;
} else {
leftPoints = this.dr.points;
rightPoints = this.fl.points;
topIndex = 6;
botIndex = 7;
}
const vpLeft = this.vpl;
const vpRight = this.vpr;
let p1 = intersection(vpLeft, leftPoints[0], vpRight, rightPoints[0]);
let p2 = intersection(vpLeft, leftPoints[1], vpRight, rightPoints[1]);
if (p1 === null) {
p1 = { x: p2.x, y: vpLeft.y };
} else if (p2 === null) {
p2 = { x: p1.x, y: vpLeft.y };
}
this.points[topIndex] = { x: p1.x, y: p1.y };
this.points[botIndex] = { x: p2.x, y: p2.y };
// Making sure that the vertical edges stay vertical
this.updatePoints();
}
}
function sortPointsClockwise(points: any[]): any[] {
points.sort((a, b): number => a.y - b.y);
// Get center y
const cy = (points[0].y + points[points.length - 1].y) / 2;
// Sort from right to left
points.sort((a, b): number => b.x - a.x);
// Get center x
const cx = (points[0].x + points[points.length - 1].x) / 2;
// Center point
const center = {
x: cx,
y: cy,
};
// Starting angle used to reference other angles
let startAng: number | undefined;
points.forEach((point): void => {
let ang = Math.atan2(point.y - center.y, point.x - center.x);
if (!startAng) {
startAng = ang;
// ensure that all points are clockwise of the start point
} else if (ang < startAng) {
ang += Math.PI * 2;
}
// eslint-disable-next-line no-param-reassign
point.angle = ang; // add the angle to the point
});
// first sort clockwise
points.sort((a, b): number => a.angle - b.angle);
return points.reverse();
}
function setupCuboidPoints(points: Point[]): any[] {
let left;
let right;
let left2;
let right2;
let p1;
let p2;
let p3;
let p4;
const height = Math.abs(points[0].x - points[1].x) < Math.abs(points[1].x - points[2].x) ?
Math.abs(points[1].y - points[0].y) : Math.abs(points[1].y - points[2].y);
// separate into left and right point
// we pick the first and third point because we know assume they will be on
// opposite corners
if (points[0].x < points[2].x) {
[left, , right] = points;
} else {
[right, , left] = points;
}
// get other 2 points using the given height
if (left.y < right.y) {
left2 = { x: left.x, y: left.y + height };
right2 = { x: right.x, y: right.y - height };
} else {
left2 = { x: left.x, y: left.y - height };
right2 = { x: right.x, y: right.y + height };
}
// get the vector for the last point relative to the previous point
const vec = {
x: points[3].x - points[2].x,
y: points[3].y - points[2].y,
};
if (left.y < left2.y) {
p1 = left;
p2 = left2;
} else {
p1 = left2;
p2 = left;
}
if (right.y < right2.y) {
p3 = right;
p4 = right2;
} else {
p3 = right2;
p4 = right;
}
const p5 = { x: p3.x + vec.x, y: p3.y + vec.y + 0.1 };
const p6 = { x: p4.x + vec.x, y: p4.y + vec.y - 0.1 };
const p7 = { x: p1.x + vec.x, y: p1.y + vec.y + 0.1 };
const p8 = { x: p2.x + vec.x, y: p2.y + vec.y - 0.1 };
p1.y += 0.1;
return [p1, p2, p3, p4, p5, p6, p7, p8];
}
export function cuboidFrom4Points(flattenedPoints: any[]): any[] {
const points: Point[] = [];
for (let i = 0; i < 4; i++) {
const [x, y] = flattenedPoints.slice(i * 2, i * 2 + 2);
points.push({ x, y });
}
const unsortedPlanePoints = points.slice(0, 3);
function rotate(array: any[], times: number): void {
let t = times;
while (t--) {
const temp = array.shift();
array.push(temp);
}
}
const plane2 = {
p1: points[0],
p2: points[0],
p3: points[0],
p4: points[0],
};
// completing the plane
const vector = {
x: points[2].x - points[1].x,
y: points[2].y - points[1].y,
};
// sorting the first plane
unsortedPlanePoints.push({
x: points[0].x + vector.x,
y: points[0].y + vector.y,
});
const sortedPlanePoints = sortPointsClockwise(unsortedPlanePoints);
let leftIndex = 0;
for (let i = 0; i < 4; i++) {
leftIndex = sortedPlanePoints[i].x < sortedPlanePoints[leftIndex].x ? i : leftIndex;
}
rotate(sortedPlanePoints, leftIndex);
const plane1 = {
p1: sortedPlanePoints[0],
p2: sortedPlanePoints[1],
p3: sortedPlanePoints[2],
p4: sortedPlanePoints[3],
};
const vec = {
x: points[3].x - points[2].x,
y: points[3].y - points[2].y,
};
// determine the orientation
const angle = Math.atan2(vec.y, vec.x);
// making the other plane
plane2.p1 = { x: plane1.p1.x + vec.x, y: plane1.p1.y + vec.y };
plane2.p2 = { x: plane1.p2.x + vec.x, y: plane1.p2.y + vec.y };
plane2.p3 = { x: plane1.p3.x + vec.x, y: plane1.p3.y + vec.y };
plane2.p4 = { x: plane1.p4.x + vec.x, y: plane1.p4.y + vec.y };
let cuboidPoints;
// right
if (Math.abs(angle) < Math.PI / 2 - 0.1) {
cuboidPoints = setupCuboidPoints(points);
// left
} else if (Math.abs(angle) > Math.PI / 2 + 0.1) {
cuboidPoints = setupCuboidPoints(points);
// down
} else if (angle > 0) {
cuboidPoints = [plane1.p1, plane2.p1, plane1.p2, plane2.p2, plane1.p3, plane2.p3, plane1.p4, plane2.p4];
cuboidPoints[0].y += 0.1;
cuboidPoints[4].y += 0.1;
// up
} else {
cuboidPoints = [plane2.p1, plane1.p1, plane2.p2, plane1.p2, plane2.p3, plane1.p3, plane2.p4, plane1.p4];
cuboidPoints[0].y += 0.1;
cuboidPoints[4].y += 0.1;
}
return cuboidPoints.reduce((arr: number[], point: any): number[] => {
arr.push(point.x);
arr.push(point.y);
return arr;
}, []);
}
File diff suppressed because it is too large Load Diff
+479
View File
@@ -0,0 +1,479 @@
// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import 'svg.select.js';
import consts from './consts';
import { translateFromSVG, pointsToNumberArray } from './shared';
import { PolyEditData, Geometry, Configuration } from './canvasModel';
import { AutoborderHandler } from './autoborderHandler';
export interface EditHandler {
edit(editData: PolyEditData): void;
transform(geometry: Geometry): void;
configure(configuration: Configuration): void;
cancel(): void;
enabled: boolean;
shapeType: string;
}
export class EditHandlerImpl implements EditHandler {
private onEditDone: (state: any, points: number[]) => void;
private autoborderHandler: AutoborderHandler;
private geometry: Geometry | null;
private canvas: SVG.Container;
private editData: PolyEditData | null;
private editedShape: SVG.Shape | null;
private editLine: SVG.PolyLine | null;
private clones: SVG.Polygon[];
private controlPointsSize: number;
private autobordersEnabled: boolean;
private intelligentCutEnabled: boolean;
private outlinedBorders: string;
private isEditing: boolean;
private setupTrailingPoint(circle: SVG.Circle): void {
circle.on('mouseenter', (): void => {
circle.attr({
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale,
});
});
circle.on('mouseleave', (): void => {
circle.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
});
});
circle.on('mousedown', (e: MouseEvent): void => {
if (e.button !== 0) return;
this.edit({ enabled: false });
});
}
private startEdit(): void {
// get started coordinates
const [clientX, clientY] = translateFromSVG(
(this.canvas.node as any) as SVGSVGElement,
this.editedShape.attr('points').split(' ')[this.editData.pointID].split(','),
);
// generate mouse event
const dummyEvent = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
clientX,
clientY,
});
// Add ability to edit shapes by sliding
// We need to remember last drawn point
// to implementation of slide drawing
const lastDrawnPoint: {
x: number;
y: number;
} = {
x: null,
y: null,
};
this.canvas.on('mousemove.edit', (e: MouseEvent): void => {
if (e.shiftKey && ['polygon', 'polyline'].includes(this.editData.state.shapeType)) {
if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) {
(this.editLine as any).draw('point', e);
} else {
const deltaThreshold = 15;
const dxsqr = (e.clientX - lastDrawnPoint.x) ** 2;
const dysqr = (e.clientY - lastDrawnPoint.y) ** 2;
const delta = Math.sqrt(dxsqr + dysqr);
if (delta > deltaThreshold) {
(this.editLine as any).draw('point', e);
}
}
}
});
this.editLine = (this.canvas as any).polyline();
if (this.editData.state.shapeType === 'polyline') {
(this.editLine as any).on('drawupdate', (e: CustomEvent): void => {
const circle = (e.target as any).instance.remember('_paintHandler').set.last();
if (circle) this.setupTrailingPoint(circle);
});
}
(this.editLine as any)
.addClass('cvat_canvas_shape_drawing')
.style({
'pointer-events': 'none',
'fill-opacity': 0,
})
.attr({
'data-origin-client-id': this.editData.state.clientID,
stroke: this.editedShape.attr('stroke'),
})
.on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry);
lastDrawnPoint.x = e.detail.event.clientX;
lastDrawnPoint.y = e.detail.event.clientY;
})
.on('drawupdate', (): void => this.transform(this.geometry))
.draw(dummyEvent, { snapToGrid: 0.1 });
if (this.editData.state.shapeType === 'points') {
this.editLine.attr('stroke-width', 0);
(this.editLine as any).draw('undo');
}
this.setupEditEvents();
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.editLine, this.editData.state.clientID);
}
}
private setupEditEvents(): void {
this.canvas.on('mousedown.edit', (e: MouseEvent): void => {
if (e.button === 0 && !e.altKey) {
(this.editLine as any).draw('point', e);
} else if (e.button === 2 && this.editLine) {
if (this.editData.state.shapeType === 'points' || this.editLine.attr('points').split(' ').length > 2) {
(this.editLine as any).draw('undo');
}
}
});
}
private selectPolygon(shape: SVG.Polygon): void {
const { offset } = this.geometry;
const points = pointsToNumberArray(shape.attr('points')).map((coord: number): number => coord - offset);
const { state } = this.editData;
this.edit({
enabled: false,
});
this.onEditDone(state, points);
}
private stopEdit(e: MouseEvent): void {
if (!this.editLine) {
return;
}
// Get stop point and all points
const stopPointID = Array.prototype.indexOf.call((e.target as HTMLElement).parentElement.children, e.target);
const oldPoints = this.editedShape.attr('points').trim().split(' ');
const linePoints = this.editLine.attr('points').trim().split(' ');
if (this.editLine.attr('points') === '0,0') {
this.cancel();
return;
}
// Compute new point array
const [start, stop] = [this.editData.pointID, stopPointID].sort((a, b): number => +a - +b);
if (this.editData.state.shapeType !== 'polygon') {
let points = null;
const { offset } = this.geometry;
if (this.editData.state.shapeType === 'polyline') {
if (start !== this.editData.pointID) {
linePoints.reverse();
}
points = oldPoints
.slice(0, start)
.concat(linePoints)
.concat(oldPoints.slice(stop + 1));
} else {
points = oldPoints.concat(linePoints.slice(0, -1));
}
points = pointsToNumberArray(points.join(' ')).map((coord: number): number => coord - offset);
const { state } = this.editData;
this.edit({
enabled: false,
});
this.onEditDone(state, points);
return;
}
const cutIndexes1 = oldPoints.reduce(
(acc: number[], _: string, i: number): number[] => (i >= stop || i <= start ? [...acc, i] : acc),
[],
);
const cutIndexes2 = oldPoints.reduce(
(acc: number[], _: string, i: number): number[] => (i <= stop && i >= start ? [...acc, i] : acc),
[],
);
const curveLength = (indexes: number[]): number => {
const points = indexes
.map((index: number): string => oldPoints[index])
.map((point: string): string[] => point.split(','))
.map((point: string[]): number[] => [+point[0], +point[1]]);
let length = 0;
for (let i = 1; i < points.length; i++) {
const dxsqr = (points[i][0] - points[i - 1][0]) ** 2;
const dysqr = (points[i][1] - points[i - 1][1]) ** 2;
length += Math.sqrt(dxsqr + dysqr);
}
return length;
};
const pointsCriteria = cutIndexes1.length > cutIndexes2.length;
const lengthCriteria = curveLength(cutIndexes1) > curveLength(cutIndexes2);
if (start !== this.editData.pointID) {
linePoints.reverse();
}
const firstPart = oldPoints
.slice(0, start)
.concat(linePoints)
.concat(oldPoints.slice(stop + 1));
const secondPart = oldPoints.slice(start, stop).concat(linePoints.slice(1).reverse());
if (firstPart.length < 3 || secondPart.length < 3) {
this.cancel();
return;
}
// We do not need these events any more
this.canvas.off('mousedown.edit');
this.canvas.off('mousemove.edit');
(this.editLine as any).draw('stop');
this.editLine.remove();
this.editLine = null;
if (pointsCriteria && lengthCriteria && this.intelligentCutEnabled) {
this.clones.push(this.canvas.polygon(firstPart.join(' ')));
this.selectPolygon(this.clones[0]);
// left indexes1 and
} else if (!pointsCriteria && !lengthCriteria && this.intelligentCutEnabled) {
this.clones.push(this.canvas.polygon(secondPart.join(' ')));
this.selectPolygon(this.clones[0]);
} else {
for (const points of [firstPart, secondPart]) {
this.clones.push(
this.canvas
.polygon(points.join(' '))
.attr('fill', this.editedShape.attr('fill'))
.attr('fill-opacity', '0.5')
.addClass('cvat_canvas_shape'),
);
}
for (const clone of this.clones) {
clone.on('click', (): void => this.selectPolygon(clone));
clone
.on('mouseenter', (): void => {
clone.addClass('cvat_canvas_shape_splitting');
})
.on('mouseleave', (): void => {
clone.removeClass('cvat_canvas_shape_splitting');
});
}
}
}
private setupPoints(enabled: boolean): void {
const stopEdit = this.stopEdit.bind(this);
const getGeometry = (): Geometry => this.geometry;
const fill = this.editedShape.attr('fill') || 'inherit';
if (enabled) {
(this.editedShape as any).selectize(true, {
deepSelect: true,
pointSize: (2 * this.controlPointsSize) / getGeometry().scale,
rotationPoint: false,
pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested
.circle(this.options.pointSize)
.stroke('black')
.fill(fill)
.center(cx, cy)
.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / getGeometry().scale,
});
circle.node.addEventListener('mouseenter', (): void => {
circle.attr({
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / getGeometry().scale,
});
circle.node.addEventListener('click', stopEdit);
circle.addClass('cvat_canvas_selected_point');
});
circle.node.addEventListener('mouseleave', (): void => {
circle.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / getGeometry().scale,
});
circle.node.removeEventListener('click', stopEdit);
circle.removeClass('cvat_canvas_selected_point');
});
return circle;
},
});
} else {
(this.editedShape as any).selectize(false, {
deepSelect: true,
});
}
}
private release(): void {
this.canvas.off('mousedown.edit');
this.canvas.off('mousemove.edit');
this.autoborderHandler.autoborder(false);
this.isEditing = false;
if (this.editedShape) {
this.setupPoints(false);
this.editedShape.remove();
this.editedShape = null;
}
if (this.editLine) {
(this.editLine as any).draw('stop');
this.editLine.remove();
this.editLine = null;
}
if (this.clones.length) {
for (const clone of this.clones) {
clone.remove();
}
this.clones = [];
}
}
private initEditing(): void {
this.editedShape = this.canvas
.select(`#cvat_canvas_shape_${this.editData.state.clientID}`).first()
.clone().attr('stroke', this.outlinedBorders);
this.setupPoints(true);
this.startEdit();
this.isEditing = true;
// draw points for this with selected and start editing till another point is clicked
// click one of two parts to remove (in case of polygon only)
// else we can start draw polyline
// after we have got shape and points, we are waiting for second point pressed on this shape
}
private closeEditing(): void {
if (this.isEditing && this.editData.state.shapeType === 'polyline') {
const { offset } = this.geometry;
const head = this.editedShape.attr('points').split(' ').slice(0, this.editData.pointID).join(' ');
const stringifiedPoints = `${head} ${this.editLine.node.getAttribute('points').slice(0, -2)}`;
const points = pointsToNumberArray(stringifiedPoints)
.slice(0, -2)
.map((coord: number): number => coord - offset);
if (points.length >= 2 * 2) { // minimumPoints * 2
const { state } = this.editData;
this.onEditDone(state, points);
}
}
this.release();
}
public constructor(
onEditDone: EditHandlerImpl['onEditDone'],
canvas: SVG.Container,
autoborderHandler: AutoborderHandler,
) {
this.autoborderHandler = autoborderHandler;
this.autobordersEnabled = false;
this.intelligentCutEnabled = false;
this.controlPointsSize = consts.BASE_POINT_SIZE;
this.outlinedBorders = 'black';
this.onEditDone = onEditDone;
this.canvas = canvas;
this.editData = null;
this.editedShape = null;
this.editLine = null;
this.geometry = null;
this.clones = [];
this.isEditing = false;
}
public edit(editData: any): void {
if (editData.enabled) {
if (['polygon', 'polyline', 'points'].includes(editData.state.shapeType)) {
this.editData = editData;
this.initEditing();
} else {
this.cancel();
}
} else {
this.closeEditing();
this.editData = editData;
}
}
public cancel(): void {
this.release();
this.onEditDone(null, null);
}
get enabled(): boolean {
return this.isEditing;
}
get shapeType(): string {
return this.editData.state.shapeType;
}
public configure(configuration: Configuration): void {
this.autobordersEnabled = configuration.autoborders;
this.outlinedBorders = configuration.outlinedBorders || 'black';
if (this.editedShape) {
this.editedShape.attr('stroke', this.outlinedBorders);
}
if (this.editLine) {
this.editLine.attr('stroke', this.outlinedBorders);
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.editLine, this.editData.state.clientID);
} else {
this.autoborderHandler.autoborder(false);
}
}
this.controlPointsSize = configuration.controlPointsSize || consts.BASE_POINT_SIZE;
this.intelligentCutEnabled = configuration.intelligentPolygonCrop;
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.editedShape) {
this.editedShape.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
});
}
if (this.editLine) {
if (this.editData.state.shapeType !== 'points') {
this.editLine.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
});
}
const paintHandler = this.editLine.remember('_paintHandler');
for (const point of paintHandler.set.members) {
point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`);
point.attr('r', `${this.controlPointsSize / geometry.scale}`);
}
}
}
}
@@ -0,0 +1,75 @@
// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import { GroupData } from './canvasModel';
import { ObjectSelector, SelectionFilter } from './objectSelector';
export interface GroupHandler {
group(groupData: GroupData, selectionFilter: SelectionFilter): void;
select(state: any): void;
cancel(): void;
}
export class GroupHandlerImpl implements GroupHandler {
private onSelectDone: (objects?: any[], duration?: number) => void;
private selector: ObjectSelector;
private initialized: boolean;
private statesToBeGrouped: any[];
private startTimestamp: number;
private release(): void {
this.selector.disable();
this.initialized = false;
}
private initGrouping(selectionFilter: SelectionFilter): void {
this.statesToBeGrouped = [];
this.selector.enable((selected) => {
this.statesToBeGrouped = selected;
}, selectionFilter);
this.initialized = true;
this.startTimestamp = Date.now();
}
private closeGrouping(): void {
if (this.initialized) {
const { statesToBeGrouped } = this;
this.release();
if (statesToBeGrouped.length) {
this.onSelectDone(statesToBeGrouped, Date.now() - this.startTimestamp);
} else {
this.onSelectDone();
}
}
}
public constructor(
onSelectDone: GroupHandlerImpl['onSelectDone'],
selector: ObjectSelector,
) {
this.onSelectDone = onSelectDone;
this.selector = selector;
this.statesToBeGrouped = [];
this.initialized = false;
this.startTimestamp = Date.now();
}
public group(groupData: GroupData, selectionFilter: SelectionFilter): void {
if (groupData.enabled) {
this.initGrouping(selectionFilter);
} else {
this.closeGrouping();
}
}
public select(objectState: any): void {
this.selector.push(objectState);
}
public cancel(): void {
this.release();
this.onSelectDone();
}
}

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