Release 0.5.0

main
Nikita Manovich 7 years ago committed by GitHub
commit 242b3dfebd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,2 +1,3 @@
exclude_paths:
- '**/3rdparty/**'
- '**/engine/js/cvat-core.min.js'

@ -6,3 +6,6 @@
/.vscode
/db.sqlite3
/keys
**/node_modules
cvat-ui
cvat-canvas

@ -24,9 +24,11 @@
"eslint:recommended",
"plugin:security/recommended",
"plugin:no-unsanitized/DOM",
"airbnb",
"airbnb-base",
],
"rules": {
"no-await-in-loop": [0],
"global-require": [0],
"no-new": [0],
"class-methods-use-this": [0],
"no-restricted-properties": [0, {
@ -41,5 +43,11 @@
// This rule actual for user input data on the node.js environment mainly.
"security/detect-object-injection": 0,
"indent": ["warn", 4],
// recently added to airbnb
"max-classes-per-file": [0],
// it was opposite before and our code has been written according to previous rule
"arrow-parens": [0],
// object spread is a modern ECMA standard. Let's do not use it without babel
"prefer-object-spread": [0],
},
};

1
.gitattributes vendored

@ -15,6 +15,7 @@ LICENSE text
*.conf text
*.mimetypes text
*.sh text eol=lf
components/openvino/eula.cfg text eol=lf
*.avi binary
*.bmp binary

7
.gitignore vendored

@ -1,5 +1,6 @@
# Project Specific
/data/
/models/
/share/
/static/
/db.sqlite3
@ -15,7 +16,11 @@ node_modules
# Ignore temporary files
docker-compose.override.yml
/.vscode
__pycache__
*.pyc
._*
# Ignore development npm files
node_modules
.DS_Store

@ -0,0 +1,420 @@
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=1
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# 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-whitelist=
# Allow optimization of some AST trees. This will activate a peephole AST
# optimizer, which will apply various small optimizations. For instance, it can
# be used to obtain the result of joining multiple strings with the addition
# operator. Joining a lot of strings can lead to a maximum recursion error in
# Pylint and this flag can prevent that. It has one side effect, the resulting
# AST will be different than the one from reality. This option is deprecated
# and it will be removed in Pylint 2.0.
optimize-ast=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# 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.
disable=all
enable= E0001,E0100,E0101,E0102,E0103,E0104,E0105,E0106,E0107,E0110,
E0113,E0114,E0115,E0116,E0117,E0108,E0202,E0203,E0211,E0236,
E0238,E0239,E0240,E0241,E0301,E0302,E0601,E0603,E0604,E0701,
E0702,E0703,E0704,E0710,E0711,E0712,E1003,E1102,E1111,E0112,
E1120,E1121,E1123,E1124,E1125,E1126,E1127,E1132,E1200,E1201,
E1205,E1206,E1300,E1301,E1302,E1303,E1304,E1305,E1306,
C0123,C0200,C0303,C1001,
W0101,W0102,W0104,W0105,W0106,W0107,W0108,W0109,W0110,W0120,
W0122,W0124,W0150,W0199,W0221,W0222,W0233,W0404,W0410,W0601,
W0602,W0604,W0611,W0612,W0622,W0623,W0702,W0705,W0711,W1300,
W1301,W1302,W1303,,W1305,W1306,W1307
R0102,R0201,R0202,R0203
# 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 reenable 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=old-octal-literal,basestring-builtin,no-absolute-import,old-division,coerce-method,long-suffix,reload-builtin,unichr-builtin,indexing-exception,raising-string,dict-iter-method,metaclass-assignment,filter-builtin-not-iterating,import-star-module-level,next-method-called,cmp-method,raw_input-builtin,old-raise-syntax,cmp-builtin,apply-builtin,getslice-method,input-builtin,backtick,coerce-builtin,range-builtin-not-iterating,xrange-builtin,using-cmp-argument,buffer-builtin,hex-method,execfile-builtin,unpacking-in-except,standarderror-builtin,round-builtin,nonzero-method,unicode-builtin,reduce-builtin,file-builtin,dict-view-method,old-ne-operator,print-statement,suppressed-message,oct-method,useless-suppression,delslice-method,long-builtin,setslice-method,zip-builtin-not-iterating,map-builtin-not-iterating,intern-builtin,parameter-unpacking
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]". This option is deprecated
# and it will be removed in Pylint 2.0.
files-output=no
# Tells whether to display a full report or only the messages
reports=yes
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This 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=
[BASIC]
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for argument names
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for variable names
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for method names
method-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for function names
function-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for attribute names
attr-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
[ELIF]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=100
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# 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
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# 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=
# 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
# 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=
# 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
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-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
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,future.builtins
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# 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
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of boolean expressions in a if statement
max-bool-expr=5
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=optparse
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in 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
# 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
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception

@ -12,5 +12,5 @@ before_script:
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml up --build -d
script:
- docker exec -it cvat /bin/bash -c 'tests/node_modules/.bin/karma start tests/karma.conf.js'
- docker exec -it cvat /bin/bash -c 'python3 manage.py test cvat/apps/engine'
- docker exec -it cvat /bin/bash -c 'cd cvat-core && npm install && npm run test && npm run coveralls'

@ -5,7 +5,24 @@
"version": "0.2.0",
"configurations": [
{
"name": "server",
"type": "chrome",
"request": "launch",
"preLaunchTask": "ui.js: server",
"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,
},
{
"name": "server: django",
"type": "python",
"request": "launch",
"stopOnEntry": false,
@ -19,10 +36,11 @@
"127.0.0.1:7000"
],
"django": true,
"cwd": "${workspaceFolder}"
"cwd": "${workspaceFolder}",
"console": "internalConsole"
},
{
"name": "client",
"name": "server: chrome",
"type": "chrome",
"request": "launch",
"url": "http://localhost:7000/",
@ -35,7 +53,7 @@
}
},
{
"name": "RQ - default",
"name": "server: RQ - default",
"type": "python",
"request": "launch",
"stopOnEntry": false,
@ -50,10 +68,11 @@
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {}
"env": {},
"console": "internalConsole"
},
{
"name": "RQ - low",
"name": "server: RQ - low",
"type": "python",
"request": "launch",
"justMyCode": false,
@ -68,10 +87,11 @@
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {}
"env": {},
"console": "internalConsole"
},
{
"name": "git",
"name": "server: git",
"type": "python",
"request": "launch",
"justMyCode": false,
@ -83,10 +103,11 @@
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {}
"env": {},
"console": "internalConsole"
},
{
"name": "migrate",
"name": "server: migrate",
"type": "python",
"request": "launch",
"justMyCode": false,
@ -98,10 +119,11 @@
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {}
"env": {},
"console": "internalConsole"
},
{
"name": "tests",
"name": "server: tests",
"type": "python",
"request": "launch",
"justMyCode": false,
@ -116,18 +138,45 @@
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {}
"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
},
{
"type": "node",
"request": "launch",
"name": "jest debug",
"program": "${workspaceFolder}/cvat-core/node_modules/.bin/jest",
"args": [
"--config",
"${workspaceFolder}/cvat-core/jest.config.js"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
}
],
"compounds": [
{
"name": "debugging",
"name": "server: debug",
"configurations": [
"client",
"server",
"RQ - default",
"RQ - low",
"git",
"server: chrome",
"server: django",
"server: RQ - default",
"server: RQ - low",
"server: git",
]
}
]

@ -0,0 +1,23 @@
{
"python.pythonPath": ".env/bin/python",
"eslint.enable": true,
"eslint.validate": [
"javascript",
"typescript"
],
"eslint.workingDirectories": [
{
"directory": "./cvat-core",
"changeProcessCWD": true
},
{
"directory": "./cvat-canvas",
"changeProcessCWD": true
},
{
"directory": ".",
"changeProcessCWD": true
}
],
"python.linting.pylintEnabled": true
}

14
.vscode/tasks.json vendored

@ -0,0 +1,14 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "ui.js: server",
"type": "npm",
"script": "start",
"path": "cvat-ui/",
"problemMatcher": []
}
]
}

@ -4,6 +4,51 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.5.0] - 2019-10-12
### Added
- A converter to YOLO format
- Installation guide
- Linear interpolation for a single point
- Video frame filter
- Running functional tests for REST API during a build
- Admins are no longer limited to a subset of python commands in the auto annotation application
- Remote data source (list of URLs to create an annotation task)
- Auto annotation using Faster R-CNN with Inception v2 (utils/open_model_zoo)
- Auto annotation using Pixel Link mobilenet v2 - text detection (utils/open_model_zoo)
- Ability to create a custom extractors for unsupported media types
- Added in PDF extractor
- Added in a command line model manager tester
- Ability to dump/load annotations in several formats from UI (CVAT, Pascal VOC, YOLO, MS COCO, png mask, TFRecord)
- Auth for REST API (api/v1/auth/): login, logout, register, ...
- Preview for the new CVAT UI (dashboard only) is available: http://localhost:9080/
### Changed
- Outside and keyframe buttons in the side panel for all interpolation shapes (they were only for boxes before)
- Improved error messages on the client side (#511)
### Removed
- "Flip images" has been removed. UI now contains rotation features.
### Fixed
- Incorrect width of shapes borders in some cases
- Annotation parser for tracks with a start frame less than the first segment frame
- Interpolation on the server near outside frames
- Dump for case when task name has a slash
- Auto annotation fail for multijob tasks
- Installation of CVAT with OpenVINO on the Windows platform
- Background color was always black in utils/mask/converter.py
- Exception in attribute annotation mode when a label are switched to a value without any attributes
- Handling of wrong labelamp json file in auto annotation (<https://github.com/opencv/cvat/issues/554>)
- No default attributes in dumped annotation (<https://github.com/opencv/cvat/issues/601>)
- Required field "Frame Filter" on admin page during a task modifying (#666)
- Dump annotation errors for a task with several segments (#610, #500)
- Invalid label parsing during a task creating (#628)
- Button "Open Task" in the annotation view
- Creating a video task with 0 overlap
### Security
- Upgraded Django, djangorestframework, and other packages
## [0.4.2] - 2019-06-03
### Fixed
- Fixed interaction with the server share in the auto annotation plugin
@ -37,9 +82,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Django 2.1.5 (security fix, https://nvd.nist.gov/vuln/detail/CVE-2019-3498)
- Several scenarious which cause code 400 after undo/redo/save have been fixed (#315)
### Security
-
## [0.3.0] - 2018-12-29
### Added
- Ability to copy Object URL and Frame URL via object context menu and player context menu respectively.

@ -53,7 +53,7 @@ $ code .
- Reload Visual Studio Code
- Select `CVAT Debugging` configuration and start debugging (F5)
- Select `server: debug` configuration and start debugging (F5)
You have done! Now it is possible to insert breakpoints and debug server and client of the tool.

@ -4,15 +4,16 @@ ARG http_proxy
ARG https_proxy
ARG no_proxy
ARG socks_proxy
ARG TZ
ENV TERM=xterm \
http_proxy=${http_proxy} \
https_proxy=${https_proxy} \
no_proxy=${no_proxy} \
socks_proxy=${socks_proxy}
ENV LANG='C.UTF-8' \
LC_ALL='C.UTF-8'
socks_proxy=${socks_proxy} \
LANG='C.UTF-8' \
LC_ALL='C.UTF-8' \
TZ=${TZ}
ARG USER
ARG DJANGO_CONFIGURATION
@ -38,10 +39,14 @@ RUN apt-get update && \
libsasl2-dev \
python3-dev \
python3-pip \
tzdata \
unzip \
unrar \
p7zip-full \
vim && \
pip3 install -U setuptools && \
ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \
add-apt-repository --remove ppa:mc3man/gstffmpeg-keep -y && \
add-apt-repository --remove ppa:mc3man/xerus-media -y && \
rm -rf /var/lib/apt/lists/*
@ -57,15 +62,12 @@ COPY components /tmp/components
# OpenVINO toolkit support
ARG OPENVINO_TOOLKIT
ENV OPENVINO_TOOLKIT=${OPENVINO_TOOLKIT}
ENV REID_MODEL_DIR=${HOME}/reid
RUN if [ "$OPENVINO_TOOLKIT" = "yes" ]; then \
/tmp/components/openvino/install.sh; \
fi
# CUDA support
ARG CUDA_SUPPORT
ENV CUDA_SUPPORT=${CUDA_SUPPORT}
RUN if [ "$CUDA_SUPPORT" = "yes" ]; then \
/tmp/components/cuda/install.sh; \
/tmp/components/openvino/install.sh && \
mkdir ${REID_MODEL_DIR} && \
wget https://download.01.org/openvinotoolkit/2018_R5/open_model_zoo/person-reidentification-retail-0079/FP32/person-reidentification-retail-0079.xml -O reid/reid.xml && \
wget https://download.01.org/openvinotoolkit/2018_R5/open_model_zoo/person-reidentification-retail-0079/FP32/person-reidentification-retail-0079.bin -O reid/reid.bin; \
fi
# Tensorflow annotation support
@ -102,7 +104,9 @@ RUN if [ "$WITH_TESTS" = "yes" ]; then \
# Install and initialize CVAT, copy all necessary files
COPY cvat/requirements/ /tmp/requirements/
COPY supervisord.conf mod_wsgi.conf wait-for-it.sh manage.py ${HOME}/
RUN pip3 install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt
RUN pip3 install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt
# pycocotools package is impossible to install with its dependencies by one pip install command
RUN pip3 install --no-cache-dir pycocotools==2.0.0
# Install git application dependencies
RUN apt-get update && \
@ -117,18 +121,17 @@ RUN apt-get update && \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ProxyCommand='nc -X 5 -x ${socks_proxy} %h %p'\"" >> ${HOME}/.bashrc; \
fi
# Download model for re-identification app
ENV REID_MODEL_DIR=${HOME}/reid
RUN if [ "$OPENVINO_TOOLKIT" = "yes" ]; then \
mkdir ${HOME}/reid && \
wget https://download.01.org/openvinotoolkit/2018_R5/open_model_zoo/person-reidentification-retail-0079/FP32/person-reidentification-retail-0079.xml -O reid/reid.xml && \
wget https://download.01.org/openvinotoolkit/2018_R5/open_model_zoo/person-reidentification-retail-0079/FP32/person-reidentification-retail-0079.bin -O reid/reid.bin; \
# CUDA support
ARG CUDA_SUPPORT
ENV CUDA_SUPPORT=${CUDA_SUPPORT}
RUN if [ "$CUDA_SUPPORT" = "yes" ]; then \
/tmp/components/cuda/install.sh; \
fi
# TODO: CHANGE URL
ARG WITH_DEXTR
ENV WITH_DEXTR=${WITH_DEXTR}
ENV DEXTR_MODEL_DIR=${HOME}/models/dextr
ENV DEXTR_MODEL_DIR=${HOME}/dextr
RUN if [ "$WITH_DEXTR" = "yes" ]; then \
mkdir ${DEXTR_MODEL_DIR} -p && \
wget https://download.01.org/openvinotoolkit/models_contrib/cvat/dextr_model_v1.zip -O ${DEXTR_MODEL_DIR}/dextr.zip && \
@ -137,8 +140,11 @@ RUN if [ "$WITH_DEXTR" = "yes" ]; then \
COPY ssh ${HOME}/.ssh
COPY cvat/ ${HOME}/cvat
COPY cvat-core/ ${HOME}/cvat-core
COPY tests ${HOME}/tests
RUN patch -p1 < ${HOME}/cvat/apps/engine/static/engine/js/3rdparty.patch
# Binary option is necessary to correctly apply the patch on Windows platform.
# https://unix.stackexchange.com/questions/239364/how-to-fix-hunk-1-failed-at-1-different-line-endings-message
RUN patch --binary -p1 < ${HOME}/cvat/apps/engine/static/engine/js/3rdparty.patch
RUN chown -R ${USER}:${USER} .
# RUN all commands below as 'django' user

@ -1,34 +1,51 @@
# Computer Vision Annotation Tool (CVAT)
[![Build Status](https://travis-ci.org/opencv/cvat.svg?branch=develop)](https://travis-ci.org/opencv/cvat)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/840351da141e4eaeac6476fd19ec0a33)](https://app.codacy.com/app/nmanovic/cvat?utm_source=github.com&utm_medium=referral&utm_content=opencv/cvat&utm_campaign=Badge_Grade_Settings)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/840351da141e4eaeac6476fd19ec0a33)](https://app.codacy.com/app/cvat/cvat?utm_source=github.com&utm_medium=referral&utm_content=opencv/cvat&utm_campaign=Badge_Grade_Dashboard)
[![Gitter chat](https://badges.gitter.im/opencv-cvat/gitter.png)](https://gitter.im/opencv-cvat)
[![Coverage Status](https://coveralls.io/repos/github/opencv/cvat/badge.svg?branch=)](https://coveralls.io/github/opencv/cvat?branch=develop)
CVAT is completely re-designed and re-implemented version of [Video Annotation Tool from Irvine, California](http://carlvondrick.com/vatic/) tool. It is free, online, interactive video and image annotation tool for computer vision. It is being used by our team to annotate million of objects with different properties. Many UI and UX decisions are based on feedbacks from professional data annotation team.
CVAT is free, online, interactive video and image annotation tool for computer vision. It is being used by our team to annotate million of objects with different properties. Many UI and UX decisions are based on feedbacks from professional data annotation team.
![CVAT screenshot](cvat/apps/documentation/static/documentation/images/cvat.jpg)
## Documentation
- [User's guide](cvat/apps/documentation/user_guide.md)
- [XML annotation format](cvat/apps/documentation/xml_format.md)
- [AWS Deployment Guide](cvat/apps/documentation/AWS-Deployment-Guide.md)
- [Questions](#questions)
- [Installation guide](cvat/apps/documentation/installation.md)
- [User's guide](cvat/apps/documentation/user_guide.md)
- [Django REST API documentation](#rest-api)
- [XML annotation format](cvat/apps/documentation/xml_format.md)
- [AWS Deployment Guide](cvat/apps/documentation/AWS-Deployment-Guide.md)
- [Questions](#questions)
## Screencasts
- [Introduction](https://youtu.be/L9_IvUIHGwM)
- [Annotation mode](https://youtu.be/6h7HxGL6Ct4)
- [Interpolation mode](https://youtu.be/U3MYDhESHo4)
- [Attribute mode](https://youtu.be/UPNfWl8Egd8)
- [Segmentation mode](https://youtu.be/Fh8oKuSUIPs)
- [Tutorial for polygons](https://www.youtube.com/watch?v=XTwfXDh4clI)
- [Semi-automatic segmentation](https://www.youtube.com/watch?v=vnqXZ-Z-VTQ)
- [Introduction](https://youtu.be/L9_IvUIHGwM)
- [Annotation mode](https://youtu.be/6h7HxGL6Ct4)
- [Interpolation mode](https://youtu.be/U3MYDhESHo4)
- [Attribute mode](https://youtu.be/UPNfWl8Egd8)
- [Segmentation mode](https://youtu.be/Fh8oKuSUIPs)
- [Tutorial for polygons](https://www.youtube.com/watch?v=XTwfXDh4clI)
- [Semi-automatic segmentation](https://www.youtube.com/watch?v=vnqXZ-Z-VTQ)
## Supported annotation formats
Format selection is possible after clicking on the Upload annotation / Dump annotation button.
| Annotation format | Dumper | Loader |
| ---------------------------------------------------------------------------------- | ------ | ------ |
| [CVAT XML v1.1 for images](cvat/apps/documentation/xml_format.md#annotation) | X | X |
| [CVAT XML v1.1 for a video](cvat/apps/documentation/xml_format.md#interpolation) | X | X |
| [Pascal VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| [YOLO](https://pjreddie.com/darknet/yolo/) | X | X |
| [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X |
| PNG mask | X | |
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tf_records) | X | X |
## Links
- [Intel AI blog: New Computer Vision Tool Accelerates Annotation of Digital Images and Video](https://www.intel.ai/introducing-cvat)
- [Intel Software: Computer Vision Annotation Tool: A Universal Approach to Data Annotation](https://software.intel.com/en-us/articles/computer-vision-annotation-tool-a-universal-approach-to-data-annotation)
- [VentureBeat: Intel open-sources CVAT, a toolkit for data labeling](https://venturebeat.com/2019/03/05/intel-open-sources-cvat-a-toolkit-for-data-labeling/)
- [Intel AI blog: New Computer Vision Tool Accelerates Annotation of Digital Images and Video](https://www.intel.ai/introducing-cvat)
- [Intel Software: Computer Vision Annotation Tool: A Universal Approach to Data Annotation](https://software.intel.com/en-us/articles/computer-vision-annotation-tool-a-universal-approach-to-data-annotation)
- [VentureBeat: Intel open-sources CVAT, a toolkit for data labeling](https://venturebeat.com/2019/03/05/intel-open-sources-cvat-a-toolkit-for-data-labeling/)
## Online Demo
@ -36,124 +53,29 @@ CVAT is completely re-designed and re-implemented version of [Video Annotation T
After you click the link above:
- Click on "GO TO WORKSPACE" and the CVAT environment will load up
- The environment is backed by a K80 GPU
- Click on "GO TO WORKSPACE" and the CVAT environment will load up
- The environment is backed by a K80 GPU
If you have any questions, please contact Onepanel directly at support@onepanel.io. If you are in the Onepanel application, you can also use the chat icon in the bottom right corner.
## LICENSE
Code released under the [MIT License](https://opensource.org/licenses/MIT).
## INSTALLATION
The instructions below should work for `Ubuntu 16.04`. It will probably work on other Operating Systems such as `Windows` and `macOS`, but may require minor modifications.
### Install [Docker CE](https://www.docker.com/community-edition) or [Docker EE](https://www.docker.com/enterprise-edition) from official site
Please read official manual [here](https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/).
### Install docker-compose (1.19.0 or newer)
```bash
sudo pip install docker-compose
```
### Build docker images
To build all necessary docker images run `docker-compose build` command. By default, in production mode the tool uses PostgreSQL as database, Redis for caching.
### Run docker containers
To start default container run `docker-compose up -d` command. Go to [localhost:8080](http://localhost:8080/). You should see a login page.
### You can include any additional components. Just add corresponding docker-compose file to build or run command:
```bash
# Build image with CUDA and OpenVINO support
docker-compose -f docker-compose.yml -f components/cuda/docker-compose.cuda.yml -f components/openvino/docker-compose.openvino.yml build
# Run containers with CUDA and OpenVINO support
docker-compose -f docker-compose.yml -f components/cuda/docker-compose.cuda.yml -f components/openvino/docker-compose.openvino.yml up -d
```
### Additional optional components
## REST API
- [Auto annotation using DL models in OpenVINO toolkit format](cvat/apps/auto_annotation/README.md)
- [Analytics: management and monitoring of data annotation team](components/analytics/README.md)
- [TF Object Detection API: auto annotation](components/tf_annotation/README.md)
- [Support for NVIDIA GPUs](components/cuda/README.md)
- [Semi-automatic segmentation with Deep Extreme Cut](cvat/apps/dextr_segmentation/README.md)
Automatically generated Swagger documentation for Django REST API is
available on ``<cvat_origin>/api/swagger`` (default: ``localhost:8080/api/swagger``).
### Create superuser account
You can [register a user](http://localhost:8080/auth/register) but by default it will not have rights even to view list of tasks. Thus you should create a superuser. The superuser can use admin panel to assign correct groups to the user. Please use the command below:
```bash
docker exec -it cvat bash -ic '/usr/bin/python3 ~/manage.py createsuperuser'
```
Type your login/password for the superuser [on the login page](http://localhost:8080/auth/login) and press **Login** button. Now you should be able to create a new annotation task. Please read documentation for more details.
### Stop all containers
The command below will stop and remove containers, networks, volumes, and images
created by `up`.
```bash
docker-compose down
```
### Advanced settings
If you want to access you instance of CVAT outside of your localhost you should specify [ALLOWED_HOSTS](https://docs.djangoproject.com/en/2.0/ref/settings/#allowed-hosts) environment variable. The best way to do that is to create [docker-compose.override.yml](https://docs.docker.com/compose/extends/) and put all your extra settings here.
```yml
version: "2.3"
services:
cvat:
environment:
ALLOWED_HOSTS: .example.com
ports:
- "80:8080"
```
### Annotation logs
It is possible to proxy annotation logs from client to ELK. To do that run the following command below:
```bash
docker-compose -f docker-compose.yml -f components/analytics/docker-compose.analytics.yml up -d --build
```
### Share path
You can use a share storage for data uploading during you are creating a task. To do that you can mount it to CVAT docker container. Example of docker-compose.override.yml for this purpose:
```yml
version: "2.3"
services:
cvat:
environment:
CVAT_SHARE_URL: "Mounted from /mnt/share host directory"
volumes:
- cvat_share:/home/django/share:ro
## LICENSE
volumes:
cvat_share:
driver_opts:
type: none
device: /mnt/share
o: bind
```
You can change the share device path to your actual share. For user convenience we have defined the enviroment variable $CVAT_SHARE_URL. This variable contains a text (url for example) which will be being shown in the client-share browser.
Code released under the [MIT License](https://opensource.org/licenses/MIT).
## Questions
CVAT usage related questions or unclear concepts can be posted in our [Gitter chat](https://gitter.im/opencv-cvat) for **quick replies** from contributors and other users.
CVAT usage related questions or unclear concepts can be posted in our
[Gitter chat](https://gitter.im/opencv-cvat) for **quick replies** from
contributors and other users.
However, if you have a feature request or a bug report that can reproduced, feel free to open an issue (with steps to reproduce the bug if it's a bug report).
However, if you have a feature request or a bug report that can reproduced,
feel free to open an issue (with steps to reproduce the bug if it's a bug
report).
If you are not sure or just want to browse other users common questions, [Gitter chat](https://gitter.im/opencv-cvat) is the way to go.
If you are not sure or just want to browse other users common questions,
[Gitter chat](https://gitter.im/opencv-cvat) is the way to go.

@ -16,7 +16,7 @@ echo "deb http://developer.download.nvidia.com/compute/machine-learning/repos/ub
CUDA_VERSION=9.0.176
NCCL_VERSION=2.1.15
CUDNN_VERSION=7.0.5.15
CUDNN_VERSION=7.6.2.24
CUDA_PKG_VERSION="9-0=${CUDA_VERSION}-1"
echo 'export PATH=/usr/local/nvidia/bin:/usr/local/cuda/bin:${PATH}' >> ${HOME}/.bashrc
echo 'export LD_LIBRARY_PATH=/usr/local/nvidia/lib:/usr/local/nvidia/lib64:${LD_LIBRARY_PATH}' >> ${HOME}/.bashrc
@ -32,3 +32,6 @@ apt-get update && apt-get install -y --no-install-recommends --allow-unauthentic
ln -s cuda-9.0 /usr/local/cuda && \
rm -rf /var/lib/apt/lists/* \
/etc/apt/sources.list.d/nvidia-ml.list /etc/apt/sources.list.d/cuda.list
pip3 uninstall -y tensorflow
pip3 install --no-cache-dir tensorflow-gpu==1.12.3

@ -26,6 +26,7 @@ apt-get update && apt-get install -y sudo cpio && \
if [ -f "install_cv_sdk_dependencies.sh" ]; then ./install_cv_sdk_dependencies.sh; \
else ./install_openvino_dependencies.sh; fi && SUDO_FORCE_REMOVE=yes apt-get remove -y sudo
cat ../eula.cfg >> silent.cfg
./install.sh -s silent.cfg

@ -12,9 +12,4 @@ tar -xzf model.tar.gz && rm model.tar.gz && \
mv faster_rcnn_inception_resnet_v2_atrous_coco_2018_01_28 ${HOME}/rcnn && cd ${HOME} && \
mv rcnn/frozen_inference_graph.pb rcnn/inference_graph.pb
if [[ "$CUDA_SUPPORT" = "yes" ]]
then
pip3 install --no-cache-dir tensorflow-gpu==1.7.0
else
pip3 install --no-cache-dir tensorflow==1.7.0
fi
# tensorflow is installed globally

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

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

@ -0,0 +1,198 @@
# 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
npm run build
npm run build -- --mode=development # without a minification
```
- Updating of a module version:
```bash
npm version patch # updated after minor fixes
npm version minor # updated after major changes which don't affect API compatibility with previous versions
npm version major # updated after major changes which affect API compatibility with previous versions
```
## Using
Canvas itself handles:
- Shape context menu (PKM)
- Image moving (mousedrag)
- Image resizing (mousewheel)
- Image fit (dblclick)
- Remove point (PKM)
- Polyshape editing (Shift + LKM)
### API Methods
```ts
enum Rotation {
ANTICLOCKWISE90,
CLOCKWISE90,
}
interface DrawData {
enabled: boolean;
shapeType?: string;
numberOfPoints?: number;
initialState?: any;
crosshair?: boolean;
}
interface GroupData {
enabled: boolean;
resetGroup?: boolean;
}
interface MergeData {
enabled: boolean;
}
interface SplitData {
enabled: boolean;
}
interface DrawnData {
shapeType: string;
points: number[];
objectType?: string;
occluded?: boolean;
attributes?: [index: number]: string;
label?: Label;
color?: string;
}
interface Canvas {
html(): HTMLDivElement;
setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void;
focus(clientID: number, padding?: number): void;
fit(): void;
grid(stepX: number, stepY: number): void;
draw(drawData: DrawData): void;
group(groupData: GroupData): void;
split(splitData: SplitData): void;
merge(mergeData: MergeData): void;
select(objectState: any): void;
cancel(): void;
}
```
### 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_grouping```,
```cvat_canvas_shape_merging```,
```cvat_canvas_shape_drawing```,
```cvat_canvas_shape_occluded```
- 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_pattern```
- Crosshair during a draw has class ```cvat_canvas_crosshair```
### Events
Standard JS events are used.
```js
- canvas.setup
- canvas.activated => ObjectState
- canvas.deactivated
- canvas.moved => {states: ObjectState[], x: number, y: number}
- canvas.find => {states: ObjectState[], x: number, y: number}
- canvas.drawn => {state: DrawnData}
- canvas.edited => {state: ObjectState, points: number[]}
- canvas.splitted => {state: ObjectState}
- canvas.groupped => {states: ObjectState[]}
- canvas.merged => {states: ObjectState[]}
- canvas.canceled
```
### WEB
```js
// Create an instance of a canvas
const canvas = new window.canvas.Canvas();
// Put canvas to a html container
htmlContainer.appendChild(canvas.html());
// Next you can use its API methods. For example:
canvas.rotate(window.Canvas.Rotation.CLOCKWISE90);
canvas.draw({
enabled: true,
shapeType: 'rectangle',
crosshair: true,
});
```
### TypeScript
- Add to ```tsconfig.json```:
```json
"compilerOptions": {
"paths": {
"cvat-canvas.node": ["3rdparty/cvat-canvas.node"]
}
}
```
- ```3rdparty``` directory contains both ```cvat-canvas.node.js``` and ```cvat-canvas.node.d.ts```.
- Add alias to ```webpack.config.js```:
```js
module.exports = {
resolve: {
alias: {
'cvat-canvas.node': path.resolve(__dirname, '3rdparty/cvat-canvas.node.js'),
}
}
}
```
Than you can use it in TypeScript:
```ts
import * as CANVAS from 'cvat-canvas.node';
// Create an instance of a canvas
const canvas = new CANVAS.Canvas();
// Put canvas to a html container
htmlContainer.appendChild(canvas.html());
// Next you can use its API methods. For example:
canvas.rotate(CANVAS.Rotation.CLOCKWISE90);
canvas.draw({
enabled: true,
shapeType: 'rectangle',
crosshair: true,
});
```
## States
![](images/states.svg)
## API Reaction
| | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING |
|------------|------|----------|-----------|---------|---------|---------|
| html() | + | + | + | + | + | + |
| setup() | + | + | + | + | + | - |
| activate() | + | - | - | - | - | - |
| rotate() | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + |
| draw() | + | - | - | - | - | - |
| split() | + | - | + | - | - | - |
| group | + | + | - | - | - | - |
| merge() | + | - | - | - | + | - |
| cancel() | - | + | + | + | + | + |

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 303 KiB

@ -0,0 +1,41 @@
{
"name": "cvat-canvas",
"version": "0.1.0",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts",
"scripts": {
"build": "tsc && webpack --config ./webpack.config.js",
"server": "nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'"
},
"author": "Intel",
"license": "MIT",
"dependencies": {
"svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.3",
"svg.js": "2.7.1",
"svg.resize.js": "1.4.3",
"svg.select.js": "3.0.1"
},
"devDependencies": {
"@babel/cli": "^7.5.5",
"@babel/core": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/preset-typescript": "^7.3.3",
"@types/node": "^12.6.8",
"@typescript-eslint/eslint-plugin": "^1.13.0",
"@typescript-eslint/parser": "^1.13.0",
"babel-loader": "^8.0.6",
"css-loader": "^3.2.0",
"dts-bundle-webpack": "^1.0.2",
"eslint": "^6.1.0",
"eslint-config-airbnb-typescript": "^4.0.1",
"eslint-config-typescript-recommended": "^1.4.17",
"eslint-plugin-import": "^2.18.2",
"nodemon": "^1.19.1",
"style-loader": "^1.0.0",
"typescript": "^3.5.3",
"webpack": "^4.36.1",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.7.2"
}
}

@ -0,0 +1,148 @@
.cvat_canvas_hidden {
display: none;
}
.cvat_canvas_shape {
fill-opacity: 0.05;
stroke-opacity: 1;
}
polyline.cvat_canvas_shape {
fill-opacity: 0;
stroke-opacity: 1;
}
.cvat_canvas_text {
font-weight: bold;
font-size: 1.2em;
fill: white;
cursor: default;
font-family: Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif;
text-shadow: 0px 0px 4px black;
user-select: none;
pointer-events: none;
}
.cvat_canvas_crosshair {
stroke: red;
}
.cvat_canvas_shape_activated {
fill-opacity: 0.3;
}
.cvat_canvas_shape_grouping {
fill: darkmagenta;
fill-opacity: 0.5;
}
polyline.cvat_canvas_shape_grouping {
stroke: darkmagenta;
stroke-opacity: 1;
}
.cvat_canvas_shape_merging {
fill: blue;
fill-opacity: 0.5;
}
polyline.cvat_canvas_shape_splitting {
stroke: dodgerblue;
stroke-opacity: 1;
}
.cvat_canvas_shape_splitting {
fill: dodgerblue;
fill-opacity: 0.5;
}
polyline.cvat_canvas_shape_merging {
stroke: blue;
stroke-opacity: 1;
}
.cvat_canvas_shape_drawing {
fill-opacity: 0.1;
stroke-opacity: 1;
fill: white;
stroke: black;
}
.cvat_canvas_shape_occluded {
stroke-dasharray: 5;
}
.svg_select_boundingRect {
opacity: 0;
pointer-events: none;
}
#cvat_canvas_wrapper {
width: 100%;
height: 93%;
border-radius: 5px;
background-color: white;
overflow: hidden;
position: relative;
}
#cvat_canvas_loading_animation {
z-index: 1;
position: absolute;
width: 100%;
height: 100%;
}
#cvat_canvas_loading_circle {
fill-opacity: 0;
stroke: #09c;
stroke-width: 3px;
stroke-dasharray: 50;
animation: loadingAnimation 1s linear infinite;
}
#cvat_canvas_text_content {
position: absolute;
z-index: 3;
pointer-events: none;
width: 100%;
height: 100%;
pointer-events: none;
}
#cvat_canvas_background {
position: absolute;
z-index: 0;
background-repeat: no-repeat;
width: 100%;
height: 100%;
box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75);
}
#cvat_canvas_grid {
position: absolute;
z-index: 2;
pointer-events: none;
width: 100%;
height: 100%;
pointer-events: none;
}
#cvat_canvas_grid_pattern {
opacity: 1;
stroke: white;
}
#cvat_canvas_content {
position: absolute;
z-index: 2;
outline: 10px solid black;
width: 100%;
height: 100%;
}
@keyframes loadingAnimation {
0% {stroke-dashoffset: 1; stroke: #09c;}
50% {stroke-dashoffset: 100; stroke: #f44;}
100% {stroke-dashoffset: 300; stroke: #09c;}
}

@ -0,0 +1,120 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
import {
Rotation,
DrawData,
MergeData,
SplitData,
GroupData,
CanvasModel,
CanvasModelImpl,
} from './canvasModel';
import {
Master,
} from './master';
import {
CanvasController,
CanvasControllerImpl,
} from './canvasController';
import {
CanvasView,
CanvasViewImpl,
} from './canvasView';
import '../css/canvas.css';
interface Canvas {
html(): HTMLDivElement;
setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void;
focus(clientID: number, padding?: number): void;
fit(): void;
grid(stepX: number, stepY: number): void;
draw(drawData: DrawData): void;
group(groupData: GroupData): void;
split(splitData: SplitData): void;
merge(mergeData: MergeData): void;
select(objectState: any): void;
cancel(): void;
}
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[]): void {
this.model.setup(frameData, objectStates);
}
public activate(clientID: number, attributeID: number = null): void {
this.model.activate(clientID, attributeID);
}
public rotate(rotation: Rotation, remember: boolean = false): void {
this.model.rotate(rotation, remember);
}
public focus(clientID: number, padding: number = 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 draw(drawData: DrawData): void {
this.model.draw(drawData);
}
public split(splitData: SplitData): void {
this.model.split(splitData);
}
public group(groupData: GroupData): void {
this.model.group(groupData);
}
public merge(mergeData: MergeData): void {
this.model.merge(mergeData);
}
public select(objectState: any): void {
this.model.select(objectState);
}
public cancel(): void {
this.model.cancel();
}
}
export {
CanvasImpl as Canvas,
Rotation,
};

@ -0,0 +1,147 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
import {
CanvasModel,
Geometry,
Position,
FocusData,
ActiveElement,
DrawData,
MergeData,
SplitData,
GroupData,
Mode,
} from './canvasModel';
export interface CanvasController {
readonly objects: any[];
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
readonly drawData: DrawData;
readonly mergeData: MergeData;
readonly splitData: SplitData;
readonly groupData: GroupData;
readonly selected: any;
mode: Mode;
geometry: Geometry;
zoom(x: number, y: number, direction: number): void;
draw(drawData: DrawData): void;
merge(mergeData: MergeData): void;
split(splitData: SplitData): void;
group(groupData: GroupData): void;
enableDrag(x: number, y: number): void;
drag(x: number, y: number): void;
disableDrag(): void;
fit(): 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, direction: number): void {
this.model.zoom(x, y, direction);
}
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 merge(mergeData: MergeData): void {
this.model.merge(mergeData);
}
public split(splitData: SplitData): void {
this.model.split(splitData);
}
public group(groupData: GroupData): void {
this.model.group(groupData);
}
public get geometry(): Geometry {
return this.model.geometry;
}
public set geometry(geometry: Geometry) {
this.model.geometry = geometry;
}
public get objects(): any[] {
return this.model.objects;
}
public get focusData(): FocusData {
return this.model.focusData;
}
public get activeElement(): ActiveElement {
return this.model.activeElement;
}
public get drawData(): DrawData {
return this.model.drawData;
}
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 selected(): any {
return this.model.selected;
}
public set mode(value: Mode) {
this.model.mode = value;
}
public get mode(): Mode {
return this.model.mode;
}
}

@ -0,0 +1,504 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
// Disable till full implementation
/* eslint class-methods-use-this: "off" */
import { MasterImpl } from './master';
export interface Size {
width: number;
height: number;
}
export interface Position {
x: number;
y: number;
}
export interface Geometry {
image: Size;
canvas: Size;
grid: Size;
top: number;
left: number;
scale: number;
offset: number;
angle: number;
}
export interface FocusData {
clientID: number;
padding: number;
}
export interface ActiveElement {
clientID: number;
attributeID: number;
}
export interface DrawData {
enabled: boolean;
shapeType?: string;
numberOfPoints?: number;
initialState?: any;
crosshair?: boolean;
}
export interface EditData {
enabled: boolean;
state: any;
pointID: number;
}
export interface GroupData {
enabled: boolean;
}
export interface MergeData {
enabled: boolean;
}
export interface SplitData {
enabled: boolean;
}
export enum FrameZoom {
MIN = 0.1,
MAX = 10,
}
export enum Rotation {
ANTICLOCKWISE90,
CLOCKWISE90,
}
export enum UpdateReasons {
IMAGE = 'image',
OBJECTS = 'objects',
ZOOM = 'zoom',
FIT = 'fit',
MOVE = 'move',
GRID = 'grid',
FOCUS = 'focus',
ACTIVATE = 'activate',
DRAW = 'draw',
MERGE = 'merge',
SPLIT = 'split',
GROUP = 'group',
SELECT = 'select',
CANCEL = 'cancel',
}
export enum Mode {
IDLE = 'idle',
DRAG = 'drag',
RESIZE = 'resize',
DRAW = 'draw',
EDIT = 'edit',
MERGE = 'merge',
SPLIT = 'split',
GROUP = 'group',
}
export interface CanvasModel {
readonly image: string;
readonly objects: any[];
readonly gridSize: Size;
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
readonly drawData: DrawData;
readonly mergeData: MergeData;
readonly splitData: SplitData;
readonly groupData: GroupData;
readonly selected: any;
geometry: Geometry;
mode: Mode;
zoom(x: number, y: number, direction: number): void;
move(topOffset: number, leftOffset: number): void;
setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID: number): void;
rotate(rotation: Rotation, remember: boolean): void;
focus(clientID: number, padding: number): void;
fit(): void;
grid(stepX: number, stepY: number): void;
draw(drawData: DrawData): void;
group(groupData: GroupData): void;
split(splitData: SplitData): void;
merge(mergeData: MergeData): void;
select(objectState: any): void;
cancel(): void;
}
export class CanvasModelImpl extends MasterImpl implements CanvasModel {
private data: {
activeElement: ActiveElement;
angle: number;
canvasSize: Size;
image: string;
imageOffset: number;
imageSize: Size;
focusData: FocusData;
gridSize: Size;
left: number;
objects: any[];
rememberAngle: boolean;
scale: number;
top: number;
drawData: DrawData;
mergeData: MergeData;
groupData: GroupData;
splitData: SplitData;
selected: any;
mode: Mode;
};
public constructor() {
super();
this.data = {
activeElement: {
clientID: null,
attributeID: null,
},
angle: 0,
canvasSize: {
height: 0,
width: 0,
},
image: '',
imageOffset: 0,
imageSize: {
height: 0,
width: 0,
},
focusData: {
clientID: 0,
padding: 0,
},
gridSize: {
height: 100,
width: 100,
},
left: 0,
objects: [],
rememberAngle: false,
scale: 1,
top: 0,
drawData: {
enabled: false,
shapeType: null,
numberOfPoints: null,
initialState: null,
},
mergeData: {
enabled: false,
},
groupData: {
enabled: false,
},
splitData: {
enabled: false,
},
selected: null,
mode: null,
};
}
public zoom(x: number, y: number, direction: number): void {
const oldScale: number = this.data.scale;
const newScale: number = direction > 0 ? oldScale * 6 / 5 : oldScale * 5 / 6;
this.data.scale = Math.min(Math.max(newScale, FrameZoom.MIN), FrameZoom.MAX);
const { angle } = this.data;
const mutiplier = Math.sin(angle * Math.PI / 180) + Math.cos(angle * Math.PI / 180);
if ((angle / 90) % 2) {
// 90, 270, ..
this.data.top += mutiplier * ((x - this.data.imageSize.width / 2)
* (oldScale / this.data.scale - 1)) * this.data.scale;
this.data.left -= mutiplier * ((y - this.data.imageSize.height / 2)
* (oldScale / this.data.scale - 1)) * this.data.scale;
} else {
this.data.left += mutiplier * ((x - this.data.imageSize.width / 2)
* (oldScale / this.data.scale - 1)) * this.data.scale;
this.data.top += mutiplier * ((y - this.data.imageSize.height / 2)
* (oldScale / this.data.scale - 1)) * this.data.scale;
}
this.notify(UpdateReasons.ZOOM);
}
public move(topOffset: number, leftOffset: number): void {
this.data.top += topOffset;
this.data.left += leftOffset;
this.notify(UpdateReasons.MOVE);
}
public setup(frameData: any, objectStates: any[]): void {
frameData.data(
(): void => {
this.data.image = '';
this.notify(UpdateReasons.IMAGE);
},
).then((data: string): void => {
this.data.imageSize = {
height: (frameData.height as number),
width: (frameData.width as number),
};
if (!this.data.rememberAngle) {
this.data.angle = 0;
}
this.data.image = data;
this.notify(UpdateReasons.IMAGE);
this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS);
}).catch((exception: any): void => {
throw exception;
});
}
public activate(clientID: number, attributeID: number): void {
if (this.data.mode !== Mode.IDLE) {
// Exception or just return?
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
this.data.activeElement = {
clientID,
attributeID,
};
this.notify(UpdateReasons.ACTIVATE);
}
public rotate(rotation: Rotation, remember: boolean = false): void {
if (rotation === Rotation.CLOCKWISE90) {
this.data.angle += 90;
} else {
this.data.angle -= 90;
}
this.data.angle %= 360;
this.data.rememberAngle = remember;
this.fit();
}
public focus(clientID: number, padding: number): void {
this.data.focusData = {
clientID,
padding,
};
this.notify(UpdateReasons.FOCUS);
}
public fit(): void {
const { angle } = this.data;
if ((angle / 90) % 2) {
// 90, 270, ..
this.data.scale = Math.min(
this.data.canvasSize.width / this.data.imageSize.height,
this.data.canvasSize.height / this.data.imageSize.width,
);
} else {
this.data.scale = Math.min(
this.data.canvasSize.width / this.data.imageSize.width,
this.data.canvasSize.height / this.data.imageSize.height,
);
}
this.data.scale = Math.min(
Math.max(this.data.scale, FrameZoom.MIN),
FrameZoom.MAX,
);
this.data.top = (this.data.canvasSize.height / 2 - this.data.imageSize.height / 2);
this.data.left = (this.data.canvasSize.width / 2 - this.data.imageSize.width / 2);
this.notify(UpdateReasons.FIT);
}
public grid(stepX: number, stepY: number): void {
this.data.gridSize = {
height: stepY,
width: stepX,
};
this.notify(UpdateReasons.GRID);
}
public draw(drawData: DrawData): void {
if (![Mode.IDLE, Mode.DRAW].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (drawData.enabled) {
if (this.data.drawData.enabled) {
throw new Error('Drawing has been already started');
} else if (!drawData.shapeType && !drawData.initialState) {
throw new Error('A shape type is not specified');
} else if (typeof (drawData.numberOfPoints) !== 'undefined') {
if (drawData.shapeType === 'polygon' && drawData.numberOfPoints < 3) {
throw new Error('A polygon consists of at least 3 points');
} else if (drawData.shapeType === 'polyline' && drawData.numberOfPoints < 2) {
throw new Error('A polyline consists of at least 2 points');
}
}
}
this.data.drawData = { ...drawData };
if (this.data.drawData.initialState) {
this.data.drawData.shapeType = this.data.drawData.initialState.shapeType;
}
this.notify(UpdateReasons.DRAW);
}
public split(splitData: SplitData): void {
if (![Mode.IDLE, Mode.SPLIT].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (this.data.splitData.enabled && splitData.enabled) {
return;
}
if (!this.data.splitData.enabled && !splitData.enabled) {
return;
}
this.data.splitData = { ...splitData };
this.notify(UpdateReasons.SPLIT);
}
public group(groupData: GroupData): void {
if (![Mode.IDLE, Mode.GROUP].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (this.data.groupData.enabled && groupData.enabled) {
return;
}
if (!this.data.groupData.enabled && !groupData.enabled) {
return;
}
this.data.groupData = { ...groupData };
this.notify(UpdateReasons.GROUP);
}
public merge(mergeData: MergeData): void {
if (![Mode.IDLE, Mode.MERGE].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (this.data.mergeData.enabled && mergeData.enabled) {
return;
}
if (!this.data.mergeData.enabled && !mergeData.enabled) {
return;
}
this.data.mergeData = { ...mergeData };
this.notify(UpdateReasons.MERGE);
}
public select(objectState: any): void {
this.data.selected = objectState;
this.notify(UpdateReasons.SELECT);
this.data.selected = null;
}
public cancel(): void {
this.notify(UpdateReasons.CANCEL);
}
public get geometry(): Geometry {
return {
angle: this.data.angle,
canvas: { ...this.data.canvasSize },
image: { ...this.data.imageSize },
grid: { ...this.data.gridSize },
left: this.data.left,
offset: this.data.imageOffset,
scale: this.data.scale,
top: this.data.top,
};
}
public set geometry(geometry: Geometry) {
this.data.angle = geometry.angle;
this.data.canvasSize = { ...geometry.canvas };
this.data.imageSize = { ...geometry.image };
this.data.gridSize = { ...geometry.grid };
this.data.left = geometry.left;
this.data.top = geometry.top;
this.data.imageOffset = geometry.offset;
this.data.scale = geometry.scale;
this.data.imageOffset = Math.floor(Math.max(
this.data.canvasSize.height / FrameZoom.MIN,
this.data.canvasSize.width / FrameZoom.MIN,
));
}
public get image(): string {
return this.data.image;
}
public get objects(): any[] {
return this.data.objects;
}
public get gridSize(): Size {
return { ...this.data.gridSize };
}
public get focusData(): FocusData {
return { ...this.data.focusData };
}
public get activeElement(): ActiveElement {
return { ...this.data.activeElement };
}
public get drawData(): DrawData {
return { ...this.data.drawData };
}
public get mergeData(): MergeData {
return { ...this.data.mergeData };
}
public get splitData(): SplitData {
return { ...this.data.splitData };
}
public get groupData(): GroupData {
return { ...this.data.groupData };
}
public get selected(): any {
return this.data.selected;
}
public set mode(value: Mode) {
this.data.mode = value;
}
public get mode(): Mode {
return this.data.mode;
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,24 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
const BASE_STROKE_WIDTH = 2;
const BASE_GRID_WIDTH = 1;
const BASE_POINT_SIZE = 5;
const TEXT_MARGIN = 10;
const AREA_THRESHOLD = 9;
const SIZE_THRESHOLD = 3;
const POINTS_STROKE_WIDTH = 1.5;
const POINTS_SELECTED_STROKE_WIDTH = 4;
export default {
BASE_STROKE_WIDTH,
BASE_GRID_WIDTH,
BASE_POINT_SIZE,
TEXT_MARGIN,
AREA_THRESHOLD,
SIZE_THRESHOLD,
POINTS_STROKE_WIDTH,
POINTS_SELECTED_STROKE_WIDTH,
};

@ -0,0 +1,568 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
import * as SVG from 'svg.js';
import consts from './consts';
import 'svg.draw.js';
import './svg.patch';
import {
DrawData,
Geometry,
} from './canvasModel';
import {
translateToSVG,
translateBetweenSVG,
displayShapeSize,
ShapeSizeElement,
pointsToString,
pointsToArray,
BBox,
Box,
} from './shared';
export interface DrawHandler {
draw(drawData: DrawData, geometry: Geometry): void;
cancel(): void;
}
export class DrawHandlerImpl implements DrawHandler {
// callback is used to notify about creating new shape
private onDrawDone: (data: object) => void;
private canvas: SVG.Container;
private text: SVG.Container;
private background: SVGSVGElement;
private crosshair: {
x: SVG.Line;
y: SVG.Line;
};
private drawData: DrawData;
private geometry: Geometry;
// we should use any instead of SVG.Shape because svg plugins cannot change declared interface
// so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist
private drawInstance: any;
private shapeSizeElement: ShapeSizeElement;
private getFinalRectCoordinates(bbox: BBox): number[] {
const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height;
let [xtl, ytl, xbr, ybr] = translateBetweenSVG(
this.canvas.node as any as SVGSVGElement,
this.background,
[bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height],
);
xtl = Math.min(Math.max(xtl, 0), frameWidth);
xbr = Math.min(Math.max(xbr, 0), frameWidth);
ytl = Math.min(Math.max(ytl, 0), frameHeight);
ybr = Math.min(Math.max(ybr, 0), frameHeight);
return [xtl, ytl, xbr, ybr];
}
private getFinalPolyshapeCoordinates(targetPoints: number[]): {
points: number[];
box: Box;
} {
const points = translateBetweenSVG(
this.canvas.node as any as SVGSVGElement,
this.background,
targetPoints,
);
const box = {
xtl: Number.MAX_SAFE_INTEGER,
ytl: Number.MAX_SAFE_INTEGER,
xbr: Number.MAX_SAFE_INTEGER,
ybr: Number.MAX_SAFE_INTEGER,
};
const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height;
for (let i = 0; i < points.length - 1; i += 2) {
points[i] = Math.min(Math.max(points[i], 0), frameWidth);
points[i + 1] = Math.min(Math.max(points[i + 1], 0), frameHeight);
box.xtl = Math.min(box.xtl, points[i]);
box.ytl = Math.min(box.ytl, points[i + 1]);
box.xbr = Math.max(box.xbr, points[i]);
box.ybr = Math.max(box.ybr, points[i + 1]);
}
return {
points,
box,
};
}
private addCrosshair(): void {
this.crosshair = {
x: this.canvas.line(0, 0, this.canvas.node.clientWidth, 0).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
y: this.canvas.line(0, 0, 0, this.canvas.node.clientHeight).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
};
}
private removeCrosshair(): void {
this.crosshair.x.remove();
this.crosshair.y.remove();
this.crosshair = null;
}
private release(): void {
this.canvas.off('mousedown.draw');
this.canvas.off('mousemove.draw');
this.canvas.off('click.draw');
if (this.drawInstance) {
// Draw plugin isn't activated when draw from initialState
// So, we don't need to use any draw events
if (!this.drawData.initialState) {
this.drawInstance.off('drawdone');
this.drawInstance.off('drawstop');
this.drawInstance.draw('stop');
}
this.drawInstance.remove();
this.drawInstance = null;
}
if (this.shapeSizeElement) {
this.shapeSizeElement.rm();
this.shapeSizeElement = null;
}
if (this.crosshair) {
this.removeCrosshair();
}
}
private initDrawing(): void {
if (this.drawData.crosshair) {
this.addCrosshair();
}
}
private closeDrawing(): void {
if (this.drawInstance) {
// Draw plugin isn't activated when draw from initialState
// So, we don't need to use any draw events
if (!this.drawData.initialState) {
const { drawInstance } = this;
this.drawInstance = null;
if (this.drawData.shapeType === 'rectangle') {
drawInstance.draw('cancel');
} else {
drawInstance.draw('done');
}
this.drawInstance = drawInstance;
this.release();
} else {
this.release();
this.onDrawDone(null);
}
// here is a cycle
// onDrawDone => controller => model => view => closeDrawing
// one call of closeDrawing is unuseful, but it's okey
}
}
private drawBox(): void {
this.drawInstance = this.canvas.rect();
this.drawInstance.draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
z_order: Number.MAX_SAFE_INTEGER,
}).on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
}).on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone({
shapeType: this.drawData.shapeType,
points: [xtl, ytl, xbr, ybr],
});
} else {
this.onDrawDone(null);
}
});
}
private drawPolyshape(): void {
this.drawInstance.attr({
z_order: Number.MAX_SAFE_INTEGER,
});
let size = this.drawData.numberOfPoints;
const sizeDecrement = function sizeDecrement(): void {
if (!--size) {
this.drawInstance.draw('done');
}
}.bind(this);
const sizeIncrement = function sizeIncrement(): void {
size++;
};
if (this.drawData.numberOfPoints) {
this.drawInstance.on('drawstart', sizeDecrement);
this.drawInstance.on('drawpoint', sizeDecrement);
this.drawInstance.on('undopoint', sizeIncrement);
}
// Add ability to cancel the latest drawn point
const handleUndo = function handleUndo(e: MouseEvent): void {
if (e.which === 3) {
e.stopPropagation();
e.preventDefault();
this.drawInstance.draw('undo');
}
}.bind(this);
this.canvas.on('mousedown.draw', handleUndo);
// Add ability to draw 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,
};
const handleSlide = function handleSlide(e: MouseEvent): void {
// TODO: Use enumeration after typification cvat-core
if (e.shiftKey && ['polygon', 'polyline'].includes(this.drawData.shapeType)) {
if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) {
this.drawInstance.draw('point', e);
} else {
const deltaTreshold = 15;
const delta = Math.sqrt(
((e.clientX - lastDrawnPoint.x) ** 2)
+ ((e.clientY - lastDrawnPoint.y) ** 2),
);
if (delta > deltaTreshold) {
this.drawInstance.draw('point', e);
}
}
}
}.bind(this);
this.canvas.on('mousemove.draw', handleSlide);
// We need scale just drawn points
const self = this;
this.drawInstance.on('drawstart drawpoint', (e: CustomEvent): void => {
self.transform(self.geometry);
lastDrawnPoint.x = e.detail.event.clientX;
lastDrawnPoint.y = e.detail.event.clientY;
});
this.drawInstance.on('drawdone', (e: CustomEvent): void => {
const targetPoints = pointsToArray((e.target as SVGElement).getAttribute('points'));
const {
points,
box,
} = this.getFinalPolyshapeCoordinates(targetPoints);
if (this.drawData.shapeType === 'polygon'
&& ((box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD)
&& points.length >= 3 * 2) {
this.onDrawDone({
shapeType: this.drawData.shapeType,
points,
});
} else if (this.drawData.shapeType === 'polyline'
&& ((box.xbr - box.xtl) >= consts.SIZE_THRESHOLD
|| (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD)
&& points.length >= 2 * 2) {
this.onDrawDone({
shapeType: this.drawData.shapeType,
points,
});
} else if (this.drawData.shapeType === 'points'
&& (e.target as any).getAttribute('points') !== '0,0') {
this.onDrawDone({
shapeType: this.drawData.shapeType,
points,
});
} else {
this.onDrawDone(null);
}
});
}
private drawPolygon(): void {
this.drawInstance = (this.canvas as any).polygon().draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.drawPolyshape();
}
private drawPolyline(): void {
this.drawInstance = (this.canvas as any).polyline().draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': 0,
});
this.drawPolyshape();
}
private drawPoints(): void {
this.drawInstance = (this.canvas as any).polygon().draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({
'stroke-width': 0,
opacity: 0,
});
this.drawPolyshape();
}
private pastePolyshape(): void {
this.canvas.on('click.draw', (e: MouseEvent): void => {
const targetPoints = (e.target as SVGElement)
.getAttribute('points')
.split(/[,\s]/g)
.map((coord): number => +coord);
const { points } = this.getFinalPolyshapeCoordinates(targetPoints);
this.release();
this.onDrawDone({
shapeType: this.drawData.shapeType,
points,
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
});
});
}
// Common settings for rectangle and polyshapes
private pasteShape(): void {
this.drawInstance.attr({
z_order: Number.MAX_SAFE_INTEGER,
});
this.canvas.on('mousemove.draw', (e: MouseEvent): void => {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
const bbox = this.drawInstance.bbox();
this.drawInstance.move(x - bbox.width / 2, y - bbox.height / 2);
});
}
private pasteBox(box: BBox): void {
this.drawInstance = (this.canvas as any).rect(box.width, box.height)
.move(box.x, box.y)
.addClass('cvat_canvas_shape_drawing').style({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.pasteShape();
this.canvas.on('click.draw', (e: MouseEvent): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
this.release();
this.onDrawDone({
shapeType: this.drawData.shapeType,
points: [xtl, ytl, xbr, ybr],
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
});
});
}
private pastePolygon(points: string): void {
this.drawInstance = (this.canvas as any).polygon(points)
.addClass('cvat_canvas_shape_drawing').style({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.pasteShape();
this.pastePolyshape();
}
private pastePolyline(points: string): void {
this.drawInstance = (this.canvas as any).polyline(points)
.addClass('cvat_canvas_shape_drawing').style({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.pasteShape();
this.pastePolyshape();
}
private pastePoints(points: string): void {
this.drawInstance = (this.canvas as any).polyline(points)
.addClass('cvat_canvas_shape_drawing').style({
'stroke-width': 0,
});
this.pasteShape();
this.pastePolyshape();
}
private startDraw(): void {
// TODO: Use enums after typification cvat-core
if (this.drawData.initialState) {
if (this.drawData.shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = translateBetweenSVG(
this.background,
this.canvas.node as any as SVGSVGElement,
this.drawData.initialState.points,
);
this.pasteBox({
x: xtl,
y: ytl,
width: xbr - xtl,
height: ybr - ytl,
});
} else {
const points = translateBetweenSVG(
this.background,
this.canvas.node as any as SVGSVGElement,
this.drawData.initialState.points,
);
const stringifiedPoints = pointsToString(points);
if (this.drawData.shapeType === 'polygon') {
this.pastePolygon(stringifiedPoints);
} else if (this.drawData.shapeType === 'polyline') {
this.pastePolyline(stringifiedPoints);
} else if (this.drawData.shapeType === 'points') {
this.pastePoints(stringifiedPoints);
}
}
} else if (this.drawData.shapeType === 'rectangle') {
this.drawBox();
// Draw instance was initialized after drawBox();
this.shapeSizeElement = displayShapeSize(this.canvas, this.text);
} else if (this.drawData.shapeType === 'polygon') {
this.drawPolygon();
} else if (this.drawData.shapeType === 'polyline') {
this.drawPolyline();
} else if (this.drawData.shapeType === 'points') {
this.drawPoints();
}
}
public constructor(
onDrawDone: (data: object) => void,
canvas: SVG.Container,
text: SVG.Container,
background: SVGSVGElement,
) {
this.onDrawDone = onDrawDone;
this.canvas = canvas;
this.text = text;
this.background = background;
this.drawData = null;
this.geometry = null;
this.crosshair = null;
this.drawInstance = null;
this.canvas.on('mousemove.crosshair', (e: MouseEvent): void => {
if (this.crosshair) {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
this.crosshair.x.attr({
y1: y,
y2: y,
});
this.crosshair.y.attr({
x1: x,
x2: x,
});
}
});
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.shapeSizeElement && this.drawInstance && this.drawData.shapeType === 'rectangle') {
this.shapeSizeElement.update(this.drawInstance);
}
if (this.crosshair) {
this.crosshair.x.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale),
});
this.crosshair.y.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale),
});
}
if (this.drawInstance) {
this.drawInstance.draw('transform');
this.drawInstance.style({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
});
const paintHandler = this.drawInstance.remember('_paintHandler');
for (const point of (paintHandler as any).set.members) {
point.style(
'stroke-width',
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
);
point.attr(
'r',
`${consts.BASE_POINT_SIZE / geometry.scale}`,
);
}
}
}
public draw(drawData: DrawData, geometry: Geometry): void {
this.geometry = geometry;
if (drawData.enabled) {
this.drawData = drawData;
this.initDrawing();
this.startDraw();
} else {
this.closeDrawing();
this.drawData = drawData;
}
}
public cancel(): void {
this.release();
this.onDrawDone(null);
// here is a cycle
// onDrawDone => controller => model => view => closeDrawing
// one call of closeDrawing is unuseful, but it's okey
}
}

@ -0,0 +1,343 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
import * as SVG from 'svg.js';
import 'svg.select.js';
import consts from './consts';
import {
translateFromSVG,
translateBetweenSVG,
pointsToArray,
} from './shared';
import {
EditData,
Geometry,
} from './canvasModel';
export interface EditHandler {
edit(editData: EditData): void;
transform(geometry: Geometry): void;
cancel(): void;
}
export class EditHandlerImpl implements EditHandler {
private onEditDone: (state: any, points: number[]) => void;
private geometry: Geometry;
private canvas: SVG.Container;
private background: SVGSVGElement;
private editData: EditData;
private editedShape: SVG.Shape;
private editLine: SVG.PolyLine;
private clones: SVG.Polygon[];
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(','),
);
// 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,
};
const handleSlide = function handleSlide(e: MouseEvent): void {
if (e.shiftKey) {
if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) {
this.editLine.draw('point', e);
} else {
const deltaTreshold = 15;
const delta = Math.sqrt(
((e.clientX - lastDrawnPoint.x) ** 2)
+ ((e.clientY - lastDrawnPoint.y) ** 2),
);
if (delta > deltaTreshold) {
this.editLine.draw('point', e);
}
}
}
}.bind(this);
this.canvas.on('mousemove.draw', handleSlide);
this.editLine = (this.canvas as any).polyline().draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({
'pointer-events': 'none',
'fill-opacity': 0,
}).on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry);
lastDrawnPoint.x = e.detail.event.clientX;
lastDrawnPoint.y = e.detail.event.clientY;
});
if (this.editData.state.shapeType === 'points') {
this.editLine.style('stroke-width', 0);
} else {
// generate mouse event
const dummyEvent = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
clientX,
clientY,
});
(this.editLine as any).draw('point', dummyEvent);
}
}
private stopEdit(e: MouseEvent): void {
function selectPolygon(shape: SVG.Polygon): void {
const points = translateBetweenSVG(
this.canvas.node as any as SVGSVGElement,
this.background,
pointsToArray(shape.attr('points')),
);
const { state } = this.editData;
this.edit({
enabled: false,
});
this.onEditDone(state, points);
}
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') {
if (start !== this.editData.pointID) {
linePoints.reverse();
}
const firstPart = oldPoints.slice(0, start)
.concat(linePoints)
.concat(oldPoints.slice(stop + 1));
linePoints.reverse();
const secondPart = oldPoints.slice(start + 1, stop)
.concat(linePoints);
if (firstPart.length < 3 || secondPart.length < 3) {
this.cancel();
return;
}
for (const points of [firstPart, secondPart]) {
this.clones.push(this.canvas.polygon(points.join(' '))
.attr('fill', this.editedShape.attr('fill'))
.style('fill-opacity', '0.5')
.addClass('cvat_canvas_shape'));
}
for (const clone of this.clones) {
clone.on('click', selectPolygon.bind(this, clone));
clone.on('mouseenter', (): void => {
clone.addClass('cvat_canvas_shape_splitting');
}).on('mouseleave', (): void => {
clone.removeClass('cvat_canvas_shape_splitting');
});
}
(this.editLine as any).draw('stop');
this.editLine.remove();
this.editLine = null;
return;
}
let points = null;
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 = translateBetweenSVG(
this.canvas.node as any as SVGSVGElement,
this.background,
pointsToArray(points.join(' ')),
);
const { state } = this.editData;
this.edit({
enabled: false,
});
this.onEditDone(state, points);
}
private setupPoints(enabled: boolean): void {
const self = this;
const stopEdit = self.stopEdit.bind(self);
if (enabled) {
(this.editedShape as any).selectize(true, {
deepSelect: true,
pointSize: 2 * consts.BASE_POINT_SIZE / self.geometry.scale,
rotationPoint: false,
pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested
.circle(this.options.pointSize)
.stroke('black')
.fill(self.editedShape.attr('fill') || 'inherit')
.center(cx, cy)
.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / self.geometry.scale,
});
circle.node.addEventListener('mouseenter', (): void => {
circle.attr({
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / self.geometry.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 / self.geometry.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('mousemove.draw');
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();
this.setupPoints(true);
this.startEdit();
// 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 {
this.release();
}
public constructor(
onEditDone: (state: any, points: number[]) => void,
canvas: SVG.Container,
background: SVGSVGElement,
) {
this.onEditDone = onEditDone;
this.canvas = canvas;
this.background = background;
this.editData = null;
this.editedShape = null;
this.editLine = null;
this.geometry = null;
this.clones = [];
}
public edit(editData: any): void {
if (editData.enabled) {
if (editData.state.shapeType !== 'rectangle') {
this.editData = editData;
this.initEditing();
} else {
this.cancel();
}
} else {
this.closeEditing();
this.editData = editData;
}
}
public cancel(): void {
this.release();
this.onEditDone(null, null);
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.editLine) {
(this.editLine as any).draw('transform');
if (this.editData.state.shapeType !== 'points') {
this.editLine.style({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
});
}
const paintHandler = this.editLine.remember('_paintHandler');
for (const point of (paintHandler as any).set.members) {
point.style(
'stroke-width',
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
);
point.attr(
'r',
`${consts.BASE_POINT_SIZE / geometry.scale}`,
);
}
}
}
}

@ -0,0 +1,208 @@
import * as SVG from 'svg.js';
import { GroupData } from './canvasModel';
import {
translateToSVG,
} from './shared';
export interface GroupHandler {
group(groupData: GroupData): void;
select(state: any): void;
cancel(): void;
}
export class GroupHandlerImpl implements GroupHandler {
// callback is used to notify about grouping end
private onGroupDone: (objects: any[]) => void;
private getStates: () => any[];
private onFindObject: (event: MouseEvent) => void;
private onSelectStart: (event: MouseEvent) => void;
private onSelectUpdate: (event: MouseEvent) => void;
private onSelectStop: (event: MouseEvent) => void;
private selectionRect: SVG.Rect;
private startSelectionPoint: {
x: number;
y: number;
};
private canvas: SVG.Container;
private initialized: boolean;
private states: any[];
private highlightedShapes: Record<number, SVG.Shape>;
private getSelectionBox(event: MouseEvent): {
xtl: number;
ytl: number;
xbr: number;
ybr: number;
} {
const point = translateToSVG(
(this.canvas.node as any as SVGSVGElement),
[event.clientX, event.clientY],
);
const stopSelectionPoint = {
x: point[0],
y: point[1],
};
return {
xtl: Math.min(this.startSelectionPoint.x, stopSelectionPoint.x),
ytl: Math.min(this.startSelectionPoint.y, stopSelectionPoint.y),
xbr: Math.max(this.startSelectionPoint.x, stopSelectionPoint.x),
ybr: Math.max(this.startSelectionPoint.y, stopSelectionPoint.y),
};
}
private release(): void {
this.canvas.node.removeEventListener('click', this.onFindObject);
this.canvas.node.removeEventListener('mousedown', this.onSelectStart);
this.canvas.node.removeEventListener('mousemove', this.onSelectUpdate);
this.canvas.node.removeEventListener('mouseup', this.onSelectStop);
this.canvas.node.removeEventListener('mouseleave', this.onSelectStop);
for (const state of this.states) {
const shape = this.highlightedShapes[state.clientID];
shape.removeClass('cvat_canvas_shape_grouping');
}
this.states = [];
this.highlightedShapes = {};
this.initialized = false;
this.selectionRect = null;
this.startSelectionPoint = {
x: null,
y: null,
};
}
private initGrouping(): void {
this.canvas.node.addEventListener('click', this.onFindObject);
this.canvas.node.addEventListener('mousedown', this.onSelectStart);
this.canvas.node.addEventListener('mousemove', this.onSelectUpdate);
this.canvas.node.addEventListener('mouseup', this.onSelectStop);
this.canvas.node.addEventListener('mouseleave', this.onSelectStop);
this.initialized = true;
}
private closeGrouping(): void {
if (this.initialized) {
const { states } = this;
this.release();
if (states.length) {
this.onGroupDone(states);
} else {
this.onGroupDone(null);
}
}
}
public constructor(
onGroupDone: (objects: any[]) => void,
getStates: () => any[],
onFindObject: (event: MouseEvent) => void,
canvas: SVG.Container,
) {
this.onGroupDone = onGroupDone;
this.getStates = getStates;
this.onFindObject = onFindObject;
this.canvas = canvas;
this.states = [];
this.highlightedShapes = {};
this.selectionRect = null;
this.startSelectionPoint = {
x: null,
y: null,
};
this.onSelectStart = function (event: MouseEvent): void {
if (!this.selectionRect) {
const point = translateToSVG(this.canvas.node, [event.clientX, event.clientY]);
this.startSelectionPoint = {
x: point[0],
y: point[1],
};
this.selectionRect = this.canvas.rect().addClass('cvat_canvas_shape_grouping');
this.selectionRect.attr({ ...this.startSelectionPoint });
}
}.bind(this);
this.onSelectUpdate = function (event: MouseEvent): void {
// called on mousemove
if (this.selectionRect) {
const box = this.getSelectionBox(event);
this.selectionRect.attr({
x: box.xtl,
y: box.ytl,
width: box.xbr - box.xtl,
height: box.ybr - box.ytl,
});
}
}.bind(this);
this.onSelectStop = function (event: MouseEvent): void {
// called on mouseup, mouseleave
if (this.selectionRect) {
this.selectionRect.remove();
this.selectionRect = null;
const box = this.getSelectionBox(event);
const shapes = (this.canvas.select('.cvat_canvas_shape') as any).members;
for (const shape of shapes) {
// TODO: Doesn't work properly for groups
const bbox = shape.bbox();
const clientID = shape.attr('clientID');
if (bbox.x > box.xtl && bbox.y > box.ytl
&& bbox.x + bbox.width < box.xbr
&& bbox.y + bbox.height < box.ybr
&& !(clientID in this.highlightedShapes)) {
const objectState = this.getStates()
.filter((state: any): boolean => state.clientID === clientID)[0];
if (objectState) {
this.states.push(objectState);
this.highlightedShapes[clientID] = shape;
(shape as any).addClass('cvat_canvas_shape_grouping');
}
}
}
}
}.bind(this);
}
/* eslint-disable-next-line */
public group(groupData: GroupData): void {
if (groupData.enabled) {
this.initGrouping();
} else {
this.closeGrouping();
}
}
public select(objectState: any): void {
const stateIndexes = this.states.map((state): number => state.clientID);
const includes = stateIndexes.indexOf(objectState.clientID);
if (includes !== -1) {
const shape = this.highlightedShapes[objectState.clientID];
this.states.splice(includes, 1);
if (shape) {
delete this.highlightedShapes[objectState.clientID];
shape.removeClass('cvat_canvas_shape_grouping');
}
} else {
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first();
if (shape) {
this.states.push(objectState);
this.highlightedShapes[objectState.clientID] = shape;
shape.addClass('cvat_canvas_shape_grouping');
}
}
}
public cancel(): void {
this.release();
this.onGroupDone(null);
}
}

@ -0,0 +1,45 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
export interface Master {
subscribe(listener: Listener): void;
unsubscribe(listener: Listener): void;
unsubscribeAll(): void;
notify(reason: string): void;
}
export interface Listener {
notify(master: Master, reason: string): void;
}
export class MasterImpl implements Master {
private listeners: Listener[];
public constructor() {
this.listeners = [];
}
public subscribe(listener: Listener): void {
this.listeners.push(listener);
}
public unsubscribe(listener: Listener): void {
for (let i = 0; i < this.listeners.length; i++) {
if (this.listeners[i] === listener) {
this.listeners.splice(i, 1);
}
}
}
public unsubscribeAll(): void {
this.listeners = [];
}
public notify(reason: string): void {
for (const listener of this.listeners) {
listener.notify(this, reason);
}
}
}

@ -0,0 +1,133 @@
import * as SVG from 'svg.js';
import { MergeData } from './canvasModel';
export interface MergeHandler {
merge(mergeData: MergeData): void;
select(state: any): void;
cancel(): void;
}
export class MergeHandlerImpl implements MergeHandler {
// callback is used to notify about merging end
private onMergeDone: (objects: any[]) => void;
private onFindObject: (event: MouseEvent) => void;
private canvas: SVG.Container;
private initialized: boolean;
private states: any[]; // are being merged
private highlightedShapes: Record<number, SVG.Shape>;
private constraints: {
labelID: number;
shapeType: string;
};
private addConstraints(): void {
const shape = this.states[0];
this.constraints = {
labelID: shape.label.id,
shapeType: shape.shapeType,
};
}
private removeConstraints(): void {
this.constraints = null;
}
private checkConstraints(state: any): boolean {
return !this.constraints || (state.label.id === this.constraints.labelID
&& state.shapeType === this.constraints.shapeType);
}
private release(): void {
this.removeConstraints();
this.canvas.node.removeEventListener('click', this.onFindObject);
for (const state of this.states) {
const shape = this.highlightedShapes[state.clientID];
shape.removeClass('cvat_canvas_shape_merging');
}
this.states = [];
this.highlightedShapes = {};
this.initialized = false;
}
private initMerging(): void {
this.canvas.node.addEventListener('click', this.onFindObject);
this.initialized = true;
}
private closeMerging(): void {
if (this.initialized) {
const { states } = this;
this.release();
if (states.length > 1) {
this.onMergeDone(states);
} else {
this.onMergeDone(null);
// here is a cycle
// onMergeDone => controller => model => view => closeMerging
// one call of closeMerging is unuseful, but it's okey
}
}
}
public constructor(
onMergeDone: (objects: any[]) => void,
onFindObject: (event: MouseEvent) => void,
canvas: SVG.Container,
) {
this.onMergeDone = onMergeDone;
this.onFindObject = onFindObject;
this.canvas = canvas;
this.states = [];
this.highlightedShapes = {};
this.constraints = null;
this.initialized = false;
}
public merge(mergeData: MergeData): void {
if (mergeData.enabled) {
this.initMerging();
} else {
this.closeMerging();
}
}
public select(objectState: any): void {
const stateIndexes = this.states.map((state): number => state.clientID);
const stateFrames = this.states.map((state): number => state.frame);
const includes = stateIndexes.indexOf(objectState.clientID);
if (includes !== -1) {
const shape = this.highlightedShapes[objectState.clientID];
this.states.splice(includes, 1);
if (shape) {
delete this.highlightedShapes[objectState.clientID];
shape.removeClass('cvat_canvas_shape_merging');
}
if (!this.states.length) {
this.removeConstraints();
}
} else {
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first();
if (shape && this.checkConstraints(objectState)
&& !stateFrames.includes(objectState.frame)) {
this.states.push(objectState);
this.highlightedShapes[objectState.clientID] = shape;
shape.addClass('cvat_canvas_shape_merging');
if (this.states.length === 1) {
this.addConstraints();
}
}
}
}
public cancel(): void {
this.release();
this.onMergeDone(null);
// here is a cycle
// onMergeDone => controller => model => view => closeMerging
// one call of closeMerging is unuseful, but it's okey
}
}

@ -0,0 +1,113 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
import * as SVG from 'svg.js';
import consts from './consts';
export interface ShapeSizeElement {
sizeElement: any;
update(shape: SVG.Shape): void;
rm(): void;
}
export interface Box {
xtl: number;
ytl: number;
xbr: number;
ybr: number;
}
export interface BBox {
width: number;
height: number;
x: number;
y: number;
}
// Translate point array from the client coordinate system
// to a coordinate system of a canvas
export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] {
const output = [];
const transformationMatrix = svg.getScreenCTM();
let pt = svg.createSVGPoint();
for (let i = 0; i < points.length - 1; i += 2) {
pt.x = points[i];
pt.y = points[i + 1];
pt = pt.matrixTransform(transformationMatrix);
output.push(pt.x, pt.y);
}
return output;
}
// Translate point array from a coordinate system of a canvas
// to the client coordinate system
export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
const output = [];
const transformationMatrix = svg.getScreenCTM().inverse();
let pt = svg.createSVGPoint();
for (let i = 0; i < points.length; i += 2) {
pt.x = points[i];
pt.y = points[i + 1];
pt = pt.matrixTransform(transformationMatrix);
output.push(pt.x, pt.y);
}
return output;
}
// Translate point array from the first canvas coordinate system
// to another
export function translateBetweenSVG(
from: SVGSVGElement,
to: SVGSVGElement,
points: number[],
): number[] {
return translateToSVG(to, translateFromSVG(from, points));
}
export function pointsToString(points: number[]): string {
return points.reduce((acc, val, idx): string => {
if (idx % 2) {
return `${acc},${val}`;
}
return `${acc} ${val}`;
}, '');
}
export function pointsToArray(points: string): number[] {
return points.trim().split(/[,\s]+/g)
.map((coord: string): number => +coord);
}
export function displayShapeSize(
shapesContainer: SVG.Container,
textContainer: SVG.Container,
): ShapeSizeElement {
const shapeSize: ShapeSizeElement = {
sizeElement: textContainer.text('').font({
weight: 'bolder',
}).fill('white').addClass('cvat_canvas_text'),
update(shape: SVG.Shape): void{
const bbox = shape.bbox();
const text = `${bbox.width.toFixed(1)}x${bbox.height.toFixed(1)}`;
const [x, y]: number[] = translateToSVG(
textContainer.node as any as SVGSVGElement,
translateFromSVG((shapesContainer.node as any as SVGSVGElement), [bbox.x, bbox.y]),
);
this.sizeElement.clear().plain(text)
.move(x + consts.TEXT_MARGIN, y + consts.TEXT_MARGIN);
},
rm(): void {
if (this.sizeElement) {
this.sizeElement.remove();
this.sizeElement = null;
}
},
};
return shapeSize;
}

@ -0,0 +1,98 @@
import * as SVG from 'svg.js';
import { SplitData } from './canvasModel';
export interface SplitHandler {
split(splitData: SplitData): void;
select(state: any): void;
cancel(): void;
}
export class SplitHandlerImpl implements SplitHandler {
// callback is used to notify about splitting end
private onSplitDone: (object: any) => void;
private onFindObject: (event: MouseEvent) => void;
private canvas: SVG.Container;
private highlightedShape: SVG.Shape;
private initialized: boolean;
private splitDone: boolean;
private resetShape(): void {
if (this.highlightedShape) {
this.highlightedShape.removeClass('cvat_canvas_shape_splitting');
this.highlightedShape.off('click.split');
this.highlightedShape = null;
}
}
private release(): void {
if (this.initialized) {
this.resetShape();
this.canvas.node.removeEventListener('mousemove', this.onFindObject);
this.initialized = false;
}
}
private initSplitting(): void {
this.canvas.node.addEventListener('mousemove', this.onFindObject);
this.initialized = true;
this.splitDone = false;
}
private closeSplitting(): void {
// Split done is true if an object was splitted
// Split also can be called with { enabled: false } without splitting an object
if (!this.splitDone) {
this.onSplitDone(null);
}
this.release();
}
public constructor(
onSplitDone: (object: any) => void,
onFindObject: (event: MouseEvent) => void,
canvas: SVG.Container,
) {
this.onSplitDone = onSplitDone;
this.onFindObject = onFindObject;
this.canvas = canvas;
this.highlightedShape = null;
this.initialized = false;
this.splitDone = false;
}
public split(splitData: SplitData): void {
if (splitData.enabled) {
this.initSplitting();
} else {
this.closeSplitting();
}
}
public select(state: any): void {
if (state.objectType === 'track') {
const shape = this.canvas.select(`#cvat_canvas_shape_${state.clientID}`).first();
if (shape && shape !== this.highlightedShape) {
this.resetShape();
this.highlightedShape = shape;
this.highlightedShape.addClass('cvat_canvas_shape_splitting');
this.canvas.node.append(this.highlightedShape.node);
this.highlightedShape.on('click.split', (): void => {
this.splitDone = true;
this.onSplitDone(state);
}, {
once: true,
});
}
} else {
this.resetShape();
}
}
public cancel(): void {
this.release();
this.onSplitDone(null);
// here is a cycle
// onSplitDone => controller => model => view => closeSplitting
// one call of closeMerging is unuseful, but it's okey
}
}

@ -0,0 +1,172 @@
import * as SVG from 'svg.js';
/* eslint-disable */
import 'svg.draggable.js';
import 'svg.resize.js';
import 'svg.select.js';
import 'svg.draw.js';
// Update constructor
const originalDraw = SVG.Element.prototype.draw;
SVG.Element.prototype.draw = function constructor(...args: any): any {
let handler = this.remember('_paintHandler');
if (!handler) {
originalDraw.call(this, ...args);
handler = this.remember('_paintHandler');
handler.set = new SVG.Set();
} else {
originalDraw.call(this, ...args);
}
return this;
};
for (const key of Object.keys(originalDraw)) {
SVG.Element.prototype.draw[key] = originalDraw[key];
}
// Create undo for polygones and polylines
function undo(): void {
if (this.set.length()) {
this.set.members.splice(-1, 1)[0].remove();
this.el.array().value.splice(-2, 1);
this.el.plot(this.el.array());
this.el.fire('undopoint');
}
}
SVG.Element.prototype.draw.extend('polyline', Object.assign({},
SVG.Element.prototype.draw.plugins.polyline,
{
undo: undo,
},
));
SVG.Element.prototype.draw.extend('polygon', Object.assign({},
SVG.Element.prototype.draw.plugins.polygon,
{
undo: undo,
},
));
// Create transform for rect, polyline and polygon
function transform(): void {
this.m = this.el.node.getScreenCTM().inverse();
this.offset = { x: window.pageXOffset, y: window.pageYOffset };
}
SVG.Element.prototype.draw.extend('rect', Object.assign({},
SVG.Element.prototype.draw.plugins.rect,
{
transform: transform,
},
));
SVG.Element.prototype.draw.extend('polyline', Object.assign({},
SVG.Element.prototype.draw.plugins.polyline,
{
transform: transform,
},
));
SVG.Element.prototype.draw.extend('polygon', Object.assign({},
SVG.Element.prototype.draw.plugins.polygon,
{
transform: transform,
},
));
// Fix method drawCircles
function drawCircles(): void {
const array = this.el.array().valueOf();
this.set.each(function (): void {
this.remove();
});
this.set.clear();
for (let i = 0; i < array.length - 1; ++i) {
[this.p.x] = array[i];
[, this.p.y] = array[i];
const p = this.p.matrixTransform(
this.parent.node.getScreenCTM()
.inverse()
.multiply(this.el.node.getScreenCTM()),
);
this.set.add(
this.parent
.circle(5)
.stroke({
width: 1,
}).fill('#ccc')
.center(p.x, p.y),
);
}
}
SVG.Element.prototype.draw.extend('line', Object.assign({},
SVG.Element.prototype.draw.plugins.line,
{
drawCircles: drawCircles,
}
));
SVG.Element.prototype.draw.extend('polyline', Object.assign({},
SVG.Element.prototype.draw.plugins.polyline,
{
drawCircles: drawCircles,
}
));
SVG.Element.prototype.draw.extend('polygon', Object.assign({},
SVG.Element.prototype.draw.plugins.polygon,
{
drawCircles: drawCircles,
}
));
// Fix method drag
const originalDraggable = SVG.Element.prototype.draggable;
SVG.Element.prototype.draggable = function constructor(...args: any): any {
let handler = this.remember('_draggable');
if (!handler) {
originalDraggable.call(this, ...args);
handler = this.remember('_draggable');
handler.drag = function(e: any) {
this.m = this.el.node.getScreenCTM().inverse();
return handler.constructor.prototype.drag.call(this, e);
}
} else {
originalDraggable.call(this, ...args);
}
return this;
};
for (const key of Object.keys(originalDraggable)) {
SVG.Element.prototype.draggable[key] = originalDraggable[key];
}
// Fix method resize
const originalResize = SVG.Element.prototype.resize;
SVG.Element.prototype.resize = function constructor(...args: any): any {
let handler = this.remember('_resizeHandler');
if (!handler) {
originalResize.call(this, ...args);
handler = this.remember('_resizeHandler');
handler.update = function(e: any) {
this.m = this.el.node.getScreenCTM().inverse();
return handler.constructor.prototype.update.call(this, e);
}
} else {
originalResize.call(this, ...args);
}
return this;
};
for (const key of Object.keys(originalResize)) {
SVG.Element.prototype.resize[key] = originalResize[key];
}

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

@ -0,0 +1,93 @@
/* eslint-disable */
const path = require('path');
const DtsBundleWebpack = require('dts-bundle-webpack')
const nodeConfig = {
target: 'node',
mode: 'production',
devtool: 'source-map',
entry: './src/typescript/canvas.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cvat-canvas.node.js',
library: 'canvas',
libraryTarget: 'commonjs',
},
resolve: {
extensions: ['.ts', '.js', '.json'],
},
module: {
rules: [{
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env'],
['@babel/typescript'],
],
sourceType: 'unambiguous',
},
},
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader']
}],
},
plugins: [
new DtsBundleWebpack({
name: 'cvat-canvas.node',
main: 'dist/declaration/canvas.d.ts',
out: '../cvat-canvas.node.d.ts',
}),
]
};
const webConfig = {
target: 'web',
mode: 'production',
devtool: 'source-map',
entry: './src/typescript/canvas.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cvat-canvas.js',
library: 'canvas',
libraryTarget: 'window',
},
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: false,
inline: true,
port: 3000,
},
resolve: {
extensions: ['.ts', '.js', '.json'],
},
module: {
rules: [{
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env'],
['@babel/typescript'],
],
sourceType: 'unambiguous',
},
},
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader']
}],
},
plugins: [
new DtsBundleWebpack({
name: 'cvat-canvas',
main: 'dist/declaration/canvas.d.ts',
out: '../cvat-canvas.d.ts',
}),
]
};
module.exports = [webConfig, nodeConfig]

@ -0,0 +1,54 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
module.exports = {
"env": {
"node": false,
"browser": true,
"es6": true,
"jquery": true,
"qunit": true,
},
"parserOptions": {
"parser": "babel-eslint",
"sourceType": "module",
"ecmaVersion": 2018,
},
"plugins": [
"security",
"no-unsanitized",
"no-unsafe-innerhtml",
],
"extends": [
"eslint:recommended",
"plugin:security/recommended",
"plugin:no-unsanitized/DOM",
"airbnb-base",
],
"rules": {
"no-await-in-loop": [0],
"global-require": [0],
"no-new": [0],
"class-methods-use-this": [0],
"no-restricted-properties": [0, {
"object": "Math",
"property": "pow",
}],
"no-plusplus": [0],
"no-param-reassign": [0],
"no-underscore-dangle": ["error", { "allowAfterThis": true }],
"no-restricted-syntax": [0, {"selector": "ForOfStatement"}],
"no-continue": [0],
"no-unsafe-innerhtml/no-unsafe-innerhtml": 1,
// This rule actual for user input data on the node.js environment mainly.
"security/detect-object-injection": 0,
"indent": ["warn", 4],
"no-useless-constructor": 0,
"func-names": [0],
"valid-typeof": [0],
"no-console": [0], // this rule deprecates console.log, console.warn etc. because "it is not good in production code"
},
};

@ -0,0 +1,6 @@
docs
node_modules
reports
package-lock.json
yarn.lock
dist

@ -0,0 +1,39 @@
# Module CVAT-CORE
## Description
This CVAT module is a clien-side JavaScipt library to management of objects, frames, logs, etc.
It contains the core logic of the Computer Vision Annotation Tool.
### Commands
- Dependencies installation
```bash
npm install
```
- Building the module from sources in the ```dist``` directory:
```bash
npm run build
npm run build -- --mode=development # without a minification
```
- Building the documentation in the ```docs``` directory:
```bash
npm run-script docs
```
- Running of tests:
```bash
npm run-script test
```
- Updating of a module version:
```bash
npm version patch # updated after minor fixes
npm version minor # updated after major changes which don't affect API compatibility with previous versions
npm version major # updated after major changes which affect API compatibility with previous versions
```
Visual studio code configurations:
- cvat.js debug starts debugging with entrypoint api.js
- cvat.js test builds library and runs entrypoint tests.js

@ -0,0 +1,32 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
const { defaults } = require('jest-config');
module.exports = {
coverageDirectory: 'reports/coverage',
coverageReporters: ['lcov'],
moduleFileExtensions: [
...defaults.moduleFileExtensions,
'ts',
'tsx',
],
reporters: [
'default',
['jest-junit', { outputDirectory: 'reports/junit' }],
],
testMatch: [
'**/tests/**/*.js',
],
testPathIgnorePatterns: [
'/node_modules/',
'/tests/mocks/*',
],
automock: false,
};

@ -0,0 +1,26 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
module.exports = {
plugins: [],
recurseDepth: 10,
source: {
includePattern: '.+\\.js(doc|x)?$',
excludePattern: '(^|\\/|\\\\)_',
},
sourceType: 'module',
tags: {
allowUnknownTags: false,
dictionaries: ['jsdoc', 'closure'],
},
templates: {
cleverLinks: false,
monospaceLinks: false,
default: {
outputSourceFiles: false,
},
},
};

@ -0,0 +1,45 @@
{
"name": "cvat-core.js",
"version": "0.1.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {
"build": "webpack",
"test": "jest --config=jest.config.js --coverage",
"docs": "jsdoc --readme README.md src/*.js -p -c jsdoc.config.js -d docs",
"coveralls": "cat ./reports/coverage/lcov.info | coveralls"
},
"author": "Intel",
"license": "MIT",
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"airbnb": "0.0.2",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.6",
"core-js": "^3.0.1",
"coveralls": "^3.0.5",
"eslint": "6.1.0",
"eslint-config-airbnb-base": "14.0.0",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-no-unsafe-innerhtml": "^1.0.16",
"eslint-plugin-no-unsanitized": "^3.0.2",
"eslint-plugin-security": "^1.4.0",
"jest": "^24.8.0",
"jest-junit": "^6.4.0",
"jsdoc": "^3.6.2",
"webpack": "^4.31.0",
"webpack-cli": "^3.3.2"
},
"dependencies": {
"axios": "^0.18.0",
"browser-or-node": "^1.2.1",
"error-stack-parser": "^2.0.2",
"form-data": "^2.5.0",
"jest-config": "^24.8.0",
"js-cookie": "^2.2.0",
"platform": "^1.3.5",
"store": "^2.0.12"
}
}

@ -0,0 +1,235 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
(() => {
/**
* Class representing an annotation loader
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Loader {
constructor(initialData) {
const data = {
display_name: initialData.display_name,
format: initialData.format,
handler: initialData.handler,
version: initialData.version,
};
Object.defineProperties(this, {
name: {
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.display_name,
},
format: {
/**
* @name format
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.format,
},
handler: {
/**
* @name handler
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.handler,
},
version: {
/**
* @name version
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.version,
},
});
}
}
/**
* Class representing an annotation dumper
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Dumper {
constructor(initialData) {
const data = {
display_name: initialData.display_name,
format: initialData.format,
handler: initialData.handler,
version: initialData.version,
};
Object.defineProperties(this, {
name: {
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.display_name,
},
format: {
/**
* @name format
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.format,
},
handler: {
/**
* @name handler
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.handler,
},
version: {
/**
* @name version
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.version,
},
});
}
}
/**
* Class representing an annotation format
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class AnnotationFormat {
constructor(initialData) {
const data = {
created_date: initialData.created_date,
updated_date: initialData.updated_date,
id: initialData.id,
owner: initialData.owner,
name: initialData.name,
handler_file: initialData.handler_file,
};
data.dumpers = initialData.dumpers.map(el => new Dumper(el));
data.loaders = initialData.loaders.map(el => new Loader(el));
// Now all fields are readonly
Object.defineProperties(this, {
id: {
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => data.id,
},
owner: {
/**
* @name owner
* @type {integer}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => data.owner,
},
name: {
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => data.name,
},
createdDate: {
/**
* @name createdDate
* @type {string}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => data.created_date,
},
updatedDate: {
/**
* @name updatedDate
* @type {string}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => data.updated_date,
},
handlerFile: {
/**
* @name handlerFile
* @type {string}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => data.handler_file,
},
loaders: {
/**
* @name loaders
* @type {module:API.cvat.classes.Loader[]}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => [...data.loaders],
},
dumpers: {
/**
* @name dumpers
* @type {module:API.cvat.classes.Dumper[]}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => [...data.dumpers],
},
});
}
}
module.exports = {
AnnotationFormat,
Loader,
Dumper,
};
})();

@ -0,0 +1,763 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => {
const {
RectangleShape,
PolygonShape,
PolylineShape,
PointsShape,
RectangleTrack,
PolygonTrack,
PolylineTrack,
PointsTrack,
Track,
Shape,
Tag,
objectStateFactory,
} = require('./annotations-objects');
const { checkObjectType } = require('./common');
const Statistics = require('./statistics');
const { Label } = require('./labels');
const {
DataError,
ArgumentError,
ScriptingError,
} = require('./exceptions');
const {
ObjectShape,
ObjectType,
} = require('./enums');
const ObjectState = require('./object-state');
const colors = [
'#0066FF', '#AF593E', '#01A368', '#FF861F', '#ED0A3F', '#FF3F34', '#76D7EA',
'#8359A3', '#FBE870', '#C5E17A', '#03BB85', '#FFDF00', '#8B8680', '#0A6B0D',
'#8FD8D8', '#A36F40', '#F653A6', '#CA3435', '#FFCBA4', '#FF99CC', '#FA9D5A',
'#FFAE42', '#A78B00', '#788193', '#514E49', '#1164B4', '#F4FA9F', '#FED8B1',
'#C32148', '#01796F', '#E90067', '#FF91A4', '#404E5A', '#6CDAE7', '#FFC1CC',
'#006A93', '#867200', '#E2B631', '#6EEB6E', '#FFC800', '#CC99BA', '#FF007C',
'#BC6CAC', '#DCCCD7', '#EBE1C2', '#A6AAAE', '#B99685', '#0086A7', '#5E4330',
'#C8A2C8', '#708EB3', '#BC8777', '#B2592D', '#497E48', '#6A2963', '#E6335F',
'#00755E', '#B5A895', '#0048ba', '#EED9C4', '#C88A65', '#FF6E4A', '#87421F',
'#B2BEB5', '#926F5B', '#00B9FB', '#6456B7', '#DB5079', '#C62D42', '#FA9C44',
'#DA8A67', '#FD7C6E', '#93CCEA', '#FCF686', '#503E32', '#FF5470', '#9DE093',
'#FF7A00', '#4F69C6', '#A50B5E', '#F0E68C', '#FDFF00', '#F091A9', '#FFFF66',
'#6F9940', '#FC74FD', '#652DC1', '#D6AEDD', '#EE34D2', '#BB3385', '#6B3FA0',
'#33CC99', '#FFDB00', '#87FF2A', '#6EEB6E', '#FFC800', '#CC99BA', '#7A89B8',
'#006A93', '#867200', '#E2B631', '#D9D6CF',
];
function shapeFactory(shapeData, clientID, injection) {
const { type } = shapeData;
const color = colors[clientID % colors.length];
let shapeModel = null;
switch (type) {
case 'rectangle':
shapeModel = new RectangleShape(shapeData, clientID, color, injection);
break;
case 'polygon':
shapeModel = new PolygonShape(shapeData, clientID, color, injection);
break;
case 'polyline':
shapeModel = new PolylineShape(shapeData, clientID, color, injection);
break;
case 'points':
shapeModel = new PointsShape(shapeData, clientID, color, injection);
break;
default:
throw new DataError(
`An unexpected type of shape "${type}"`,
);
}
return shapeModel;
}
function trackFactory(trackData, clientID, injection) {
if (trackData.shapes.length) {
const { type } = trackData.shapes[0];
const color = colors[clientID % colors.length];
let trackModel = null;
switch (type) {
case 'rectangle':
trackModel = new RectangleTrack(trackData, clientID, color, injection);
break;
case 'polygon':
trackModel = new PolygonTrack(trackData, clientID, color, injection);
break;
case 'polyline':
trackModel = new PolylineTrack(trackData, clientID, color, injection);
break;
case 'points':
trackModel = new PointsTrack(trackData, clientID, color, injection);
break;
default:
throw new DataError(
`An unexpected type of track "${type}"`,
);
}
return trackModel;
}
console.warn('The track without any shapes had been found. It was ignored.');
return null;
}
class Collection {
constructor(data) {
this.startFrame = data.startFrame;
this.stopFrame = data.stopFrame;
this.frameMeta = data.frameMeta;
this.labels = data.labels.reduce((labelAccumulator, label) => {
labelAccumulator[label.id] = label;
return labelAccumulator;
}, {});
this.shapes = {}; // key is a frame
this.tags = {}; // key is a frame
this.tracks = [];
this.objects = {}; // key is a client id
this.count = 0;
this.flush = false;
this.collectionZ = {}; // key is a frame, {max, min} are values
this.groups = {
max: 0,
}; // it is an object to we can pass it as an argument by a reference
this.injection = {
labels: this.labels,
collectionZ: this.collectionZ,
groups: this.groups,
frameMeta: this.frameMeta,
};
}
import(data) {
for (const tag of data.tags) {
const clientID = ++this.count;
const tagModel = new Tag(tag, clientID, this.injection);
this.tags[tagModel.frame] = this.tags[tagModel.frame] || [];
this.tags[tagModel.frame].push(tagModel);
this.objects[clientID] = tagModel;
}
for (const shape of data.shapes) {
const clientID = ++this.count;
const shapeModel = shapeFactory(shape, clientID, this.injection);
this.shapes[shapeModel.frame] = this.shapes[shapeModel.frame] || [];
this.shapes[shapeModel.frame].push(shapeModel);
this.objects[clientID] = shapeModel;
}
for (const track of data.tracks) {
const clientID = ++this.count;
const trackModel = trackFactory(track, clientID, this.injection);
// The function can return null if track doesn't have any shapes.
// In this case a corresponded message will be sent to the console
if (trackModel) {
this.tracks.push(trackModel);
this.objects[clientID] = trackModel;
}
}
return this;
}
export() {
const data = {
tracks: this.tracks.filter(track => !track.removed)
.map(track => track.toJSON()),
shapes: Object.values(this.shapes)
.reduce((accumulator, value) => {
accumulator.push(...value);
return accumulator;
}, []).filter(shape => !shape.removed)
.map(shape => shape.toJSON()),
tags: Object.values(this.tags).reduce((accumulator, value) => {
accumulator.push(...value);
return accumulator;
}, []).filter(tag => !tag.removed)
.map(tag => tag.toJSON()),
};
return data;
}
get(frame) {
const { tracks } = this;
const shapes = this.shapes[frame] || [];
const tags = this.tags[frame] || [];
const objects = tracks.concat(shapes).concat(tags).filter(object => !object.removed);
// filtering here
const objectStates = [];
for (const object of objects) {
const stateData = object.get(frame);
if (stateData.outside && !stateData.keyframe) {
continue;
}
const objectState = objectStateFactory.call(object, frame, stateData);
objectStates.push(objectState);
}
return objectStates;
}
merge(objectStates) {
checkObjectType('shapes for merge', objectStates, null, Array);
if (!objectStates.length) return;
const objectsForMerge = objectStates.map((state) => {
checkObjectType('object state', state, null, ObjectState);
const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') {
throw new ArgumentError(
'The object has not been saved yet. Call ObjectState.put([state]) before you can merge it',
);
}
return object;
});
const keyframes = {}; // frame: position
const { label, shapeType } = objectStates[0];
if (!(label.id in this.labels)) {
throw new ArgumentError(
`Unknown label for the task: ${label.id}`,
);
}
if (!Object.values(ObjectShape).includes(shapeType)) {
throw new ArgumentError(
`Got unknown shapeType "${shapeType}"`,
);
}
const labelAttributes = label.attributes.reduce((accumulator, attribute) => {
accumulator[attribute.id] = attribute;
return accumulator;
}, {});
for (let i = 0; i < objectsForMerge.length; i++) {
// For each state get corresponding object
const object = objectsForMerge[i];
const state = objectStates[i];
if (state.label.id !== label.id) {
throw new ArgumentError(
`All shape labels are expected to be ${label.name}, but got ${state.label.name}`,
);
}
if (state.shapeType !== shapeType) {
throw new ArgumentError(
`All shapes are expected to be ${shapeType}, but got ${state.shapeType}`,
);
}
// If this object is shape, get it position and save as a keyframe
if (object instanceof Shape) {
// Frame already saved and it is not outside
if (object.frame in keyframes && !keyframes[object.frame].outside) {
throw new ArgumentError(
'Expected only one visible shape per frame',
);
}
keyframes[object.frame] = {
type: shapeType,
frame: object.frame,
points: [...object.points],
occluded: object.occluded,
zOrder: object.zOrder,
outside: false,
attributes: Object.keys(object.attributes).reduce((accumulator, attrID) => {
// We save only mutable attributes inside a keyframe
if (attrID in labelAttributes && labelAttributes[attrID].mutable) {
accumulator.push({
spec_id: +attrID,
value: object.attributes[attrID],
});
}
return accumulator;
}, []),
};
// Push outside shape after each annotation shape
// Any not outside shape rewrites it
if (!((object.frame + 1) in keyframes)) {
keyframes[object.frame + 1] = JSON
.parse(JSON.stringify(keyframes[object.frame]));
keyframes[object.frame + 1].outside = true;
keyframes[object.frame + 1].frame++;
}
} else if (object instanceof Track) {
// If this object is track, iterate through all its
// keyframes and push copies to new keyframes
const attributes = {}; // id:value
for (const keyframe of Object.keys(object.shapes)) {
const shape = object.shapes[keyframe];
// Frame already saved and it is not outside
if (keyframe in keyframes && !keyframes[keyframe].outside) {
// This shape is outside and non-outside shape already exists
if (shape.outside) {
continue;
}
throw new ArgumentError(
'Expected only one visible shape per frame',
);
}
// We do not save an attribute if it has the same value
// We save only updates
let updatedAttributes = false;
for (const attrID in shape.attributes) {
if (!(attrID in attributes)
|| attributes[attrID] !== shape.attributes[attrID]) {
updatedAttributes = true;
attributes[attrID] = shape.attributes[attrID];
}
}
keyframes[keyframe] = {
type: shapeType,
frame: +keyframe,
points: [...shape.points],
occluded: shape.occluded,
outside: shape.outside,
zOrder: shape.zOrder,
attributes: updatedAttributes ? Object.keys(attributes)
.reduce((accumulator, attrID) => {
accumulator.push({
spec_id: +attrID,
value: attributes[attrID],
});
return accumulator;
}, []) : [],
};
}
} else {
throw new ArgumentError(
`Trying to merge unknown object type: ${object.constructor.name}. `
+ 'Only shapes and tracks are expected.',
);
}
}
let firstNonOutside = false;
for (const frame of Object.keys(keyframes).sort((a, b) => +a - +b)) {
// Remove all outside frames at the begin
firstNonOutside = firstNonOutside || keyframes[frame].outside;
if (!firstNonOutside && keyframes[frame].outside) {
delete keyframes[frame];
} else {
break;
}
}
const clientID = ++this.count;
const track = {
frame: Math.min.apply(null, Object.keys(keyframes).map(frame => +frame)),
shapes: Object.values(keyframes),
group: 0,
label_id: label.id,
attributes: Object.keys(objectStates[0].attributes)
.reduce((accumulator, attrID) => {
if (!labelAttributes[attrID].mutable) {
accumulator.push({
spec_id: +attrID,
value: objectStates[0].attributes[attrID],
});
}
return accumulator;
}, []),
};
const trackModel = trackFactory(track, clientID, this.injection);
this.tracks.push(trackModel);
this.objects[clientID] = trackModel;
// Remove other shapes
for (const object of objectsForMerge) {
object.removed = true;
if (typeof (object.resetCache) === 'function') {
object.resetCache();
}
}
}
split(objectState, frame) {
checkObjectType('object state', objectState, null, ObjectState);
checkObjectType('frame', frame, 'integer', null);
const object = this.objects[objectState.clientID];
if (typeof (object) === 'undefined') {
throw new ArgumentError(
'The object has not been saved yet. Call annotations.put([state]) before',
);
}
if (objectState.objectType !== ObjectType.TRACK) {
return;
}
const keyframes = Object.keys(object.shapes).sort((a, b) => +a - +b);
if (frame <= +keyframes[0] || frame > keyframes[keyframes.length - 1]) {
return;
}
const labelAttributes = object.label.attributes.reduce((accumulator, attribute) => {
accumulator[attribute.id] = attribute;
return accumulator;
}, {});
const exported = object.toJSON();
const position = {
type: objectState.shapeType,
points: [...objectState.points],
occluded: objectState.occluded,
outside: objectState.outside,
zOrder: 0,
attributes: Object.keys(objectState.attributes)
.reduce((accumulator, attrID) => {
if (!labelAttributes[attrID].mutable) {
accumulator.push({
spec_id: +attrID,
value: objectState.attributes[attrID],
});
}
return accumulator;
}, []),
frame,
};
const prev = {
frame: exported.frame,
group: 0,
label_id: exported.label_id,
attributes: exported.attributes,
shapes: [],
};
const next = JSON.parse(JSON.stringify(prev));
next.frame = frame;
next.shapes.push(JSON.parse(JSON.stringify(position)));
exported.shapes.map((shape) => {
delete shape.id;
if (shape.frame < frame) {
prev.shapes.push(JSON.parse(JSON.stringify(shape)));
} else if (shape.frame > frame) {
next.shapes.push(JSON.parse(JSON.stringify(shape)));
}
return shape;
});
prev.shapes.push(position);
prev.shapes[prev.shapes.length - 1].outside = true;
let clientID = ++this.count;
const prevTrack = trackFactory(prev, clientID, this.injection);
this.tracks.push(prevTrack);
this.objects[clientID] = prevTrack;
clientID = ++this.count;
const nextTrack = trackFactory(next, clientID, this.injection);
this.tracks.push(nextTrack);
this.objects[clientID] = nextTrack;
// Remove source object
object.removed = true;
object.resetCache();
}
group(objectStates, reset) {
checkObjectType('shapes for group', objectStates, null, Array);
const objectsForGroup = objectStates.map((state) => {
checkObjectType('object state', state, null, ObjectState);
const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') {
throw new ArgumentError(
'The object has not been saved yet. Call annotations.put([state]) before',
);
}
return object;
});
const groupIdx = reset ? 0 : ++this.groups.max;
for (const object of objectsForGroup) {
object.group = groupIdx;
if (typeof (object.resetCache) === 'function') {
object.resetCache();
}
}
return groupIdx;
}
clear() {
this.shapes = {};
this.tags = {};
this.tracks = [];
this.objects = {}; // by id
this.count = 0;
this.flush = true;
}
statistics() {
const labels = {};
const skeleton = {
rectangle: {
shape: 0,
track: 0,
},
polygon: {
shape: 0,
track: 0,
},
polyline: {
shape: 0,
track: 0,
},
points: {
shape: 0,
track: 0,
},
tags: 0,
manually: 0,
interpolated: 0,
total: 0,
};
const total = JSON.parse(JSON.stringify(skeleton));
for (const label of Object.values(this.labels)) {
const { name } = label;
labels[name] = JSON.parse(JSON.stringify(skeleton));
}
for (const object of Object.values(this.objects)) {
let objectType = null;
if (object instanceof Shape) {
objectType = 'shape';
} else if (object instanceof Track) {
objectType = 'track';
} else if (object instanceof Tag) {
objectType = 'tag';
} else {
throw new ScriptingError(
`Unexpected object type: "${objectType}"`,
);
}
const label = object.label.name;
if (objectType === 'tag') {
labels[label].tags++;
labels[label].manually++;
labels[label].total++;
} else {
const { shapeType } = object;
labels[label][shapeType][objectType]++;
if (objectType === 'track') {
const keyframes = Object.keys(object.shapes)
.sort((a, b) => +a - +b).map(el => +el);
let prevKeyframe = keyframes[0];
let visible = false;
for (const keyframe of keyframes) {
if (visible) {
const interpolated = keyframe - prevKeyframe - 1;
labels[label].interpolated += interpolated;
labels[label].total += interpolated;
}
visible = !object.shapes[keyframe].outside;
prevKeyframe = keyframe;
if (visible) {
labels[label].manually++;
labels[label].total++;
}
}
const lastKey = keyframes[keyframes.length - 1];
if (lastKey !== this.stopFrame && !object.shapes[lastKey].outside) {
const interpolated = this.stopFrame - lastKey;
labels[label].interpolated += interpolated;
labels[label].total += interpolated;
}
} else {
labels[label].manually++;
labels[label].total++;
}
}
}
for (const label of Object.keys(labels)) {
for (const key of Object.keys(labels[label])) {
if (typeof (labels[label][key]) === 'object') {
for (const objectType of Object.keys(labels[label][key])) {
total[key][objectType] += labels[label][key][objectType];
}
} else {
total[key] += labels[label][key];
}
}
}
return new Statistics(labels, total);
}
put(objectStates) {
checkObjectType('shapes for put', objectStates, null, Array);
const constructed = {
shapes: [],
tracks: [],
tags: [],
};
function convertAttributes(accumulator, attrID) {
const specID = +attrID;
const value = this.attributes[attrID];
checkObjectType('attribute id', specID, 'integer', null);
checkObjectType('attribute value', value, 'string', null);
accumulator.push({
spec_id: specID,
value,
});
return accumulator;
}
for (const state of objectStates) {
checkObjectType('object state', state, null, ObjectState);
checkObjectType('state client ID', state.clientID, 'undefined', null);
checkObjectType('state frame', state.frame, 'integer', null);
checkObjectType('state attributes', state.attributes, null, Object);
checkObjectType('state label', state.label, null, Label);
const attributes = Object.keys(state.attributes)
.reduce(convertAttributes.bind(state), []);
const labelAttributes = state.label.attributes.reduce((accumulator, attribute) => {
accumulator[attribute.id] = attribute;
return accumulator;
}, {});
// Construct whole objects from states
if (state.objectType === 'tag') {
constructed.tags.push({
attributes,
frame: state.frame,
label_id: state.label.id,
group: 0,
});
} else {
checkObjectType('state occluded', state.occluded, 'boolean', null);
checkObjectType('state points', state.points, null, Array);
for (const coord of state.points) {
checkObjectType('point coordinate', coord, 'number', null);
}
if (!Object.values(ObjectShape).includes(state.shapeType)) {
throw new ArgumentError(
'Object shape must be one of: '
+ `${JSON.stringify(Object.values(ObjectShape))}`,
);
}
if (state.objectType === 'shape') {
constructed.shapes.push({
attributes,
frame: state.frame,
group: 0,
label_id: state.label.id,
occluded: state.occluded || false,
points: [...state.points],
type: state.shapeType,
z_order: 0,
});
} else if (state.objectType === 'track') {
constructed.tracks.push({
attributes: attributes
.filter(attr => !labelAttributes[attr.spec_id].mutable),
frame: state.frame,
group: 0,
label_id: state.label.id,
shapes: [{
attributes: attributes
.filter(attr => labelAttributes[attr.spec_id].mutable),
frame: state.frame,
occluded: state.occluded || false,
outside: false,
points: [...state.points],
type: state.shapeType,
z_order: 0,
}],
});
} else {
throw new ArgumentError(
'Object type must be one of: '
+ `${JSON.stringify(Object.values(ObjectType))}`,
);
}
}
}
// Add constructed objects to a collection
this.import(constructed);
}
select(objectStates, x, y) {
checkObjectType('shapes for select', objectStates, null, Array);
checkObjectType('x coordinate', x, 'number', null);
checkObjectType('y coordinate', y, 'number', null);
let minimumDistance = null;
let minimumState = null;
for (const state of objectStates) {
checkObjectType('object state', state, null, ObjectState);
if (state.outside) continue;
const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') {
throw new ArgumentError(
'The object has not been saved yet. Call annotations.put([state]) before',
);
}
const distance = object.constructor.distance(state.points, x, y);
if (distance !== null && (minimumDistance === null || distance < minimumDistance)) {
minimumDistance = distance;
minimumState = state;
}
}
return {
state: minimumState,
distance: minimumDistance,
};
}
}
module.exports = Collection;
})();

File diff suppressed because it is too large Load Diff

@ -0,0 +1,279 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => {
const serverProxy = require('./server-proxy');
const { Task } = require('./session');
const { ScriptingError } = ('./exceptions');
class AnnotationsSaver {
constructor(version, collection, session) {
this.sessionType = session instanceof Task ? 'task' : 'job';
this.id = session.id;
this.version = version;
this.collection = collection;
this.initialObjects = {};
this.hash = this._getHash();
// We need use data from export instead of initialData
// Otherwise we have differ keys order and JSON comparison code incorrect
const exported = this.collection.export();
this._resetState();
for (const shape of exported.shapes) {
this.initialObjects.shapes[shape.id] = shape;
}
for (const track of exported.tracks) {
this.initialObjects.tracks[track.id] = track;
}
for (const tag of exported.tags) {
this.initialObjects.tags[tag.id] = tag;
}
}
_resetState() {
this.initialObjects = {
shapes: {},
tracks: {},
tags: {},
};
}
_getHash() {
const exported = this.collection.export();
return JSON.stringify(exported);
}
async _request(data, action) {
const result = await serverProxy.annotations.updateAnnotations(
this.sessionType,
this.id,
data,
action,
);
return result;
}
async _put(data) {
const result = await this._request(data, 'put');
return result;
}
async _create(created) {
const result = await this._request(created, 'create');
return result;
}
async _update(updated) {
const result = await this._request(updated, 'update');
return result;
}
async _delete(deleted) {
const result = await this._request(deleted, 'delete');
return result;
}
_split(exported) {
const splitted = {
created: {
shapes: [],
tracks: [],
tags: [],
},
updated: {
shapes: [],
tracks: [],
tags: [],
},
deleted: {
shapes: [],
tracks: [],
tags: [],
},
};
// Find created and updated objects
for (const type of Object.keys(exported)) {
for (const object of exported[type]) {
if (object.id in this.initialObjects[type]) {
const exportedHash = JSON.stringify(object);
const initialHash = JSON.stringify(this.initialObjects[type][object.id]);
if (exportedHash !== initialHash) {
splitted.updated[type].push(object);
}
} else if (typeof (object.id) === 'undefined') {
splitted.created[type].push(object);
} else {
throw new ScriptingError(
`Id of object is defined "${object.id}"`
+ 'but it absents in initial state',
);
}
}
}
// Now find deleted objects
const indexes = {
shapes: exported.shapes.map((object) => +object.id),
tracks: exported.tracks.map((object) => +object.id),
tags: exported.tags.map((object) => +object.id),
};
for (const type of Object.keys(this.initialObjects)) {
for (const id of Object.keys(this.initialObjects[type])) {
if (!indexes[type].includes(+id)) {
const object = this.initialObjects[type][id];
splitted.deleted[type].push(object);
}
}
}
return splitted;
}
_updateCreatedObjects(saved, indexes) {
const savedLength = saved.tracks.length
+ saved.shapes.length + saved.tags.length;
const indexesLength = indexes.tracks.length
+ indexes.shapes.length + indexes.tags.length;
if (indexesLength !== savedLength) {
throw new ScriptingError(
'Number of indexes is differed by number of saved objects'
+ `${indexesLength} vs ${savedLength}`,
);
}
// Updated IDs of created objects
for (const type of Object.keys(indexes)) {
for (let i = 0; i < indexes[type].length; i++) {
const clientID = indexes[type][i];
this.collection.objects[clientID].serverID = saved[type][i].id;
if (type === 'tracks') {
// We have to reset cache because of old value of serverID was saved there
this.collection.objects[clientID].resetCache();
}
}
}
}
_receiveIndexes(exported) {
// Receive client indexes before saving
const indexes = {
tracks: exported.tracks.map((track) => track.clientID),
shapes: exported.shapes.map((shape) => shape.clientID),
tags: exported.tags.map((tag) => tag.clientID),
};
// Remove them from the request body
exported.tracks.concat(exported.shapes).concat(exported.tags)
.map((value) => {
delete value.clientID;
return value;
});
return indexes;
}
async save(onUpdate) {
if (typeof onUpdate !== 'function') {
onUpdate = (message) => {
console.log(message);
};
}
try {
const exported = this.collection.export();
const { flush } = this.collection;
if (flush) {
onUpdate('New objects are being saved..');
const indexes = this._receiveIndexes(exported);
const savedData = await this._put({ ...exported, version: this.version });
this.version = savedData.version;
this.collection.flush = false;
onUpdate('Saved objects are being updated in the client');
this._updateCreatedObjects(savedData, indexes);
onUpdate('Initial state is being updated');
this._resetState();
for (const type of Object.keys(this.initialObjects)) {
for (const object of savedData[type]) {
this.initialObjects[type][object.id] = object;
}
}
} else {
const {
created,
updated,
deleted,
} = this._split(exported);
onUpdate('New objects are being saved..');
const indexes = this._receiveIndexes(created);
const createdData = await this._create({ ...created, version: this.version });
this.version = createdData.version;
onUpdate('Saved objects are being updated in the client');
this._updateCreatedObjects(createdData, indexes);
onUpdate('Initial state is being updated');
for (const type of Object.keys(this.initialObjects)) {
for (const object of createdData[type]) {
this.initialObjects[type][object.id] = object;
}
}
onUpdate('Changed objects are being saved..');
this._receiveIndexes(updated);
const updatedData = await this._update({ ...updated, version: this.version });
this.version = updatedData.version;
onUpdate('Initial state is being updated');
for (const type of Object.keys(this.initialObjects)) {
for (const object of updatedData[type]) {
this.initialObjects[type][object.id] = object;
}
}
onUpdate('Changed objects are being saved..');
this._receiveIndexes(deleted);
const deletedData = await this._delete({ ...deleted, version: this.version });
this._version = deletedData.version;
onUpdate('Initial state is being updated');
for (const type of Object.keys(this.initialObjects)) {
for (const object of deletedData[type]) {
delete this.initialObjects[type][object.id];
}
}
}
this.hash = this._getHash();
onUpdate('Saving is done');
} catch (error) {
onUpdate(`Can not save annotations: ${error.message}`);
throw error;
}
}
hasUnsavedChanges() {
return this._getHash() !== this.hash;
}
}
module.exports = AnnotationsSaver;
})();

@ -0,0 +1,242 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => {
const serverProxy = require('./server-proxy');
const Collection = require('./annotations-collection');
const AnnotationsSaver = require('./annotations-saver');
const { checkObjectType } = require('./common');
const { Task } = require('./session');
const {
Loader,
Dumper,
} = require('./annotation-format.js');
const {
ScriptingError,
DataError,
ArgumentError,
} = require('./exceptions');
const jobCache = new WeakMap();
const taskCache = new WeakMap();
function getCache(sessionType) {
if (sessionType === 'task') {
return taskCache;
}
if (sessionType === 'job') {
return jobCache;
}
throw new ScriptingError(
`Unknown session type was received ${sessionType}`,
);
}
async function getAnnotationsFromServer(session) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (!cache.has(session)) {
const rawAnnotations = await serverProxy.annotations
.getAnnotations(sessionType, session.id);
// Get meta information about frames
const startFrame = sessionType === 'job' ? session.startFrame : 0;
const stopFrame = sessionType === 'job' ? session.stopFrame : session.size - 1;
const frameMeta = {};
for (let i = startFrame; i <= stopFrame; i++) {
frameMeta[i] = await session.frames.get(i);
}
const collection = new Collection({
labels: session.labels || session.task.labels,
startFrame,
stopFrame,
frameMeta,
}).import(rawAnnotations);
const saver = new AnnotationsSaver(rawAnnotations.version, collection, session);
cache.set(session, {
collection,
saver,
});
}
}
async function getAnnotations(session, frame, filter) {
await getAnnotationsFromServer(session);
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
return cache.get(session).collection.get(frame, filter);
}
async function saveAnnotations(session, onUpdate) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
await cache.get(session).saver.save(onUpdate);
}
// If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it
}
function mergeAnnotations(session, objectStates) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.merge(objectStates);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function splitAnnotations(session, objectState, frame) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.split(objectState, frame);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function groupAnnotations(session, objectStates, reset) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.group(objectStates, reset);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function hasUnsavedChanges(session) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).saver.hasUnsavedChanges();
}
return false;
}
async function clearAnnotations(session, reload) {
checkObjectType('reload', reload, 'boolean', null);
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
cache.get(session).collection.clear();
}
if (reload) {
cache.delete(session);
await getAnnotationsFromServer(session);
}
}
function annotationsStatistics(session) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.statistics();
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function putAnnotations(session, objectStates) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.put(objectStates);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function selectObject(session, objectStates, x, y) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.select(objectStates, x, y);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
async function uploadAnnotations(session, file, loader) {
const sessionType = session instanceof Task ? 'task' : 'job';
if (!(loader instanceof Loader)) {
throw new ArgumentError(
'A loader must be instance of Loader class',
);
}
await serverProxy.annotations.uploadAnnotations(sessionType, session.id, file, loader.name);
}
async function dumpAnnotations(session, name, dumper) {
if (!(dumper instanceof Dumper)) {
throw new ArgumentError(
'A dumper must be instance of Dumper class',
);
}
let result = null;
const sessionType = session instanceof Task ? 'task' : 'job';
if (sessionType === 'job') {
result = await serverProxy.annotations
.dumpAnnotations(session.task.id, name, dumper.name);
} else {
result = await serverProxy.annotations
.dumpAnnotations(session.id, name, dumper.name);
}
return result;
}
module.exports = {
getAnnotations,
putAnnotations,
saveAnnotations,
hasUnsavedChanges,
mergeAnnotations,
splitAnnotations,
groupAnnotations,
clearAnnotations,
annotationsStatistics,
selectObject,
uploadAnnotations,
dumpAnnotations,
};
})();

@ -0,0 +1,172 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* eslint prefer-arrow-callback: [ "error", { "allowNamedFunctions": true } ] */
/* global
require:false
*/
(() => {
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const {
isBoolean,
isInteger,
isEnum,
isString,
checkFilter,
} = require('./common');
const {
TaskStatus,
TaskMode,
} = require('./enums');
const User = require('./user');
const { AnnotationFormat } = require('./annotation-format.js');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
function implementAPI(cvat) {
cvat.plugins.list.implementation = PluginRegistry.list;
cvat.plugins.register.implementation = PluginRegistry.register.bind(cvat);
cvat.server.about.implementation = async () => {
const result = await serverProxy.server.about();
return result;
};
cvat.server.share.implementation = async (directory) => {
const result = await serverProxy.server.share(directory);
return result;
};
cvat.server.formats.implementation = async () => {
const result = await serverProxy.server.formats();
return result.map(el => new AnnotationFormat(el));
};
cvat.server.register.implementation = async (username, firstName, lastName,
email, password1, password2) => {
await serverProxy.server.register(username, firstName, lastName, email,
password1, password2);
};
cvat.server.login.implementation = async (username, password) => {
await serverProxy.server.login(username, password);
};
cvat.server.logout.implementation = async () => {
await serverProxy.server.logout();
};
cvat.server.authorized.implementation = async () => {
const result = await serverProxy.server.authorized();
return result;
};
cvat.users.get.implementation = async (filter) => {
checkFilter(filter, {
self: isBoolean,
});
let users = null;
if ('self' in filter && filter.self) {
users = await serverProxy.users.getSelf();
users = [users];
} else {
users = await serverProxy.users.getUsers();
}
users = users.map(user => new User(user));
return users;
};
cvat.jobs.get.implementation = async (filter) => {
checkFilter(filter, {
taskID: isInteger,
jobID: isInteger,
});
if (('taskID' in filter) && ('jobID' in filter)) {
throw new ArgumentError(
'Only one of fields "taskID" and "jobID" allowed simultaneously',
);
}
if (!Object.keys(filter).length) {
throw new ArgumentError(
'Job filter must not be empty',
);
}
let tasks = null;
if ('taskID' in filter) {
tasks = await serverProxy.tasks.getTasks(`id=${filter.taskID}`);
} else {
const job = await serverProxy.jobs.getJob(filter.jobID);
if (typeof (job.task_id) !== 'undefined') {
tasks = await serverProxy.tasks.getTasks(`id=${job.task_id}`);
}
}
// If task was found by its id, then create task instance and get Job instance from it
if (tasks !== null && tasks.length) {
const task = new Task(tasks[0]);
return filter.jobID ? task.jobs.filter(job => job.id === filter.jobID) : task.jobs;
}
return [];
};
cvat.tasks.get.implementation = async (filter) => {
checkFilter(filter, {
page: isInteger,
name: isString,
id: isInteger,
owner: isString,
assignee: isString,
search: isString,
status: isEnum.bind(TaskStatus),
mode: isEnum.bind(TaskMode),
});
if ('search' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError(
'Do not use the filter field "search" with others',
);
}
}
if ('id' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError(
'Do not use the filter field "id" with others',
);
}
}
const searchParams = new URLSearchParams();
for (const field of ['name', 'owner', 'assignee', 'search', 'status', 'mode', 'id', 'page']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]);
}
}
const tasksData = await serverProxy.tasks.getTasks(searchParams.toString());
const tasks = tasksData.map(task => new Task(task));
tasks.count = tasksData.count;
return tasks;
};
return cvat;
}
module.exports = implementAPI;
})();

@ -0,0 +1,520 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
/**
* External API which should be used by for development
* @module API
*/
function build() {
const PluginRegistry = require('./plugins');
const User = require('./user');
const ObjectState = require('./object-state');
const Statistics = require('./statistics');
const { Job, Task } = require('./session');
const { Attribute, Label } = require('./labels');
const {
ShareFileType,
TaskStatus,
TaskMode,
AttributeType,
ObjectType,
ObjectShape,
VisibleState,
LogType,
} = require('./enums');
const {
Exception,
ArgumentError,
DataError,
ScriptingError,
PluginError,
ServerError,
} = require('./exceptions');
const pjson = require('../package.json');
const config = require('./config');
/**
* API entrypoint
* @namespace cvat
* @memberof module:API
*/
const cvat = {
/**
* Namespace is used for an interaction with a server
* @namespace server
* @package
* @memberof module:API.cvat
*/
server: {
/**
* @typedef {Object} ServerInfo
* @property {string} name A name of the tool
* @property {string} description A description of the tool
* @property {string} version A version of the tool
* @global
*/
/**
* Method returns some information about the annotation tool
* @method about
* @async
* @memberof module:API.cvat.server
* @return {ServerInfo}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async about() {
const result = await PluginRegistry
.apiWrapper(cvat.server.about);
return result;
},
/**
* @typedef {Object} FileInfo
* @property {string} name A name of a file
* @property {module:API.cvat.enums.ShareFileType} type
* A type of a file
* @global
*/
/**
* Method returns a list of files in a specified directory on a share
* @method share
* @async
* @memberof module:API.cvat.server
* @param {string} [directory=/] - Share directory path
* @returns {FileInfo[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async share(directory = '/') {
const result = await PluginRegistry
.apiWrapper(cvat.server.share, directory);
return result;
},
/**
* Method returns available annotation formats
* @method formats
* @async
* @memberof module:API.cvat.server
* @returns {module:API.cvat.classes.AnnotationFormat[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async formats() {
const result = await PluginRegistry
.apiWrapper(cvat.server.formats);
return result;
},
/**
* Method allows to register on a server
* @method register
* @async
* @memberof module:API.cvat.server
* @param {string} username An username for the new account
* @param {string} firstName A first name for the new account
* @param {string} lastName A last name for the new account
* @param {string} email A email address for the new account
* @param {string} password1 A password for the new account
* @param {string} password2 The confirmation password for the new account
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async register(username, firstName, lastName, email, password1, password2) {
const result = await PluginRegistry
.apiWrapper(cvat.server.register, username, firstName,
lastName, email, password1, password2);
return result;
},
/**
* Method allows to login on a server
* @method login
* @async
* @memberof module:API.cvat.server
* @param {string} username An username of an account
* @param {string} password A password of an account
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async login(username, password) {
const result = await PluginRegistry
.apiWrapper(cvat.server.login, username, password);
return result;
},
/**
* Method allows to logout from the server
* @method logout
* @async
* @memberof module:API.cvat.server
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async logout() {
const result = await PluginRegistry
.apiWrapper(cvat.server.logout);
return result;
},
/**
* Method allows to know whether you are authorized on the server
* @method authorized
* @async
* @memberof module:API.cvat.server
* @returns {boolean}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async authorized() {
const result = await PluginRegistry
.apiWrapper(cvat.server.authorized);
return result;
},
},
/**
* Namespace is used for getting tasks
* @namespace tasks
* @memberof module:API.cvat
*/
tasks: {
/**
* @typedef {Object} TaskFilter
* @property {string} name Check if name contains this value
* @property {module:API.cvat.enums.TaskStatus} status
* Check if status contains this value
* @property {module:API.cvat.enums.TaskMode} mode
* Check if mode contains this value
* @property {integer} id Check if id equals this value
* @property {integer} page Get specific page
* (default REST API returns 20 tasks per request.
* In order to get more, it is need to specify next page)
* @property {string} owner Check if owner user contains this value
* @property {string} assignee Check if assigneed contains this value
* @property {string} search Combined search of contains among all fields
* @global
*/
/**
* Method returns list of tasks corresponding to a filter
* @method get
* @async
* @memberof module:API.cvat.tasks
* @param {TaskFilter} [filter={}] task filter
* @returns {module:API.cvat.classes.Task[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async get(filter = {}) {
const result = await PluginRegistry
.apiWrapper(cvat.tasks.get, filter);
return result;
},
},
/**
* Namespace is used for getting jobs
* @namespace jobs
* @memberof module:API.cvat
*/
jobs: {
/**
* @typedef {Object} JobFilter
* Only one of fields is allowed simultaneously
* @property {integer} taskID filter all jobs of specific task
* @property {integer} jobID filter job with a specific id
* @global
*/
/**
* Method returns list of jobs corresponding to a filter
* @method get
* @async
* @memberof module:API.cvat.jobs
* @param {JobFilter} filter job filter
* @returns {module:API.cvat.classes.Job[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async get(filter = {}) {
const result = await PluginRegistry
.apiWrapper(cvat.jobs.get, filter);
return result;
},
},
/**
* Namespace is used for getting users
* @namespace users
* @memberof module:API.cvat
*/
users: {
/**
* @typedef {Object} UserFilter
* @property {boolean} self get only self
* @global
*/
/**
* Method returns list of users corresponding to a filter
* @method get
* @async
* @memberof module:API.cvat.users
* @param {UserFilter} [filter={}] user filter
* @returns {module:API.cvat.classes.User[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async get(filter = {}) {
const result = await PluginRegistry
.apiWrapper(cvat.users.get, filter);
return result;
},
},
/**
* Namespace is used for plugin management
* @namespace plugins
* @memberof module:API.cvat
*/
plugins: {
/**
* @typedef {Object} Plugin
* A plugin is a Javascript object. It must have properties are listed below. <br>
* It also mustn't have property 'functions' which is used internally. <br>
* You can expand any API method including class methods. <br>
* In order to expand class method just use a class name
* in a cvat space (example is listed below).
*
* @property {string} name A name of a plugin
* @property {string} description A description of a plugin
* Example plugin implementation listed below:
* @example
* plugin = {
* name: 'Example Plugin',
* description: 'This example plugin demonstrates how plugin system in CVAT works',
* cvat: {
* server: {
* about: {
* // Plugin adds some actions after executing the cvat.server.about()
* // For example it adds a field with installed plugins to a result
* // An argument "self" is a plugin itself
* // An argument "result" is a return value of cvat.server.about()
* // All next arguments are arguments of a wrapped function
* // (in this case the wrapped function doesn't have any arguments)
* async leave(self, result) {
* result.plugins = await self.internal.getPlugins();
* // Note that a method leave must return "result" (changed or not)
* // Otherwise API won't work as expected
* return result;
* },
* },
* },
* // In this example plugin also wraps a class method
* classes: {
* Job: {
* prototype: {
* annotations: {
* put: {
* // The first argument "self" is a plugin, like in a case above
* // The second argument is an argument of the
* // Job.annotations.put()
* // It contains an array of objects to put
* // In this sample we round objects coordinates and save them
* enter(self, objects) {
* for (const obj of objects) {
* if (obj.type != 'tag') {
* const points = obj.position.map((point) => {
* const roundPoint = {
* x: Math.round(point.x),
* y: Math.round(point.y),
* };
* return roundPoint;
* });
* }
* }
* },
* },
* },
* },
* },
* },
* },
* // In general you can add any others members to your plugin
* // Members below are only examples
* internal: {
* async getPlugins() {
* // Collect information about installed plugins
* const plugins = await cvat.plugins.list();
* return plugins.map((el) => {
* return {
* name: el.name,
* description: el.description,
* };
* });
* },
* },
* };
* @global
*/
/**
* Method returns list of installed plugins
* @method list
* @async
* @memberof module:API.cvat.plugins
* @returns {Plugin[]}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async list() {
const result = await PluginRegistry
.apiWrapper(cvat.plugins.list);
return result;
},
/**
* Install plugin to CVAT
* @method register
* @async
* @memberof module:API.cvat.plugins
* @param {Plugin} [plugin] plugin for registration
* @throws {module:API.cvat.exceptions.PluginError}
*/
async register(plugin) {
const result = await PluginRegistry
.apiWrapper(cvat.plugins.register, plugin);
return result;
},
},
/**
* Namespace contains some changeable configurations
* @namespace config
* @memberof module:API.cvat
*/
config: {
/**
* @memberof module:API.cvat.config
* @property {string} backendAPI host with a backend api
* @memberof module:API.cvat.config
* @property {string} proxy Axios proxy settings.
* For more details please read <a href="https://github.com/axios/axios"> here </a>
* @memberof module:API.cvat.config
* @property {integer} taskID this value is displayed in a logs if available
* @memberof module:API.cvat.config
* @property {integer} jobID this value is displayed in a logs if available
* @memberof module:API.cvat.config
* @property {integer} clientID read only auto-generated
* value which is displayed in a logs
* @memberof module:API.cvat.config
*/
get backendAPI() {
return config.backendAPI;
},
set backendAPI(value) {
config.backendAPI = value;
},
get proxy() {
return config.proxy;
},
set proxy(value) {
config.proxy = value;
},
get taskID() {
return config.taskID;
},
set taskID(value) {
config.taskID = value;
},
get jobID() {
return config.jobID;
},
set jobID(value) {
config.jobID = value;
},
get clientID() {
return config.clientID;
},
},
/**
* Namespace contains some library information e.g. api version
* @namespace client
* @memberof module:API.cvat
*/
client: {
/**
* @property {string} version Client version.
* Format: <b>{major}.{minor}.{patch}</b>
* <li style="margin-left: 10px;"> A major number is changed after an API becomes
* incompatible with a previous version
* <li style="margin-left: 10px;"> A minor number is changed after an API expands
* <li style="margin-left: 10px;"> A patch number is changed after an each build
* @memberof module:API.cvat.client
* @readonly
*/
version: `${pjson.version}`,
},
/**
* Namespace is used for access to enums
* @namespace enums
* @memberof module:API.cvat
*/
enums: {
ShareFileType,
TaskStatus,
TaskMode,
AttributeType,
ObjectType,
ObjectShape,
VisibleState,
LogType,
},
/**
* Namespace is used for access to exceptions
* @namespace exceptions
* @memberof module:API.cvat
*/
exceptions: {
Exception,
ArgumentError,
DataError,
ScriptingError,
PluginError,
ServerError,
},
/**
* Namespace is used for access to classes
* @namespace classes
* @memberof module:API.cvat
*/
classes: {
Task,
User,
Job,
Attribute,
Label,
Statistics,
ObjectState,
},
};
cvat.server = Object.freeze(cvat.server);
cvat.tasks = Object.freeze(cvat.tasks);
cvat.jobs = Object.freeze(cvat.jobs);
cvat.users = Object.freeze(cvat.users);
cvat.plugins = Object.freeze(cvat.plugins);
cvat.client = Object.freeze(cvat.client);
cvat.enums = Object.freeze(cvat.enums);
const implementAPI = require('./api-implementation');
Math.clamp = function (value, min, max) {
return Math.min(Math.max(value, min), max);
};
const implemented = Object.freeze(implementAPI(cvat));
return implemented;
}
module.exports = build();

@ -0,0 +1,90 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => {
const { ArgumentError } = require('./exceptions');
function isBoolean(value) {
return typeof (value) === 'boolean';
}
function isInteger(value) {
return typeof (value) === 'number' && Number.isInteger(value);
}
// Called with specific Enum context
function isEnum(value) {
for (const key in this) {
if (Object.prototype.hasOwnProperty.call(this, key)) {
if (this[key] === value) {
return true;
}
}
}
return false;
}
function isString(value) {
return typeof (value) === 'string';
}
function checkFilter(filter, fields) {
for (const prop in filter) {
if (Object.prototype.hasOwnProperty.call(filter, prop)) {
if (!(prop in fields)) {
throw new ArgumentError(
`Unsupported filter property has been recieved: "${prop}"`,
);
} else if (!fields[prop](filter[prop])) {
throw new ArgumentError(
`Received filter property "${prop}" is not satisfied for checker`,
);
}
}
}
}
function checkObjectType(name, value, type, instance) {
if (type) {
if (typeof (value) !== type) {
// specific case for integers which aren't native type in JS
if (type === 'integer' && Number.isInteger(value)) {
return;
}
throw new ArgumentError(
`"${name}" is expected to be "${type}", but "${typeof (value)}" has been got.`,
);
}
} else if (instance) {
if (!(value instanceof instance)) {
if (value !== undefined) {
throw new ArgumentError(
`"${name}" is expected to be ${instance.name}, but `
+ `"${value.constructor.name}" has been got`,
);
}
throw new ArgumentError(
`"${name}" is expected to be ${instance.name}, but "undefined" has been got.`,
);
}
}
}
module.exports = {
isBoolean,
isInteger,
isEnum,
isString,
checkFilter,
checkObjectType,
};
})();

@ -0,0 +1,12 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
module.exports = {
backendAPI: 'http://localhost:7000/api/v1',
proxy: false,
taskID: undefined,
jobID: undefined,
clientID: +Date.now().toString().substr(-6),
};

@ -0,0 +1,195 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
(() => {
/**
* Share files types
* @enum {string}
* @name ShareFileType
* @memberof module:API.cvat.enums
* @property {string} DIR 'DIR'
* @property {string} REG 'REG'
* @readonly
*/
const ShareFileType = Object.freeze({
DIR: 'DIR',
REG: 'REG',
});
/**
* Task statuses
* @enum {string}
* @name TaskStatus
* @memberof module:API.cvat.enums
* @property {string} ANNOTATION 'annotation'
* @property {string} VALIDATION 'validation'
* @property {string} COMPLETED 'completed'
* @readonly
*/
const TaskStatus = Object.freeze({
ANNOTATION: 'annotation',
VALIDATION: 'validation',
COMPLETED: 'completed',
});
/**
* Task modes
* @enum {string}
* @name TaskMode
* @memberof module:API.cvat.enums
* @property {string} ANNOTATION 'annotation'
* @property {string} INTERPOLATION 'interpolation'
* @readonly
*/
const TaskMode = Object.freeze({
ANNOTATION: 'annotation',
INTERPOLATION: 'interpolation',
});
/**
* Attribute types
* @enum {string}
* @name AttributeType
* @memberof module:API.cvat.enums
* @property {string} CHECKBOX 'checkbox'
* @property {string} SELECT 'select'
* @property {string} RADIO 'radio'
* @property {string} NUMBER 'number'
* @property {string} TEXT 'text'
* @readonly
*/
const AttributeType = Object.freeze({
CHECKBOX: 'checkbox',
RADIO: 'radio',
SELECT: 'select',
NUMBER: 'number',
TEXT: 'text',
});
/**
* Object types
* @enum {string}
* @name ObjectType
* @memberof module:API.cvat.enums
* @property {string} TAG 'tag'
* @property {string} SHAPE 'shape'
* @property {string} TRACK 'track'
* @readonly
*/
const ObjectType = Object.freeze({
TAG: 'tag',
SHAPE: 'shape',
TRACK: 'track',
});
/**
* Object shapes
* @enum {string}
* @name ObjectShape
* @memberof module:API.cvat.enums
* @property {string} RECTANGLE 'rectangle'
* @property {string} POLYGON 'polygon'
* @property {string} POLYLINE 'polyline'
* @property {string} POINTS 'points'
* @readonly
*/
const ObjectShape = Object.freeze({
RECTANGLE: 'rectangle',
POLYGON: 'polygon',
POLYLINE: 'polyline',
POINTS: 'points',
});
/**
* Object visibility states
* @enum {string}
* @name ObjectShape
* @memberof module:API.cvat.enums
* @property {string} ALL 'all'
* @property {string} SHAPE 'shape'
* @property {string} NONE 'none'
* @readonly
*/
const VisibleState = Object.freeze({
ALL: 'all',
SHAPE: 'shape',
NONE: 'none',
});
/**
* Event types
* @enum {number}
* @name LogType
* @memberof module:API.cvat.enums
* @property {number} pasteObject 0
* @property {number} changeAttribute 1
* @property {number} dragObject 2
* @property {number} deleteObject 3
* @property {number} pressShortcut 4
* @property {number} resizeObject 5
* @property {number} sendLogs 6
* @property {number} saveJob 7
* @property {number} jumpFrame 8
* @property {number} drawObject 9
* @property {number} changeLabel 10
* @property {number} sendTaskInfo 11
* @property {number} loadJob 12
* @property {number} moveImage 13
* @property {number} zoomImage 14
* @property {number} lockObject 15
* @property {number} mergeObjects 16
* @property {number} copyObject 17
* @property {number} propagateObject 18
* @property {number} undoAction 19
* @property {number} redoAction 20
* @property {number} sendUserActivity 21
* @property {number} sendException 22
* @property {number} changeFrame 23
* @property {number} debugInfo 24
* @property {number} fitImage 25
* @property {number} rotateImage 26
* @readonly
*/
const LogType = {
pasteObject: 0,
changeAttribute: 1,
dragObject: 2,
deleteObject: 3,
pressShortcut: 4,
resizeObject: 5,
sendLogs: 6,
saveJob: 7,
jumpFrame: 8,
drawObject: 9,
changeLabel: 10,
sendTaskInfo: 11,
loadJob: 12,
moveImage: 13,
zoomImage: 14,
lockObject: 15,
mergeObjects: 16,
copyObject: 17,
propagateObject: 18,
undoAction: 19,
redoAction: 20,
sendUserActivity: 21,
sendException: 22,
changeFrame: 23,
debugInfo: 24,
fitImage: 25,
rotateImage: 26,
};
module.exports = {
ShareFileType,
TaskStatus,
TaskMode,
AttributeType,
ObjectType,
ObjectShape,
VisibleState,
LogType,
};
})();

@ -0,0 +1,272 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => {
const Platform = require('platform');
const ErrorStackParser = require('error-stack-parser');
const config = require('./config');
/**
* Base exception class
* @memberof module:API.cvat.exceptions
* @extends Error
* @ignore
*/
class Exception extends Error {
/**
* @param {string} message - Exception message
*/
constructor(message) {
super(message);
const time = new Date().toISOString();
const system = Platform.os.toString();
const client = `${Platform.name} ${Platform.version}`;
const info = ErrorStackParser.parse(this)[0];
const filename = `${info.fileName}`;
const line = info.lineNumber;
const column = info.columnNumber;
const {
jobID,
taskID,
clientID,
} = config;
const projID = undefined; // wasn't implemented
Object.defineProperties(this, Object.freeze({
system: {
/**
* @name system
* @type {string}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => system,
},
client: {
/**
* @name client
* @type {string}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => client,
},
time: {
/**
* @name time
* @type {string}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => time,
},
jobID: {
/**
* @name jobID
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => jobID,
},
taskID: {
/**
* @name taskID
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => taskID,
},
projID: {
/**
* @name projID
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => projID,
},
clientID: {
/**
* @name clientID
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => clientID,
},
filename: {
/**
* @name filename
* @type {string}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => filename,
},
line: {
/**
* @name line
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => line,
},
column: {
/**
* @name column
* @type {integer}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
*/
get: () => column,
},
}));
}
/**
* Save an exception on a server
* @name save
* @method
* @memberof Exception
* @instance
* @async
*/
async save() {
const exceptionObject = {
system: this.system,
client: this.client,
time: this.time,
job_id: this.jobID,
task_id: this.taskID,
proj_id: this.projID,
client_id: this.clientID,
message: this.message,
filename: this.filename,
line: this.line,
column: this.column,
stack: this.stack,
};
try {
const serverProxy = require('./server-proxy');
await serverProxy.server.exception(exceptionObject);
} catch (exception) {
// add event
}
}
}
/**
* Exceptions are referred with arguments data
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
class ArgumentError extends Exception {
/**
* @param {string} message - Exception message
*/
constructor(message) {
super(message);
}
}
/**
* Unexpected problems with data which are not connected with a user input
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
class DataError extends Exception {
/**
* @param {string} message - Exception message
*/
constructor(message) {
super(message);
}
}
/**
* Unexpected situations in code
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
class ScriptingError extends Exception {
/**
* @param {string} message - Exception message
*/
constructor(message) {
super(message);
}
}
/**
* Plugin-referred exceptions
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
class PluginError extends Exception {
/**
* @param {string} message - Exception message
*/
constructor(message) {
super(message);
}
}
/**
* Exceptions in interaction with a server
* @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception
*/
class ServerError extends Exception {
/**
* @param {string} message - Exception message
* @param {(string|integer)} code - Response code
*/
constructor(message, code) {
super(message);
Object.defineProperties(this, Object.freeze({
/**
* @name code
* @type {(string|integer)}
* @memberof module:API.cvat.exceptions.ServerError
* @readonly
* @instance
*/
code: {
get: () => code,
},
}));
}
}
module.exports = {
Exception,
ArgumentError,
DataError,
ScriptingError,
PluginError,
ServerError,
};
})();

@ -0,0 +1,144 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
global:false
*/
(() => {
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions');
const { isBrowser, isNode } = require('browser-or-node');
// This is the frames storage
const frameDataCache = {};
const frameCache = {};
/**
* Class provides meta information about specific frame and frame itself
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class FrameData {
constructor(width, height, tid, number) {
Object.defineProperties(this, Object.freeze({
/**
* @name width
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
width: {
value: width,
writable: false,
},
/**
* @name height
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
height: {
value: height,
writable: false,
},
tid: {
value: tid,
writable: false,
},
number: {
value: number,
writable: false,
},
}));
}
/**
* Method returns URL encoded image which can be placed in the img tag
* @method data
* @returns {string}
* @memberof module:API.cvat.classes.FrameData
* @instance
* @async
* @param {function} [onServerRequest = () => {}]
* callback which will be called if data absences local
* @throws {module:API.cvat.exception.ServerError}
* @throws {module:API.cvat.exception.PluginError}
*/
async data(onServerRequest = () => {}) {
const result = await PluginRegistry
.apiWrapper.call(this, FrameData.prototype.data, onServerRequest);
return result;
}
}
FrameData.prototype.data.implementation = async function (onServerRequest) {
return new Promise(async (resolve, reject) => {
try {
if (this.number in frameCache[this.tid]) {
resolve(frameCache[this.tid][this.number]);
} else {
onServerRequest();
const frame = await serverProxy.frames.getData(this.tid, this.number);
if (isNode) {
frameCache[this.tid][this.number] = global.Buffer.from(frame, 'binary').toString('base64');
resolve(frameCache[this.tid][this.number]);
} else if (isBrowser) {
const reader = new FileReader();
reader.onload = () => {
frameCache[this.tid][this.number] = reader.result;
resolve(frameCache[this.tid][this.number]);
};
reader.readAsDataURL(frame);
}
}
} catch (exception) {
reject(exception);
}
});
};
async function getFrame(taskID, mode, frame) {
if (!(taskID in frameDataCache)) {
frameDataCache[taskID] = {};
frameDataCache[taskID].meta = await serverProxy.frames.getMeta(taskID);
frameCache[taskID] = {};
}
if (!(frame in frameDataCache[taskID])) {
let size = null;
if (mode === 'interpolation') {
[size] = frameDataCache[taskID].meta;
} else if (mode === 'annotation') {
if (frame >= frameDataCache[taskID].meta.length) {
throw new ArgumentError(
`Meta information about frame ${frame} can't be received from the server`,
);
} else {
size = frameDataCache[taskID].meta[frame];
}
} else {
throw new ArgumentError(
`Invalid mode is specified ${mode}`,
);
}
frameDataCache[taskID][frame] = new FrameData(size.width, size.height, taskID, frame);
}
return frameDataCache[taskID][frame];
}
module.exports = {
FrameData,
getFrame,
};
})();

@ -0,0 +1,210 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => {
const { AttributeType } = require('./enums');
const { ArgumentError } = require('./exceptions');
/**
* Class representing an attribute
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Attribute {
constructor(initialData) {
const data = {
id: undefined,
default_value: undefined,
input_type: undefined,
mutable: undefined,
name: undefined,
values: undefined,
};
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
if (Object.prototype.hasOwnProperty.call(initialData, key)) {
if (Array.isArray(initialData[key])) {
data[key] = [...initialData[key]];
} else {
data[key] = initialData[key];
}
}
}
}
if (!Object.values(AttributeType).includes(data.input_type)) {
throw new ArgumentError(
`Got invalid attribute type ${data.input_type}`,
);
}
Object.defineProperties(this, Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* @name defaultValue
* @type {(string|integer|boolean)}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
defaultValue: {
get: () => data.default_value,
},
/**
* @name inputType
* @type {module:API.cvat.enums.AttributeType}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
inputType: {
get: () => data.input_type,
},
/**
* @name mutable
* @type {boolean}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
mutable: {
get: () => data.mutable,
},
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
name: {
get: () => data.name,
},
/**
* @name values
* @type {(string[]|integer[]|boolean[])}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
*/
values: {
get: () => [...data.values],
},
}));
}
toJSON() {
const object = {
name: this.name,
mutable: this.mutable,
input_type: this.inputType,
default_value: this.defaultValue,
values: this.values,
};
if (typeof (this.id) !== 'undefined') {
object.id = this.id;
}
return object;
}
}
/**
* Class representing a label
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Label {
constructor(initialData) {
const data = {
id: undefined,
name: undefined,
};
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
if (Object.prototype.hasOwnProperty.call(initialData, key)) {
data[key] = initialData[key];
}
}
}
data.attributes = [];
if (Object.prototype.hasOwnProperty.call(initialData, 'attributes')
&& Array.isArray(initialData.attributes)) {
for (const attrData of initialData.attributes) {
data.attributes.push(new Attribute(attrData));
}
}
Object.defineProperties(this, Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
name: {
get: () => data.name,
},
/**
* @name attributes
* @type {module:API.cvat.classes.Attribute[]}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
attributes: {
get: () => [...data.attributes],
},
}));
}
toJSON() {
const object = {
name: this.name,
attributes: [...this.attributes.map(el => el.toJSON())],
};
if (typeof (this.id) !== 'undefined') {
object.id = this.id;
}
return object;
}
}
module.exports = {
Attribute,
Label,
};
})();

@ -0,0 +1,42 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => {
const PluginRegistry = require('./plugins');
/**
* Class describe scheme of a log object
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Log {
constructor(logType, continuous, details) {
this.type = logType;
this.continuous = continuous;
this.details = details;
}
/**
* Method closes a continue log
* @method close
* @memberof module:API.cvat.classes.Log
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
*/
async close() {
const result = await PluginRegistry
.apiWrapper.call(this, Log.prototype.close);
return result;
}
}
module.exports = Log;
})();

@ -0,0 +1,419 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => {
const PluginRegistry = require('./plugins');
const { ArgumentError } = require('./exceptions');
/**
* Class representing a state of an object on a specific frame
* @memberof module:API.cvat.classes
*/
class ObjectState {
/**
* @param {Object} serialized - is an dictionary which contains
* initial information about an ObjectState;
* Necessary fields: objectType, shapeType
* (don't have setters)
* Necessary fields for objects which haven't been added to collection yet: frame
* (doesn't have setters)
* Optional fields: points, group, zOrder, outside, occluded,
* attributes, lock, label, mode, color, keyframe, clientID, serverID
* These fields can be set later via setters
*/
constructor(serialized) {
const data = {
label: null,
attributes: {},
points: null,
outside: null,
occluded: null,
keyframe: null,
group: null,
zOrder: null,
lock: null,
color: null,
visibility: null,
clientID: serialized.clientID,
serverID: serialized.serverID,
frame: serialized.frame,
objectType: serialized.objectType,
shapeType: serialized.shapeType,
updateFlags: {},
};
// Shows whether any properties updated since last reset() or interpolation
Object.defineProperty(data.updateFlags, 'reset', {
value: function reset() {
this.label = false;
this.attributes = false;
this.points = false;
this.outside = false;
this.occluded = false;
this.keyframe = false;
this.group = false;
this.zOrder = false;
this.lock = false;
this.color = false;
this.visibility = false;
},
writable: false,
});
Object.defineProperties(this, Object.freeze({
// Internal property. We don't need document it.
updateFlags: {
get: () => data.updateFlags,
},
frame: {
/**
* @name frame
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.frame,
},
objectType: {
/**
* @name objectType
* @type {module:API.cvat.enums.ObjectType}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.objectType,
},
shapeType: {
/**
* @name shapeType
* @type {module:API.cvat.enums.ObjectShape}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.shapeType,
},
clientID: {
/**
* @name clientID
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.clientID,
},
serverID: {
/**
* @name serverID
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.serverID,
},
label: {
/**
* @name shape
* @type {module:API.cvat.classes.Label}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.label,
set: (labelInstance) => {
data.updateFlags.label = true;
data.label = labelInstance;
},
},
color: {
/**
* @name color
* @type {string}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.color,
set: (color) => {
data.updateFlags.color = true;
data.color = color;
},
},
visibility: {
/**
* @name visibility
* @type {module:API.cvat.enums.VisibleState}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.visibility,
set: (visibility) => {
data.updateFlags.visibility = true;
data.visibility = visibility;
},
},
points: {
/**
* @name points
* @type {number[]}
* @memberof module:API.cvat.classes.ObjectState
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
*/
get: () => data.points,
set: (points) => {
if (Array.isArray(points)) {
data.updateFlags.points = true;
data.points = [...points];
} else {
throw new ArgumentError(
'Points are expected to be an array '
+ `but got ${typeof (points) === 'object'
? points.constructor.name : typeof (points)}`,
);
}
},
},
group: {
/**
* @name group
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.group,
set: (group) => {
data.updateFlags.group = true;
data.group = group;
},
},
zOrder: {
/**
* @name zOrder
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.zOrder,
set: (zOrder) => {
data.updateFlags.zOrder = true;
data.zOrder = zOrder;
},
},
outside: {
/**
* @name outside
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.outside,
set: (outside) => {
data.updateFlags.outside = true;
data.outside = outside;
},
},
keyframe: {
/**
* @name keyframe
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.keyframe,
set: (keyframe) => {
data.updateFlags.keyframe = true;
data.keyframe = keyframe;
},
},
occluded: {
/**
* @name occluded
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.occluded,
set: (occluded) => {
data.updateFlags.occluded = true;
data.occluded = occluded;
},
},
lock: {
/**
* @name lock
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.lock,
set: (lock) => {
data.updateFlags.lock = true;
data.lock = lock;
},
},
attributes: {
/**
* Object is id:value pairs where "id" is an integer
* attribute identifier and "value" is an attribute value
* @name attributes
* @type {Object}
* @memberof module:API.cvat.classes.ObjectState
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
*/
get: () => data.attributes,
set: (attributes) => {
if (typeof (attributes) !== 'object') {
throw new ArgumentError(
'Attributes are expected to be an object '
+ `but got ${typeof (attributes) === 'object'
? attributes.constructor.name : typeof (attributes)}`,
);
}
for (const attrID of Object.keys(attributes)) {
data.updateFlags.attributes = true;
data.attributes[attrID] = attributes[attrID];
}
},
},
}));
this.label = serialized.label;
this.group = serialized.group;
this.zOrder = serialized.zOrder;
this.outside = serialized.outside;
this.keyframe = serialized.keyframe;
this.occluded = serialized.occluded;
this.color = serialized.color;
this.lock = serialized.lock;
this.visibility = serialized.visibility;
// It can be undefined in a constructor and it can be defined later
if (typeof (serialized.points) !== 'undefined') {
this.points = serialized.points;
}
if (typeof (serialized.attributes) !== 'undefined') {
this.attributes = serialized.attributes;
}
data.updateFlags.reset();
}
/**
* Method saves/updates an object state in a collection
* @method save
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @returns {module:API.cvat.classes.ObjectState} updated state of an object
*/
async save() {
const result = await PluginRegistry
.apiWrapper.call(this, ObjectState.prototype.save);
return result;
}
/**
* Method deletes an object from a collection
* @method delete
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @param {boolean} [force=false] delete object even if it is locked
* @async
* @returns {boolean} true if object has been deleted
* @throws {module:API.cvat.exceptions.PluginError}
*/
async delete(force = false) {
const result = await PluginRegistry
.apiWrapper.call(this, ObjectState.prototype.delete, force);
return result;
}
/**
* Set the highest ZOrder within a frame
* @method up
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
*/
async up() {
const result = await PluginRegistry
.apiWrapper.call(this, ObjectState.prototype.up);
return result;
}
/**
* Set the lowest ZOrder within a frame
* @method down
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
*/
async down() {
const result = await PluginRegistry
.apiWrapper.call(this, ObjectState.prototype.down);
return result;
}
}
// Updates element in collection which contains it
ObjectState.prototype.save.implementation = async function () {
if (this.hidden && this.hidden.save) {
return this.hidden.save();
}
return this;
};
// Delete element from a collection which contains it
ObjectState.prototype.delete.implementation = async function (force) {
if (this.hidden && this.hidden.delete) {
return this.hidden.delete(force);
}
return false;
};
ObjectState.prototype.up.implementation = async function () {
if (this.hidden && this.hidden.up) {
return this.hidden.up();
}
return false;
};
ObjectState.prototype.down.implementation = async function () {
if (this.hidden && this.hidden.down) {
return this.hidden.down();
}
return false;
};
module.exports = ObjectState;
})();

@ -0,0 +1,113 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => {
const { PluginError } = require('./exceptions');
const plugins = [];
class PluginRegistry {
static async apiWrapper(wrappedFunc, ...args) {
// I have to optimize the wrapper
const pluginList = await PluginRegistry.list();
for (const plugin of pluginList) {
const pluginDecorators = plugin.functions
.filter(obj => obj.callback === wrappedFunc)[0];
if (pluginDecorators && pluginDecorators.enter) {
try {
await pluginDecorators.enter.call(this, plugin, ...args);
} catch (exception) {
if (exception instanceof PluginError) {
throw exception;
} else {
throw new PluginError(`Exception in plugin ${plugin.name}: ${exception.toString()}`);
}
}
}
}
let result = await wrappedFunc.implementation.call(this, ...args);
for (const plugin of pluginList) {
const pluginDecorators = plugin.functions
.filter(obj => obj.callback === wrappedFunc)[0];
if (pluginDecorators && pluginDecorators.leave) {
try {
result = await pluginDecorators.leave.call(this, plugin, result, ...args);
} catch (exception) {
if (exception instanceof PluginError) {
throw exception;
} else {
throw new PluginError(`Exception in plugin ${plugin.name}: ${exception.toString()}`);
}
}
}
}
return result;
}
// Called with cvat context
static async register(plug) {
const functions = [];
if (typeof (plug) !== 'object') {
throw new PluginError(`Plugin should be an object, but got "${typeof (plug)}"`);
}
if (!('name' in plug) || typeof (plug.name) !== 'string') {
throw new PluginError('Plugin must contain a "name" field and it must be a string');
}
if (!('description' in plug) || typeof (plug.description) !== 'string') {
throw new PluginError('Plugin must contain a "description" field and it must be a string');
}
if ('functions' in plug) {
throw new PluginError('Plugin must not contain a "functions" field');
}
(function traverse(plugin, api) {
const decorator = {};
for (const key in plugin) {
if (Object.prototype.hasOwnProperty.call(plugin, key)) {
if (typeof (plugin[key]) === 'object') {
if (Object.prototype.hasOwnProperty.call(api, key)) {
traverse(plugin[key], api[key]);
}
} else if (['enter', 'leave'].includes(key)
&& typeof (api) === 'function'
&& typeof (plugin[key] === 'function')) {
decorator.callback = api;
decorator[key] = plugin[key];
}
}
}
if (Object.keys(decorator).length) {
functions.push(decorator);
}
}(plug, {
cvat: this,
}));
Object.defineProperty(plug, 'functions', {
value: functions,
writable: false,
});
plugins.push(plug);
}
static async list() {
return plugins;
}
}
module.exports = PluginRegistry;
})();

@ -0,0 +1,589 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => {
const FormData = require('form-data');
const {
ServerError,
} = require('./exceptions');
const store = require('store');
const config = require('./config');
function generateError(errorData, baseMessage) {
if (errorData.response) {
const message = `${baseMessage}. `
+ `${errorData.message}. ${JSON.stringify(errorData.response.data) || ''}.`;
return new ServerError(message, errorData.response.status);
}
// Server is unavailable (no any response)
const message = `${baseMessage}. `
+ `${errorData.message}.`; // usually is "Error Network"
return new ServerError(message, 0);
}
class ServerProxy {
constructor() {
const Axios = require('axios');
Axios.defaults.withCredentials = true;
Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN';
Axios.defaults.xsrfCookieName = 'csrftoken';
let token = store.get('token');
if (token) {
Axios.defaults.headers.common.Authorization = `Token ${token}`;
}
async function about() {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/server/about`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData, 'Could not get "about" information from the server');
}
return response.data;
}
async function share(directory) {
const { backendAPI } = config;
directory = encodeURIComponent(directory);
let response = null;
try {
response = await Axios.get(`${backendAPI}/server/share?directory=${directory}`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData, 'Could not get "share" information from the server');
}
return response.data;
}
async function exception(exceptionObject) {
const { backendAPI } = config;
try {
await Axios.post(`${backendAPI}/server/exception`, JSON.stringify(exceptionObject), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData, 'Could not send an exception to the server');
}
}
async function formats() {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/server/annotation/formats`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData, 'Could not get annotation formats from the server');
}
return response.data;
}
async function register(username, firstName, lastName, email, password1, password2) {
let response = null;
try {
const data = JSON.stringify({
username,
first_name: firstName,
last_name: lastName,
email,
password1,
password2,
});
response = await Axios.post(`${config.backendAPI}/auth/register`, data, {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData, `Could not register '${username}' user on the server`);
}
return response.data;
}
async function login(username, password) {
const authenticationData = ([
`${encodeURIComponent('username')}=${encodeURIComponent(username)}`,
`${encodeURIComponent('password')}=${encodeURIComponent(password)}`,
]).join('&').replace(/%20/g, '+');
let authenticationResponse = null;
try {
authenticationResponse = await Axios.post(
`${config.backendAPI}/auth/login`,
authenticationData, {
proxy: config.proxy,
},
);
} catch (errorData) {
throw generateError(errorData, 'Could not login on a server');
}
if (authenticationResponse.headers['set-cookie']) {
// Browser itself setup cookie and header is none
// In NodeJS we need do it manually
const cookies = authenticationResponse.headers['set-cookie'].join(';');
Axios.defaults.headers.common.Cookie = cookies;
}
token = authenticationResponse.data.key;
store.set('token', token);
Axios.defaults.headers.common.Authorization = `Token ${token}`;
}
async function logout() {
try {
await Axios.post(`${config.backendAPI}/auth/logout`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData, 'Could not logout from the server');
}
store.remove('token');
Axios.defaults.headers.common.Authorization = '';
}
async function authorized() {
try {
await module.exports.users.getSelf();
} catch (serverError) {
if (serverError.code === 401) {
return false;
}
throw serverError;
}
return true;
}
async function getTasks(filter = '') {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/tasks?${filter}`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData, 'Could not get tasks from a server');
}
response.data.results.count = response.data.count;
return response.data.results;
}
async function saveTask(id, taskData) {
const { backendAPI } = config;
try {
await Axios.patch(`${backendAPI}/tasks/${id}`, JSON.stringify(taskData), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData, 'Could not save the task on the server');
}
}
async function deleteTask(id) {
const { backendAPI } = config;
try {
await Axios.delete(`${backendAPI}/tasks/${id}`);
} catch (errorData) {
throw generateError(errorData, 'Could not delete the task from the server');
}
}
async function createTask(taskData, files, onUpdate) {
const { backendAPI } = config;
async function wait(id) {
return new Promise((resolve, reject) => {
async function checkStatus() {
try {
const response = await Axios.get(`${backendAPI}/tasks/${id}/status`);
if (['Queued', 'Started'].includes(response.data.state)) {
if (response.data.message !== '') {
onUpdate(response.data.message);
}
setTimeout(checkStatus, 1000);
} else if (response.data.state === 'Finished') {
resolve();
} else if (response.data.state === 'Failed') {
// If request has been successful, but task hasn't been created
// Then passed data is wrong and we can pass code 400
const message = 'Could not create the task on the server. '
+ `${response.data.message}.`;
reject(new ServerError(message, 400));
} else {
// If server has another status, it is unexpected
// Therefore it is server error and we can pass code 500
reject(new ServerError(
`Unknown task state has been received: ${response.data.state}`,
500,
));
}
} catch (errorData) {
reject(
generateError(errorData, 'Could not put task to the server'),
);
}
}
setTimeout(checkStatus, 1000);
});
}
const batchOfFiles = new FormData();
for (const key in files) {
if (Object.prototype.hasOwnProperty.call(files, key)) {
for (let i = 0; i < files[key].length; i++) {
batchOfFiles.append(`${key}[${i}]`, files[key][i]);
}
}
}
let response = null;
onUpdate('The task is being created on the server..');
try {
response = await Axios.post(`${backendAPI}/tasks`, JSON.stringify(taskData), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData, 'Could not put task to the server');
}
onUpdate('The data is being uploaded to the server..');
try {
await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, batchOfFiles, {
proxy: config.proxy,
});
} catch (errorData) {
try {
await deleteTask(response.data.id);
} catch (_) {
// ignore
}
throw generateError(errorData, 'Could not put data to the server');
}
try {
await wait(response.data.id);
} catch (createException) {
await deleteTask(response.data.id);
throw createException;
}
const createdTask = await getTasks(`?id=${response.id}`);
return createdTask[0];
}
async function getJob(jobID) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/jobs/${jobID}`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData, 'Could not get jobs from a server');
}
return response.data;
}
async function saveJob(id, jobData) {
const { backendAPI } = config;
try {
await Axios.patch(`${backendAPI}/jobs/${id}`, JSON.stringify(jobData), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData, 'Could not save the job on the server');
}
}
async function getUsers() {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/users`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData, 'Could not get users from the server');
}
return response.data.results;
}
async function getSelf() {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/users/self`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData, 'Could not get user data from the server');
}
return response.data;
}
async function getData(tid, frame) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/tasks/${tid}/frames/${frame}`, {
proxy: config.proxy,
responseType: 'blob',
});
} catch (errorData) {
throw generateError(
errorData,
`Could not get frame ${frame} for the task ${tid} from the server`,
);
}
return response.data;
}
async function getMeta(tid) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/tasks/${tid}/frames/meta`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(
errorData,
`Could not get frame meta info for the task ${tid} from the server`,
);
}
return response.data;
}
// Session is 'task' or 'job'
async function getAnnotations(session, id) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/${session}s/${id}/annotations`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(
errorData,
`Could not get annotations for the ${session} ${id} from the server`,
);
}
return response.data;
}
// Session is 'task' or 'job'
async function updateAnnotations(session, id, data, action) {
const { backendAPI } = config;
let requestFunc = null;
let url = null;
if (action.toUpperCase() === 'PUT') {
requestFunc = Axios.put.bind(Axios);
url = `${backendAPI}/${session}s/${id}/annotations`;
} else {
requestFunc = Axios.patch.bind(Axios);
url = `${backendAPI}/${session}s/${id}/annotations?action=${action}`;
}
let response = null;
try {
response = await requestFunc(url, JSON.stringify(data), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(
errorData,
`Could not ${action} annotations for the ${session} ${id} on the server`,
);
}
return response.data;
}
// Session is 'task' or 'job'
async function uploadAnnotations(session, id, file, format) {
const { backendAPI } = config;
let annotationData = new FormData();
annotationData.append('annotation_file', file);
return new Promise((resolve, reject) => {
async function request() {
try {
const response = await Axios
.put(`${backendAPI}/${session}s/${id}/annotations?format=${format}`, annotationData, {
proxy: config.proxy,
});
if (response.status === 202) {
annotationData = new FormData();
setTimeout(request, 3000);
} else {
resolve();
}
} catch (errorData) {
reject(generateError(
errorData,
`Could not upload annotations for the ${session} ${id}`,
));
}
}
setTimeout(request);
});
}
// Session is 'task' or 'job'
async function dumpAnnotations(id, name, format) {
const { backendAPI } = config;
const filename = name.replace(/\//g, '_');
let url = `${backendAPI}/tasks/${id}/annotations/${filename}?format=${format}`;
return new Promise((resolve, reject) => {
async function request() {
try {
const response = await Axios
.get(`${url}`, {
proxy: config.proxy,
});
if (response.status === 202) {
setTimeout(request, 3000);
} else {
url = `${url}&action=download`;
resolve(url);
}
} catch (errorData) {
reject(generateError(
errorData,
`Could not dump annotations for the task ${id} from the server`,
));
}
}
setTimeout(request);
});
}
Object.defineProperties(this, Object.freeze({
server: {
value: Object.freeze({
about,
share,
formats,
exception,
login,
logout,
authorized,
register,
}),
writable: false,
},
tasks: {
value: Object.freeze({
getTasks,
saveTask,
createTask,
deleteTask,
}),
writable: false,
},
jobs: {
value: Object.freeze({
getJob,
saveJob,
}),
writable: false,
},
users: {
value: Object.freeze({
getUsers,
getSelf,
}),
writable: false,
},
frames: {
value: Object.freeze({
getData,
getMeta,
}),
writable: false,
},
annotations: {
value: Object.freeze({
updateAnnotations,
getAnnotations,
dumpAnnotations,
uploadAnnotations,
}),
writable: false,
},
}));
}
}
const serverProxy = new ServerProxy();
module.exports = serverProxy;
})();

File diff suppressed because it is too large Load Diff

@ -0,0 +1,91 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
(() => {
/**
* Class representing collection statistics
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Statistics {
constructor(label, total) {
Object.defineProperties(this, Object.freeze({
/**
* Statistics by labels with a structure:
* @example
* {
* label: {
* boxes: {
* tracks: 10,
* shapes: 11,
* },
* polygons: {
* tracks: 13,
* shapes: 14,
* },
* polylines: {
* tracks: 16,
* shapes: 17,
* },
* points: {
* tracks: 19,
* shapes: 20,
* },
* tags: 66,
* manually: 186,
* interpolated: 500,
* total: 608,
* }
* }
* @name label
* @type {Object}
* @memberof module:API.cvat.classes.Statistics
* @readonly
* @instance
*/
label: {
get: () => JSON.parse(JSON.stringify(label)),
},
/**
* Total statistics (covers all labels) with a structure:
* @example
* {
* boxes: {
* tracks: 10,
* shapes: 11,
* },
* polygons: {
* tracks: 13,
* shapes: 14,
* },
* polylines: {
* tracks: 16,
* shapes: 17,
* },
* points: {
* tracks: 19,
* shapes: 20,
* },
* tags: 66,
* manually: 186,
* interpolated: 500,
* total: 608,
* }
* @name total
* @type {Object}
* @memberof module:API.cvat.classes.Statistics
* @readonly
* @instance
*/
total: {
get: () => JSON.parse(JSON.stringify(total)),
},
}));
}
}
module.exports = Statistics;
})();

@ -0,0 +1,151 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
(() => {
/**
* Class representing a user
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class User {
constructor(initialData) {
const data = {
id: null,
username: null,
email: null,
first_name: null,
last_name: null,
groups: null,
last_login: null,
date_joined: null,
is_staff: null,
is_superuser: null,
is_active: null,
};
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property)
&& property in initialData) {
data[property] = initialData[property];
}
}
Object.defineProperties(this, Object.freeze({
id: {
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.id,
},
username: {
/**
* @name username
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.username,
},
email: {
/**
* @name email
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.email,
},
firstName: {
/**
* @name firstName
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.first_name,
},
lastName: {
/**
* @name lastName
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.last_name,
},
groups: {
/**
* @name groups
* @type {string[]}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => JSON.parse(JSON.stringify(data.groups)),
},
lastLogin: {
/**
* @name lastLogin
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.last_login,
},
dateJoined: {
/**
* @name dateJoined
* @type {string}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.date_joined,
},
isStaff: {
/**
* @name isStaff
* @type {boolean}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.is_staff,
},
isSuperuser: {
/**
* @name isSuperuser
* @type {boolean}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.is_superuser,
},
isActive: {
/**
* @name isActive
* @type {boolean}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => data.is_active,
},
}));
}
}
module.exports = User;
})();

@ -0,0 +1,710 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock');
return mock;
});
// Initialize api
window.cvat = require('../../src/api');
const serverProxy = require('../../src/server-proxy');
// Test cases
describe('Feature: get annotations', () => {
test('get annotations from a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
expect(Array.isArray(annotations)).toBeTruthy();
expect(annotations).toHaveLength(11);
for (const state of annotations) {
expect(state).toBeInstanceOf(window.cvat.classes.ObjectState);
}
});
test('get annotations from a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 101 }))[0];
const annotations0 = await job.annotations.get(0);
const annotations10 = await job.annotations.get(10);
expect(Array.isArray(annotations0)).toBeTruthy();
expect(Array.isArray(annotations10)).toBeTruthy();
expect(annotations0).toHaveLength(1);
expect(annotations10).toHaveLength(2);
for (const state of annotations0.concat(annotations10)) {
expect(state).toBeInstanceOf(window.cvat.classes.ObjectState);
}
});
test('get annotations for frame out of task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
// Out of task
expect(task.annotations.get(500))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
// Out of task
expect(task.annotations.get(-1))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get annotations for frame out of job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 101 }))[0];
// Out of segment
expect(job.annotations.get(500))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
// Out of segment
expect(job.annotations.get(-1))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
// TODO: Test filter (hasn't been implemented yet)
});
describe('Feature: put annotations', () => {
test('put a shape to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
let annotations = await task.annotations.get(1);
const { length } = annotations;
const state = new window.cvat.classes.ObjectState({
frame: 1,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
});
await task.annotations.put([state]);
annotations = await task.annotations.get(1);
expect(annotations).toHaveLength(length + 1);
});
test('put a shape to a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
let annotations = await job.annotations.get(5);
const { length } = annotations;
const state = new window.cvat.classes.ObjectState({
frame: 5,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
points: [0, 0, 100, 100],
occluded: false,
label: job.task.labels[0],
});
await job.annotations.put([state]);
annotations = await job.annotations.get(5);
expect(annotations).toHaveLength(length + 1);
});
test('put a track to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
let annotations = await task.annotations.get(1);
const { length } = annotations;
const state = new window.cvat.classes.ObjectState({
frame: 1,
objectType: window.cvat.enums.ObjectType.TRACK,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
});
await task.annotations.put([state]);
annotations = await task.annotations.get(1);
expect(annotations).toHaveLength(length + 1);
});
test('put a track to a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
let annotations = await job.annotations.get(5);
const { length } = annotations;
const state = new window.cvat.classes.ObjectState({
frame: 5,
objectType: window.cvat.enums.ObjectType.TRACK,
shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
points: [0, 0, 100, 100],
occluded: false,
label: job.task.labels[0],
});
await job.annotations.put([state]);
annotations = await job.annotations.get(5);
expect(annotations).toHaveLength(length + 1);
});
test('put object without objectType to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
await task.annotations.clear(true);
const state = new window.cvat.classes.ObjectState({
frame: 1,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
});
expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('put shape with bad attributes to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
await task.annotations.clear(true);
const state = new window.cvat.classes.ObjectState({
frame: 1,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
attributes: { 'bad key': 55 },
occluded: true,
label: task.labels[0],
});
expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('put shape without points and with invalud points to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
await task.annotations.clear(true);
const state = new window.cvat.classes.ObjectState({
frame: 1,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
occluded: true,
points: [],
label: task.labels[0],
});
await expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.DataError);
delete state.points;
await expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.DataError);
state.points = ['150,50 250,30'];
expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('put shape without type to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
await task.annotations.clear(true);
const state = new window.cvat.classes.ObjectState({
frame: 1,
objectType: window.cvat.enums.ObjectType.SHAPE,
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
});
expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('put shape without label and with bad label to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
await task.annotations.clear(true);
const state = new window.cvat.classes.ObjectState({
frame: 1,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
occluded: true,
});
await expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.label = 'bad label';
await expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.label = {};
await expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('put shape with bad frame to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
await task.annotations.clear(true);
const state = new window.cvat.classes.ObjectState({
frame: '5',
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
});
expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: check unsaved changes', () => {
test('check unsaved changes in a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
const annotations = await task.annotations.get(0);
annotations[0].keyframe = true;
await annotations[0].save();
expect(await task.annotations.hasUnsavedChanges()).toBe(true);
});
test('check unsaved changes in a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
const annotations = await job.annotations.get(0);
annotations[0].occluded = true;
await annotations[0].save();
expect(await job.annotations.hasUnsavedChanges()).toBe(true);
});
});
describe('Feature: save annotations', () => {
test('create & save annotations for a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
let annotations = await task.annotations.get(0);
const { length } = annotations;
const state = new window.cvat.classes.ObjectState({
frame: 0,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
});
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
await task.annotations.put([state]);
expect(await task.annotations.hasUnsavedChanges()).toBe(true);
await task.annotations.save();
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
annotations = await task.annotations.get(0);
expect(annotations).toHaveLength(length + 1);
});
test('update & save annotations for a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0);
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
annotations[0].occluded = true;
await annotations[0].save();
expect(await task.annotations.hasUnsavedChanges()).toBe(true);
await task.annotations.save();
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
});
test('delete & save annotations for a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0);
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
await annotations[0].delete();
expect(await task.annotations.hasUnsavedChanges()).toBe(true);
await task.annotations.save();
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
});
test('create & save annotations for a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
let annotations = await job.annotations.get(0);
const { length } = annotations;
const state = new window.cvat.classes.ObjectState({
frame: 0,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: job.task.labels[0],
});
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
await job.annotations.put([state]);
expect(await job.annotations.hasUnsavedChanges()).toBe(true);
await job.annotations.save();
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
annotations = await job.annotations.get(0);
expect(annotations).toHaveLength(length + 1);
});
test('update & save annotations for a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const annotations = await job.annotations.get(0);
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
annotations[0].points = [0, 100, 200, 300];
await annotations[0].save();
expect(await job.annotations.hasUnsavedChanges()).toBe(true);
await job.annotations.save();
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
});
test('delete & save annotations for a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const annotations = await job.annotations.get(0);
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
await annotations[0].delete();
expect(await job.annotations.hasUnsavedChanges()).toBe(true);
await job.annotations.save();
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
});
test('delete & save annotations for a job when there are a track and a shape with the same id', async () => {
const job = (await window.cvat.jobs.get({ jobID: 112 }))[0];
const annotations = await job.annotations.get(0);
let okay = false;
// Temporary override this method because we need to know what data
// have been sent to a server
const oldImplementation = serverProxy.annotations.updateAnnotations;
serverProxy.annotations.updateAnnotations = async (session, id, data, action) => {
const result = await oldImplementation
.call(serverProxy.annotations, session, id, data, action);
if (action === 'delete') {
okay = okay || (action === 'delete' && !!(data.shapes.length || data.tracks.length));
}
return result;
};
await annotations[0].delete();
await job.annotations.save();
serverProxy.annotations.updateAnnotations = oldImplementation;
expect(okay).toBe(true);
});
});
describe('Feature: merge annotations', () => {
test('merge annotations in a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations0 = await task.annotations.get(0);
const annotations1 = await task.annotations.get(1);
const states = [annotations0[0], annotations1[0]];
await task.annotations.merge(states);
const merged0 = (await task.annotations.get(0))
.filter((state) => state.objectType === window.cvat.enums.ObjectType.TRACK);
const merged1 = (await task.annotations.get(1))
.filter((state) => state.objectType === window.cvat.enums.ObjectType.TRACK);
expect(merged0).toHaveLength(1);
expect(merged1).toHaveLength(1);
expect(merged0[0].points).toEqual(states[0].points);
expect(merged1[0].points).toEqual(states[1].points);
});
test('merge annotations in a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const annotations0 = await job.annotations.get(0);
const annotations1 = await job.annotations.get(1);
const states = [annotations0[0], annotations1[0]];
await job.annotations.merge(states);
const merged0 = (await job.annotations.get(0))
.filter((state) => state.objectType === window.cvat.enums.ObjectType.TRACK);
const merged1 = (await job.annotations.get(1))
.filter((state) => state.objectType === window.cvat.enums.ObjectType.TRACK);
expect(merged0).toHaveLength(1);
expect(merged1).toHaveLength(1);
expect(merged0[0].points).toEqual(states[0].points);
expect(merged1[0].points).toEqual(states[1].points);
});
test('trying to merge not object state', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations0 = await task.annotations.get(0);
const states = [annotations0[0], {}];
expect(task.annotations.merge(states))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('trying to merge object state which is not saved in a collection', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations0 = await task.annotations.get(0);
const state = new window.cvat.classes.ObjectState({
frame: 0,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
});
const states = [annotations0[0], state];
expect(task.annotations.merge(states))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('trying to merge with bad label', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations0 = await task.annotations.get(0);
const annotations1 = await task.annotations.get(1);
const states = [annotations0[0], annotations1[0]];
states[0].label = new window.cvat.classes.Label({
id: 500,
name: 'new_label',
attributes: [],
});
expect(task.annotations.merge(states))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('trying to merge with different shape types', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations0 = await task.annotations.get(0);
const annotations1 = (await task.annotations.get(1))
.filter((state) => state.shapeType === window.cvat.enums.ObjectShape.POLYGON);
const states = [annotations0[0], annotations1[0]];
expect(task.annotations.merge(states))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('trying to merge with different labels', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations0 = await task.annotations.get(0);
const annotations1 = await task.annotations.get(1);
const states = [annotations0[0], annotations1[0]];
states[1].label = new window.cvat.classes.Label({
id: 500,
name: 'new_label',
attributes: [],
});
expect(task.annotations.merge(states))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: split annotations', () => {
test('split annotations in a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations4 = await task.annotations.get(4);
const annotations5 = await task.annotations.get(5);
expect(annotations4[0].clientID).toBe(annotations5[0].clientID);
await task.annotations.split(annotations5[0], 5);
const splitted4 = await task.annotations.get(4);
const splitted5 = (await task.annotations.get(5)).filter((state) => !state.outside);
expect(splitted4[0].clientID).not.toBe(splitted5[0].clientID);
});
test('split annotations in a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 101 }))[0];
const annotations4 = await job.annotations.get(4);
const annotations5 = await job.annotations.get(5);
expect(annotations4[0].clientID).toBe(annotations5[0].clientID);
await job.annotations.split(annotations5[0], 5);
const splitted4 = await job.annotations.get(4);
const splitted5 = (await job.annotations.get(5)).filter((state) => !state.outside);
expect(splitted4[0].clientID).not.toBe(splitted5[0].clientID);
});
test('split on a bad frame', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations4 = await task.annotations.get(4);
const annotations5 = await task.annotations.get(5);
expect(annotations4[0].clientID).toBe(annotations5[0].clientID);
expect(task.annotations.split(annotations5[0], 'bad frame'))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: group annotations', () => {
test('group annotations in a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
let annotations = await task.annotations.get(0);
const groupID = await task.annotations.group(annotations);
expect(typeof (groupID)).toBe('number');
annotations = await task.annotations.get(0);
for (const state of annotations) {
expect(state.group).toBe(groupID);
}
});
test('group annotations in a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
let annotations = await job.annotations.get(0);
const groupID = await job.annotations.group(annotations);
expect(typeof (groupID)).toBe('number');
annotations = await job.annotations.get(0);
for (const state of annotations) {
expect(state.group).toBe(groupID);
}
});
test('trying to group object state which has not been saved in a collection', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
await task.annotations.clear(true);
const state = new window.cvat.classes.ObjectState({
frame: 0,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
});
expect(task.annotations.group([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('trying to group not object state', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
expect(task.annotations.group(annotations.concat({})))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: clear annotations', () => {
test('clear annotations in a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
let annotations = await task.annotations.get(0);
expect(annotations.length).not.toBe(0);
await task.annotations.clear();
annotations = await task.annotations.get(0);
expect(annotations.length).toBe(0);
});
test('clear annotations in a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
let annotations = await job.annotations.get(0);
expect(annotations.length).not.toBe(0);
await job.annotations.clear();
annotations = await job.annotations.get(0);
expect(annotations.length).toBe(0);
});
test('clear annotations with reload in a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
let annotations = await task.annotations.get(0);
expect(annotations.length).not.toBe(0);
annotations[0].occluded = true;
await annotations[0].save();
expect(await task.annotations.hasUnsavedChanges()).toBe(true);
await task.annotations.clear(true);
annotations = await task.annotations.get(0);
expect(annotations.length).not.toBe(0);
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
});
test('clear annotations with reload in a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
let annotations = await job.annotations.get(0);
expect(annotations.length).not.toBe(0);
annotations[0].occluded = true;
await annotations[0].save();
expect(await job.annotations.hasUnsavedChanges()).toBe(true);
await job.annotations.clear(true);
annotations = await job.annotations.get(0);
expect(annotations.length).not.toBe(0);
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
});
test('clear annotations with bad reload parameter', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
await task.annotations.clear(true);
expect(task.annotations.clear('reload'))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: get statistics', () => {
test('get statistics from a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
await task.annotations.clear(true);
const statistics = await task.annotations.statistics();
expect(statistics).toBeInstanceOf(window.cvat.classes.Statistics);
expect(statistics.total.total).toBe(29);
});
test('get statistics from a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 101 }))[0];
await job.annotations.clear(true);
const statistics = await job.annotations.statistics();
expect(statistics).toBeInstanceOf(window.cvat.classes.Statistics);
expect(statistics.total.total).toBe(512);
});
});
describe('Feature: select object', () => {
test('select object in a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
let result = await task.annotations.select(annotations, 1430, 765);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE);
result = await task.annotations.select(annotations, 1415, 765);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYGON);
expect(result.state.points.length).toBe(10);
result = await task.annotations.select(annotations, 1083, 543);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POINTS);
expect(result.state.points.length).toBe(16);
result = await task.annotations.select(annotations, 613, 811);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYGON);
expect(result.state.points.length).toBe(94);
});
test('select object in a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const annotations = await job.annotations.get(0);
let result = await job.annotations.select(annotations, 490, 540);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE);
result = await job.annotations.select(annotations, 430, 260);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYLINE);
result = await job.annotations.select(annotations, 1473, 250);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE);
result = await job.annotations.select(annotations, 1490, 237);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYGON);
expect(result.state.points.length).toBe(94);
});
test('trying to select from not object states', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
expect(task.annotations.select(annotations.concat({}), 500, 500))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('trying to select with invalid coordinates', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
expect(task.annotations.select(annotations, null, null))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.select(annotations, null, null))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.select(annotations, '5', '10'))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});

@ -0,0 +1,71 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock');
return mock;
});
// Initialize api
window.cvat = require('../../src/api');
const { FrameData } = require('../../src/frames');
describe('Feature: get frame meta', () => {
test('get meta for a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const frame = await task.frames.get(0);
expect(frame).toBeInstanceOf(FrameData);
});
test('get meta for a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const frame = await job.frames.get(0);
expect(frame).toBeInstanceOf(FrameData);
});
test('pass frame number out of a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
expect(task.frames.get(100))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.frames.get(-1))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('pass bad frame number', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
expect(task.frames.get('5'))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('do not pass any frame number', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
expect(task.frames.get())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: get frame data', () => {
test('get frame data for a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const frame = await task.frames.get(0);
const frameData = await frame.data();
expect(typeof (frameData)).toBe('string');
});
test('get frame data for a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const frame = await job.frames.get(0);
const frameData = await frame.data();
expect(typeof (frameData)).toBe('string');
});
});

@ -0,0 +1,118 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock');
return mock;
});
// Initialize api
window.cvat = require('../../src/api');
const { Job } = require('../../src/session');
// Test cases
describe('Feature: get a list of jobs', () => {
test('get jobs by a task id', async () => {
const result = await window.cvat.jobs.get({
taskID: 3,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(2);
for (const el of result) {
expect(el).toBeInstanceOf(Job);
}
expect(result[0].task.id).toBe(3);
expect(result[0].task).toBe(result[1].task);
});
test('get jobs by an unknown task id', async () => {
const result = await window.cvat.jobs.get({
taskID: 50,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(0);
});
test('get jobs by a job id', async () => {
const result = await window.cvat.jobs.get({
jobID: 1,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(1);
expect(result[0].id).toBe(1);
});
test('get jobs by an unknown job id', async () => {
const result = await window.cvat.jobs.get({
taskID: 50,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(0);
});
test('get jobs by invalid filter with both taskID and jobID', async () => {
expect(window.cvat.jobs.get({
taskID: 1,
jobID: 1,
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get jobs by invalid job id', async () => {
expect(window.cvat.jobs.get({
jobID: '1',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get jobs by invalid task id', async () => {
expect(window.cvat.jobs.get({
taskID: '1',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get jobs by unknown filter', async () => {
expect(window.cvat.jobs.get({
unknown: 50,
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: save job', () => {
test('save status of a job', async () => {
let result = await window.cvat.jobs.get({
jobID: 1,
});
result[0].status = 'validation';
await result[0].save();
result = await window.cvat.jobs.get({
jobID: 1,
});
expect(result[0].status).toBe('validation');
});
test('save invalid status of a job', async () => {
const result = await window.cvat.jobs.get({
jobID: 1,
});
await result[0].save();
expect(() => {
result[0].status = 'invalid';
}).toThrow(window.cvat.exceptions.ArgumentError);
});
});

@ -0,0 +1,347 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock');
return mock;
});
// Initialize api
window.cvat = require('../../src/api');
describe('Feature: set attributes for an object state', () => {
test('set a valid value', () => {
const state = new window.cvat.classes.ObjectState({
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
frame: 5,
});
const attributes = {
5: 'man',
6: 'glasses',
};
state.attributes = attributes;
expect(state.attributes).toEqual(attributes);
});
test('trying to set a bad value', () => {
const state = new window.cvat.classes.ObjectState({
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
frame: 5,
});
let attributes = 'bad attribute';
expect(() => {
state.attributes = attributes;
}).toThrow(window.cvat.exceptions.ArgumentError);
attributes = 5;
expect(() => {
state.attributes = attributes;
}).toThrow(window.cvat.exceptions.ArgumentError);
attributes = false;
expect(() => {
state.attributes = attributes;
}).toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: set points for an object state', () => {
test('set a valid value', () => {
const state = new window.cvat.classes.ObjectState({
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
frame: 5,
});
const points = [1, 2, 3, 4];
state.points = points;
expect(state.points).toEqual(points);
});
test('trying to set a bad value', () => {
const state = new window.cvat.classes.ObjectState({
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
frame: 5,
});
let points = 'bad points';
expect(() => {
state.points = points;
}).toThrow(window.cvat.exceptions.ArgumentError);
points = 5;
expect(() => {
state.points = points;
}).toThrow(window.cvat.exceptions.ArgumentError);
points = false;
expect(() => {
state.points = points;
}).toThrow(window.cvat.exceptions.ArgumentError);
points = {};
expect(() => {
state.points = points;
}).toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: save object from its state', () => {
test('save valid values for a shape', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
let state = annotations[0];
expect(state.objectType).toBe(window.cvat.enums.ObjectType.SHAPE);
expect(state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE);
state.points = [0, 0, 100, 100];
state.occluded = true;
[, state.label] = task.labels;
state.lock = true;
state = await state.save();
expect(state).toBeInstanceOf(window.cvat.classes.ObjectState);
expect(state.label.id).toBe(task.labels[1].id);
expect(state.lock).toBe(true);
expect(state.occluded).toBe(true);
expect(state.points).toEqual([0, 0, 100, 100]);
});
test('save valid values for a track', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(10);
let state = annotations[1];
expect(state.objectType).toBe(window.cvat.enums.ObjectType.TRACK);
expect(state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE);
state.occluded = true;
state.lock = true;
state.points = [100, 200, 200, 400];
state.attributes = {
1: 'sitting',
3: 'female',
2: '10',
4: 'true',
};
state = await state.save();
expect(state).toBeInstanceOf(window.cvat.classes.ObjectState);
expect(state.lock).toBe(true);
expect(state.occluded).toBe(true);
expect(state.points).toEqual([100, 200, 200, 400]);
expect(state.attributes[1]).toBe('sitting');
expect(state.attributes[2]).toBe('10');
expect(state.attributes[3]).toBe('female');
expect(state.attributes[4]).toBe('true');
state.lock = false;
[state.label] = task.labels;
state = await state.save();
expect(state.label.id).toBe(task.labels[0].id);
state.outside = true;
state = await state.save();
expect(state.lock).toBe(false);
expect(state.outside).toBe(true);
state.keyframe = false;
state = await state.save();
expect(state.keyframe).toBe(false);
});
test('save bad values for a shape', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
const state = annotations[0];
state.occluded = 'false';
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
const oldPoints = state.points;
state.occluded = false;
state.points = ['100', '50', '100', {}];
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.points = oldPoints;
state.lock = 'true';
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
const oldLabel = state.label;
state.lock = false;
state.label = 1;
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.label = oldLabel;
state.attributes = { 1: {}, 2: false, 3: () => {} };
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('save bad values for a track', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0);
const state = annotations[0];
state.occluded = 'false';
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
const oldPoints = state.points;
state.occluded = false;
state.points = ['100', '50', '100', {}];
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.points = oldPoints;
state.lock = 'true';
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
const oldLabel = state.label;
state.lock = false;
state.label = 1;
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.label = oldLabel;
state.outside = 5;
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.outside = false;
state.keyframe = '10';
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.keyframe = true;
state.attributes = { 1: {}, 2: false, 3: () => {} };
await expect(state.save())
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('trying to change locked shape', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0);
let state = annotations[0];
state.lock = true;
state = await state.save();
const { points } = state;
state.points = [0, 0, 500, 500];
state = await state.save();
expect(state.points).toEqual(points);
});
test('trying to set too small area of a shape', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0);
let state = annotations[0];
const { points } = state;
state.points = [0, 0, 2, 2]; // area is 4
state = await state.save();
expect(state.points).toEqual(points);
});
test('trying to set too small area of a track', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0);
let state = annotations[0];
const { points } = state;
state.points = [0, 0, 2, 2]; // area is 4
state = await state.save();
expect(state.points).toEqual(points);
});
test('trying to set too small length of a shape', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
let state = annotations[8];
const { points } = state;
state.points = [0, 0, 2, 2]; // length is 2
state = await state.save();
expect(state.points).toEqual(points);
});
});
describe('Feature: delete object', () => {
test('delete a shape', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotationsBefore = await task.annotations.get(0);
const { length } = annotationsBefore;
await annotationsBefore[0].delete();
const annotationsAfter = await task.annotations.get(0);
expect(annotationsAfter).toHaveLength(length - 1);
});
test('delete a track', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotationsBefore = await task.annotations.get(0);
const { length } = annotationsBefore;
await annotationsBefore[0].delete();
const annotationsAfter = await task.annotations.get(0);
expect(annotationsAfter).toHaveLength(length - 1);
});
});
describe('Feature: change z order of an object', () => {
test('up z order for a shape', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
const state = annotations[0];
const { zOrder } = state;
await state.up();
expect(state.zOrder).toBeGreaterThan(zOrder);
});
test('up z order for a track', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0);
const state = annotations[0];
const { zOrder } = state;
await state.up();
expect(state.zOrder).toBeGreaterThan(zOrder);
});
test('down z order for a shape', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
const state = annotations[0];
const { zOrder } = state;
await state.down();
expect(state.zOrder).toBeLessThan(zOrder);
});
test('down z order for a track', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0);
const state = annotations[0];
const { zOrder } = state;
await state.down();
expect(state.zOrder).toBeLessThan(zOrder);
});
});

@ -0,0 +1,98 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock');
return mock;
});
// Initialize api
window.cvat = require('../../src/api');
describe('Feature: dummy feature', () => {
test('dummy test', async () => {
// TODO: Write test after design of plugin system
});
});
/*
const plugin = {
name: 'Example Plugin',
description: 'This example plugin demonstrates how plugin system in CVAT works',
cvat: {
server: {
about: {
async leave(self, result) {
result.plugins = await self.internal.getPlugins();
return result;
},
},
},
classes: {
Job: {
prototype: {
annotations: {
put: {
enter(self, objects) {
for (const obj of objects) {
if (obj.type !== 'tag') {
const points = obj.position.map((point) => {
const roundPoint = {
x: Math.round(point.x),
y: Math.round(point.y),
};
return roundPoint;
});
obj.points = points;
}
}
},
},
},
},
},
},
},
internal: {
async getPlugins() {
const plugins = await window.cvat.plugins.list();
return plugins.map((el) => {
const obj = {
name: el.name,
description: el.description,
};
return obj;
});
},
},
};
async function test() {
await window.cvat.plugins.register(plugin);
await window.cvat.server.login('admin', 'nimda760');
try {
console.log(JSON.stringify(await window.cvat.server.about()));
console.log(await window.cvat.users.get({ self: false }));
console.log(await window.cvat.users.get({ self: true }));
console.log(JSON.stringify(await window.cvat.jobs.get({ taskID: 8 })));
console.log(JSON.stringify(await window.cvat.jobs.get({ jobID: 10 })));
console.log(await window.cvat.tasks.get());
console.log(await window.cvat.tasks.get({ id: 8 }));
console.log('Done.');
} catch (exception) {
console.log(exception.constructor.name);
console.log(exception.message);
}
}
*/

@ -0,0 +1,96 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock');
return mock;
});
// Initialize api
window.cvat = require('../../src/api');
const {
AnnotationFormat,
Loader,
Dumper,
} = require('../../src/annotation-format');
// Test cases
describe('Feature: get info about cvat', () => {
test('get info about server', async () => {
const result = await window.cvat.server.about();
expect(result).toBeInstanceOf(Object);
expect('name' in result).toBeTruthy();
expect('description' in result).toBeTruthy();
expect('version' in result).toBeTruthy();
});
});
describe('Feature: get share storage info', () => {
test('get files in a root of a share storage', async () => {
const result = await window.cvat.server.share();
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(5);
});
test('get files in a some dir of a share storage', async () => {
const result = await window.cvat.server.share('images');
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(8);
});
test('get files in a some unknown dir of a share storage', async () => {
expect(window.cvat.server.share(
'Unknown Directory',
)).rejects.toThrow(window.cvat.exceptions.ServerError);
});
});
describe('Feature: get annotation formats', () => {
test('get annotation formats from a server', async () => {
const result = await window.cvat.server.formats();
expect(Array.isArray(result)).toBeTruthy();
for (const format of result) {
expect(format).toBeInstanceOf(AnnotationFormat);
}
});
});
describe('Feature: get annotation loaders', () => {
test('get annotation formats from a server', async () => {
const result = await window.cvat.server.formats();
expect(Array.isArray(result)).toBeTruthy();
for (const format of result) {
expect(format).toBeInstanceOf(AnnotationFormat);
const { loaders } = format;
expect(Array.isArray(loaders)).toBeTruthy();
for (const loader of loaders) {
expect(loader).toBeInstanceOf(Loader);
}
}
});
});
describe('Feature: get annotation dumpers', () => {
test('get annotation formats from a server', async () => {
const result = await window.cvat.server.formats();
expect(Array.isArray(result)).toBeTruthy();
for (const format of result) {
expect(format).toBeInstanceOf(AnnotationFormat);
const { dumpers } = format;
expect(Array.isArray(dumpers)).toBeTruthy();
for (const dumper of dumpers) {
expect(dumper).toBeInstanceOf(Dumper);
}
}
});
});

@ -0,0 +1,186 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock');
return mock;
});
// Initialize api
window.cvat = require('../../src/api');
const { Task } = require('../../src/session');
// Test cases
describe('Feature: get a list of tasks', () => {
test('get all tasks', async () => {
const result = await window.cvat.tasks.get();
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(6);
for (const el of result) {
expect(el).toBeInstanceOf(Task);
}
});
test('get a task by an id', async () => {
const result = await window.cvat.tasks.get({
id: 3,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(Task);
expect(result[0].id).toBe(3);
});
test('get a task by an unknown id', async () => {
const result = await window.cvat.tasks.get({
id: 50,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(0);
});
test('get a task by an invalid id', async () => {
expect(window.cvat.tasks.get({
id: '50',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get tasks by filters', async () => {
const result = await window.cvat.tasks.get({
mode: 'interpolation',
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(3);
for (const el of result) {
expect(el).toBeInstanceOf(Task);
expect(el.mode).toBe('interpolation');
}
});
test('get tasks by invalid filters', async () => {
expect(window.cvat.tasks.get({
unknown: '5',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get task by name, status and mode', async () => {
const result = await window.cvat.tasks.get({
mode: 'interpolation',
status: 'annotation',
name: 'Test Task',
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(1);
for (const el of result) {
expect(el).toBeInstanceOf(Task);
expect(el.mode).toBe('interpolation');
expect(el.status).toBe('annotation');
expect(el.name).toBe('Test Task');
}
});
});
describe('Feature: save a task', () => {
test('save some changed fields in a task', async () => {
let result = await window.cvat.tasks.get({
id: 2,
});
result[0].bugTracker = 'newBugTracker';
result[0].zOrder = true;
result[0].name = 'New Task Name';
result[0].save();
result = await window.cvat.tasks.get({
id: 2,
});
expect(result[0].bugTracker).toBe('newBugTracker');
expect(result[0].zOrder).toBe(true);
expect(result[0].name).toBe('New Task Name');
});
test('save some new labels in a task', async () => {
let result = await window.cvat.tasks.get({
id: 2,
});
const labelsLength = result[0].labels.length;
const newLabel = new window.cvat.classes.Label({
name: 'My boss\'s car',
attributes: [{
default_value: 'false',
input_type: 'checkbox',
mutable: true,
name: 'parked',
values: ['false'],
}],
});
result[0].labels = [newLabel];
result[0].save();
result = await window.cvat.tasks.get({
id: 2,
});
expect(result[0].labels).toHaveLength(labelsLength + 1);
const appendedLabel = result[0].labels.filter((el) => el.name === 'My boss\'s car');
expect(appendedLabel).toHaveLength(1);
expect(appendedLabel[0].attributes).toHaveLength(1);
expect(appendedLabel[0].attributes[0].name).toBe('parked');
expect(appendedLabel[0].attributes[0].defaultValue).toBe('false');
expect(appendedLabel[0].attributes[0].mutable).toBe(true);
expect(appendedLabel[0].attributes[0].inputType).toBe('checkbox');
});
test('save new task without an id', async () => {
const task = new window.cvat.classes.Task({
name: 'New Task',
labels: [{
name: 'My boss\'s car',
attributes: [{
default_value: 'false',
input_type: 'checkbox',
mutable: true,
name: 'parked',
values: ['false'],
}],
}],
bug_tracker: 'bug tracker value',
image_quality: 50,
z_order: true,
});
const result = await task.save();
expect(typeof (result.id)).toBe('number');
});
});
describe('Feature: delete a task', () => {
test('delete a task', async () => {
let result = await window.cvat.tasks.get({
id: 3,
});
await result[0].delete();
result = await window.cvat.tasks.get({
id: 3,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(0);
});
});

@ -0,0 +1,54 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock');
return mock;
});
// Initialize api
window.cvat = require('../../src/api');
const User = require('../../src/user');
// Test cases
describe('Feature: get a list of users', () => {
test('get all users', async () => {
const result = await window.cvat.users.get();
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(2);
for (const el of result) {
expect(el).toBeInstanceOf(User);
}
});
test('get only self', async () => {
const result = await window.cvat.users.get({
self: true,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(User);
});
test('get users with unknown filter key', async () => {
expect(window.cvat.users.get({
unknown: '50',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get users with invalid filter key', async () => {
expect(window.cvat.users.get({
self: 1,
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});

File diff suppressed because it is too large Load Diff

@ -0,0 +1,302 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* eslint import/no-extraneous-dependencies: 0 */
/* global
require:false
*/
const {
tasksDummyData,
aboutDummyData,
formatsDummyData,
shareDummyData,
usersDummyData,
taskAnnotationsDummyData,
jobAnnotationsDummyData,
frameMetaDummyData,
} = require('./dummy-data.mock');
class ServerProxy {
constructor() {
async function about() {
return JSON.parse(JSON.stringify(aboutDummyData));
}
async function share(directory) {
let position = shareDummyData;
// Emulation of internal directories
if (directory.length > 1) {
const components = directory.split('/');
for (const component of components) {
const idx = position.map(x => x.name).indexOf(component);
if (idx !== -1 && 'children' in position[idx]) {
position = position[idx].children;
} else {
throw new window.cvat.exceptions.ServerError(
`${component} is not a valid directory`,
400,
);
}
}
}
return JSON.parse(JSON.stringify(position));
}
async function formats() {
return JSON.parse(JSON.stringify(formatsDummyData));
}
async function exception() {
return null;
}
async function login() {
return null;
}
async function logout() {
return null;
}
async function getTasks(filter = '') {
function QueryStringToJSON(query) {
const pairs = [...new URLSearchParams(query).entries()];
const result = {};
for (const pair of pairs) {
const [key, value] = pair;
if (['id'].includes(key)) {
result[key] = +value;
} else {
result[key] = value;
}
}
return JSON.parse(JSON.stringify(result));
}
// Emulation of a query filter
const queries = QueryStringToJSON(filter);
const result = tasksDummyData.results.filter((x) => {
for (const key in queries) {
if (Object.prototype.hasOwnProperty.call(queries, key)) {
// TODO: Particular match for some fields is not checked
if (queries[key] !== x[key]) {
return false;
}
}
}
return true;
});
return result;
}
async function saveTask(id, taskData) {
const object = tasksDummyData.results.filter(task => task.id === id)[0];
for (const prop in taskData) {
if (Object.prototype.hasOwnProperty.call(taskData, prop)
&& Object.prototype.hasOwnProperty.call(object, prop)) {
object[prop] = taskData[prop];
}
}
}
async function createTask(taskData) {
const id = Math.max(...tasksDummyData.results.map(el => el.id)) + 1;
tasksDummyData.results.push({
id,
url: `http://localhost:7000/api/v1/tasks/${id}`,
name: taskData.name,
size: 5000,
mode: 'interpolation',
owner: 2,
assignee: null,
bug_tracker: taskData.bug_tracker,
created_date: '2019-05-16T13:08:00.621747+03:00',
updated_date: '2019-05-16T13:08:00.621797+03:00',
overlap: taskData.overlap ? taskData.overlap : 5,
segment_size: taskData.segment_size ? taskData.segment_size : 5000,
z_order: taskData.z_order,
flipped: false,
status: 'annotation',
image_quality: taskData.image_quality,
labels: JSON.parse(JSON.stringify(taskData.labels)),
});
const createdTask = await getTasks(`?id=${id}`);
return createdTask[0];
}
async function deleteTask(id) {
const tasks = tasksDummyData.results;
const task = tasks.filter(el => el.id === id)[0];
if (task) {
tasks.splice(tasks.indexOf(task), 1);
}
}
async function getJob(jobID) {
const jobs = tasksDummyData.results.reduce((acc, task) => {
for (const segment of task.segments) {
for (const job of segment.jobs) {
const copy = JSON.parse(JSON.stringify(job));
copy.start_frame = segment.start_frame;
copy.stop_frame = segment.stop_frame;
copy.task_id = task.id;
acc.push(copy);
}
}
return acc;
}, []).filter(job => job.id === jobID);
return jobs[0] || {
detail: 'Not found.',
};
}
async function saveJob(id, jobData) {
const object = tasksDummyData.results.reduce((acc, task) => {
for (const segment of task.segments) {
for (const job of segment.jobs) {
acc.push(job);
}
}
return acc;
}, []).filter(job => job.id === id)[0];
for (const prop in jobData) {
if (Object.prototype.hasOwnProperty.call(jobData, prop)
&& Object.prototype.hasOwnProperty.call(object, prop)) {
object[prop] = jobData[prop];
}
}
}
async function getUsers() {
return JSON.parse(JSON.stringify(usersDummyData)).results;
}
async function getSelf() {
return JSON.parse(JSON.stringify(usersDummyData)).results[0];
}
async function getData() {
return 'DUMMY_IMAGE';
}
async function getMeta(tid) {
return JSON.parse(JSON.stringify(frameMetaDummyData[tid]));
}
async function getAnnotations(session, id) {
if (session === 'task') {
return JSON.parse(JSON.stringify(taskAnnotationsDummyData[id]));
}
if (session === 'job') {
return JSON.parse(JSON.stringify(jobAnnotationsDummyData[id]));
}
return null;
}
async function updateAnnotations(session, id, data, action) {
// Actually we do not change our dummy data
// We just update the argument in some way and return it
data.version += 1;
if (action === 'create') {
let idGenerator = 1000;
data.tracks.concat(data.tags).concat(data.shapes).map((el) => {
el.id = ++idGenerator;
return el;
});
return data;
}
if (action === 'update') {
return data;
}
if (action === 'delete') {
return data;
}
return null;
}
Object.defineProperties(this, Object.freeze({
server: {
value: Object.freeze({
about,
share,
formats,
exception,
login,
logout,
}),
writable: false,
},
tasks: {
value: Object.freeze({
getTasks,
saveTask,
createTask,
deleteTask,
}),
writable: false,
},
jobs: {
value: Object.freeze({
getJob,
saveJob,
}),
writable: false,
},
users: {
value: Object.freeze({
getUsers,
getSelf,
}),
writable: false,
},
frames: {
value: Object.freeze({
getData,
getMeta,
}),
writable: false,
},
annotations: {
value: {
updateAnnotations,
getAnnotations,
},
// To implement on of important tests
writable: true,
},
}));
}
}
const serverProxy = new ServerProxy();
module.exports = serverProxy;

@ -0,0 +1,72 @@
/* global
require:true,
__dirname:true,
*/
const path = require('path');
const nodeConfig = {
target: 'node',
mode: 'production',
devtool: 'source-map',
entry: './src/api.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cvat-core.node.js',
library: 'cvat',
libraryTarget: 'commonjs',
},
module: {
rules: [{
test: /.js?$/,
exclude: /node_modules/,
}],
},
externals: {
canvas: 'commonjs canvas',
},
stats: {
warnings: false,
},
};
const webConfig = {
target: 'web',
mode: 'production',
devtool: 'source-map',
entry: './src/api.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cvat-core.min.js',
library: 'cvat',
libraryTarget: 'window',
},
module: {
rules: [{
test: /.js?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: {
chrome: 58,
},
useBuiltIns: 'usage',
corejs: 3,
loose: false,
spec: false,
debug: false,
include: [],
exclude: [],
}],
],
sourceType: 'unambiguous',
},
},
}],
},
};
module.exports = [nodeConfig, webConfig];

@ -0,0 +1,2 @@
build
node_modules

@ -0,0 +1,9 @@
REACT_APP_VERSION=${npm_package_version}
REACT_APP_API_PROTOCOL=http
REACT_APP_API_HOST=localhost
REACT_APP_API_PORT=7000
REACT_APP_API_HOST_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT_APP_API_PORT}
REACT_APP_API_FULL_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT_APP_API_PORT}/api/v1
SKIP_PREFLIGHT_CHECK=true

@ -0,0 +1,9 @@
REACT_APP_VERSION=${npm_package_version}
REACT_APP_API_PROTOCOL=http
REACT_APP_API_HOST=localhost
REACT_APP_API_PORT=8080
REACT_APP_API_HOST_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT_APP_API_PORT}
REACT_APP_API_FULL_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT_APP_API_PORT}/api/v1
SKIP_PREFLIGHT_CHECK=true

23
cvat-ui/.gitignore vendored

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

@ -0,0 +1,36 @@
FROM ubuntu:18.04 AS cvat-ui
ARG http_proxy
ARG https_proxy
ARG no_proxy
ARG socks_proxy
ENV TERM=xterm \
http_proxy=${http_proxy} \
https_proxy=${https_proxy} \
no_proxy=${no_proxy} \
socks_proxy=${socks_proxy}
ENV LANG='C.UTF-8' \
LC_ALL='C.UTF-8'
# Install necessary apt packages
RUN apt update && apt install -yq nodejs npm curl && \
npm install -g n && n 10.16.3
# Create output directory
RUN mkdir /tmp/cvat-ui
WORKDIR /tmp/cvat-ui/
# Install dependencies
COPY package*.json /tmp/cvat-ui/
RUN npm install
# Build source code
COPY . /tmp/cvat-ui/
RUN mv .env.production .env && npm run build
FROM nginx
# Replace default.conf configuration to remove unnecessary rules
COPY react_nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=cvat-ui /tmp/cvat-ui/build /usr/share/nginx/html/

@ -0,0 +1,44 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br>
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br>
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br>
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br>
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

@ -0,0 +1,14 @@
const { override, fixBabelImports, addLessLoader } = require('customize-cra');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
addLessLoader({
javascriptEnabled: true,
// modifyVars: { '@primary-color': '#1DA57A' },
}),
);

File diff suppressed because it is too large Load Diff

@ -0,0 +1,56 @@
{
"name": "cvat-ui",
"version": "0.1.0",
"license": "MIT",
"private": true,
"dependencies": {
"@types/jest": "24.0.13",
"@types/node": "^12.0.3",
"@types/react": "16.8.19",
"@types/react-dom": "16.8.4",
"@types/react-redux": "^7.1.1",
"@types/react-router-dom": "^4.3.4",
"@types/redux-logger": "^3.0.7",
"antd": "^3.19.1",
"babel-plugin-import": "^1.11.2",
"customize-cra": "^0.2.12",
"less": "^3.9.0",
"less-loader": "^5.0.0",
"node-sass": "^4.12.0",
"query-string": "^6.8.1",
"react": "^16.8.6",
"react-app-rewired": "^2.1.3",
"react-dom": "^16.8.6",
"react-redux": "^7.1.0",
"react-router-dom": "^5.0.1",
"react-scripts": "3.0.1",
"react-scripts-ts": "^3.1.0",
"redux": "^4.0.4",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"source-map-explorer": "^1.8.0",
"typescript": "3.4.5"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

@ -0,0 +1 @@
<svg width="98" height="27" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M101 0v29l-52.544.001C44.326 35.511 35.598 40 25.5 40 11.417 40 0 31.27 0 20.5S11.417 1 25.5 1c4.542 0 8.807.908 12.5 2.5V0h63z" id="a"/></defs><g transform="translate(-2 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M48.142 1c4.736 0 6.879 3.234 6.879 5.904v2.068h-4.737V6.904c0-.79-.789-2.144-2.142-2.144-1.654 0-2.368 1.354-2.368 2.144v15.192c0 .79.714 2.144 2.368 2.144 1.353 0 2.142-1.354 2.142-2.144v-2.068h4.737v2.068c0 2.67-2.143 5.904-6.88 5.904C42.956 28 41 24.766 41 22.134V6.904C41 4.234 42.955 1 48.142 1zM19-6c9.389 0 17 7.611 17 17s-7.611 17-17 17S2 20.389 2 11 9.611-6 19-6zm42.256 7.338l3.345 19.48h.075l3.42-19.48h5l-6.052 26.324h-5L56.22 1.338h5.037zm20.706 0l5.413 26.324h-4.699l-.94-6.13h-4.548l-.902 6.13h-4.435l5.413-26.324h4.698zm18.038 0v3.723h-4.849v22.6h-4.699v-22.6h-4.81V1.338H100zM19 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm60.557 4.295h-.113l-1.466 9.439h3.007l-1.428-9.439z" fill="#000" fill-rule="nonzero" mask="url(#b)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1 @@
<svg width="90" height="78" xmlns="http://www.w3.org/2000/svg"><path d="M84.27 0c2.753 0 5.007 2.167 5.12 4.874l.005.215v67.148c0 2.734-2.183 4.972-4.908 5.085l-.217.004H5.123c-2.751 0-5.005-2.167-5.118-4.874L0 72.237V5.09C0 2.355 2.183.117 4.907.004L5.123 0H84.27zm1.58 16.242H3.546v55.995c0 .816.632 1.488 1.434 1.56l.144.007H84.27c.824 0 1.501-.627 1.574-1.424l.007-.143V16.242zM12.658 38.48h4.328c.59 0 1.076.452 1.138 1.031l.007.126v1.03h15.02v-1.03c0-.596.446-1.087 1.02-1.15l.125-.007h4.328c.59 0 1.076.451 1.138 1.03l.006.127v4.372c0 .596-.446 1.087-1.02 1.15l-.124.007h-1.02v10.548h1.019c.59 0 1.077.451 1.139 1.031l.006.126v4.372c0 .596-.446 1.087-1.02 1.15l-.125.007h-4.327a1.15 1.15 0 0 1-1.139-1.03l-.006-.127v-1.03H18.13v1.03c0 .596-.446 1.087-1.02 1.15l-.125.007h-4.328a1.15 1.15 0 0 1-1.138-1.03l-.007-.127v-4.372c0-.596.447-1.087 1.02-1.15l.125-.007h1.019V45.166h-1.02a1.15 1.15 0 0 1-1.138-1.03l-.006-.127v-4.372c0-.596.446-1.087 1.02-1.15l.125-.007h4.328zm3.183 19.548h-2.038v2.058h2.038v-2.058zm21.638 0h-2.039v2.058h2.039v-2.058zM33.15 42.98H18.13v1.03c0 .595-.447 1.087-1.02 1.15l-.125.006h-1.019v10.548h1.019c.59 0 1.076.451 1.138 1.031l.007.126V57.9h15.02V56.87c0-.596.446-1.087 1.02-1.15l.125-.007h1.019V45.166h-1.019a1.15 1.15 0 0 1-1.139-1.031l-.006-.126v-1.03zm21.575-7.62c.398 0 .72.338.72.755v.67h9.458v-.67c0-.417.323-.755.721-.755h2.725c.398 0 .72.338.72.754v2.852c0 .416-.322.754-.72.754h-.641v6.88h.64c.399 0 .722.337.722.754v2.852c0 .416-.323.754-.721.754h-2.725c-.398 0-.721-.338-.721-.754v-.672h-9.457v.672c0 .416-.322.754-.72.754H52c-.398 0-.72-.338-.72-.754v-2.852c0-.416.322-.754.72-.754h.642v-6.88H52c-.398 0-.72-.337-.72-.754v-2.851c0-.417.322-.755.72-.755zm-.72 12.749H52.72v1.342h1.283V48.11zm13.623 0h-1.283v1.342h1.283V48.11zm-2.725-9.814h-9.457v.671c0 .417-.323.755-.721.755h-.641V46.6h.641c.399 0 .721.337.721.754v.671h9.457v-.67c0-.417.323-.755.72-.755h.642v-6.88h-.64c-.4 0-.722-.338-.722-.754v-.671zm-49.063 2.5h-2.038v2.058h2.038v-2.058zm21.638-.001H35.44v2.058h2.038v-2.058zm30.15-3.925h-1.283v1.342h1.283V36.87zm-13.624 0h-1.283v1.343h1.283v-1.343zM9.098 19.76c.93 0 1.692.711 1.766 1.616l.006.145v.118c0 .973-.793 1.761-1.772 1.761-.93 0-1.693-.711-1.767-1.616l-.005-.145v-.118c0-.973.793-1.761 1.772-1.761zm21.65 0c.978 0 1.771.788 1.771 1.76 0 .974-.793 1.762-1.771 1.762H16.423c-.98 0-1.772-.788-1.772-1.761 0-.973.792-1.761 1.772-1.761h14.325zm13.988 0c.98 0 1.772.788 1.772 1.76 0 .974-.792 1.762-1.772 1.762h-5.29a1.768 1.768 0 0 1-1.772-1.761c0-.973.796-1.761 1.772-1.761h5.29zm9.868 0c.977 0 1.773.788 1.773 1.76 0 .974-.796 1.762-1.773 1.762H53.05a1.766 1.766 0 0 1-1.772-1.761c0-.973.793-1.761 1.772-1.761h1.553zm17.107 0c.977 0 1.772.788 1.772 1.76 0 .974-.795 1.762-1.772 1.762h-8.194c-.98 0-1.773-.788-1.773-1.761 0-.973.793-1.761 1.773-1.761h8.194zm12.56-16.238H5.122c-.82 0-1.5.628-1.572 1.424l-.006.143v7.631H85.85V5.09c0-.863-.709-1.567-1.58-1.567zM9.125 6.24c.98 0 1.772.787 1.772 1.76s-.793 1.761-1.772 1.761c-.977 0-1.8-.788-1.8-1.76 0-.974.761-1.761 1.741-1.761h.059zm7.354 0c.979 0 1.772.787 1.772 1.76s-.793 1.761-1.772 1.761c-.977 0-1.829-.788-1.829-1.76 0-.974.734-1.761 1.712-1.761h.117zm7.297 0c.977 0 1.773.787 1.773 1.76s-.796 1.761-1.773 1.761c-.979 0-1.8-.788-1.8-1.76 0-.974.764-1.761 1.743-1.761h.057z" fill="#9B9B9B" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>CVAT</title>
<script src="./cvat-core.min.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

@ -0,0 +1,15 @@
{
"short_name": "CVAT",
"name": "Computer Vision Annotation Tool",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

@ -0,0 +1,7 @@
server {
root /usr/share/nginx/html;
# Any route that doesn't have a file extension (e.g. /devices)
location / {
try_files $uri $uri/ /index.html;
}
}

@ -0,0 +1,75 @@
import { Dispatch, ActionCreator } from 'redux';
export const dumpAnnotation = () => (dispatch: Dispatch) => {
dispatch({
type: 'DUMP_ANNOTATION',
});
}
export const dumpAnnotationSuccess = (downloadLink: string) => (dispatch: Dispatch) => {
dispatch({
type: 'DUMP_ANNOTATION_SUCCESS',
payload: downloadLink,
});
}
export const dumpAnnotationError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'DUMP_ANNOTATION_ERROR',
payload: error,
});
}
export const uploadAnnotation = () => (dispatch: Dispatch) => {
dispatch({
type: 'UPLOAD_ANNOTATION',
});
}
export const uploadAnnotationSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'UPLOAD_ANNOTATION_SUCCESS',
});
}
export const uploadAnnotationError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'UPLOAD_ANNOTATION_ERROR',
payload: error,
});
}
export const dumpAnnotationAsync = (task: any, dumper: any) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(dumpAnnotation());
return task.annotations.dump(task.name, dumper).then(
(downloadLink: string) => {
dispatch(dumpAnnotationSuccess(downloadLink));
},
(error: any) => {
dispatch(dumpAnnotationError(error));
throw error;
},
);
};
}
export const uploadAnnotationAsync = (task: any, file: File, loader: any) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(uploadAnnotation());
return task.annotations.upload(file, loader).then(
(response: any) => {
dispatch(uploadAnnotationSuccess());
},
(error: any) => {
dispatch(uploadAnnotationError(error));
throw error;
},
);
};
}

@ -0,0 +1,172 @@
import { History } from 'history';
import { Dispatch, ActionCreator } from 'redux';
export const login = () => (dispatch: Dispatch) => {
dispatch({
type: 'LOGIN',
});
}
export const loginSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'LOGIN_SUCCESS',
});
}
export const loginError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'LOGIN_ERROR',
payload: error,
});
}
export const logout = () => (dispatch: Dispatch) => {
dispatch({
type: 'LOGOUT',
});
}
export const logoutSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'LOGOUT_SUCCESS',
});
}
export const logoutError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'LOGOUT_ERROR',
payload: error,
});
}
export const isAuthenticated = () => (dispatch: Dispatch) => {
dispatch({
type: 'IS_AUTHENTICATED',
});
}
export const isAuthenticatedSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'IS_AUTHENTICATED_SUCCESS',
});
}
export const isAuthenticatedFail = () => (dispatch: Dispatch) => {
dispatch({
type: 'IS_AUTHENTICATED_FAIL',
});
}
export const isAuthenticatedError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'IS_AUTHENTICATED_ERROR',
payload: error,
});
}
export const register = () => (dispatch: Dispatch) => {
dispatch({
type: 'REGISTER',
});
}
export const registerSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'REGISTER_SUCCESS',
});
}
export const registerError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'REGISTER_ERROR',
payload: error,
});
}
export const loginAsync = (username: string, password: string, history: History) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(login());
return (window as any).cvat.server.login(username, password).then(
(loggedIn: any) => {
dispatch(loginSuccess());
history.push(history.location.state ? history.location.state.from : '/tasks');
},
(error: any) => {
dispatch(loginError(error));
throw error;
},
);
};
}
export const logoutAsync = () => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(logout());
return (window as any).cvat.server.logout().then(
(loggedOut: any) => {
dispatch(logoutSuccess());
},
(error: any) => {
dispatch(logoutError(error));
throw error;
},
);
};
}
export const isAuthenticatedAsync = () => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(isAuthenticated());
return (window as any).cvat.server.authorized().then(
(isAuthenticated: boolean) => {
isAuthenticated ? dispatch(isAuthenticatedSuccess()) : dispatch(isAuthenticatedFail());
},
(error: any) => {
dispatch(isAuthenticatedError(error));
throw error;
},
);
};
}
export const registerAsync = (
username: string,
firstName: string,
lastName: string,
email: string,
password: string,
passwordConfirmation: string,
history: History,
) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(register());
return (window as any).cvat.server.register(
username,
firstName,
lastName,
email,
password,
passwordConfirmation,
).then(
(registered: any) => {
dispatch(registerSuccess());
history.replace('/login');
},
(error: any) => {
dispatch(registerError(error));
throw error;
},
);
};
}

@ -0,0 +1,109 @@
import { Dispatch, ActionCreator } from 'redux';
export const getServerInfo = () => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SERVER_INFO',
});
}
export const getServerInfoSuccess = (information: null) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SERVER_INFO_SUCCESS',
payload: information,
});
}
export const getServerInfoError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SERVER_INFO_ERROR',
payload: error,
});
}
export const getShareFiles = () => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SHARE_FILES',
});
}
export const getShareFilesSuccess = (files: []) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SHARE_FILES_SUCCESS',
payload: files,
});
}
export const getShareFilesError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SHARE_FILES_ERROR',
payload: error,
});
}
export const getAnnotationFormats = () => (dispatch: Dispatch) => {
dispatch({
type: 'GET_ANNOTATION_FORMATS',
});
}
export const getAnnotationFormatsSuccess = (annotationFormats: []) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_ANNOTATION_FORMATS_SUCCESS',
payload: annotationFormats,
});
}
export const getAnnotationFormatsError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_ANNOTATION_FORMATS_ERROR',
payload: error,
});
}
export const getServerInfoAsync = () => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(getServerInfo());
return (window as any).cvat.server.about().then(
(information: any) => {
dispatch(getServerInfoSuccess(information));
},
(error: any) => {
dispatch(getServerInfoError(error));
},
);
};
}
export const getShareFilesAsync = (directory: string) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(getShareFiles());
return (window as any).cvat.server.share(directory).then(
(files: any) => {
dispatch(getShareFilesSuccess(files));
},
(error: any) => {
dispatch(getShareFilesError(error));
},
);
};
}
export const getAnnotationFormatsAsync = () => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(getAnnotationFormats());
return (window as any).cvat.server.formats().then(
(formats: any) => {
dispatch(getAnnotationFormatsSuccess(formats));
},
(error: any) => {
dispatch(getAnnotationFormatsError(error));
throw error;
},
);
};
}

@ -0,0 +1,9 @@
import { Dispatch } from 'redux';
export const filterTasks = (queryParams: { search?: string, page?: number }) => (dispatch: Dispatch) => {
dispatch({
type: 'FILTER_TASKS',
payload: queryParams,
});
}

@ -0,0 +1,175 @@
import { History } from 'history';
import { Dispatch, ActionCreator } from 'redux';
import queryString from 'query-string';
import setQueryObject from '../utils/tasks-filter'
export const getTasks = () => (dispatch: Dispatch) => {
dispatch({
type: 'GET_TASKS',
});
}
export const getTasksSuccess = (tasks: []) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_TASKS_SUCCESS',
payload: tasks,
});
}
export const getTasksError = (error: {}) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_TASKS_ERROR',
payload: error,
});
}
export const createTask = () => (dispatch: Dispatch) => {
dispatch({
type: 'CREATE_TASK',
});
}
export const createTaskSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'CREATE_TASK_SUCCESS',
});
}
export const createTaskError = (error: {}) => (dispatch: Dispatch) => {
dispatch({
type: 'CREATE_TASK_ERROR',
payload: error,
});
}
export const updateTask = () => (dispatch: Dispatch) => {
dispatch({
type: 'UPDATE_TASK',
});
}
export const updateTaskSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'UPDATE_TASK_SUCCESS',
});
}
export const updateTaskError = (error: {}) => (dispatch: Dispatch) => {
dispatch({
type: 'UPDATE_TASK_ERROR',
payload: error,
});
}
export const deleteTask = () => (dispatch: Dispatch) => {
dispatch({
type: 'DELETE_TASK',
});
}
export const deleteTaskSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'DELETE_TASK_SUCCESS',
});
}
export const deleteTaskError = (error: {}) => (dispatch: Dispatch) => {
dispatch({
type: 'DELETE_TASK_ERROR',
payload: error,
});
}
export const getTasksAsync = (queryObject = {}) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(getTasks());
return (window as any).cvat.tasks.get(queryObject).then(
(tasks: any) => {
dispatch(getTasksSuccess(tasks));
},
(error: any) => {
dispatch(getTasksError(error));
throw error;
},
);
};
}
export const createTaskAsync = (task: any) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(createTask());
return task.save().then(
(created: any) => {
dispatch(createTaskSuccess());
return dispatch(getTasksAsync());
},
(error: any) => {
dispatch(createTaskError(error));
throw error;
},
);
};
}
export const updateTaskAsync = (task: any) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(updateTask());
return task.save().then(
(updated: any) => {
dispatch(updateTaskSuccess());
return dispatch(getTasksAsync());
},
(error: any) => {
dispatch(updateTaskError(error));
throw error;
},
);
};
}
export const deleteTaskAsync = (task: any, history: History) => {
return (dispatch: ActionCreator<Dispatch>, getState: any) => {
dispatch(deleteTask());
return task.delete().then(
(deleted: any) => {
dispatch(deleteTaskSuccess());
const state = getState();
const queryObject = {
page: state.tasksFilter.currentPage,
search: state.tasksFilter.searchQuery,
}
if (state.tasks.tasks.length === 1 && state.tasks.tasksCount !== 1) {
queryObject.page = queryObject.page - 1;
history.push({ search: queryString.stringify(queryObject) });
} else if (state.tasks.tasksCount === 1) {
return dispatch(getTasksAsync());
} else {
const query = setQueryObject(queryObject);
return dispatch(getTasksAsync(query));
}
},
(error: any) => {
dispatch(deleteTaskError(error));
throw error;
},
);
};
}

@ -0,0 +1,40 @@
import { Dispatch, ActionCreator } from 'redux';
export const getUsers = () => (dispatch: Dispatch) => {
dispatch({
type: 'GET_USERS',
});
}
export const getUsersSuccess = (users: [], isCurrentUser: boolean) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_USERS_SUCCESS',
payload: users,
currentUser: isCurrentUser ? (users as any)[0] : isCurrentUser,
});
}
export const getUsersError = (error: {}) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_USERS_ERROR',
payload: error,
});
}
export const getUsersAsync = (filter = {}) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(getUsers());
return (window as any).cvat.users.get(filter).then(
(users: any) => {
dispatch(getUsersSuccess(users, (filter as any).self));
},
(error: any) => {
dispatch(getUsersError(error));
throw error;
},
);
};
}

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

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

Loading…
Cancel
Save