Merge pull request #2107 from opencv/release-1.1.0

Release 1.1.0
main
Nikita Manovich 6 years ago committed by GitHub
commit 8eb7c13619
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

1
.gitattributes vendored

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

@ -23,7 +23,7 @@
/datumaro/ @zhiltsov-max
/cvat/apps/dataset_manager/ @zhiltsov-max
# Advanced components (e.g. OpenVINO)
# Advanced components (e.g. analytics)
/components/ @azhavoro
# Infrastructure

2
.gitignore vendored

@ -7,12 +7,12 @@
/.env
/keys
/logs
/components/openvino/*.tgz
/profiles
/ssh/*
!/ssh/README.md
node_modules
/Mask_RCNN/
/letsencrypt-webroot/
# Ignore temporary files
docker-compose.override.yml

@ -20,7 +20,7 @@ persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
load-plugins=pylint_django
# Use multiple processes to speed up Pylint.
jobs=1
@ -66,8 +66,8 @@ enable= E0001,E0100,E0101,E0102,E0103,E0104,E0105,E0106,E0107,E0110,
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
R0102,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

@ -0,0 +1,20 @@
{
"extends": "stylelint-config-standard",
"rules": {
"indentation": 4,
"value-keyword-case": null,
"selector-combinator-space-after": null,
"no-descending-specificity": null,
"at-rule-no-unknown": [true, {
"ignoreAtRules": ["extend"]
}],
"selector-type-no-unknown": [true, {
"ignoreTypes": ["first-child"]
}]
},
"ignoreFiles": [
"**/*.js",
"**/*.ts",
"**/*.py"
]
}

@ -5,12 +5,29 @@ language: python
python:
- "3.5"
cache:
npm: true
directories:
- ~/.cache
addons:
apt:
packages:
- libgconf-2-4
services:
- docker
env:
- CONTAINER_COVERAGE_DATA_DIR="/coverage_data"
HOST_COVERAGE_DATA_DIR="${TRAVIS_BUILD_DIR}"
DJANGO_SU_NAME="admin"
DJANGO_SU_EMAIL="admin@localhost.company"
DJANGO_SU_PASSWORD="12qwaszx"
NODE_VERSION="12"
before_install:
- nvm install ${NODE_VERSION}
before_script:
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml build
@ -20,6 +37,13 @@ script:
# FIXME: Git package and application name conflict in PATH and try to leave only one python test execution
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'coverage run -a manage.py test cvat/apps && coverage run -a manage.py test --pattern="_test*.py" cvat/apps/dataset_manager/tests cvat/apps/engine/tests utils/cli && coverage run -a manage.py test datumaro/ && mv .coverage ${CONTAINER_COVERAGE_DATA_DIR}'
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-data && npm install && cd ../cvat-core && npm install && npm run test && coveralls-lcov -v -n ./reports/coverage/lcov.info > ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json'
# Up all containers
- docker-compose up -d
# Create superuser
- docker exec -it cvat bash -ic "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
# Install Cypress and run tests
- cd ./tests && npm install
- $(npm bin)/cypress run --headless --browser chrome && cd ..
after_success:
# https://coveralls-python.readthedocs.io/en/latest/usage/multilang.html

@ -27,7 +27,7 @@
"request": "launch",
"stopOnEntry": false,
"justMyCode": false,
"pythonPath": "${config:python.pythonPath}",
"pythonPath": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"runserver",
@ -58,7 +58,7 @@
"request": "launch",
"stopOnEntry": false,
"justMyCode": false,
"pythonPath": "${config:python.pythonPath}",
"pythonPath": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"rqworker",
@ -77,7 +77,7 @@
"request": "launch",
"stopOnEntry": false,
"justMyCode": false,
"pythonPath": "${config:python.pythonPath}",
"pythonPath": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"rqscheduler",
@ -93,7 +93,7 @@
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"pythonPath": "${config:python.pythonPath}",
"pythonPath":"${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"rqworker",
@ -112,7 +112,7 @@
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"pythonPath": "${config:python.pythonPath}",
"pythonPath": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"update_git_states"
@ -128,7 +128,7 @@
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"pythonPath": "${config:python.pythonPath}",
"pythonPath": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"migrate"
@ -144,7 +144,7 @@
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"pythonPath": "${config:python.pythonPath}",
"pythonPath": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"test",

@ -4,6 +4,131 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.0] - 2020-08-31
### Added
- Siammask tracker as DL serverless function (<https://github.com/opencv/cvat/pull/1988>)
- [Datumaro] Added model info and source info commands (<https://github.com/opencv/cvat/pull/1973>)
- [Datumaro] Dataset statistics (<https://github.com/opencv/cvat/pull/1668>)
- Ability to change label color in tasks and predefined labels (<https://github.com/opencv/cvat/pull/2014>)
- [Datumaro] Multi-dataset merge (https://github.com/opencv/cvat/pull/1695)
- Ability to configure email verification for new users (<https://github.com/opencv/cvat/pull/1929>)
- Link to django admin page from UI (<https://github.com/opencv/cvat/pull/2068>)
- Notification message when users use wrong browser (<https://github.com/opencv/cvat/pull/2070>)
### Changed
- Shape coordinates are rounded to 2 digits in dumped annotations (<https://github.com/opencv/cvat/pull/1970>)
- COCO format does not produce polygon points for bbox annotations (<https://github.com/opencv/cvat/pull/1953>)
### Fixed
- Issue loading openvino models for semi-automatic and automatic annotation (<https://github.com/opencv/cvat/pull/1996>)
- Basic functions of CVAT works without activated nuclio dashboard
- Fixed a case in which exported masks could have wrong color order (<https://github.com/opencv/cvat/issues/2032>)
- Fixed error with creating task with labels with the same name (<https://github.com/opencv/cvat/pull/2031>)
- Django RQ dashboard view (<https://github.com/opencv/cvat/pull/2069>)
## [1.1.0-beta] - 2020-08-03
### Added
- DL models as serverless functions (<https://github.com/opencv/cvat/pull/1767>)
- Source type support for tags, shapes and tracks (<https://github.com/opencv/cvat/pull/1192>)
- Source type support for CVAT Dumper/Loader (<https://github.com/opencv/cvat/pull/1192>)
- Intelligent polygon editing (<https://github.com/opencv/cvat/pull/1921>)
- Support creating multiple jobs for each task through python cli (https://github.com/opencv/cvat/pull/1950)
- python cli over https (<https://github.com/opencv/cvat/pull/1942>)
- Error message when plugins weren't able to initialize instead of infinite loading (<https://github.com/opencv/cvat/pull/1966>)
- Ability to change user password (<https://github.com/opencv/cvat/pull/1954>)
### Changed
- Smaller object details (<https://github.com/opencv/cvat/pull/1877>)
- `COCO` format does not convert bboxes to polygons on export (<https://github.com/opencv/cvat/pull/1953>)
- It is impossible to submit a DL model in OpenVINO format using UI. Now you can deploy new models on the server using serverless functions (<https://github.com/opencv/cvat/pull/1767>)
- Files and folders under share path are now alphabetically sorted
### Removed
- Removed OpenVINO and CUDA components because they are not necessary anymore (<https://github.com/opencv/cvat/pull/1767>)
- Removed the old UI code (<https://github.com/opencv/cvat/pull/1964>)
### Fixed
- Some objects aren't shown on canvas sometimes. For example after propagation on of objects is invisible (<https://github.com/opencv/cvat/pull/1834>)
- CVAT doesn't offer to restore state after an error (<https://github.com/opencv/cvat/pull/1874>)
- Cannot read property 'shapeType' of undefined because of zOrder related issues (<https://github.com/opencv/cvat/pull/1874>)
- Cannot read property 'pinned' of undefined because of zOrder related issues (<https://github.com/opencv/cvat/pull/1874>)
- Do not iterate over hidden objects in aam (which are invisible because of zOrder) (<https://github.com/opencv/cvat/pull/1874>)
- Cursor position is reset after changing a text field (<https://github.com/opencv/cvat/pull/1874>)
- Hidden points and cuboids can be selected to be groupped (<https://github.com/opencv/cvat/pull/1874>)
- `outside` annotations should not be in exported images (<https://github.com/opencv/cvat/issues/1620>)
- `CVAT for video format` import error with interpolation (<https://github.com/opencv/cvat/issues/1893>)
- `Image compression` definition mismatch (<https://github.com/opencv/cvat/issues/1900>)
- Points are dublicated during polygon interpolation sometimes (<https://github.com/opencv/cvat/pull/1892>)
- When redraw a shape with activated autobordering, previous points are visible (<https://github.com/opencv/cvat/pull/1892>)
- No mapping between side object element and context menu in some attributes (<https://github.com/opencv/cvat/pull/1923>)
- Interpolated shapes exported as `keyframe = True` (<https://github.com/opencv/cvat/pull/1937>)
- Stylelint filetype scans (<https://github.com/opencv/cvat/pull/1952>)
- Fixed toolip closing issue (<https://github.com/opencv/cvat/pull/1955>)
- Clearing frame cache when close a task (<https://github.com/opencv/cvat/pull/1966>)
- Increase rate of throttling policy for unauthenticated users (<https://github.com/opencv/cvat/pull/1969>)
## [1.1.0-alpha] - 2020-06-30
### Added
- Throttling policy for unauthenticated users (<https://github.com/opencv/cvat/pull/1531>)
- Added default label color table for mask export (<https://github.com/opencv/cvat/pull/1549>)
- Added environment variables for Redis and Postgres hosts for Kubernetes deployment support (<https://github.com/opencv/cvat/pull/1641>)
- Added visual identification for unavailable formats (<https://github.com/opencv/cvat/pull/1567>)
- Shortcut to change color of an activated shape in new UI (Enter) (<https://github.com/opencv/cvat/pull/1683>)
- Shortcut to switch split mode (<https://github.com/opencv/cvat/pull/1683>)
- Built-in search for labels when create an object or change a label (<https://github.com/opencv/cvat/pull/1683>)
- Better validation of labels and attributes in raw viewer (<https://github.com/opencv/cvat/pull/1727>)
- ClamAV antivirus integration (<https://github.com/opencv/cvat/pull/1712>)
- Added canvas background color selector (<https://github.com/opencv/cvat/pull/1705>)
- SCSS files linting with Stylelint tool (<https://github.com/opencv/cvat/pull/1766>)
- Supported import and export or single boxes in MOT format (https://github.com/opencv/cvat/pull/1764)
- [Datumaro] Added `stats` command, which shows some dataset statistics like image mean and std (https://github.com/opencv/cvat/pull/1734)
- Add option to upload annotations upon task creation on CLI
- Polygon and polylines interpolation (<https://github.com/opencv/cvat/pull/1571>)
- Ability to redraw shape from scratch (Shift + N) for an activated shape (<https://github.com/opencv/cvat/pull/1571>)
- Highlights for the first point of a polygon/polyline and direction (<https://github.com/opencv/cvat/pull/1571>)
- Ability to change orientation for poylgons/polylines in context menu (<https://github.com/opencv/cvat/pull/1571>)
- Ability to set the first point for polygons in points context menu (<https://github.com/opencv/cvat/pull/1571>)
- Added new tag annotation workspace (<https://github.com/opencv/cvat/pull/1570>)
- Appearance block in attribute annotation mode (<https://github.com/opencv/cvat/pull/1820>)
- Keyframe navigations and some switchers in attribute annotation mode (<https://github.com/opencv/cvat/pull/1820>)
- [Datumaro] Added `convert` command to convert datasets directly (<https://github.com/opencv/cvat/pull/1837>)
- [Datumaro] Added an option to specify image extension when exporting datasets (<https://github.com/opencv/cvat/pull/1799>)
- [Datumaro] Added image copying when exporting datasets, if possible (<https://github.com/opencv/cvat/pull/1799>)
### Changed
- Removed information about e-mail from the basic user information (<https://github.com/opencv/cvat/pull/1627>)
- Update https install manual. Makes it easier and more robust. Includes automatic renewing of lets encrypt certificates.
- Settings page move to the modal. (<https://github.com/opencv/cvat/pull/1705>)
- Implemented import and export of annotations with relative image paths (<https://github.com/opencv/cvat/pull/1463>)
- Using only single click to start editing or remove a point (<https://github.com/opencv/cvat/pull/1571>)
- Added support for attributes in VOC XML format (https://github.com/opencv/cvat/pull/1792)
- Added annotation attributes in COCO format (https://github.com/opencv/cvat/pull/1782)
- Colorized object items in the side panel (<https://github.com/opencv/cvat/pull/1753>)
- [Datumaro] Annotation-less files are not generated anymore in COCO format, unless tasks explicitly requested (<https://github.com/opencv/cvat/pull/1799>)
### Fixed
- Problem with exported frame stepped image task (<https://github.com/opencv/cvat/issues/1613>)
- Fixed dataset filter item representation for imageless dataset items (<https://github.com/opencv/cvat/pull/1593>)
- Fixed interpreter crash when trying to import `tensorflow` with no AVX instructions available (<https://github.com/opencv/cvat/pull/1567>)
- Kibana wrong working time calculation with new annotation UI use (<https://github.com/opencv/cvat/pull/1654>)
- Wrong rexex for account name validation (<https://github.com/opencv/cvat/pull/1667>)
- Wrong description on register view for the username field (<https://github.com/opencv/cvat/pull/1667>)
- Wrong resolution for resizing a shape (<https://github.com/opencv/cvat/pull/1667>)
- React warning because of not unique keys in labels viewer (<https://github.com/opencv/cvat/pull/1727>)
- Fixed issue tracker (<https://github.com/opencv/cvat/pull/1705>)
- Fixed canvas fit after sidebar open/close event (<https://github.com/opencv/cvat/pull/1705>)
- A couple of exceptions in AAM related with early object activation (<https://github.com/opencv/cvat/pull/1755>)
- Propagation from the latest frame (<https://github.com/opencv/cvat/pull/1800>)
- Number attribute value validation (didn't work well with floats) (<https://github.com/opencv/cvat/pull/1800>)
- Logout doesn't work (<https://github.com/opencv/cvat/pull/1812>)
- Annotations aren't updated after reopening a task (<https://github.com/opencv/cvat/pull/1753>)
- Labels aren't updated after reopening a task (<https://github.com/opencv/cvat/pull/1753>)
- Canvas isn't fitted after collapsing side panel in attribute annotation mode (<https://github.com/opencv/cvat/pull/1753>)
- Error when interpolating polygons (<https://github.com/opencv/cvat/pull/1878>)
### Security
- SQL injection in Django `CVE-2020-9402` (<https://github.com/opencv/cvat/pull/1657>)
## [1.0.0] - 2020-05-29
### Added
- cvat-ui: cookie policy drawer for login page (<https://github.com/opencv/cvat/pull/1511>)
@ -54,6 +179,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- A problem with mask to polygons conversion when polygons are too small (<https://github.com/opencv/cvat/pull/1581>)
- Unable to upload video with uneven size (<https://github.com/opencv/cvat/pull/1594>)
- Fixed an issue with `z_order` having no effect on segmentations (<https://github.com/opencv/cvat/pull/1589>)
### Security
- Permission group whitelist check for analytics view (<https://github.com/opencv/cvat/pull/1608>)
## [1.0.0-beta.2] - 2020-04-30

File diff suppressed because one or more lines are too long

@ -40,7 +40,6 @@ RUN apt-get update && \
libavfilter-dev \
libavformat-dev \
libavutil-dev \
libldap2-dev \
libswresample-dev \
libswscale-dev \
libldap2-dev \
@ -56,7 +55,7 @@ RUN apt-get update && \
curl && \
curl https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \
apt-get --no-install-recommends install -y git-lfs && git lfs install && \
python3 -m pip install --no-cache-dir -U pip==20.0.1 setuptools && \
python3 -m pip install --no-cache-dir -U pip==20.0.1 setuptools==49.6.0 wheel==0.35.1 && \
ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \
add-apt-repository --remove ppa:mc3man/gstffmpeg-keep -y && \
@ -77,33 +76,6 @@ RUN adduser --shell /bin/bash --disabled-password --gecos "" ${USER} && \
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 && \
mkdir ${REID_MODEL_DIR} && \
curl https://download.01.org/openvinotoolkit/2018_R5/open_model_zoo/person-reidentification-retail-0079/FP32/person-reidentification-retail-0079.xml -o reid/reid.xml && \
curl 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
ARG TF_ANNOTATION
ENV TF_ANNOTATION=${TF_ANNOTATION}
ENV TF_ANNOTATION_MODEL_PATH=${HOME}/rcnn/inference_graph
RUN if [ "$TF_ANNOTATION" = "yes" ]; then \
bash -i /tmp/components/tf_annotation/install.sh; \
fi
# Auto segmentation support. by Mohammad
ARG AUTO_SEGMENTATION
ENV AUTO_SEGMENTATION=${AUTO_SEGMENTATION}
ENV AUTO_SEGMENTATION_PATH=${HOME}/Mask_RCNN
RUN if [ "$AUTO_SEGMENTATION" = "yes" ]; then \
bash -i /tmp/components/auto_segmentation/install.sh; \
fi
# 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}/
@ -111,22 +83,17 @@ RUN python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGUR
# pycocotools package is impossible to install with its dependencies by one pip install command
RUN python3 -m pip install --no-cache-dir pycocotools==2.0.0
# 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}/dextr
RUN if [ "$WITH_DEXTR" = "yes" ]; then \
mkdir ${DEXTR_MODEL_DIR} -p && \
curl https://download.01.org/openvinotoolkit/models_contrib/cvat/dextr_model_v1.zip -o ${DEXTR_MODEL_DIR}/dextr.zip && \
7z e ${DEXTR_MODEL_DIR}/dextr.zip -o${DEXTR_MODEL_DIR} && rm ${DEXTR_MODEL_DIR}/dextr.zip; \
ARG CLAM_AV
ENV CLAM_AV=${CLAM_AV}
RUN if [ "$CLAM_AV" = "yes" ]; then \
apt-get update && \
apt-get --no-install-recommends install -yq \
clamav \
libclamunrar9 && \
sed -i 's/ReceiveTimeout 30/ReceiveTimeout 300/g' /etc/clamav/freshclam.conf && \
freshclam && \
chown -R ${USER}:${USER} /var/lib/clamav && \
rm -rf /var/lib/apt/lists/*; \
fi
COPY ssh ${HOME}/.ssh
@ -139,9 +106,6 @@ COPY datumaro/ ${HOME}/datumaro
RUN python3 -m pip install --no-cache-dir -r ${HOME}/datumaro/requirements.txt
# 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,11 +1,11 @@
FROM cvat
FROM cvat/server
ENV DJANGO_CONFIGURATION=testing
USER root
RUN curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list && \
curl https://deb.nodesource.com/setup_9.x | bash - && \
curl https://deb.nodesource.com/setup_12.x | bash - && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
apt-utils \
@ -23,19 +23,4 @@ RUN gem install coveralls-lcov
COPY .coveragerc .
# RUN all commands below as 'django' user
USER ${USER}
RUN mkdir -p tests && cd tests && npm install \
eslint \
eslint-detailed-reporter \
karma \
karma-chrome-launcher \
karma-coveralls \
karma-coverage \
karma-junit-reporter \
karma-qunit \
qunit; \
echo "export PATH=~/tests/node_modules/.bin:${PATH}" >> ~/.bashrc;
ENTRYPOINT []

@ -21,6 +21,8 @@ COPY cvat-canvas/package*.json /tmp/cvat-canvas/
COPY cvat-ui/package*.json /tmp/cvat-ui/
COPY cvat-data/package*.json /tmp/cvat-data/
RUN npm config set loglevel info
# Install cvat-data dependencies
WORKDIR /tmp/cvat-data/
RUN npm install

@ -4,14 +4,13 @@
[![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)
[![codebeat badge](https://codebeat.co/badges/53cd0d16-fddc-46f8-903c-f43ed9abb6dd)](https://codebeat.co/projects/github-com-opencv-cvat-develop)
[![DOI](https://zenodo.org/badge/139156354.svg)](https://zenodo.org/badge/latestdoi/139156354)
CVAT is free, online, interactive video and image annotation
tool for computer vision. It is being used by our team to
annotate million of objects with different properties. Many UI
and UX decisions are based on feedbacks from professional data annotation team.
Try it online [cvat.org](https://cvat.org).
and UX decisions are based on feedbacks from professional data
annotation team. Try it online [cvat.org](https://cvat.org).
![CVAT screenshot](cvat/apps/documentation/static/documentation/images/cvat.jpg)
@ -24,17 +23,20 @@ Try it online [cvat.org](https://cvat.org).
- [Command line interface](utils/cli/)
- [XML annotation format](cvat/apps/documentation/xml_format.md)
- [AWS Deployment Guide](cvat/apps/documentation/AWS-Deployment-Guide.md)
- [Frequently asked questions](cvat/apps/documentation/faq.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/JERohTFp-NI)
- [Annotation mode](https://youtu.be/vH_639N67HI)
- [Interpolation of bounding boxes](https://youtu.be/Hc3oudNuDsY)
- [Interpolation of polygons](https://youtu.be/K4nis9lk92s)
- [Tag_annotation_video](https://youtu.be/62bI4mF-Xfk)
- [Attribute mode](https://youtu.be/iIkJsOkDzVA)
- [Segmentation mode](https://youtu.be/9Fe_GzMLo3E)
- [Tutorial for polygons](https://youtu.be/C7-r9lZbjBw)
- [Semi-automatic segmentation](https://youtu.be/9HszWP_qsRQ)
## Supported annotation formats
@ -56,10 +58,18 @@ via its command line tool and Python library.
| [MOT](https://motchallenge.net/) | X | X |
| [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | 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/)
## Deep learning models for automatic labeling
| Name | Type | Framework |
| ------------------------------------------------------------------------------------------------------- | ---------- | ---------- |
| [Deep Extreme Cut](/serverless/openvino/dextr/nuclio) | interactor | OpenVINO |
| [Faster RCNN](/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | detector | TensorFlow |
| [Mask RCNN](/serverless/openvino/omz/public/mask_rcnn_inception_resnet_v2_atrous_coco/nuclio) | detector | OpenVINO |
| [YOLO v3](/serverless/openvino/omz/public/yolo-v3-tf/nuclio) | detector | OpenVINO |
| [Text detection v4](/serverless/openvino/omz/intel/text-detection-0004/nuclio) | detector | OpenVINO |
| [Semantic segmentation for ADAS](/serverless/openvino/omz/intel/semantic-segmentation-adas-0001/nuclio) | detector | OpenVINO |
| [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow |
| [Object reidentification](/serverless/openvino/omz/intel/person-reidentification-retail-300/nuclio) | reid | OpenVINO |
## Online demo: [cvat.org](https://cvat.org)
@ -69,7 +79,6 @@ are visible to users.
Disabled features:
- [Analytics: management and monitoring of data annotation team](/components/analytics/README.md)
- [Support for NVIDIA GPUs](/components/cuda/README.md)
Limitations:
- No more than 10 tasks per user
@ -103,3 +112,8 @@ If you are not sure or just want to browse other users common questions,
Other ways to ask questions and get our support:
* [\#cvat](https://stackoverflow.com/search?q=%23cvat) tag on StackOverflow*
* [Forum on Intel Developer Zone](https://software.intel.com/en-us/forums/computer-vision)
## 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/)

@ -31,7 +31,7 @@ services:
cvat_kibana_setup:
container_name: cvat_kibana_setup
image: cvat
image: cvat/server
volumes: ['./components/analytics/kibana:/home/django/kibana:ro']
depends_on: ['cvat']
working_dir: '/home/django'
@ -63,7 +63,7 @@ services:
DJANGO_LOG_SERVER_PORT: 5000
DJANGO_LOG_VIEWER_HOST: kibana
DJANGO_LOG_VIEWER_PORT: 5601
no_proxy: kibana,logstash,${no_proxy}
no_proxy: kibana,logstash,nuclio,${no_proxy}
volumes:
cvat_events:

@ -99,10 +99,10 @@
"_id": "ec510550-c238-11e8-8e1b-758ef07f6de8",
"_type": "index-pattern",
"_source": {
"fields": "[{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@version.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"application\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"application.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"box count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"duration\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"event.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"frame count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"object count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"points count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"polygon count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"polyline count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"task\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"task.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"timestamp\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"track count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"userid\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"userid.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"working time\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]",
"fields": "[{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@version.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"application\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"application.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"box count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"duration\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"event.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"frame count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"object count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"points count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"polygon count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"polyline count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"task\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"task.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"timestamp\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"track count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"userid\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"userid.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"working_time\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]",
"title": "cvat*",
"timeFieldName": "@timestamp",
"fieldFormatMap": "{\"duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\",\"outputFormat\":\"asSeconds\"}},\"working time\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\",\"outputFormat\":\"asHours\"}}}"
"fieldFormatMap": "{\"duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\",\"outputFormat\":\"asSeconds\"}},\"working_time\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\",\"outputFormat\":\"asHours\"}}}"
},
"_meta": {
"savedObjectVersion": 2
@ -147,7 +147,7 @@
"_type": "visualization",
"_source": {
"title": "List of users",
"visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"2\",\"params\":{\"customLabel\":\"User\",\"field\":\"userid.keyword\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"order\":\"asc\",\"orderBy\":\"_key\",\"otherBucket\":true,\"otherBucketLabel\":\"Other\",\"size\":1000},\"schema\":\"bucket\",\"type\":\"terms\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"customBucket\":{\"enabled\":true,\"id\":\"3-bucket\",\"params\":{\"customInterval\":\"2h\",\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":{\"aggFilter\":[],\"deprecate\":false,\"editor\":false,\"group\":\"none\",\"max\":null,\"min\":0,\"name\":\"bucketAgg\",\"params\":[],\"title\":\"Bucket Agg\"},\"type\":\"date_histogram\"},\"customLabel\":\"Activity\",\"customMetric\":{\"enabled\":true,\"id\":\"3-metric\",\"params\":{},\"schema\":{\"aggFilter\":[\"!top_hits\",\"!percentiles\",\"!percentile_ranks\",\"!median\",\"!std_dev\",\"!sum_bucket\",\"!avg_bucket\",\"!min_bucket\",\"!max_bucket\",\"!derivative\",\"!moving_avg\",\"!serial_diff\",\"!cumulative_sum\"],\"deprecate\":false,\"editor\":false,\"group\":\"none\",\"max\":null,\"min\":0,\"name\":\"metricAgg\",\"params\":[],\"title\":\"Metric Agg\"},\"type\":\"count\"}},\"schema\":\"metric\",\"type\":\"sum_bucket\"},{\"enabled\":true,\"id\":\"1\",\"params\":{\"customLabel\":\"Working Time (h)\",\"field\":\"working time\"},\"schema\":\"metric\",\"type\":\"sum\"}],\"params\":{\"perPage\":20,\"showMetricsAtAllLevels\":false,\"showPartialRows\":false,\"showTotal\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"totalFunc\":\"sum\"},\"title\":\"List of users\",\"type\":\"table\"}",
"visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"2\",\"params\":{\"customLabel\":\"User\",\"field\":\"userid.keyword\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"order\":\"asc\",\"orderBy\":\"_key\",\"otherBucket\":true,\"otherBucketLabel\":\"Other\",\"size\":1000},\"schema\":\"bucket\",\"type\":\"terms\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"customBucket\":{\"enabled\":true,\"id\":\"3-bucket\",\"params\":{\"customInterval\":\"2h\",\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":{\"aggFilter\":[],\"deprecate\":false,\"editor\":false,\"group\":\"none\",\"max\":null,\"min\":0,\"name\":\"bucketAgg\",\"params\":[],\"title\":\"Bucket Agg\"},\"type\":\"date_histogram\"},\"customLabel\":\"Activity\",\"customMetric\":{\"enabled\":true,\"id\":\"3-metric\",\"params\":{},\"schema\":{\"aggFilter\":[\"!top_hits\",\"!percentiles\",\"!percentile_ranks\",\"!median\",\"!std_dev\",\"!sum_bucket\",\"!avg_bucket\",\"!min_bucket\",\"!max_bucket\",\"!derivative\",\"!moving_avg\",\"!serial_diff\",\"!cumulative_sum\"],\"deprecate\":false,\"editor\":false,\"group\":\"none\",\"max\":null,\"min\":0,\"name\":\"metricAgg\",\"params\":[],\"title\":\"Metric Agg\"},\"type\":\"count\"}},\"schema\":\"metric\",\"type\":\"sum_bucket\"},{\"enabled\":true,\"id\":\"1\",\"params\":{\"customLabel\":\"Working Time (h)\",\"field\":\"working_time\"},\"schema\":\"metric\",\"type\":\"sum\"}],\"params\":{\"perPage\":20,\"showMetricsAtAllLevels\":false,\"showPartialRows\":false,\"showTotal\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"totalFunc\":\"sum\"},\"title\":\"List of users\",\"type\":\"table\"}",
"uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}",
"description": "",
"version": 1,
@ -164,7 +164,7 @@
"_type": "visualization",
"_source": {
"title": "List of tasks",
"visState": "{\"title\":\"List of tasks\",\"type\":\"table\",\"params\":{\"perPage\":20,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":2,\"direction\":\"desc\"},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"sum\",\"schema\":\"metric\",\"params\":{\"field\":\"working time\",\"customLabel\":\"Working time (h)\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"task.keyword\",\"size\":1000,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Task\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"userid.keyword\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"_key\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"User\"}}]}",
"visState": "{\"title\":\"List of tasks\",\"type\":\"table\",\"params\":{\"perPage\":20,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":2,\"direction\":\"desc\"},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"sum\",\"schema\":\"metric\",\"params\":{\"field\":\"working_time\",\"customLabel\":\"Working time (h)\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"task.keyword\",\"size\":1000,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Task\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"userid.keyword\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"_key\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"User\"}}]}",
"uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":2,\"direction\":\"desc\"}}}}",
"description": "",
"version": 1,

@ -65,6 +65,7 @@ filter {
mutate {
replace => { "type" => "client" }
rename => ["working time", "working_time"]
copy => {
"job_id" => "task"
"username" => "userid"

@ -1,38 +0,0 @@
## [Keras+Tensorflow Mask R-CNN Segmentation](https://github.com/matterport/Mask_RCNN)
### What is it?
- This application allows you automatically to segment many various objects on images.
- It's based on Feature Pyramid Network (FPN) and a ResNet101 backbone.
- It uses a pre-trained model on MS COCO dataset
- It supports next classes (use them in "labels" row):
```python
'BG', 'person', 'bicycle', 'car', 'motorcycle', 'airplane',
'bus', 'train', 'truck', 'boat', 'traffic light',
'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird',
'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear',
'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie',
'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball',
'kite', 'baseball bat', 'baseball glove', 'skateboard',
'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup',
'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza',
'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed',
'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote',
'keyboard', 'cell phone', 'microwave', 'oven', 'toaster',
'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors',
'teddy bear', 'hair drier', 'toothbrush'.
```
- Component adds "Run Auto Segmentation" button into dashboard.
### Build docker image
```bash
# From project root directory
docker-compose -f docker-compose.yml -f components/auto_segmentation/docker-compose.auto_segmentation.yml build
```
### Run docker container
```bash
# From project root directory
docker-compose -f docker-compose.yml -f components/auto_segmentation/docker-compose.auto_segmentation.yml up -d
```

@ -1,13 +0,0 @@
#
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
#
version: "2.3"
services:
cvat:
build:
context: .
args:
AUTO_SEGMENTATION: "yes"

@ -1,13 +0,0 @@
#!/bin/bash
#
set -e
MASK_RCNN_URL=https://github.com/matterport/Mask_RCNN
cd ${HOME} && \
git clone ${MASK_RCNN_URL}.git && \
curl -L ${MASK_RCNN_URL}/releases/download/v2.0/mask_rcnn_coco.h5 -o Mask_RCNN/mask_rcnn_coco.h5
# TODO remove useless files
# tensorflow and Keras are installed globally

@ -1,41 +0,0 @@
## [NVIDIA CUDA Toolkit](https://developer.nvidia.com/cuda-toolkit)
### Requirements
* NVIDIA GPU with a compute capability [3.0 - 7.2]
* Latest GPU driver
### Installation
#### Install the latest driver for your graphics card
```bash
sudo add-apt-repository ppa:graphics-drivers/ppa
sudo apt-get update
sudo apt-cache search nvidia-* # find latest nvidia driver
sudo apt-get --no-install-recommends install nvidia-* # install the nvidia driver
sudo apt-get --no-install-recommends install mesa-common-dev
sudo apt-get --no-install-recommends install freeglut3-dev
sudo apt-get --no-install-recommends install nvidia-modprobe
```
#### Reboot your PC and verify installation by `nvidia-smi` command.
#### Install [Nvidia-Docker](https://github.com/NVIDIA/nvidia-docker)
Please be sure that installation was successful.
```bash
docker info | grep 'Runtimes' # output should contains 'nvidia'
```
### Build docker image
```bash
# From project root directory
docker-compose -f docker-compose.yml -f components/cuda/docker-compose.cuda.yml build
```
### Run docker container
```bash
# From project root directory
docker-compose -f docker-compose.yml -f components/cuda/docker-compose.cuda.yml up -d
```

@ -1,23 +0,0 @@
#
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
#
version: "2.3"
services:
cvat:
build:
context: .
args:
CUDA_SUPPORT: "yes"
runtime: "nvidia"
environment:
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: compute,utility
# That environment variable is used by the Nvidia Container Runtime.
# The Nvidia Container Runtime parses this as:
# :space:: logical OR
# ,: Logical AND
# https://gitlab.com/nvidia/container-images/cuda/issues/31#note_149432780
NVIDIA_REQUIRE_CUDA: "cuda>=10.0 brand=tesla,driver>=384,driver<385 brand=tesla,driver>=410,driver<411"

@ -1,38 +0,0 @@
#!/bin/bash
#
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
#
set -e
NVIDIA_GPGKEY_SUM=d1be581509378368edeec8c1eb2958702feedf3bc3d17011adbf24efacce4ab5 && \
NVIDIA_GPGKEY_FPR=ae09fe4bbd223a84b2ccfce3f60f4b3d7fa2af80 && \
apt-key adv --fetch-keys http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/x86_64/7fa2af80.pub && \
apt-key adv --export --no-emit-version -a $NVIDIA_GPGKEY_FPR | tail -n +5 > cudasign.pub && \
echo "$NVIDIA_GPGKEY_SUM cudasign.pub" | sha256sum -c --strict - && rm cudasign.pub && \
echo "deb http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/x86_64 /" > /etc/apt/sources.list.d/cuda.list && \
echo "deb http://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu1604/x86_64 /" > /etc/apt/sources.list.d/nvidia-ml.list
CUDA_VERSION=10.0.130
NCCL_VERSION=2.5.6
CUDNN_VERSION=7.6.5.32
CUDA_PKG_VERSION="10-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
apt-get update && apt-get install -y --no-install-recommends --allow-unauthenticated \
cuda-cudart-$CUDA_PKG_VERSION \
cuda-compat-10-0 \
cuda-libraries-$CUDA_PKG_VERSION \
cuda-nvtx-$CUDA_PKG_VERSION \
libnccl2=$NCCL_VERSION-1+cuda10.0 \
libcudnn7=$CUDNN_VERSION-1+cuda10.0 && \
ln -s cuda-10.0 /usr/local/cuda && \
apt-mark hold libnccl2 libcudnn7 && \
rm -rf /var/lib/apt/lists/* \
/etc/apt/sources.list.d/nvidia-ml.list /etc/apt/sources.list.d/cuda.list
python3 -m pip uninstall -y tensorflow
python3 -m pip install --no-cache-dir tensorflow-gpu==1.15.2

@ -1,45 +0,0 @@
## [Intel OpenVINO toolkit](https://software.intel.com/en-us/openvino-toolkit)
### Requirements
* Intel Core with 6th generation and higher or Intel Xeon CPUs.
### Preparation
- Download the latest [OpenVINO toolkit](https://software.intel.com/en-us/openvino-toolkit) .tgz installer
(offline or online) for Ubuntu platforms. Note that OpenVINO does not maintain forward compatability between
Intermediate Representations (IRs), so the version of OpenVINO in CVAT and the version used to translate the
models needs to be the same.
- Put downloaded file into ```cvat/components/openvino```.
- Accept EULA in the `cvat/components/openvino/eula.cfg` file.
### Build docker image
```bash
# From project root directory
docker-compose -f docker-compose.yml -f components/openvino/docker-compose.openvino.yml build
```
### Run docker container
```bash
# From project root directory
docker-compose -f docker-compose.yml -f components/openvino/docker-compose.openvino.yml up -d
```
You should be able to login and see the web interface for CVAT now, complete with the new "Model Manager" button.
### OpenVINO Models
Clone the [Open Model Zoo](https://github.com/opencv/open_model_zoo). `$ git clone https://github.com/opencv/open_model_zoo.git`
Install the appropriate libraries. Currently that command would be `$ pip install -r open_model_zoo/tools/downloader/requirements.in`
Download the models using `downloader.py` file in `open_model_zoo/tools/downloader/`.
The `--name` command can be used to specify specific models.
The `--print_all` command can print all the available models.
Specific models that are already integrated into Cvat can be found [here](https://github.com/opencv/cvat/tree/develop/utils/open_model_zoo).
From the web user interface in CVAT, upload the models using the model manager.
You'll need to include the xml and bin file from the model downloader.
You'll need to include the python and JSON files from scratch or by using the ones in the CVAT libary.
See [here](https://github.com/opencv/cvat/tree/develop/cvat/apps/auto_annotation) for instructions for creating custom
python and JSON files.

@ -1,13 +0,0 @@
#
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
#
version: "2.3"
services:
cvat:
build:
context: .
args:
OPENVINO_TOOLKIT: "yes"

@ -1,3 +0,0 @@
# Accept actual EULA from openvino installation archive. Valid values are: {accept, decline}
ACCEPT_EULA=accept

@ -1,41 +0,0 @@
#!/bin/bash
#
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
#
set -e
if [[ `lscpu | grep -o "GenuineIntel"` != "GenuineIntel" ]]; then
echo "OpenVINO supports only Intel CPUs"
exit 1
fi
if [[ `lscpu | grep -o "sse4" | head -1` != "sse4" ]] && [[ `lscpu | grep -o "avx2" | head -1` != "avx2" ]]; then
echo "OpenVINO expects your CPU to support SSE4 or AVX2 instructions"
exit 1
fi
cd /tmp/components/openvino
tar -xzf `ls | grep "openvino_toolkit"`
cd `ls -d */ | grep "openvino_toolkit"`
apt-get update && apt-get --no-install-recommends 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
cd /tmp/components && rm openvino -r
if [ -f "/opt/intel/computer_vision_sdk/bin/setupvars.sh" ]; then
echo "source /opt/intel/computer_vision_sdk/bin/setupvars.sh" >> ${HOME}/.bashrc;
echo -e '\nexport IE_PLUGINS_PATH=${IE_PLUGINS_PATH}' >> /opt/intel/computer_vision_sdk/bin/setupvars.sh;
else
echo "source /opt/intel/openvino/bin/setupvars.sh" >> ${HOME}/.bashrc;
echo -e '\nexport IE_PLUGINS_PATH=${IE_PLUGINS_PATH}' >> /opt/intel/openvino/bin/setupvars.sh;
fi

@ -1,41 +0,0 @@
## [Tensorflow Object Detector](https://github.com/tensorflow/models/tree/master/research/object_detection)
### What is it?
* This application allows you automatically to annotate many various objects on images.
* It uses [Faster RCNN Inception Resnet v2 Atrous Coco Model](http://download.tensorflow.org/models/object_detection/faster_rcnn_inception_resnet_v2_atrous_coco_2018_01_28.tar.gz) from [tensorflow detection model zoo](https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md)
* It can work on CPU (with Tensorflow or OpenVINO) or GPU (with Tensorflow GPU).
* It supports next classes (just specify them in "labels" row):
```
'surfboard', 'car', 'skateboard', 'boat', 'clock',
'cat', 'cow', 'knife', 'apple', 'cup', 'tv',
'baseball_bat', 'book', 'suitcase', 'tennis_racket',
'stop_sign', 'couch', 'cell_phone', 'keyboard',
'cake', 'tie', 'frisbee', 'truck', 'fire_hydrant',
'snowboard', 'bed', 'vase', 'teddy_bear',
'toaster', 'wine_glass', 'traffic_light',
'broccoli', 'backpack', 'carrot', 'potted_plant',
'donut', 'umbrella', 'parking_meter', 'bottle',
'sandwich', 'motorcycle', 'bear', 'banana',
'person', 'scissors', 'elephant', 'dining_table',
'toothbrush', 'toilet', 'skis', 'bowl', 'sheep',
'refrigerator', 'oven', 'microwave', 'train',
'orange', 'mouse', 'laptop', 'bench', 'bicycle',
'fork', 'kite', 'zebra', 'baseball_glove', 'bus',
'spoon', 'horse', 'handbag', 'pizza', 'sports_ball',
'airplane', 'hair_drier', 'hot_dog', 'remote',
'sink', 'dog', 'bird', 'giraffe', 'chair'.
```
* Component adds "Run TF Annotation" button into dashboard.
### Build docker image
```bash
# From project root directory
docker-compose -f docker-compose.yml -f components/tf_annotation/docker-compose.tf_annotation.yml build
```
### Run docker container
```bash
# From project root directory
docker-compose -f docker-compose.yml -f components/tf_annotation/docker-compose.tf_annotation.yml up -d
```

@ -1,13 +0,0 @@
#
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
#
version: "2.3"
services:
cvat:
build:
context: .
args:
TF_ANNOTATION: "yes"

@ -1,15 +0,0 @@
#!/bin/bash
#
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
#
set -e
cd ${HOME} && \
curl http://download.tensorflow.org/models/object_detection/faster_rcnn_inception_resnet_v2_atrous_coco_2018_01_28.tar.gz -o model.tar.gz && \
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
# tensorflow is installed globally

@ -117,6 +117,7 @@ Canvas itself handles:
mode(): Mode;
cancel(): void;
configure(configuration: Configuration): void;
isAbleToChangeFrame(): boolean;
}
```
@ -188,8 +189,7 @@ Standard JS events are used.
| | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS |
|--------------|------|-------|-------|------|-------|------|------|--------|-------------|-------------|
| html() | + | + | + | + | + | + | + | + | + | + |
| setup() | + | + | + | + | + | +/- | +/- | +/- | + | + |
| setup() | + | + | + | +/- | + | +/- | +/- | +/- | + | + |
| activate() | + | - | - | - | - | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | + | + | + | + |
@ -208,3 +208,6 @@ Standard JS events are used.
| setZLayer() | + | + | + | + | + | + | + | + | + | + |
You can call setup() during editing, dragging, and resizing only to update objects, not to change a frame.
You can change frame during draw only when you do not redraw an existing object
Other methods do not change state and can be used everytime.

@ -1,6 +1,6 @@
{
"name": "cvat-canvas",
"version": "1.1.1",
"version": "2.0.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -3223,9 +3223,9 @@
"dev": true
},
"elliptic": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.0.tgz",
"integrity": "sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"dev": true,
"requires": {
"bn.js": "^4.4.0",
@ -5949,9 +5949,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true
},
"lodash._reinterpolate": {
@ -10431,9 +10431,9 @@
}
},
"websocket-extensions": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
"integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
"dev": true
},
"which": {

@ -1,6 +1,6 @@
{
"name": "cvat-canvas",
"version": "1.1.1",
"version": "2.0.2",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts",
"scripts": {

@ -42,7 +42,7 @@ polyline.cvat_shape_drawing_opacity {
fill: white;
cursor: default;
font-family: Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif;
text-shadow: 0px 0px 4px black;
text-shadow: 0 0 4px black;
user-select: none;
pointer-events: none;
}
@ -54,47 +54,55 @@ polyline.cvat_shape_drawing_opacity {
.cvat_canvas_shape_grouping {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: darkmagenta;
}
polyline.cvat_canvas_shape_grouping {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: darkmagenta;
}
.cvat_canvas_shape_merging {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: blue;
}
polyline.cvat_canvas_shape_merging {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: blue;
}
polyline.cvat_canvas_shape_splitting {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: dodgerblue;
}
.cvat_canvas_shape_splitting {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: dodgerblue;
}
.cvat_canvas_shape_drawing {
@extend .cvat_shape_drawing_opacity;
fill: white;
stroke: black;
}
.cvat_canvas_zoom_selection {
@extend .cvat_shape_action_dasharray;
stroke: #096dd9;
fill-opacity: 0;
}
@ -103,7 +111,8 @@ polyline.cvat_canvas_shape_splitting {
stroke-dasharray: 5;
}
.cvat_canvas_shape .svg_select_points, .cvat_canvas_shape .cvat_canvas_cuboid_projections {
.cvat_canvas_shape .svg_select_points,
.cvat_canvas_shape .cvat_canvas_cuboid_projections {
stroke-dasharray: none;
}
@ -130,20 +139,24 @@ polyline.cvat_canvas_shape_splitting {
pointer-events: none;
}
.svg_select_points_lb:hover, .svg_select_points_rt:hover {
.svg_select_points_lb:hover,
.svg_select_points_rt:hover {
cursor: nesw-resize;
}
.svg_select_points_lt:hover, .svg_select_points_rb:hover {
.svg_select_points_lt:hover,
.svg_select_points_rb:hover {
cursor: nwse-resize;
}
.svg_select_points_l:hover, .svg_select_points_r:hover,
.svg_select_points_l:hover,
.svg_select_points_r:hover,
.svg_select_points_ew:hover {
cursor: ew-resize;
}
.svg_select_points_t:hover, .svg_select_points_b:hover {
.svg_select_points_t:hover,
.svg_select_points_b:hover {
cursor: ns-resize;
}
@ -151,12 +164,31 @@ polyline.cvat_canvas_shape_splitting {
cursor: move;
}
.cvat_canvas_first_poly_point {
fill: lightgray;
}
.cvat_canvas_poly_direction {
fill: lightgray;
stroke: black;
&:hover {
fill: black;
stroke: lightgray;
}
&:active {
fill: lightgray;
stroke: black;
}
}
#cvat_canvas_wrapper {
width: calc(100% - 10px);
height: calc(100% - 10px);
margin: 5px;
border-radius: 5px;
background-color: white;
background-color: inherit;
overflow: hidden;
position: relative;
}
@ -183,7 +215,6 @@ polyline.cvat_canvas_shape_splitting {
pointer-events: none;
width: 100%;
height: 100%;
pointer-events: none;
}
#cvat_canvas_background {
@ -192,7 +223,7 @@ polyline.cvat_canvas_shape_splitting {
background-repeat: no-repeat;
width: 100%;
height: 100%;
box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75);
box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.75);
}
#cvat_canvas_bitmap {
@ -202,7 +233,7 @@ polyline.cvat_canvas_shape_splitting {
background: black;
width: 100%;
height: 100%;
box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75);
box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.75);
}
#cvat_canvas_grid {
@ -211,7 +242,6 @@ polyline.cvat_canvas_shape_splitting {
pointer-events: none;
width: 100%;
height: 100%;
pointer-events: none;
}
#cvat_canvas_grid_pattern {
@ -229,7 +259,18 @@ polyline.cvat_canvas_shape_splitting {
}
@keyframes loadingAnimation {
0% {stroke-dashoffset: 1; stroke: #09c;}
50% {stroke-dashoffset: 100; stroke: #f44;}
100% {stroke-dashoffset: 300; stroke: #09c;}
0% {
stroke-dashoffset: 1;
stroke: #09c;
}
50% {
stroke-dashoffset: 100;
stroke: #f44;
}
100% {
stroke-dashoffset: 300;
stroke: #09c;
}
}

@ -13,14 +13,14 @@ interface TransformedShape {
}
export interface AutoborderHandler {
autoborder(enabled: boolean, currentShape?: SVG.Shape, ignoreCurrent?: boolean): void;
autoborder(enabled: boolean, currentShape?: SVG.Shape, currentID?: number): void;
transform(geometry: Geometry): void;
updateObjects(): void;
}
export class AutoborderHandlerImpl implements AutoborderHandler {
private currentShape: SVG.Shape | null;
private ignoreCurrent: boolean;
private currentID?: number;
private frameContent: SVGSVGElement;
private enabled: boolean;
private scale: number;
@ -34,7 +34,7 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
public constructor(frameContent: SVGSVGElement) {
this.frameContent = frameContent;
this.ignoreCurrent = false;
this.currentID = undefined;
this.currentShape = null;
this.enabled = false;
this.scale = 1;
@ -231,7 +231,8 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
this.removeMarkers();
const currentClientID = this.currentShape.node.dataset.originClientId;
const shapes = Array.from(this.frameContent.getElementsByClassName('cvat_canvas_shape'));
const shapes = Array.from(this.frameContent.getElementsByClassName('cvat_canvas_shape'))
.filter((shape: HTMLElement): boolean => +shape.getAttribute('clientID') !== this.currentID);
const transformedShapes = shapes.map((shape: HTMLElement): TransformedShape | null => {
const color = shape.getAttribute('fill');
const clientID = shape.getAttribute('clientID');
@ -277,12 +278,12 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
public autoborder(
enabled: boolean,
currentShape?: SVG.Shape,
ignoreCurrent: boolean = false,
currentID?: number,
): void {
if (enabled && !this.enabled && currentShape) {
this.enabled = true;
this.currentShape = currentShape;
this.ignoreCurrent = ignoreCurrent;
this.currentID = currentID;
this.updateObjects();
} else {
this.release();

@ -36,8 +36,7 @@ const CanvasVersion = pjson.version;
interface Canvas {
html(): HTMLDivElement;
setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void;
setup(frameData: any, objectStates: any[], zLayer?: number): void;
activate(clientID: number | null, attributeID?: number): void;
rotate(rotationAngle: number): void;
focus(clientID: number, padding?: number): void;
@ -58,6 +57,7 @@ interface Canvas {
mode(): Mode;
cancel(): void;
configure(configuration: Configuration): void;
isAbleToChangeFrame(): boolean;
}
class CanvasImpl implements Canvas {
@ -75,12 +75,8 @@ class CanvasImpl implements Canvas {
return this.view.html();
}
public setZLayer(zLayer: number | null): void {
this.model.setZLayer(zLayer);
}
public setup(frameData: any, objectStates: any[]): void {
this.model.setup(frameData, objectStates);
public setup(frameData: any, objectStates: any[], zLayer = 0): void {
this.model.setup(frameData, objectStates, zLayer);
}
public fitCanvas(): void {
@ -153,6 +149,10 @@ class CanvasImpl implements Canvas {
public configure(configuration: Configuration): void {
this.model.configure(configuration);
}
public isAbleToChangeFrame(): boolean {
return this.model.isAbleToChangeFrame();
}
}
export {

@ -66,6 +66,7 @@ export interface DrawData {
numberOfPoints?: number;
initialState?: any;
crosshair?: boolean;
redraw?: number;
}
export interface EditData {
@ -97,7 +98,6 @@ export enum UpdateReasons {
IMAGE_FITTED = 'image_fitted',
IMAGE_MOVED = 'image_moved',
GRID_UPDATED = 'grid_updated',
SET_Z_LAYER = 'set_z_layer',
OBJECTS_UPDATED = 'objects_updated',
SHAPE_ACTIVATED = 'shape_activated',
@ -147,11 +147,10 @@ export interface CanvasModel {
geometry: Geometry;
mode: Mode;
setZLayer(zLayer: number | null): void;
zoom(x: number, y: number, direction: number): void;
move(topOffset: number, leftOffset: number): void;
setup(frameData: any, objectStates: any[]): void;
setup(frameData: any, objectStates: any[], zLayer: number): void;
activate(clientID: number | null, attributeID: number | null): void;
rotate(rotationAngle: number): void;
focus(clientID: number, padding: number): void;
@ -169,6 +168,7 @@ export interface CanvasModel {
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
isAbleToChangeFrame(): boolean;
configure(configuration: Configuration): void;
cancel(): void;
}
@ -256,11 +256,6 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
};
}
public setZLayer(zLayer: number | null): void {
this.data.zLayer = zLayer;
this.notify(UpdateReasons.SET_Z_LAYER);
}
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;
@ -335,7 +330,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.ZOOM_CANVAS);
}
public setup(frameData: any, objectStates: any[]): void {
public setup(frameData: any, objectStates: any[], zLayer: number): void {
if (this.data.imageID !== frameData.number) {
if ([Mode.EDIT, Mode.DRAG, Mode.RESIZE].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
@ -343,6 +338,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
if (frameData.number === this.data.imageID) {
this.data.zLayer = zLayer;
this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED);
return;
@ -367,6 +363,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.image = data;
this.notify(UpdateReasons.IMAGE_CHANGED);
this.data.zLayer = zLayer;
this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED);
}).catch((exception: any): void => {
@ -382,10 +379,17 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
if (this.data.mode !== Mode.IDLE && clientID !== null) {
// Exception or just return?
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (typeof (clientID) === 'number') {
const [state] = this.objects
.filter((_state: any): boolean => _state.clientID === clientID);
if (!state || state.objectType === 'tag') {
return;
}
}
this.data.activeElement = {
clientID,
attributeID,
@ -465,10 +469,24 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
}
this.data.drawData = { ...drawData };
if (this.data.drawData.initialState) {
this.data.drawData.shapeType = this.data.drawData.initialState.shapeType;
if (typeof (drawData.redraw) === 'number') {
const clientID = drawData.redraw;
const [state] = this.data.objects
.filter((_state: any): boolean => _state.clientID === clientID);
if (state) {
this.data.drawData = { ...drawData };
this.data.drawData.shapeType = state.shapeType;
} else {
return;
}
} else {
this.data.drawData = { ...drawData };
if (this.data.drawData.initialState) {
this.data.drawData.shapeType = this.data.drawData.initialState.shapeType;
}
}
this.notify(UpdateReasons.DRAW);
}
@ -548,6 +566,13 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.CONFIG_UPDATED);
}
public isAbleToChangeFrame(): boolean {
const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE].includes(this.data.mode)
|| (this.data.mode === Mode.DRAW && typeof (this.data.drawData.redraw) === 'number');
return !isUnable;
}
public cancel(): void {
this.notify(UpdateReasons.CANCEL);
}

@ -21,8 +21,11 @@ import consts from './consts';
import {
translateToSVG,
translateFromSVG,
pointsToArray,
pointsToNumberArray,
parsePoints,
displayShapeSize,
scalarProduct,
vectorLength,
ShapeSizeElement,
DrawnState,
} from './shared';
@ -71,6 +74,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
private autoborderHandler: AutoborderHandler;
private activeElement: ActiveElement;
private configuration: Configuration;
private serviceFlags: {
drawHidden: Record<number, boolean>;
};
private set mode(value: Mode) {
this.controller.mode = value;
@ -80,8 +86,75 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.controller.mode;
}
private isServiceHidden(clientID: number): boolean {
return this.serviceFlags.drawHidden[clientID] || false;
}
private setupServiceHidden(clientID: number, value: boolean): void {
this.serviceFlags.drawHidden[clientID] = value;
const shape = this.svgShapes[clientID];
const text = this.svgTexts[clientID];
const state = this.drawnStates[clientID];
if (value) {
if (shape) {
(state.shapeType === 'points' ? shape.remember('_selectHandler').nested : shape)
.addClass('cvat_canvas_hidden');
}
if (text) {
text.addClass('cvat_canvas_hidden');
}
} else {
delete this.serviceFlags.drawHidden[clientID];
if (state) {
if (!state.outside && !state.hidden) {
if (shape) {
(state.shapeType === 'points' ? shape.remember('_selectHandler').nested : shape)
.removeClass('cvat_canvas_hidden');
}
if (text) {
text.removeClass('cvat_canvas_hidden');
this.updateTextPosition(
text,
shape,
);
}
}
}
}
}
private onDrawDone(data: object | null, duration: number, continueDraw?: boolean): void {
const hiddenBecauseOfDraw = Object.keys(this.serviceFlags.drawHidden)
.map((_clientID): number => +_clientID);
if (hiddenBecauseOfDraw.length) {
for (const hidden of hiddenBecauseOfDraw) {
this.setupServiceHidden(hidden, false);
}
}
if (data) {
const { clientID, points } = data as any;
if (typeof (clientID) === 'number') {
const event: CustomEvent = new CustomEvent('canvas.canceled', {
bubbles: false,
cancelable: true,
});
this.canvas.dispatchEvent(event);
const [state] = this.controller.objects
.filter((_state: any): boolean => (
_state.clientID === clientID
));
this.onEditDone(state, points);
return;
}
const { zLayer } = this.controller;
const event: CustomEvent = new CustomEvent('canvas.drawn', {
bubbles: false,
@ -323,6 +396,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
);
}
for (const element of
window.document.getElementsByClassName('cvat_canvas_poly_direction')) {
const angle = (element as any).instance.data('angle');
(element as any).instance.style({
transform: `scale(${1 / this.geometry.scale}) rotate(${angle}deg)`,
});
}
for (const element of
window.document.getElementsByClassName('cvat_canvas_selected_point')) {
const previousWidth = element.getAttribute('stroke-width') as string;
@ -425,13 +507,88 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
private hideDirection(shape: SVG.Polygon | SVG.PolyLine): void {
/* eslint class-methods-use-this: 0 */
const handler = shape.remember('_selectHandler');
if (!handler || !handler.nested) return;
const nested = handler.nested as SVG.Parent;
if (nested.children().length) {
nested.children()[0].removeClass('cvat_canvas_first_poly_point');
}
const node = nested.node as SVG.LinkedHTMLElement;
const directions = node.getElementsByClassName('cvat_canvas_poly_direction');
for (const direction of directions) {
const { instance } = (direction as any);
instance.off('click');
instance.remove();
}
}
private showDirection(state: any, shape: SVG.Polygon | SVG.PolyLine): void {
const path = consts.ARROW_PATH;
const points = parsePoints(state.points);
const handler = shape.remember('_selectHandler');
if (!handler || !handler.nested) return;
const firstCircle = handler.nested.children()[0];
const secondCircle = handler.nested.children()[1];
firstCircle.addClass('cvat_canvas_first_poly_point');
const [cx, cy] = [
(secondCircle.cx() + firstCircle.cx()) / 2,
(secondCircle.cy() + firstCircle.cy()) / 2,
];
const [firstPoint, secondPoint] = points.slice(0, 2);
const xAxis = { i: 1, j: 0 };
const baseVector = { i: secondPoint.x - firstPoint.x, j: secondPoint.y - firstPoint.y };
const baseVectorLength = vectorLength(baseVector);
let cosinus = 0;
if (baseVectorLength !== 0) {
// two points have the same coordinates
cosinus = scalarProduct(xAxis, baseVector)
/ (vectorLength(xAxis) * baseVectorLength);
}
const angle = Math.acos(cosinus) * (Math.sign(baseVector.j) || 1) * 180 / Math.PI;
const pathElement = handler.nested.path(path).fill('white')
.stroke({
width: 1,
color: 'black',
}).addClass('cvat_canvas_poly_direction').style({
'transform-origin': `${cx}px ${cy}px`,
transform: `scale(${1 / this.geometry.scale}) rotate(${angle}deg)`,
}).move(cx, cy);
pathElement.on('click', (e: MouseEvent): void => {
if (e.button === 0) {
e.stopPropagation();
if (state.shapeType === 'polygon') {
const reversedPoints = [points[0], ...points.slice(1).reverse()];
this.onEditDone(state, pointsToNumberArray(reversedPoints));
} else {
const reversedPoints = points.reverse();
this.onEditDone(state, pointsToNumberArray(reversedPoints));
}
}
});
pathElement.data('angle', angle);
pathElement.dmove(-pathElement.width() / 2, -pathElement.height() / 2);
}
private selectize(value: boolean, shape: SVG.Element): void {
const self = this;
const { offset } = this.controller.geometry;
const translate = (points: number[]): number[] => points
.map((coord: number): number => coord - offset);
function dblClickHandler(e: MouseEvent): void {
function mousedownHandler(e: MouseEvent): void {
if (e.button !== 0) return;
e.preventDefault();
const pointID = Array.prototype.indexOf
.call(((e.target as HTMLElement).parentElement as HTMLElement).children, e.target);
@ -440,45 +597,52 @@ export class CanvasViewImpl implements CanvasView, Listener {
.filter((_state: any): boolean => (
_state.clientID === self.activeElement.clientID
));
if (state.shapeType === 'rectangle') {
e.preventDefault();
return;
if (['polygon', 'polyline', 'points'].includes(state.shapeType)) {
if (e.ctrlKey) {
const { points } = state;
self.onEditDone(
state,
points.slice(0, pointID * 2).concat(points.slice(pointID * 2 + 2)),
);
} else if (e.shiftKey) {
self.canvas.dispatchEvent(new CustomEvent('canvas.editstart', {
bubbles: false,
cancelable: true,
}));
self.mode = Mode.EDIT;
self.deactivate();
self.editHandler.edit({
enabled: true,
state,
pointID,
});
}
}
}
}
function dblClickHandler(e: MouseEvent): void {
e.preventDefault();
if (self.activeElement.clientID !== null) {
const [state] = self.controller.objects
.filter((_state: any): boolean => (
_state.clientID === self.activeElement.clientID
));
if (state.shapeType === 'cuboid') {
if (e.shiftKey) {
const points = translate(pointsToArray((e.target as any)
const points = translate(pointsToNumberArray((e.target as any)
.parentElement.parentElement.instance.attr('points')));
self.onEditDone(
state,
points,
);
e.preventDefault();
return;
}
}
if (e.ctrlKey) {
const { points } = state;
self.onEditDone(
state,
points.slice(0, pointID * 2).concat(points.slice(pointID * 2 + 2)),
);
} else if (e.shiftKey) {
self.canvas.dispatchEvent(new CustomEvent('canvas.editstart', {
bubbles: false,
cancelable: true,
}));
self.mode = Mode.EDIT;
self.deactivate();
self.editHandler.edit({
enabled: true,
state,
pointID,
});
}
}
e.preventDefault();
}
function contextMenuHandler(e: MouseEvent): void {
@ -524,6 +688,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
});
circle.on('dblclick', dblClickHandler);
circle.on('mousedown', mousedownHandler);
circle.on('contextmenu', contextMenuHandler);
circle.addClass('cvat_canvas_selected_point');
});
@ -534,6 +699,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
});
circle.off('dblclick', dblClickHandler);
circle.off('mousedown', mousedownHandler);
circle.off('contextmenu', contextMenuHandler);
circle.removeClass('cvat_canvas_selected_point');
});
@ -565,6 +731,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
};
this.configuration = model.configuration;
this.mode = Mode.IDLE;
this.serviceFlags = {
drawHidden: {},
};
// Create HTML elements
this.loadingAnimation = window.document
@ -742,12 +911,32 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (reason === UpdateReasons.CONFIG_UPDATED) {
const { activeElement } = this;
this.deactivate();
if (model.configuration.displayAllText && !this.configuration.displayAllText) {
for (const i in this.drawnStates) {
if (!(i in this.svgTexts)) {
this.svgTexts[i] = this.addText(this.drawnStates[i]);
this.updateTextPosition(
this.svgTexts[i],
this.svgShapes[i],
);
}
}
} else if (model.configuration.displayAllText === false
&& this.configuration.displayAllText) {
for (const i in this.drawnStates) {
if (i in this.svgTexts && Number.parseInt(i, 10) !== activeElement.clientID) {
this.svgTexts[i].remove();
delete this.svgTexts[i];
}
}
}
this.configuration = model.configuration;
this.activate(activeElement);
this.editHandler.configurate(this.configuration);
this.drawHandler.configurate(this.configuration);
// todo: setup text, add if doesn't exist and enabled
// remove if exist and not enabled
// this.setupObjects([]);
// this.setupObjects(model.objects);
@ -802,7 +991,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
} else if (reason === UpdateReasons.IMAGE_MOVED) {
this.moveCanvas();
} else if ([UpdateReasons.OBJECTS_UPDATED, UpdateReasons.SET_Z_LAYER].includes(reason)) {
} else if ([UpdateReasons.OBJECTS_UPDATED].includes(reason)) {
if (this.mode === Mode.GROUP) {
this.groupHandler.resetSelectedObjects();
}
@ -864,6 +1053,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (data.enabled && this.mode === Mode.IDLE) {
this.canvas.style.cursor = 'crosshair';
this.mode = Mode.DRAW;
if (typeof (data.redraw) === 'number') {
this.setupServiceHidden(data.redraw, true);
}
this.drawHandler.draw(data, this.geometry);
} else {
this.canvas.style.cursor = '';
@ -936,7 +1128,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (model.imageBitmap
&& [UpdateReasons.IMAGE_CHANGED,
UpdateReasons.OBJECTS_UPDATED,
UpdateReasons.SET_Z_LAYER,
].includes(reason)
) {
this.redrawBitmap();
@ -1036,6 +1227,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
pinned: state.pinned,
updated: state.updated,
frame: state.frame,
label: state.label,
};
}
@ -1045,18 +1237,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
const drawnState = this.drawnStates[clientID];
const shape = this.svgShapes[state.clientID];
const text = this.svgTexts[state.clientID];
const isInvisible = state.hidden || state.outside;
const isInvisible = state.hidden || state.outside
|| this.isServiceHidden(state.clientID);
if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) {
if (isInvisible) {
(state.shapeType === 'points' ? shape.remember('_selectHandler').nested : shape)
.style('display', 'none');
.addClass('cvat_canvas_hidden');
if (text) {
text.addClass('cvat_canvas_hidden');
}
} else {
(state.shapeType === 'points' ? shape.remember('_selectHandler').nested : shape)
.style('display', '');
.removeClass('cvat_canvas_hidden');
if (text) {
text.removeClass('cvat_canvas_hidden');
this.updateTextPosition(
@ -1147,59 +1340,57 @@ export class CanvasViewImpl implements CanvasView, Listener {
const { displayAllText } = this.configuration;
for (const state of states) {
if (state.objectType === 'tag') {
this.addTag(state);
const points: number[] = (state.points as number[]);
const translatedPoints: number[] = translate(points);
// TODO: Use enums after typification cvat-core
if (state.shapeType === 'rectangle') {
this.svgShapes[state.clientID] = this
.addRect(translatedPoints, state);
} else {
const points: number[] = (state.points as number[]);
const translatedPoints: number[] = translate(points);
const stringified = translatedPoints.reduce(
(acc: string, val: number, idx: number): string => {
if (idx % 2) {
return `${acc}${val} `;
}
// TODO: Use enums after typification cvat-core
if (state.shapeType === 'rectangle') {
return `${acc}${val},`;
}, '',
);
if (state.shapeType === 'polygon') {
this.svgShapes[state.clientID] = this
.addRect(translatedPoints, state);
.addPolygon(stringified, state);
} else if (state.shapeType === 'polyline') {
this.svgShapes[state.clientID] = this
.addPolyline(stringified, state);
} else if (state.shapeType === 'points') {
this.svgShapes[state.clientID] = this
.addPoints(stringified, state);
} else if (state.shapeType === 'cuboid') {
this.svgShapes[state.clientID] = this
.addCuboid(stringified, state);
} else {
const stringified = translatedPoints.reduce(
(acc: string, val: number, idx: number): string => {
if (idx % 2) {
return `${acc}${val} `;
}
return `${acc}${val},`;
}, '',
);
if (state.shapeType === 'polygon') {
this.svgShapes[state.clientID] = this
.addPolygon(stringified, state);
} else if (state.shapeType === 'polyline') {
this.svgShapes[state.clientID] = this
.addPolyline(stringified, state);
} else if (state.shapeType === 'points') {
this.svgShapes[state.clientID] = this
.addPoints(stringified, state);
} else if (state.shapeType === 'cuboid') {
this.svgShapes[state.clientID] = this
.addCuboid(stringified, state);
}
continue;
}
}
this.svgShapes[state.clientID].on('click.canvas', (): void => {
this.canvas.dispatchEvent(new CustomEvent('canvas.clicked', {
bubbles: false,
cancelable: true,
detail: {
state,
},
}));
});
this.svgShapes[state.clientID].on('click.canvas', (): void => {
this.canvas.dispatchEvent(new CustomEvent('canvas.clicked', {
bubbles: false,
cancelable: true,
detail: {
state,
},
}));
});
if (displayAllText) {
this.svgTexts[state.clientID] = this.addText(state);
this.updateTextPosition(
this.svgTexts[state.clientID],
this.svgShapes[state.clientID],
);
}
if (displayAllText) {
this.svgTexts[state.clientID] = this.addText(state);
this.updateTextPosition(
this.svgTexts[state.clientID],
this.svgShapes[state.clientID],
);
}
this.saveState(state);
@ -1327,16 +1518,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
const shape = this.svgShapes[clientID];
let text = this.svgTexts[clientID];
if (!text) {
text = this.addText(state);
this.svgTexts[state.clientID] = text;
this.updateTextPosition(
text,
shape,
);
}
if (state.lock) {
return;
}
@ -1354,29 +1535,39 @@ export class CanvasViewImpl implements CanvasView, Listener {
(shape as any).attr('projections', true);
}
let text = this.svgTexts[clientID];
if (!text) {
text = this.addText(state);
this.svgTexts[state.clientID] = text;
}
const hideText = (): void => {
if (text) {
text.addClass('cvat_canvas_hidden');
}
};
const showText = (): void => {
if (text) {
text.removeClass('cvat_canvas_hidden');
this.updateTextPosition(text, shape);
}
};
if (!state.pinned) {
shape.addClass('cvat_canvas_shape_draggable');
(shape as any).draggable().on('dragstart', (): void => {
this.mode = Mode.DRAG;
if (text) {
text.addClass('cvat_canvas_hidden');
}
hideText();
}).on('dragend', (e: CustomEvent): void => {
if (text) {
text.removeClass('cvat_canvas_hidden');
this.updateTextPosition(
text,
shape,
);
}
showText();
this.mode = Mode.IDLE;
const p1 = e.detail.handler.startPoints.point;
const p2 = e.detail.p;
const delta = 1;
const { offset } = this.controller.geometry;
if (Math.sqrt(((p1.x - p2.x) ** 2) + ((p1.y - p2.y) ** 2)) >= delta) {
const points = pointsToArray(
const points = pointsToNumberArray(
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} `
+ `${shape.attr('x') + shape.attr('width')},`
+ `${shape.attr('y') + shape.attr('height')}`,
@ -1399,17 +1590,32 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.selectize(true, shape);
}
const showDirection = (): void => {
if (['polygon', 'polyline'].includes(state.shapeType)) {
this.showDirection(state, shape as SVG.Polygon | SVG.PolyLine);
}
};
const hideDirection = (): void => {
if (['polygon', 'polyline'].includes(state.shapeType)) {
this.hideDirection(shape as SVG.Polygon | SVG.PolyLine);
}
};
showDirection();
let shapeSizeElement: ShapeSizeElement | null = null;
let resized = false;
(shape as any).resize().on('resizestart', (): void => {
(shape as any).resize({
snapToGrid: 0.1,
}).on('resizestart', (): void => {
this.mode = Mode.RESIZE;
resized = false;
hideDirection();
hideText();
if (state.shapeType === 'rectangle') {
shapeSizeElement = displayShapeSize(this.adoptedContent, this.adoptedText);
}
resized = false;
if (text) {
text.addClass('cvat_canvas_hidden');
}
}).on('resizing', (): void => {
resized = true;
if (shapeSizeElement) {
@ -1420,20 +1626,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
shapeSizeElement.rm();
}
if (text) {
text.removeClass('cvat_canvas_hidden');
this.updateTextPosition(
text,
shape,
);
}
showDirection();
showText();
this.mode = Mode.IDLE;
if (resized) {
const { offset } = this.controller.geometry;
const points = pointsToArray(
const points = pointsToNumberArray(
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} `
+ `${shape.attr('x') + shape.attr('width')},`
+ `${shape.attr('y') + shape.attr('height')}`,
@ -1451,6 +1652,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
});
this.updateTextPosition(text, shape);
this.canvas.dispatchEvent(new CustomEvent('canvas.activated', {
bubbles: false,
cancelable: true,
@ -1530,14 +1732,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
private addText(state: any): SVG.Text {
const { undefinedAttrValue } = this.configuration;
const { label, clientID, attributes } = state;
const {
label,
clientID,
attributes,
source,
} = state;
const attrNames = label.attributes.reduce((acc: any, val: any): void => {
acc[val.id] = val.name;
return acc;
}, {});
return this.adoptedText.text((block): void => {
block.tspan(`${label.name} ${clientID}`).style('text-transform', 'uppercase');
block.tspan(`${label.name} ${clientID} (${source})`).style('text-transform', 'uppercase');
for (const attrID of Object.keys(attributes)) {
const value = attributes[attrID] === undefinedAttrValue
? '' : attributes[attrID];
@ -1568,8 +1775,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
rect.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside) {
rect.style('display', 'none');
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
rect.addClass('cvat_canvas_hidden');
}
return rect;
@ -1591,8 +1798,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
polygon.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside) {
polygon.style('display', 'none');
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
polygon.addClass('cvat_canvas_hidden');
}
return polygon;
@ -1614,8 +1821,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
polyline.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside) {
polyline.style('display', 'none');
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
polyline.addClass('cvat_canvas_hidden');
}
return polyline;
@ -1638,8 +1845,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
cube.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside) {
cube.style('display', 'none');
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
cube.addClass('cvat_canvas_hidden');
}
return cube;
@ -1682,8 +1889,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
const group = this.setupPoints(shape, state);
if (state.hidden || state.outside) {
group.style('display', 'none');
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
group.addClass('cvat_canvas_hidden');
}
shape.remove = (): SVG.PolyLine => {
@ -1694,9 +1901,4 @@ export class CanvasViewImpl implements CanvasView, Listener {
return shape;
}
/* eslint-disable-next-line */
private addTag(state: any): void {
console.log(state);
}
}

@ -14,6 +14,8 @@ const MIN_EDGE_LENGTH = 3;
const CUBOID_ACTIVE_EDGE_STROKE_WIDTH = 2.5;
const CUBOID_UNACTIVE_EDGE_STROKE_WIDTH = 1.75;
const UNDEFINED_ATTRIBUTE_VALUE = '__undefined__';
const ARROW_PATH = 'M13.162 6.284L.682.524a.483.483 0 0 0-.574.134.477.477 0 '
+ '0 0-.012.59L4.2 6.72.096 12.192a.479.479 0 0 0 .585.724l12.48-5.76a.48.48 0 0 0 0-.872z';
export default {
BASE_STROKE_WIDTH,
@ -28,4 +30,5 @@ export default {
CUBOID_ACTIVE_EDGE_STROKE_WIDTH,
CUBOID_UNACTIVE_EDGE_STROKE_WIDTH,
UNDEFINED_ATTRIBUTE_VALUE,
ARROW_PATH,
};

@ -11,8 +11,8 @@ import {
translateToSVG,
displayShapeSize,
ShapeSizeElement,
pointsToString,
pointsToArray,
stringifyPoints,
pointsToNumberArray,
BBox,
Box,
} from './shared';
@ -264,12 +264,13 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance.on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType } = this.drawData;
const { shapeType, redraw: clientID } = this.drawData;
this.release();
if (this.canceled) return;
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone({
clientID,
shapeType,
points: [xtl, ytl, xbr, ybr],
}, Date.now() - this.startTimestamp);
@ -298,12 +299,13 @@ export class DrawHandlerImpl implements DrawHandler {
if (numberOfPoints === 4) {
const bbox = (e.target as SVGPolylineElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType } = this.drawData;
const { shapeType, redraw: clientID } = this.drawData;
this.cancel();
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone({
shapeType,
clientID,
points: [xtl, ytl, xbr, ybr],
}, Date.now() - this.startTimestamp);
}
@ -356,6 +358,7 @@ export class DrawHandlerImpl implements DrawHandler {
if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) {
this.drawInstance.draw('point', e);
} else {
this.drawInstance.draw('update', e);
const deltaTreshold = 15;
const delta = Math.sqrt(
((e.clientX - lastDrawnPoint.x) ** 2)
@ -379,8 +382,8 @@ export class DrawHandlerImpl implements DrawHandler {
});
this.drawInstance.on('drawdone', (e: CustomEvent): void => {
const targetPoints = pointsToArray((e.target as SVGElement).getAttribute('points'));
const { shapeType } = this.drawData;
const targetPoints = pointsToNumberArray((e.target as SVGElement).getAttribute('points'));
const { shapeType, redraw: clientID } = this.drawData;
const { points, box } = shapeType === 'cuboid' ? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
this.release();
@ -390,6 +393,7 @@ export class DrawHandlerImpl implements DrawHandler {
&& ((box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD)
&& points.length >= 3 * 2) {
this.onDrawDone({
clientID,
shapeType,
points,
}, Date.now() - this.startTimestamp);
@ -398,12 +402,14 @@ export class DrawHandlerImpl implements DrawHandler {
|| (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD)
&& points.length >= 2 * 2) {
this.onDrawDone({
clientID,
shapeType,
points,
}, Date.now() - this.startTimestamp);
} else if (shapeType === 'points'
&& (e.target as any).getAttribute('points') !== '0,0') {
this.onDrawDone({
clientID,
shapeType,
points,
}, Date.now() - this.startTimestamp);
@ -411,6 +417,7 @@ export class DrawHandlerImpl implements DrawHandler {
} else if (shapeType === 'cuboid'
&& points.length === 4 * 2) {
this.onDrawDone({
clientID,
shapeType,
points: cuboidFrom4Points(points),
}, Date.now() - this.startTimestamp);
@ -426,7 +433,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawPolyshape();
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, false);
this.autoborderHandler.autoborder(true, this.drawInstance, this.drawData.redraw);
}
}
@ -439,7 +446,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawPolyshape();
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, false);
this.autoborderHandler.autoborder(true, this.drawInstance, this.drawData.redraw);
}
}
@ -471,7 +478,7 @@ export class DrawHandlerImpl implements DrawHandler {
if (this.canceled) return;
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
const d = { x: (xbr - xtl) * 0.1, y: (ybr - ytl)*0.1}
const d = { x: (xbr - xtl) * 0.1, y: (ybr - ytl) * 0.1 };
this.onDrawDone({
shapeType,
points: cuboidFrom4Points([xtl, ybr, xbr, ybr, xbr, ytl, xbr + d.x, ytl - d.y]),
@ -673,7 +680,7 @@ export class DrawHandlerImpl implements DrawHandler {
} else {
const points = this.drawData.initialState.points
.map((coord: number): number => coord + offset);
const stringifiedPoints = pointsToString(points);
const stringifiedPoints = stringifyPoints(points);
if (this.drawData.shapeType === 'polygon') {
this.pastePolygon(stringifiedPoints);
@ -760,7 +767,11 @@ export class DrawHandlerImpl implements DrawHandler {
this.autobordersEnabled = configuration.autoborders;
if (this.drawInstance) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, false);
this.autoborderHandler.autoborder(
true,
this.drawInstance,
this.drawData.redraw,
);
} else {
this.autoborderHandler.autoborder(false);
}

@ -6,7 +6,7 @@ import * as SVG from 'svg.js';
import 'svg.select.js';
import consts from './consts';
import { translateFromSVG, pointsToArray } from './shared';
import { translateFromSVG, pointsToNumberArray } from './shared';
import { EditData, Geometry, Configuration } from './canvasModel';
import { AutoborderHandler } from './autoborderHandler';
@ -28,6 +28,38 @@ export class EditHandlerImpl implements EditHandler {
private clones: SVG.Polygon[];
private autobordersEnabled: boolean;
private setupTrailingPoint(circle: SVG.Circle): void {
const head = this.editedShape.attr('points').split(' ').slice(0, this.editData.pointID).join(' ');
circle.on('mouseenter', (): void => {
circle.attr({
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale,
});
});
circle.on('mouseleave', (): void => {
circle.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
});
});
const minimumPoints = 2;
circle.on('mousedown', (e: MouseEvent): void => {
if (e.button !== 0) return;
const { offset } = this.geometry;
const stringifiedPoints = `${head} ${this.editLine.node.getAttribute('points').slice(0, -2)}`;
const points = pointsToNumberArray(stringifiedPoints).slice(0, -2)
.map((coord: number): number => coord - offset);
if (points.length >= minimumPoints * 2) {
const { state } = this.editData;
this.edit({
enabled: false,
});
this.onEditDone(state, points);
}
});
}
private startEdit(): void {
// get started coordinates
const [clientX, clientY] = translateFromSVG(
@ -72,9 +104,18 @@ export class EditHandlerImpl implements EditHandler {
});
this.editLine = (this.canvas as any).polyline();
if (this.editData.state.shapeType === 'polyline') {
(this.editLine as any).on('drawpoint', (e: CustomEvent): void => {
const circle = (e.target as any).instance.remember('_paintHandler').set.last();
if (circle) this.setupTrailingPoint(circle);
});
}
const strokeColor = this.editedShape.attr('stroke');
(this.editLine as any).addClass('cvat_canvas_shape_drawing').style({
'pointer-events': 'none',
'fill-opacity': 0,
'stroke': strokeColor,
}).attr({
'data-origin-client-id': this.editData.state.clientID,
}).on('drawstart drawpoint', (e: CustomEvent): void => {
@ -90,7 +131,7 @@ export class EditHandlerImpl implements EditHandler {
this.setupEditEvents();
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.editLine, true);
this.autoborderHandler.autoborder(true, this.editLine, this.editData.state.clientID);
}
}
@ -110,7 +151,7 @@ export class EditHandlerImpl implements EditHandler {
private selectPolygon(shape: SVG.Polygon): void {
const { offset } = this.geometry;
const points = pointsToArray(shape.attr('points'))
const points = pointsToNumberArray(shape.attr('points'))
.map((coord: number): number => coord - offset);
const { state } = this.editData;
@ -140,24 +181,87 @@ export class EditHandlerImpl implements EditHandler {
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();
if (this.editData.state.shapeType !== 'polygon') {
let points = null;
const { offset } = this.geometry;
if (this.editData.state.shapeType === 'polyline') {
if (start !== this.editData.pointID) {
linePoints.reverse();
}
points = oldPoints.slice(0, start)
.concat(linePoints)
.concat(oldPoints.slice(stop + 1));
} else {
points = oldPoints.concat(linePoints.slice(0, -1));
}
const firstPart = oldPoints.slice(0, start)
.concat(linePoints)
.concat(oldPoints.slice(stop + 1));
points = pointsToNumberArray(points.join(' '))
.map((coord: number): number => coord - offset);
linePoints.reverse();
const secondPart = oldPoints.slice(start + 1, stop)
.concat(linePoints);
const { state } = this.editData;
this.edit({
enabled: false,
});
this.onEditDone(state, points);
if (firstPart.length < 3 || secondPart.length < 3) {
this.cancel();
return;
return;
}
const cutIndexes1 = oldPoints.reduce((acc: string[], _: string, i: number) =>
i >= stop || i <= start ? [...acc, i] : acc, []);
const cutIndexes2 = oldPoints.reduce((acc: string[], _: string, i: number) =>
i <= stop && i >= start ? [...acc, i] : acc, []);
const curveLength = (indexes: number[]) => {
const points = indexes.map((index: number): string => oldPoints[index])
.map((point: string): string[] => point.split(','))
.map((point: string[]): number[] => [+point[0], +point[1]]);
let length = 0;
for (let i = 1; i < points.length; i++) {
length += Math.sqrt(
(points[i][0] - points[i - 1][0]) ** 2
+ (points[i][1] - points[i - 1][1]) ** 2,
);
}
return length;
}
const pointsCriteria = cutIndexes1.length > cutIndexes2.length;
const lengthCriteria = curveLength(cutIndexes1) > curveLength(cutIndexes2);
if (start !== this.editData.pointID) {
linePoints.reverse();
}
const firstPart = oldPoints.slice(0, start)
.concat(linePoints)
.concat(oldPoints.slice(stop + 1));
const secondPart = oldPoints.slice(start, stop)
.concat(linePoints.slice(1).reverse());
if (firstPart.length < 3 || secondPart.length < 3) {
this.cancel();
return;
}
// We do not need these events any more
this.canvas.off('mousedown.edit');
this.canvas.off('mousemove.edit');
(this.editLine as any).draw('stop');
this.editLine.remove();
this.editLine = null;
if (pointsCriteria && lengthCriteria) {
this.clones.push(this.canvas.polygon(firstPart.join(' ')));
this.selectPolygon(this.clones[0]);
// left indexes1 and
} else if (!pointsCriteria && !lengthCriteria) {
this.clones.push(this.canvas.polygon(secondPart.join(' ')));
this.selectPolygon(this.clones[0]);
} else {
for (const points of [firstPart, secondPart]) {
this.clones.push(this.canvas.polygon(points.join(' '))
.attr('fill', this.editedShape.attr('fill'))
@ -173,39 +277,9 @@ export class EditHandlerImpl implements EditHandler {
clone.removeClass('cvat_canvas_shape_splitting');
});
}
// We do not need these events any more
this.canvas.off('mousedown.edit');
this.canvas.off('mousemove.edit');
(this.editLine as any).draw('stop');
this.editLine.remove();
this.editLine = null;
return;
}
let points = null;
const { offset } = this.geometry;
if (this.editData.state.shapeType === 'polyline') {
if (start !== this.editData.pointID) {
linePoints.reverse();
}
points = oldPoints.slice(0, start)
.concat(linePoints)
.concat(oldPoints.slice(stop + 1));
} else {
points = oldPoints.concat(linePoints.slice(0, -1));
}
points = pointsToArray(points.join(' '))
.map((coord: number): number => coord - offset);
const { state } = this.editData;
this.edit({
enabled: false,
});
this.onEditDone(state, points);
return;
}
private setupPoints(enabled: boolean): void {
@ -337,7 +411,11 @@ export class EditHandlerImpl implements EditHandler {
this.autobordersEnabled = configuration.autoborders;
if (this.editLine) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.editLine, true);
this.autoborderHandler.autoborder(
true,
this.editLine,
this.editData.state.clientID,
);
} else {
this.autoborderHandler.autoborder(false);
}

@ -95,7 +95,9 @@ export class GroupHandlerImpl implements GroupHandler {
this.selectionRect = null;
const box = this.getSelectionBox(event);
const shapes = (this.canvas.select('.cvat_canvas_shape') as any).members;
const shapes = (this.canvas.select('.cvat_canvas_shape') as any).members.filter(
(shape: SVG.Shape): boolean => !shape.hasClass('cvat_canvas_hidden'),
);
for (const shape of shapes) {
// TODO: Doesn't work properly for groups
const bbox = shape.bbox();

@ -29,6 +29,12 @@ interface Point {
x: number;
y: number;
}
interface Vector2D {
i: number;
j: number;
}
export interface DrawnState {
clientID: number;
outside?: boolean;
@ -42,6 +48,7 @@ export interface DrawnState {
pinned?: boolean;
updated: number;
frame: number;
label: any;
}
// Translate point array from the canvas coordinate system
@ -76,21 +83,6 @@ export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
return output;
}
export function pointsToString(points: number[]): string {
return points.reduce((acc, val, idx): string => {
if (idx % 2) {
return `${acc},${val}`;
}
return `${acc} ${val}`.trim();
}, '');
}
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,
@ -120,25 +112,59 @@ export function displayShapeSize(
return shapeSize;
}
export function convertToArray(points: Point[]): number[][] {
const arr: number[][] = [];
points.forEach((point: Point): void => {
arr.push([point.x, point.y]);
});
return arr;
export function pointsToNumberArray(points: string | Point[]): number[] {
if (Array.isArray(points)) {
return points.reduce((acc: number[], point: Point): number[] => {
acc.push(point.x, point.y);
return acc;
}, []);
}
return points.trim().split(/[,\s]+/g)
.map((coord: string): number => +coord);
}
export function parsePoints(stringified: string): Point[] {
return stringified.trim().split(/\s/).map((point: string): Point => {
export function parsePoints(source: string | number[]): Point[] {
if (Array.isArray(source)) {
return source.reduce((acc: Point[], _: number, index: number): Point[] => {
if (index % 2) {
acc.push({
x: source[index - 1],
y: source[index],
});
}
return acc;
}, []);
}
return source.trim().split(/\s/).map((point: string): Point => {
const [x, y] = point.split(',').map((coord: string): number => +coord);
return { x, y };
});
}
export function stringifyPoints(points: Point[]): string {
export function stringifyPoints(points: (Point | number)[]): string {
if (typeof (points[0]) === 'number') {
return points.reduce((acc: string, val: number, idx: number): string => {
if (idx % 2) {
return `${acc},${val}`;
}
return `${acc} ${val}`.trim();
}, '');
}
return points.map((point: Point): string => `${point.x},${point.y}`).join(' ');
}
export function clamp(x: number, min: number, max: number): number {
return Math.min(Math.max(x, min), max);
}
export function scalarProduct(a: Vector2D, b: Vector2D): number {
return a.i * b.i + a.j * b.j;
}
export function vectorLength(vector: Vector2D): number {
return Math.sqrt((vector.i ** 2) + (vector.j ** 2));
}

@ -17,7 +17,7 @@ import {
Orientation,
Edge,
} from './cuboid';
import { parsePoints, stringifyPoints, clamp } from './shared';
import { parsePoints, clamp } from './shared';
// Update constructor
const originalDraw = SVG.Element.prototype.draw;
@ -174,7 +174,8 @@ SVG.Element.prototype.resize = function constructor(...args: any): any {
originalResize.call(this, ...args);
handler = this.remember('_resizeHandler');
handler.resize = function(e: any) {
if (e.detail.event.button === 0) {
const { event } = e.detail;
if (event.button === 0 && !event.shiftKey && !event.ctrlKey) {
return handler.constructor.prototype.resize.call(this, e);
}
}

@ -60,10 +60,12 @@ const webConfig = {
target: 'web',
mode: 'production',
devtool: 'source-map',
entry: './src/typescript/canvas.ts',
entry: {
'cvat-canvas': './src/typescript/canvas.ts',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cvat-canvas.js',
filename: '[name].[contenthash].js',
library: 'canvas',
libraryTarget: 'window',
},

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

@ -1,7 +1,7 @@
# Module CVAT-CORE
## Description
This CVAT module is a clien-side JavaScipt library to management of objects, frames, logs, etc.
This CVAT module is a client-side JavaScipt library to management of objects, frames, logs, etc.
It contains the core logic of the Computer Vision Annotation Tool.
## Versioning

Binary file not shown.

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "2.0.1",
"version": "3.5.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {
@ -27,7 +27,7 @@
"eslint-plugin-security": "^1.4.0",
"jest": "^24.8.0",
"jest-junit": "^6.4.0",
"jsdoc": "^3.6.2",
"jsdoc": "^3.6.4",
"webpack": "^4.31.0",
"webpack-cli": "^3.3.2"
},

@ -15,6 +15,7 @@
name: initialData.name,
format: initialData.ext,
version: initialData.version,
enabled: initialData.enabled,
};
Object.defineProperties(this, {
@ -48,6 +49,16 @@
*/
get: () => data.version,
},
enabled: {
/**
* @name enabled
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.enabled,
},
});
}
}
@ -63,6 +74,7 @@
name: initialData.name,
format: initialData.ext,
version: initialData.version,
enabled: initialData.enabled,
};
Object.defineProperties(this, {
@ -96,6 +108,16 @@
*/
get: () => data.version,
},
enabled: {
/**
* @name enabled
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.enabled,
},
});
}
}

@ -402,6 +402,7 @@
frame: Math.min.apply(null, Object.keys(keyframes).map((frame) => +frame)),
shapes: Object.values(keyframes),
group: 0,
source: objectStates[0].source,
label_id: label.id,
attributes: Object.keys(objectStates[0].attributes)
.reduce((accumulator, attrID) => {
@ -763,6 +764,7 @@
points: [...state.points],
type: state.shapeType,
z_order: state.zOrder,
source: state.source,
});
} else if (state.objectType === 'track') {
constructed.tracks.push({
@ -770,6 +772,7 @@
.filter((attr) => !labelAttributes[attr.spec_id].mutable),
frame: state.frame,
group: 0,
source: state.source,
label_id: state.label.id,
shapes: [{
attributes: attributes
@ -797,15 +800,17 @@
.concat(imported.tracks)
.concat(imported.shapes);
this.history.do(HistoryActions.CREATED_OBJECTS, () => {
importedArray.forEach((object) => {
object.removed = true;
});
}, () => {
importedArray.forEach((object) => {
object.removed = false;
});
}, importedArray.map((object) => object.clientID), objectStates[0].frame);
if (objectStates.length) {
this.history.do(HistoryActions.CREATED_OBJECTS, () => {
importedArray.forEach((object) => {
object.removed = true;
});
}, () => {
importedArray.forEach((object) => {
object.removed = false;
});
}, importedArray.map((object) => object.clientID), objectStates[0].frame);
}
return importedArray.map((value) => value.clientID);
}

@ -15,6 +15,7 @@
} = require('./common');
const {
colors,
Source,
ObjectShape,
ObjectType,
AttributeType,
@ -156,8 +157,7 @@
if (type === AttributeType.NUMBER) {
return +value >= +values[0]
&& +value <= +values[1]
&& !((+value - +values[0]) % +values[2]);
&& +value <= +values[1];
}
if (type === AttributeType.CHECKBOX) {
@ -184,6 +184,7 @@
this.removed = false;
this.lock = false;
this.color = color;
this.source = data.source;
this.updated = Date.now();
this.attributes = data.attributes.reduce((attributeAccumulator, attr) => {
attributeAccumulator[attr.spec_id] = attr.value;
@ -333,7 +334,9 @@
const { width, height } = this.frameMeta[frame];
fittedPoints = fitPoints(this.shapeType, data.points, width, height);
if ((!checkShapeArea(this.shapeType, fittedPoints)) || checkOutside(fittedPoints, width, height)) {
if ((!checkShapeArea(this.shapeType, fittedPoints))
|| checkOutside(fittedPoints, width, height)
) {
fittedPoints = [];
}
}
@ -491,6 +494,7 @@
frame: this.frame,
label_id: this.label.id,
group: this.group,
source: this.source,
};
}
@ -519,51 +523,67 @@
updated: this.updated,
pinned: this.pinned,
frame,
source: this.source,
};
}
_savePoints(points, frame) {
const undoPoints = this.points;
const redoPoints = points;
const undoSource = this.source;
const redoSource = Source.MANUAL;
this.history.do(HistoryActions.CHANGED_POINTS, () => {
this.points = undoPoints;
this.source = undoSource;
this.updated = Date.now();
}, () => {
this.points = redoPoints;
this.source = redoSource;
this.updated = Date.now();
}, [this.clientID], frame);
this.source = Source.MANUAL;
this.points = points;
}
_saveOccluded(occluded, frame) {
const undoOccluded = this.occluded;
const redoOccluded = occluded;
const undoSource = this.source;
const redoSource = Source.MANUAL;
this.history.do(HistoryActions.CHANGED_OCCLUDED, () => {
this.occluded = undoOccluded;
this.source = undoSource;
this.updated = Date.now();
}, () => {
this.occluded = redoOccluded;
this.source = redoSource;
this.updated = Date.now();
}, [this.clientID], frame);
this.source = Source.MANUAL;
this.occluded = occluded;
}
_saveZOrder(zOrder, frame) {
const undoZOrder = this.zOrder;
const redoZOrder = zOrder;
const undoSource = this.source;
const redoSource = Source.MANUAL;
this.history.do(HistoryActions.CHANGED_ZORDER, () => {
this.zOrder = undoZOrder;
this.source = undoSource;
this.updated = Date.now();
}, () => {
this.zOrder = redoZOrder;
this.source = redoSource;
this.updated = Date.now();
}, [this.clientID], frame);
this.source = Source.MANUAL;
this.zOrder = zOrder;
}
@ -658,6 +678,7 @@
frame: this.frame,
label_id: this.label.id,
group: this.group,
source: this.source,
attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => {
if (!labelAttributes[attrId].mutable) {
attributeAccumulator.push({
@ -725,6 +746,7 @@
last,
},
frame,
source: this.source,
};
}
@ -910,13 +932,14 @@
}, [this.clientID], frame);
}
_appendShapeActionToHistory(actionType, frame, undoShape, redoShape) {
_appendShapeActionToHistory(actionType, frame, undoShape, redoShape, undoSource, redoSource) {
this.history.do(actionType, () => {
if (!undoShape) {
delete this.shapes[frame];
} else {
this.shapes[frame] = undoShape;
}
this.source = undoSource;
this.updated = Date.now();
}, () => {
if (!redoShape) {
@ -924,6 +947,7 @@
} else {
this.shapes[frame] = redoShape;
}
this.source = redoSource;
this.updated = Date.now();
}, [this.clientID], frame);
}
@ -931,6 +955,8 @@
_savePoints(points, frame) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoSource = this.source;
const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], points } : {
frame,
@ -942,17 +968,22 @@
};
this.shapes[frame] = redoShape;
this.source = Source.MANUAL;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_POINTS,
frame,
undoShape,
redoShape,
undoSource,
redoSource,
);
}
_saveOutside(frame, outside) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoSource = this.source;
const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], outside } : {
frame,
@ -964,17 +995,22 @@
};
this.shapes[frame] = redoShape;
this.source = Source.MANUAL;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_OUTSIDE,
frame,
undoShape,
redoShape,
undoSource,
redoSource,
);
}
_saveOccluded(occluded, frame) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoSource = this.source;
const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], occluded } : {
frame,
@ -986,17 +1022,22 @@
};
this.shapes[frame] = redoShape;
this.source = Source.MANUAL;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_OCCLUDED,
frame,
undoShape,
redoShape,
undoSource,
redoSource,
);
}
_saveZOrder(zOrder, frame) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoSource = this.source;
const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], zOrder } : {
frame,
@ -1008,11 +1049,14 @@
};
this.shapes[frame] = redoShape;
this.source = Source.MANUAL;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_ZORDER,
frame,
undoShape,
redoShape,
undoSource,
redoSource,
);
}
@ -1025,6 +1069,8 @@
return;
}
const undoSource = this.source;
const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = keyframe ? {
frame,
@ -1033,8 +1079,10 @@
outside: current.outside,
occluded: current.occluded,
attributes: {},
source: current.source,
} : undefined;
this.source = Source.MANUAL;
if (redoShape) {
this.shapes[frame] = redoShape;
} else {
@ -1046,6 +1094,8 @@
frame,
undoShape,
redoShape,
undoSource,
redoSource,
);
}
@ -1163,6 +1213,7 @@
frame: this.frame,
label_id: this.label.id,
group: this.group,
source: this.source,
attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => {
attributeAccumulator.push({
spec_id: attrId,
@ -1193,6 +1244,7 @@
color: this.color,
updated: this.updated,
frame,
source: this.source,
};
}
@ -1534,13 +1586,12 @@
}
interpolatePosition(leftPosition, rightPosition, offset) {
const positionOffset = leftPosition.points.map((point, index) => (
rightPosition.points[index] - point
))
));
return {
points: leftPosition.points.map((point ,index) => (
points: leftPosition.points.map((point, index) => (
point + positionOffset[index] * offset
)),
occluded: leftPosition.occluded,
@ -1556,385 +1607,274 @@
}
interpolatePosition(leftPosition, rightPosition, offset) {
function findBox(points) {
let xmin = Number.MAX_SAFE_INTEGER;
let ymin = Number.MAX_SAFE_INTEGER;
let xmax = Number.MIN_SAFE_INTEGER;
let ymax = Number.MIN_SAFE_INTEGER;
for (let i = 0; i < points.length; i += 2) {
if (points[i] < xmin) xmin = points[i];
if (points[i + 1] < ymin) ymin = points[i + 1];
if (points[i] > xmax) xmax = points[i];
if (points[i + 1] > ymax) ymax = points[i + 1];
}
if (offset === 0) {
return {
xmin,
ymin,
xmax,
ymax,
points: [...leftPosition.points],
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
}
function normalize(points, box) {
const normalized = [];
const width = box.xmax - box.xmin;
const height = box.ymax - box.ymin;
function toArray(points) {
return points.reduce((acc, val) => {
acc.push(val.x, val.y);
return acc;
}, []);
}
for (let i = 0; i < points.length; i += 2) {
normalized.push(
(points[i] - box.xmin) / width,
(points[i + 1] - box.ymin) / height,
);
}
function toPoints(array) {
return array.reduce((acc, _, index) => {
if (index % 2) {
acc.push({
x: array[index - 1],
y: array[index],
});
}
return acc;
}, []);
}
return normalized;
function curveLength(points) {
return points.slice(1).reduce((acc, _, index) => {
const dx = points[index + 1].x - points[index].x;
const dy = points[index + 1].y - points[index].y;
return acc + Math.sqrt(dx ** 2 + dy ** 2);
}, 0);
}
function denormalize(points, box) {
const denormalized = [];
const width = box.xmax - box.xmin;
const height = box.ymax - box.ymin;
function curveToOffsetVec(points, length) {
const offsetVector = [0]; // with initial value
let accumulatedLength = 0;
for (let i = 0; i < points.length; i += 2) {
denormalized.push(
points[i] * width + box.xmin,
points[i + 1] * height + box.ymin,
);
}
points.slice(1).forEach((_, index) => {
const dx = points[index + 1].x - points[index].x;
const dy = points[index + 1].y - points[index].y;
accumulatedLength += Math.sqrt(dx ** 2 + dy ** 2);
offsetVector.push(accumulatedLength / length);
});
return denormalized;
return offsetVector;
}
function toPoints(array) {
const points = [];
for (let i = 0; i < array.length; i += 2) {
points.push({
x: array[i],
y: array[i + 1],
});
function findNearestPair(value, curve) {
let minimum = [0, Math.abs(value - curve[0])];
for (let i = 1; i < curve.length; i++) {
const distance = Math.abs(value - curve[i]);
if (distance < minimum[1]) {
minimum = [i, distance];
}
}
return points;
return minimum[0];
}
function toArray(points) {
const array = [];
for (const point of points) {
array.push(point.x, point.y);
function matchLeftRight(leftCurve, rightCurve) {
const matching = {};
for (let i = 0; i < leftCurve.length; i++) {
matching[i] = [findNearestPair(leftCurve[i], rightCurve)];
}
return array;
return matching;
}
function computeDistances(source, target) {
const distances = {};
for (let i = 0; i < source.length; i++) {
distances[i] = distances[i] || {};
for (let j = 0; j < target.length; j++) {
const dx = source[i].x - target[j].x;
const dy = source[i].y - target[j].y;
function matchRightLeft(leftCurve, rightCurve, leftRightMatching) {
const matchedRightPoints = Object.values(leftRightMatching).flat();
const unmatchedRightPoints = rightCurve.map((_, index) => index)
.filter((index) => !matchedRightPoints.includes(index));
const updatedMatching = { ...leftRightMatching };
distances[i][j] = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
}
for (const rightPoint of unmatchedRightPoints) {
const leftPoint = findNearestPair(rightCurve[rightPoint], leftCurve);
updatedMatching[leftPoint].push(rightPoint);
}
return distances;
for (const key of Object.keys(updatedMatching)) {
const sortedRightIndexes = updatedMatching[key]
.sort((a, b) => a - b);
updatedMatching[key] = sortedRightIndexes;
}
return updatedMatching;
}
function truncateByThreshold(mapping, threshold) {
for (const key of Object.keys(mapping)) {
if (mapping[key].distance > threshold) {
delete mapping[key];
function reduceInterpolation(interpolatedPoints, matching, leftPoints, rightPoints) {
function averagePoint(points) {
let sumX = 0;
let sumY = 0;
for (const point of points) {
sumX += point.x;
sumY += point.y;
}
}
}
// https://en.wikipedia.org/wiki/Stable_marriage_problem
// TODO: One of important part of the algorithm is to correctly match
// "corner" points. Thus it is possible for each of such point calculate
// a descriptor (d) and use (x, y, d) to calculate the distance. One more
// idea is to be sure that order or matched points is preserved. For example,
// if p1 matches q1 and p2 matches q2 and between p1 and p2 we don't have any
// points thus we should not have points between q1 and q2 as well.
function stableMarriageProblem(men, women, distances) {
const menPreferences = {};
for (const man of men) {
menPreferences[man] = women.concat()
.sort((w1, w2) => distances[man][w1] - distances[man][w2]);
return {
x: sumX / points.length,
y: sumY / points.length,
};
}
// Start alghoritm with max N^2 complexity
const womenMaybe = {}; // id woman:id man,distance
const menBusy = {}; // id man:boolean
let prefIndex = 0;
// While there is at least one free man
while (Object.values(menBusy).length !== men.length) {
// Every man makes offer to the best woman
for (const man of men) {
// The man have already found a woman
if (menBusy[man]) {
continue;
}
function computeDistance(point1, point2) {
return Math.sqrt(
((point1.x - point2.x) ** 2) + ((point1.y - point2.y) ** 2),
);
}
const woman = menPreferences[man][prefIndex];
const distance = distances[man][woman];
function minimizeSegment(baseLength, N, startInterpolated, stopInterpolated) {
const threshold = baseLength / (2 * N);
const minimized = [interpolatedPoints[startInterpolated]];
let latestPushed = startInterpolated;
for (let i = startInterpolated + 1; i < stopInterpolated; i++) {
const distance = computeDistance(
interpolatedPoints[latestPushed], interpolatedPoints[i],
);
// A women chooses the best offer and says "maybe"
if (woman in womenMaybe && womenMaybe[woman].distance > distance) {
// A woman got better offer
const prevChoice = womenMaybe[woman].value;
delete womenMaybe[woman];
delete menBusy[prevChoice];
if (distance >= threshold) {
minimized.push(interpolatedPoints[i]);
latestPushed = i;
}
}
if (!(woman in womenMaybe)) {
womenMaybe[woman] = {
value: man,
distance,
};
minimized.push(interpolatedPoints[stopInterpolated]);
if (minimized.length === 2) {
const distance = computeDistance(
interpolatedPoints[startInterpolated],
interpolatedPoints[stopInterpolated],
);
menBusy[man] = true;
if (distance < threshold) {
return [averagePoint(minimized)];
}
}
prefIndex++;
return minimized;
}
const result = {};
for (const woman of Object.keys(womenMaybe)) {
result[womenMaybe[woman].value] = {
value: woman,
distance: womenMaybe[woman].distance,
};
const reduced = [];
const interpolatedIndexes = {};
let accumulated = 0;
for (let i = 0; i < leftPoints.length; i++) {
// eslint-disable-next-line
interpolatedIndexes[i] = matching[i].map(() => accumulated++);
}
return result;
}
function leftSegment(start, stop) {
const startInterpolated = interpolatedIndexes[start][0];
const stopInterpolated = interpolatedIndexes[stop][0];
function getMapping(source, target) {
function sumEdges(points) {
let result = 0;
for (let i = 1; i < points.length; i += 2) {
const distance = Math.sqrt(Math.pow(points[i].x - points[i - 1].x, 2)
+ Math.pow(points[i].y - points[i - 1].y, 2));
result += distance;
if (startInterpolated === stopInterpolated) {
reduced.push(interpolatedPoints[startInterpolated]);
return;
}
// Corner case when work with one point
// Mapping in this case can't be wrong
if (!result) {
return Number.MAX_SAFE_INTEGER;
}
const baseLength = curveLength(leftPoints.slice(start, stop + 1));
const N = stop - start + 1;
return result;
reduced.push(
...minimizeSegment(baseLength, N, startInterpolated, stopInterpolated),
);
}
function computeDeviation(points, average) {
let result = 0;
for (let i = 1; i < points.length; i += 2) {
const distance = Math.sqrt(Math.pow(points[i].x - points[i - 1].x, 2)
+ Math.pow(points[i].y - points[i - 1].y, 2));
result += Math.pow(distance - average, 2);
}
return result;
}
function rightSegment(leftPoint) {
const start = matching[leftPoint][0];
const [stop] = matching[leftPoint].slice(-1);
const startInterpolated = interpolatedIndexes[leftPoint][0];
const [stopInterpolated] = interpolatedIndexes[leftPoint].slice(-1);
const baseLength = curveLength(rightPoints.slice(start, stop + 1));
const N = stop - start + 1;
const processedSource = [];
const processedTarget = [];
const distances = computeDistances(source, target);
const mapping = stableMarriageProblem(Array.from(source.keys()),
Array.from(target.keys()), distances);
const average = (sumEdges(target)
+ sumEdges(source)) / (target.length + source.length);
const meanSquareDeviation = Math.sqrt((computeDeviation(source, average)
+ computeDeviation(target, average)) / (source.length + target.length));
const threshold = average + 3 * meanSquareDeviation; // 3 sigma rule
truncateByThreshold(mapping, threshold);
for (const key of Object.keys(mapping)) {
mapping[key] = mapping[key].value;
reduced.push(
...minimizeSegment(baseLength, N, startInterpolated, stopInterpolated),
);
}
// const receivingOrder = Object.keys(mapping).map(x => +x).sort((a,b) => a - b);
const receivingOrder = this.appendMapping(mapping, source, target);
let previousOpened = null;
for (let i = 0; i < leftPoints.length; i++) {
if (matching[i].length === 1) {
// check if left segment is opened
if (previousOpened !== null) {
// check if we should continue the left segment
if (matching[i][0] === matching[previousOpened][0]) {
continue;
} else {
// left segment found
const start = previousOpened;
const stop = i - 1;
leftSegment(start, stop);
// start next left segment
previousOpened = i;
}
} else {
// start next left segment
previousOpened = i;
}
} else {
// check if left segment is opened
if (previousOpened !== null) {
// left segment found
const start = previousOpened;
const stop = i - 1;
leftSegment(start, stop);
previousOpened = null;
}
for (const pointIdx of receivingOrder) {
processedSource.push(source[pointIdx]);
processedTarget.push(target[mapping[pointIdx]]);
// right segment found
rightSegment(i);
}
}
return [processedSource, processedTarget];
}
// check if there is an opened segment
if (previousOpened !== null) {
leftSegment(previousOpened, leftPoints.length - 1);
}
if (offset === 0) {
return {
points: [...leftPosition.points],
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
return reduced;
}
let leftBox = findBox(leftPosition.points);
let rightBox = findBox(rightPosition.points);
// Sometimes (if shape has one point or shape is line),
// We can get box with zero area
// Next computation will be with NaN in this case
// We have to prevent it
const delta = 1;
if (leftBox.xmax - leftBox.xmin < delta || rightBox.ymax - rightBox.ymin < delta) {
leftBox = {
xmin: 0,
xmax: 1024, // TODO: Get actual image size
ymin: 0,
ymax: 768,
};
rightBox = leftBox;
}
// the algorithm below is based on fact that both left and right
// polyshapes have the same start point and the same draw direction
const leftPoints = toPoints(leftPosition.points);
const rightPoints = toPoints(rightPosition.points);
const leftOffsetVec = curveToOffsetVec(leftPoints, curveLength(leftPoints));
const rightOffsetVec = curveToOffsetVec(rightPoints, curveLength(rightPoints));
const leftPoints = toPoints(normalize(leftPosition.points, leftBox));
const rightPoints = toPoints(normalize(rightPosition.points, rightBox));
const matching = matchLeftRight(leftOffsetVec, rightOffsetVec);
const completedMatching = matchRightLeft(
leftOffsetVec, rightOffsetVec, matching,
);
let newLeftPoints = [];
let newRightPoints = [];
if (leftPoints.length > rightPoints.length) {
const [
processedRight,
processedLeft,
] = getMapping.call(this, rightPoints, leftPoints);
newLeftPoints = processedLeft;
newRightPoints = processedRight;
} else {
const [
processedLeft,
processedRight,
] = getMapping.call(this, leftPoints, rightPoints);
newLeftPoints = processedLeft;
newRightPoints = processedRight;
}
const interpolatedPoints = Object.keys(completedMatching)
.map((leftPointIdx) => +leftPointIdx).sort((a, b) => a - b)
.reduce((acc, leftPointIdx) => {
const leftPoint = leftPoints[leftPointIdx];
for (const rightPointIdx of completedMatching[leftPointIdx]) {
const rightPoint = rightPoints[rightPointIdx];
acc.push({
x: leftPoint.x + (rightPoint.x - leftPoint.x) * offset,
y: leftPoint.y + (rightPoint.y - leftPoint.y) * offset,
});
}
const absoluteLeftPoints = denormalize(toArray(newLeftPoints), leftBox);
const absoluteRightPoints = denormalize(toArray(newRightPoints), rightBox);
return acc;
}, []);
const interpolation = [];
for (let i = 0; i < absoluteLeftPoints.length; i++) {
interpolation.push(absoluteLeftPoints[i] + (
absoluteRightPoints[i] - absoluteLeftPoints[i]) * offset);
}
const reducedPoints = reduceInterpolation(
interpolatedPoints,
completedMatching,
leftPoints,
rightPoints,
);
return {
points: interpolation,
points: toArray(reducedPoints),
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
}
// mapping is predicted order of points sourse_idx:target_idx
// some points from source and target can absent in mapping
// source, target - arrays of points. Target array size >= sourse array size
appendMapping(mapping, source, target) {
const targetMatched = Object.values(mapping).map((x) => +x);
const sourceMatched = Object.keys(mapping).map((x) => +x);
const orderForReceive = [];
function findNeighbors(point) {
let prev = point;
let next = point;
if (!targetMatched.length) {
// Prevent infinity loop
throw new ScriptingError('Interpolation mapping is empty');
}
while (!targetMatched.includes(prev)) {
prev--;
if (prev < 0) {
prev = target.length - 1;
}
}
while (!targetMatched.includes(next)) {
next++;
if (next >= target.length) {
next = 0;
}
}
return [prev, next];
}
function computeOffset(point, prev, next) {
const pathPoints = [];
while (prev !== next) {
pathPoints.push(target[prev]);
prev++;
if (prev >= target.length) {
prev = 0;
}
}
pathPoints.push(target[next]);
let curveLength = 0;
let offset = 0;
let iCrossed = false;
for (let k = 1; k < pathPoints.length; k++) {
const p1 = pathPoints[k];
const p2 = pathPoints[k - 1];
const distance = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
if (!iCrossed) {
offset += distance;
}
curveLength += distance;
if (target[point] === pathPoints[k]) {
iCrossed = true;
}
}
if (!curveLength) {
return 0;
}
return offset / curveLength;
}
for (let i = 0; i < target.length; i++) {
const index = targetMatched.indexOf(i);
if (index === -1) {
// We have to find a neighbours which have been mapped
const [prev, next] = findNeighbors(i);
// Now compute edge offset
const offset = computeOffset(i, prev, next);
// Get point between two neighbors points
const prevPoint = target[prev];
const nextPoint = target[next];
const autoPoint = {
x: prevPoint.x + (nextPoint.x - prevPoint.x) * offset,
y: prevPoint.y + (nextPoint.y - prevPoint.y) * offset,
};
// Put it into matched
source.push(autoPoint);
mapping[source.length - 1] = i;
orderForReceive.push(source.length - 1);
} else {
orderForReceive.push(sourceMatched[index]);
}
}
return orderForReceive;
}
}
class PolygonTrack extends PolyTrack {
@ -1945,6 +1885,26 @@
checkNumberOfPoints(this.shapeType, shape.points);
}
}
interpolatePosition(leftPosition, rightPosition, offset) {
const copyLeft = {
...leftPosition,
points: [...leftPosition.points, leftPosition.points[0], leftPosition.points[1]],
};
const copyRight = {
...rightPosition,
points: [...rightPosition.points, rightPosition.points[0], rightPosition.points[1]],
};
const result = PolyTrack.prototype.interpolatePosition
.call(this, copyLeft, copyRight, offset);
return {
...result,
points: result.points.slice(0, -2),
};
}
}
class PolylineTrack extends PolyTrack {
@ -1965,6 +1925,27 @@
checkNumberOfPoints(this.shapeType, shape.points);
}
}
interpolatePosition(leftPosition, rightPosition, offset) {
// interpolate only when one point in both left and right positions
if (leftPosition.points.length === 2 && rightPosition.points.length === 2) {
return {
points: leftPosition.points.map(
(value, index) => value + (rightPosition.points[index] - value) * offset,
),
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
}
return {
points: [...leftPosition.points],
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
}
}
class CuboidTrack extends Track {
@ -1978,13 +1959,12 @@
}
interpolatePosition(leftPosition, rightPosition, offset) {
const positionOffset = leftPosition.points.map((point, index) => (
rightPosition.points[index] - point
))
));
return {
points: leftPosition.points.map((point ,index) => (
points: leftPosition.points.map((point, index) => (
point + positionOffset[index] * offset
)),
occluded: leftPosition.occluded,

@ -104,7 +104,7 @@
const keys = ['id', 'label_id', 'group', 'frame',
'occluded', 'z_order', 'points', 'type', 'shapes',
'attributes', 'value', 'spec_id', 'outside'];
'attributes', 'value', 'spec_id', 'source', 'outside'];
// Find created and updated objects
for (const type of Object.keys(exported)) {

@ -77,6 +77,15 @@
}
}
async function closeSession(session) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
cache.delete(session);
}
}
async function getAnnotations(session, frame, allTracks, filters) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
@ -365,5 +374,6 @@
redoActions,
clearActions,
getActions,
closeSession,
};
})();

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
@ -12,6 +12,7 @@
(() => {
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const lambdaManager = require('./lambda-manager');
const {
isBoolean,
isInteger,
@ -20,10 +21,7 @@
checkFilter,
} = require('./common');
const {
TaskStatus,
TaskMode,
} = require('./enums');
const { TaskStatus, TaskMode } = require('./enums');
const User = require('./user');
const { AnnotationFormats } = require('./annotation-formats.js');
@ -54,6 +52,13 @@
cvat.plugins.list.implementation = PluginRegistry.list;
cvat.plugins.register.implementation = PluginRegistry.register.bind(cvat);
cvat.lambda.list.implementation = lambdaManager.list.bind(lambdaManager);
cvat.lambda.run.implementation = lambdaManager.run.bind(lambdaManager);
cvat.lambda.call.implementation = lambdaManager.call.bind(lambdaManager);
cvat.lambda.cancel.implementation = lambdaManager.cancel.bind(lambdaManager);
cvat.lambda.listen.implementation = lambdaManager.listen.bind(lambdaManager);
cvat.lambda.requests.implementation = lambdaManager.requests.bind(lambdaManager);
cvat.server.about.implementation = async () => {
const result = await serverProxy.server.about();
return result;
@ -76,8 +81,10 @@
cvat.server.register.implementation = async (username, firstName, lastName,
email, password1, password2, userConfirmations) => {
await serverProxy.server.register(username, firstName, lastName, email,
password1, password2, userConfirmations);
const user = await serverProxy.server.register(username, firstName,
lastName, email, password1, password2, userConfirmations);
return new User(user);
};
cvat.server.login.implementation = async (username, password) => {
@ -88,6 +95,10 @@
await serverProxy.server.logout();
};
cvat.server.changePassword.implementation = async (oldPassword, newPassword1, newPassword2) => {
await serverProxy.server.changePassword(oldPassword, newPassword1, newPassword2);
};
cvat.server.authorized.implementation = async () => {
const result = await serverProxy.server.authorized();
return result;

@ -20,6 +20,7 @@ function build() {
const Statistics = require('./statistics');
const { Job, Task } = require('./session');
const { Attribute, Label } = require('./labels');
const MLModel = require('./ml-model');
const {
ShareFileType,
@ -30,7 +31,9 @@ function build() {
ObjectShape,
LogType,
HistoryActions,
RQStatus,
colors,
Source,
} = require('./enums');
const {
@ -127,10 +130,10 @@ function build() {
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async userAgreements() {
const result = await PluginRegistry
.apiWrapper(cvat.server.userAgreements);
return result;
async userAgreements() {
const result = await PluginRegistry
.apiWrapper(cvat.server.userAgreements);
return result;
},
/**
@ -145,10 +148,19 @@ function build() {
* @param {string} password1 A password for the new account
* @param {string} password2 The confirmation password for the new account
* @param {Object} userConfirmations An user confirmations of terms of use if needed
* @returns {Object} response data
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async register(username, firstName, lastName, email, password1, password2, userConfirmations) {
async register(
username,
firstName,
lastName,
email,
password1,
password2,
userConfirmations,
) {
const result = await PluginRegistry
.apiWrapper(cvat.server.register, username, firstName,
lastName, email, password1, password2, userConfirmations);
@ -182,6 +194,19 @@ function build() {
.apiWrapper(cvat.server.logout);
return result;
},
/**
* Method allows to change user password
* @method changePassword
* @async
* @memberof module:API.cvat.server
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async changePassword(oldPassword, newPassword1, newPassword2) {
const result = await PluginRegistry
.apiWrapper(cvat.server.changePassword, oldPassword, newPassword1, newPassword2);
return result;
},
/**
* Method allows to know whether you are authorized on the server
* @method authorized
@ -423,6 +448,119 @@ function build() {
return result;
},
},
/**
* Namespace is used for serverless functions management (mainly related with DL models)
* @namespace lambda
* @memberof module:API.cvat
*/
lambda: {
/**
* Method returns list of available serverless models
* @method list
* @async
* @memberof module:API.cvat.lambda
* @returns {module:API.cvat.classes.MLModel[]}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async list() {
const result = await PluginRegistry
.apiWrapper(cvat.lambda.list);
return result;
},
/**
* Run long-time request for a function on a specific task
* @method run
* @async
* @memberof module:API.cvat.lambda
* @param {module:API.cvat.classes.Task} task task to be annotated
* @param {module:API.cvat.classes.MLModel} model model used to get annotation
* @param {object} [args] extra arguments
* @returns {string} requestID
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async run(task, model, args) {
const result = await PluginRegistry
.apiWrapper(cvat.lambda.run, task, model, args);
return result;
},
/**
* Run short-time request for a function on a specific task
* @method call
* @async
* @memberof module:API.cvat.lambda
* @param {module:API.cvat.classes.Task} task task to be annotated
* @param {module:API.cvat.classes.MLModel} model model used to get annotation
* @param {object} [args] extra arguments
* @returns {string} requestID
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async call(task, model, args) {
const result = await PluginRegistry
.apiWrapper(cvat.lambda.call, task, model, args);
return result;
},
/**
* Cancel running of a serverless function for a specific task
* @method cancel
* @async
* @memberof module:API.cvat.lambda
* @param {string} requestID
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async cancel(requestID) {
const result = await PluginRegistry
.apiWrapper(cvat.lambda.cancel, requestID);
return result;
},
/**
* @callback onRequestStatusChange
* @param {string} status
* @param {number} progress
* @param {string} [message]
* @global
*/
/**
* Listen for a specific request
* @method listen
* @async
* @memberof module:API.cvat.lambda
* @param {string} requestID
* @param {onRequestStatusChange} onChange
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async listen(requestID, onChange) {
const result = await PluginRegistry
.apiWrapper(cvat.lambda.listen, requestID, onChange);
return result;
},
/**
* Get active lambda requests
* @method requests
* @async
* @memberof module:API.cvat.lambda
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async requests() {
const result = await PluginRegistry
.apiWrapper(cvat.lambda.requests);
return result;
},
},
/**
* Namespace to working with logs
* @namespace logger
@ -530,7 +668,9 @@ function build() {
ObjectShape,
LogType,
HistoryActions,
RQStatus,
colors,
Source,
},
/**
* Namespace is used for access to exceptions
@ -559,6 +699,7 @@ function build() {
Label,
Statistics,
ObjectState,
MLModel,
},
};
@ -567,6 +708,7 @@ function build() {
cvat.jobs = Object.freeze(cvat.jobs);
cvat.users = Object.freeze(cvat.users);
cvat.plugins = Object.freeze(cvat.plugins);
cvat.lambda = Object.freeze(cvat.lambda);
cvat.client = Object.freeze(cvat.client);
cvat.enums = Object.freeze(cvat.enums);

@ -34,6 +34,26 @@
COMPLETED: 'completed',
});
/**
* List of RQ statuses
* @enum {string}
* @name RQStatus
* @memberof module:API.cvat.enums
* @property {string} QUEUED 'queued'
* @property {string} STARTED 'started'
* @property {string} FINISHED 'finished'
* @property {string} FAILED 'failed'
* @property {string} UNKNOWN 'unknown'
* @readonly
*/
const RQStatus = Object.freeze({
QUEUED: 'queued',
STARTED: 'started',
FINISHED: 'finished',
FAILED: 'failed',
UNKNOWN: 'unknown',
});
/**
* Task modes
* @enum {string}
@ -104,6 +124,20 @@
CUBOID: 'cuboid',
});
/**
* Annotation type
* @enum {string}
* @name Source
* @memberof module:API.cvat.enums
* @property {string} MANUAL 'manual'
* @property {string} AUTO 'auto'
* @readonly
*/
const Source = Object.freeze({
MANUAL:'manual',
AUTO:'auto',
});
/**
* Logger event types
* @enum {string}
@ -190,6 +224,7 @@
* @property {string} CHANGED_LOCK Changed lock
* @property {string} CHANGED_COLOR Changed color
* @property {string} CHANGED_HIDDEN Changed hidden
* @property {string} CHANGED_SOURCE Changed source
* @property {string} MERGED_OBJECTS Merged objects
* @property {string} SPLITTED_TRACK Splitted track
* @property {string} GROUPED_OBJECTS Grouped objects
@ -209,6 +244,7 @@
CHANGED_PINNED: 'Changed pinned',
CHANGED_COLOR: 'Changed color',
CHANGED_HIDDEN: 'Changed hidden',
CHANGED_SOURCE: 'Changed source',
MERGED_OBJECTS: 'Merged objects',
SPLITTED_TRACK: 'Splitted track',
GROUPED_OBJECTS: 'Grouped objects',
@ -216,6 +252,18 @@
REMOVED_OBJECT: 'Removed object',
});
/**
* Enum string values.
* @name ModelType
* @memberof module:API.cvat.enums
* @enum {string}
*/
const ModelType = {
DETECTOR: 'detector',
INTERACTOR: 'interactor',
TRACKER: 'tracker',
};
/**
* Array of hex colors
* @name colors
@ -224,10 +272,11 @@
* @readonly
*/
const colors = [
'#FF355E', '#E936A7', '#FD5B78', '#FF007C', '#FF00CC', '#66FF66',
'#50BFE6', '#CCFF00', '#FFFF66', '#FF9966', '#FF6037', '#FFCC33',
'#AAF0D1', '#FF3855', '#FFF700', '#A7F432', '#FF5470', '#FAFA37',
'#FF7A00', '#FF9933', '#AFE313', '#00CC99', '#FF5050', '#733380',
'#33ddff', '#fa3253', '#34d1b7', '#ff007c', '#ff6037', '#ddff33',
'#24b353', '#b83df5', '#66ff66', '#32b7fa', '#ffcc33', '#83e070',
'#fafa37', '#5986b3', '#8c78f0', '#ff6a4d', '#f078f0', '#2a7dd1',
'#b25050', '#cc3366', '#cc9933', '#aaf0d1', '#ff00cc', '#3df53d',
'#fa32b7', '#fa7dbb', '#ff355e', '#f59331', '#3d3df5', '#733380',
];
module.exports = {
@ -238,7 +287,10 @@
ObjectType,
ObjectShape,
LogType,
ModelType,
HistoryActions,
RQStatus,
colors,
Source,
};
})();

@ -605,10 +605,18 @@
};
}
function clear(taskID) {
if (taskID in frameDataCache) {
frameDataCache[taskID].frameBuffer.clear();
delete frameDataCache[taskID];
}
}
module.exports = {
FrameData,
getFrame,
getRanges,
getPreview,
clear,
};
})();

@ -10,7 +10,6 @@
(() => {
const {
AttributeType,
colors,
} = require('./enums');
const { ArgumentError } = require('./exceptions');
@ -150,9 +149,6 @@
}
}
if (typeof (data.id) !== 'undefined') {
data.color = colors[data.id % colors.length];
}
data.attributes = [];
if (Object.prototype.hasOwnProperty.call(initialData, 'attributes')
@ -193,10 +189,10 @@
color: {
get: () => data.color,
set: (color) => {
if (colors.includes(color)) {
if (typeof color === 'string' && color.match(/^#[0-9a-f]{6}$|^$/)) {
data.color = color;
} else {
throw new ArgumentError('Trying to set unknown color');
throw new ArgumentError('Trying to set wrong color format');
}
},
},
@ -217,6 +213,7 @@
const object = {
name: this.name,
attributes: [...this.attributes.map((el) => el.toJSON())],
color: this.color,
};
if (typeof (this.id) !== 'undefined') {

@ -0,0 +1,126 @@
/*
* Copyright (C) 2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const MLModel = require('./ml-model');
const { RQStatus } = require('./enums');
class LambdaManager {
constructor() {
this.listening = {};
this.cachedList = null;
}
async list() {
if (Array.isArray(this.cachedList)) {
return [...this.cachedList];
}
const result = await serverProxy.lambda.list();
const models = [];
for (const model of result) {
models.push(new MLModel({
id: model.id,
name: model.name,
description: model.description,
framework: model.framework,
labels: [...model.labels],
type: model.kind,
}));
}
this.cachedList = models;
return models;
}
async run(task, model, args) {
if (!(task instanceof Task)) {
throw new ArgumentError(
`Argument task is expected to be an instance of Task class, but got ${typeof (task)}`,
);
}
if (!(model instanceof MLModel)) {
throw new ArgumentError(
`Argument model is expected to be an instance of MLModel class, but got ${typeof (model)}`,
);
}
if (args && typeof (args) !== 'object') {
throw new ArgumentError(
`Argument args is expected to be an object, but got ${typeof (model)}`,
);
}
const body = args;
body.task = task.id;
body.function = model.id;
const result = await serverProxy.lambda.run(body);
return result.id;
}
async call(task, model, args) {
const body = args;
body.task = task.id;
const result = await serverProxy.lambda.call(model.id, body);
return result;
}
async requests() {
const result = await serverProxy.lambda.requests();
return result.filter((request) => ['queued', 'started'].includes(request.status));
}
async cancel(requestID) {
if (typeof (requestID) !== 'string') {
throw new ArgumentError(`Request id argument is required to be a string. But got ${requestID}`);
}
if (this.listening[requestID]) {
clearTimeout(this.listening[requestID].timeout);
delete this.listening[requestID];
}
await serverProxy.lambda.cancel(requestID);
}
async listen(requestID, onUpdate) {
const timeoutCallback = async () => {
try {
this.listening[requestID].timeout = null;
const response = await serverProxy.lambda.status(requestID);
if (response.status === RQStatus.QUEUED || response.status === RQStatus.STARTED) {
onUpdate(response.status, response.progress || 0);
this.listening[requestID].timeout = setTimeout(timeoutCallback, 2000);
} else {
if (response.status === RQStatus.FINISHED) {
onUpdate(response.status, response.progress || 100);
} else {
onUpdate(response.status, response.progress || 0, response.exc_info || '');
}
delete this.listening[requestID];
}
} catch (error) {
onUpdate(RQStatus.UNKNOWN, 0, `Could not get a status of the request ${requestID}. ${error.toString()}`);
}
};
this.listening[requestID] = {
onUpdate,
timeout: setTimeout(timeoutCallback, 2000),
};
}
}
module.exports = new LambdaManager();

@ -0,0 +1,73 @@
/*
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/**
* Class representing a machine learning model
* @memberof module:API.cvat.classes
*/
class MLModel {
constructor(data) {
this._id = data.id;
this._name = data.name;
this._labels = data.labels;
this._framework = data.framework;
this._description = data.description;
this._type = data.type;
}
/**
* @returns {string}
* @readonly
*/
get id() {
return this._id;
}
/**
* @returns {string}
* @readonly
*/
get name() {
return this._name;
}
/**
* @returns {string[]}
* @readonly
*/
get labels() {
if (Array.isArray(this._labels)) {
return [...this._labels];
}
return [];
}
/**
* @returns {string}
* @readonly
*/
get framework() {
return this._framework;
}
/**
* @returns {string}
* @readonly
*/
get description() {
return this._description;
}
/**
* @returns {module:API.cvat.enums.ModelType}
* @readonly
*/
get type() {
return this._type;
}
}
module.exports = MLModel;

@ -3,6 +3,8 @@
* SPDX-License-Identifier: MIT
*/
const { Source } = require('./enums');
/* global
require:false
*/
@ -22,7 +24,7 @@
* </br> Necessary fields: objectType, shapeType, frame, updated, group
* </br> Optional fields: keyframes, clientID, serverID
* </br> Optional fields which can be set later: points, zOrder, outside,
* occluded, hidden, attributes, lock, label, color, keyframe
* occluded, hidden, attributes, lock, label, color, keyframe, source
*/
constructor(serialized) {
const data = {
@ -39,6 +41,7 @@
color: null,
hidden: null,
pinned: null,
source: Source.MANUAL,
keyframes: serialized.keyframes,
group: serialized.group,
updated: serialized.updated,
@ -109,6 +112,16 @@
*/
get: () => data.shapeType,
},
source: {
/**
* @name source
* @type {module:API.cvat.enums.Source}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.source,
},
clientID: {
/**
* @name clientID
@ -344,6 +357,9 @@
this.label = serialized.label;
this.lock = serialized.lock;
if ([Source.MANUAL, Source.AUTO].includes(serialized.source)) {
data.source = serialized.source;
}
if (typeof (serialized.zOrder) === 'number') {
this.zOrder = serialized.zOrder;
}

@ -162,7 +162,6 @@
response = await Axios.get(`${backendAPI}/restrictions/user-agreements`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
@ -170,7 +169,15 @@
return response.data;
}
async function register(username, firstName, lastName, email, password1, password2, confirmations) {
async function register(
username,
firstName,
lastName,
email,
password1,
password2,
confirmations,
) {
let response = null;
try {
const data = JSON.stringify({
@ -239,6 +246,24 @@
Axios.defaults.headers.common.Authorization = '';
}
async function changePassword(oldPassword, newPassword1, newPassword2) {
try {
const data = JSON.stringify({
old_password: oldPassword,
new_password1: newPassword1,
new_password2:newPassword2,
});
await Axios.post(`${config.backendAPI}/auth/password/change`, data, {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}
async function authorized() {
try {
await module.exports.users.getSelf();
@ -662,6 +687,96 @@
}
}
async function getLambdaFunctions() {
const { backendAPI } = config;
try {
const response = await Axios.get(`${backendAPI}/lambda/functions`, {
proxy: config.proxy,
});
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
}
async function runLambdaRequest(body) {
const { backendAPI } = config;
try {
const response = await Axios.post(`${backendAPI}/lambda/requests`,
JSON.stringify(body), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
}
async function callLambdaFunction(funId, body) {
const { backendAPI } = config;
try {
const response = await Axios.post(`${backendAPI}/lambda/functions/${funId}`,
JSON.stringify(body), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
}
async function getLambdaRequests() {
const { backendAPI } = config;
try {
const response = await Axios.get(`${backendAPI}/lambda/requests`, {
proxy: config.proxy,
});
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
}
async function getRequestStatus(requestID) {
const { backendAPI } = config;
try {
const response = await Axios.get(`${backendAPI}/lambda/requests/${requestID}`, {
proxy: config.proxy,
});
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
}
async function cancelLambdaRequest(requestId) {
const { backendAPI } = config;
try {
await Axios.delete(
`${backendAPI}/lambda/requests/${requestId}`, {
method: 'DELETE',
},
);
} catch (errorData) {
throw generateError(errorData);
}
}
Object.defineProperties(this, Object.freeze({
server: {
value: Object.freeze({
@ -671,6 +786,7 @@
exception,
login,
logout,
changePassword,
authorized,
register,
request: serverRequest,
@ -731,6 +847,18 @@
}),
writable: false,
},
lambda: {
value: Object.freeze({
list: getLambdaFunctions,
status: getRequestStatus,
requests: getLambdaRequests,
run: runLambdaRequest,
call: callLambdaFunction,
cancel: cancelLambdaRequest,
}),
writable: false,
},
}));
}
}

@ -11,7 +11,12 @@
const PluginRegistry = require('./plugins');
const loggerStorage = require('./logger-storage');
const serverProxy = require('./server-proxy');
const { getFrame, getRanges, getPreview } = require('./frames');
const {
getFrame,
getRanges,
getPreview,
clear: clearFrames,
} = require('./frames');
const { ArgumentError } = require('./exceptions');
const { TaskStatus } = require('./enums');
const { Label } = require('./labels');
@ -1309,6 +1314,21 @@
};
}
/**
* Method removes all task related data from the client (annotations, history, etc.)
* @method close
* @returns {module:API.cvat.classes.Task}
* @memberof module:API.cvat.classes.Task
* @readonly
* @async
* @instance
* @throws {module:API.cvat.exceptions.PluginError}
*/
async close() {
const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.close);
return result;
}
/**
* Method updates data of a created task or creates new task from scratch
* @method save
@ -1372,6 +1392,7 @@
redoActions,
clearActions,
getActions,
closeSession,
} = require('./annotations');
buildDublicatedAPI(Job.prototype);
@ -1576,6 +1597,16 @@
return result;
};
Task.prototype.close.implementation = function closeTask() {
clearFrames(this.id);
for (const job of this.jobs) {
closeSession(job);
}
closeSession(this);
return this;
};
Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) {
// TODO: Add ability to change an owner and an assignee
if (typeof (this.id) !== 'undefined') {

@ -23,6 +23,7 @@
is_staff: null,
is_superuser: null,
is_active: null,
email_verification_required: null,
};
for (const property in data) {
@ -143,6 +144,16 @@
*/
get: () => data.is_active,
},
isVerified: {
/**
* @name isVerified
* @type {boolean}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => !data.email_verification_required,
},
}));
}
}

@ -30,10 +30,12 @@ const webConfig = {
target: 'web',
mode: 'production',
devtool: 'source-map',
entry: './src/api.js',
entry: {
'cvat-core': './src/api.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cvat-core.min.js',
filename: '[name].[contenthash].min.js',
library: 'cvat',
libraryTarget: 'window',
},
@ -58,7 +60,7 @@ const webConfig = {
loader: 'worker-loader',
options: {
publicPath: '/static/engine/js/3rdparty/',
name: '[name].js',
name: '[name].[contenthash].js',
},
},
}, {
@ -68,7 +70,7 @@ const webConfig = {
loader: 'worker-loader',
options: {
publicPath: '/static/engine/js/',
name: '[name].js',
name: '[name].[contenthash].js',
},
},
},

@ -1,6 +1,6 @@
{
"name": "cvat-data",
"version": "1.0.0",
"version": "1.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

@ -1,6 +1,6 @@
{
"name": "cvat-data",
"version": "1.0.0",
"version": "1.0.1",
"description": "",
"main": "src/js/cvat-data.js",
"devDependencies": {

@ -295,7 +295,26 @@ class FrameProvider {
worker.terminate();
};
worker.onmessage = (event) => {
worker.onmessage = async (event) => {
if (event.data.isRaw) {
// safary doesn't support createImageBitmap
// there is a way to polyfill it with using document.createElement
// but document.createElement doesn't work in worker
// so, we get raw data and decode it here, no other way
const createImageBitmap = async function(blob) {
return new Promise((resolve,reject) => {
let img = document.createElement('img');
img.addEventListener('load', function() {
resolve(this);
});
img.src = URL.createObjectURL(blob);
});
};
event.data.data = await createImageBitmap(event.data.data);
}
this._frames[event.data.index] = event.data.data;
if (this._decodingBlocks[`${start}:${end}`].resolveCallback) {

@ -20,13 +20,22 @@ onmessage = (e) => {
const fileIndex = index++;
if (fileIndex <= end) {
_zip.file(relativePath).async('blob').then((fileData) => {
createImageBitmap(fileData).then((img) => {
if (self.createImageBitmap) {
createImageBitmap(fileData).then((img) => {
postMessage({
fileName: relativePath,
index: fileIndex,
data: img,
});
});
} else {
postMessage({
fileName: relativePath,
index: fileIndex,
data: img,
data: fileData,
isRaw: true,
});
});
}
});
}
});

@ -9,10 +9,12 @@ const CopyPlugin = require('copy-webpack-plugin');
const cvatData = {
target: 'web',
mode: 'production',
entry: './src/js/cvat-data.js',
entry: {
'cvat-data': './src/js/cvat-data.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cvat-data.min.js',
filename: '[name].[contenthash].min.js',
library: 'cvatData',
libraryTarget: 'window',
},
@ -39,7 +41,7 @@ const cvatData = {
loader: 'worker-loader',
options: {
publicPath: '/',
name: '[name].js',
name: '[name].[contenthash].js',
},
},
}, {
@ -48,7 +50,7 @@ const cvatData = {
loader: 'worker-loader',
options: {
publicPath: '/3rdparty/',
name: '3rdparty/[name].js',
name: '3rdparty/[name].[contenthash].js',
},
},
},

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.2.0",
"version": "1.8.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -13,14 +13,19 @@
}
},
"@ant-design/create-react-context": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@ant-design/create-react-context/-/create-react-context-0.2.4.tgz",
"integrity": "sha512-8sw+/w6r+aEbd+OJ62ojoSE4zDt/3yfQydmbWFznoftjr8v/opOswGjM+/MU0rSaREbluqzOmZ6xdecHpSaS2w==",
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@ant-design/create-react-context/-/create-react-context-0.2.5.tgz",
"integrity": "sha512-1rMAa4qgP2lfl/QBH9i78+Gjxtj9FTMpMyDGZsEBW5Kih72EuUo9958mV8PgpRkh4uwPSQ7vVZWXeyNZXVAFDg==",
"requires": {
"gud": "^1.0.0",
"warning": "^4.0.3"
}
},
"@ant-design/css-animation": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@ant-design/css-animation/-/css-animation-1.7.2.tgz",
"integrity": "sha512-bvVOe7A+r7lws58B7r+fgnQDK90cV45AXuvGx6i5CCSX1W/M3AJnHsNggDANBxEtWdNdFWcDd5LorB+RdSIlBw=="
},
"@ant-design/icons": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-2.1.1.tgz",
@ -975,6 +980,11 @@
"integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==",
"dev": true
},
"@icons/material": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
"integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw=="
},
"@types/eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
@ -1036,6 +1046,11 @@
"integrity": "sha512-opgSsy+cEF9N8MgaVPnWVtdJ3o4mV2aMHvDq7thkQUFt0EuOHJon4rQpJfhjmNHB+ikl0Cd6WhWIErOyQ+f7tw==",
"dev": true
},
"@types/platform": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/platform/-/platform-1.3.2.tgz",
"integrity": "sha512-Tn6OuJDAG7bJbyi4R7HqcxXp1w2lmIxVXqyNhPt1Bm0FO2EWIi3CI87JVzF7ncqK0ZMPuUycS3wTMIk85EeF1Q=="
},
"@types/prop-types": {
"version": "15.7.3",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
@ -1056,6 +1071,14 @@
"csstype": "^2.2.0"
}
},
"@types/react-color": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.2.tgz",
"integrity": "sha512-FhrRy0xEYEpysl1iKL11ynJc79H6ztyYc4xD1pliZyygEChleTlHGohb/bClTYPN8XeSw6yaz45l3YW5SGYftQ==",
"requires": {
"@types/react": "*"
}
},
"@types/react-dom": {
"version": "16.9.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.3.tgz",
@ -1506,9 +1529,9 @@
}
},
"antd": {
"version": "3.25.2",
"resolved": "https://registry.npmjs.org/antd/-/antd-3.25.2.tgz",
"integrity": "sha512-+qF1bgU7rUkPIkggIIV0fmm+9pPacl50BBd6NNUR2+kKJOFYjwrnP39ZqJRsYNy5bX9VgR454fz9KEuW7HPjog==",
"version": "3.26.17",
"resolved": "https://registry.npmjs.org/antd/-/antd-3.26.17.tgz",
"integrity": "sha512-P9uSK8SZ/1AvhQCC6aaLEkVrQhjbfZyUnqNV+lDnPqtudnZD2Ycy7Og+/EhuOBsQpYQvVT2aPLMgQWFv8tdJkA==",
"requires": {
"@ant-design/create-react-context": "^0.2.4",
"@ant-design/icons": "~2.1.1",
@ -1521,18 +1544,19 @@
"css-animation": "^1.5.0",
"dom-closest": "^0.2.0",
"enquire.js": "^2.1.6",
"is-mobile": "^2.1.0",
"lodash": "^4.17.13",
"moment": "^2.24.0",
"omit.js": "^1.0.2",
"prop-types": "^15.7.2",
"raf": "^3.4.1",
"rc-animate": "^2.10.2",
"rc-calendar": "~9.15.5",
"rc-calendar": "~9.15.7",
"rc-cascader": "~0.17.4",
"rc-checkbox": "~2.1.6",
"rc-collapse": "~1.11.3",
"rc-dialog": "~7.5.2",
"rc-drawer": "~3.0.0",
"rc-dialog": "~7.6.0",
"rc-drawer": "~3.1.1",
"rc-dropdown": "~2.4.1",
"rc-editor-mention": "^1.1.13",
"rc-form": "^2.4.10",
@ -1540,7 +1564,7 @@
"rc-mentions": "~0.4.0",
"rc-menu": "~7.5.1",
"rc-notification": "~3.3.1",
"rc-pagination": "~1.20.5",
"rc-pagination": "~1.20.11",
"rc-progress": "~2.5.0",
"rc-rate": "~2.5.0",
"rc-resize-observer": "^0.1.0",
@ -1548,15 +1572,15 @@
"rc-slider": "~8.7.1",
"rc-steps": "~3.5.0",
"rc-switch": "~1.9.0",
"rc-table": "~6.9.4",
"rc-tabs": "~9.6.4",
"rc-table": "~6.10.5",
"rc-tabs": "~9.7.0",
"rc-time-picker": "~3.7.1",
"rc-tooltip": "~3.7.3",
"rc-tree": "~2.1.0",
"rc-tree-select": "~2.9.1",
"rc-trigger": "^2.6.2",
"rc-upload": "~2.9.1",
"rc-util": "^4.10.0",
"rc-util": "^4.16.1",
"react-lazy-load": "^3.0.13",
"react-lifecycles-compat": "^3.0.4",
"react-slick": "~0.25.2",
@ -3306,7 +3330,6 @@
"convert-source-map": "^1.1.0",
"fs-readdir-recursive": "^1.1.0",
"glob": "^7.0.0",
"lodash": "^4.17.13",
"mkdirp": "^0.5.1",
"output-file-sync": "^2.0.0",
"slash": "^2.0.0",
@ -3336,7 +3359,6 @@
"convert-source-map": "^1.1.0",
"debug": "^4.1.0",
"json5": "^2.1.0",
"lodash": "^4.17.13",
"resolve": "^1.3.2",
"semver": "^5.4.1",
"source-map": "^0.5.0"
@ -3372,7 +3394,6 @@
"requires": {
"@babel/types": "^7.5.5",
"jsesc": "^2.5.1",
"lodash": "^4.17.13",
"source-map": "^0.5.0",
"trim-right": "^1.0.1"
}
@ -3423,8 +3444,7 @@
"integrity": "sha512-fTfxx7i0B5NJqvUOBBGREnrqbTxRh7zinBANpZXAVDlsZxYdclDp467G1sQ8VZYMnAURY3RpBUAgOYT9GfzHBg==",
"requires": {
"@babel/helper-function-name": "^7.1.0",
"@babel/types": "^7.5.5",
"lodash": "^4.17.13"
"@babel/types": "^7.5.5"
}
},
"@babel/helper-explode-assignable-expression": {
@ -3487,8 +3507,7 @@
"@babel/helper-simple-access": "^7.1.0",
"@babel/helper-split-export-declaration": "^7.4.4",
"@babel/template": "^7.4.4",
"@babel/types": "^7.5.5",
"lodash": "^4.17.13"
"@babel/types": "^7.5.5"
}
},
"@babel/helper-optimise-call-expression": {
@ -3507,10 +3526,7 @@
"@babel/helper-regex": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.5.5.tgz",
"integrity": "sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw==",
"requires": {
"lodash": "^4.17.13"
}
"integrity": "sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw=="
},
"@babel/helper-remap-async-to-generator": {
"version": "7.1.0",
@ -3622,7 +3638,6 @@
"requires": {
"@babel/types": "^7.8.7",
"jsesc": "^2.5.1",
"lodash": "^4.17.13",
"source-map": "^0.5.0"
}
},
@ -3734,8 +3749,7 @@
"@babel/parser": "^7.8.6",
"@babel/types": "^7.8.6",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.13"
"globals": "^11.1.0"
}
},
"@babel/types": {
@ -3744,7 +3758,6 @@
"integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==",
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
@ -3888,8 +3901,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.5.5.tgz",
"integrity": "sha512-82A3CLRRdYubkG85lKwhZB0WZoHxLGsJdux/cOVaJCJpvYFl1LVzAIFyRsa7CvXqW8rBM4Zf3Bfn8PHt5DP0Sg==",
"requires": {
"@babel/helper-plugin-utils": "^7.0.0",
"lodash": "^4.17.13"
"@babel/helper-plugin-utils": "^7.0.0"
}
},
"@babel/plugin-transform-classes": {
@ -4232,8 +4244,7 @@
"@babel/parser": "^7.5.5",
"@babel/types": "^7.5.5",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.13"
"globals": "^11.1.0"
},
"dependencies": {
"debug": {
@ -4257,7 +4268,6 @@
"integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==",
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
@ -5134,7 +5144,6 @@
"browserify-rsa": "^4.0.0",
"create-hash": "^1.1.0",
"create-hmac": "^1.1.2",
"elliptic": "^6.0.0",
"inherits": "^2.0.1",
"parse-asn1": "^5.0.0"
}
@ -5677,8 +5686,7 @@
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
"integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
"requires": {
"bn.js": "^4.1.0",
"elliptic": "^6.0.0"
"bn.js": "^4.1.0"
}
},
"create-error-class": {
@ -6167,20 +6175,6 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.199.tgz",
"integrity": "sha512-gachlDdHSK47s0N2e58GH9HMC6Z4ip0SfmYUa5iEbE50AKaOUXysaJnXMfKj0xB245jWbYcyFSH+th3rqsF8hA=="
},
"elliptic": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.0.tgz",
"integrity": "sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg==",
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
}
},
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
@ -6292,7 +6286,6 @@
"js-yaml": "^3.13.1",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.3.0",
"lodash": "^4.17.14",
"minimatch": "^3.0.4",
"mkdirp": "^0.5.1",
"natural-compare": "^1.4.0",
@ -7215,7 +7208,6 @@
"integrity": "sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==",
"requires": {
"glob": "~7.1.1",
"lodash": "~4.17.12",
"minimatch": "~3.0.2"
}
},
@ -7434,7 +7426,6 @@
"requires": {
"http-proxy": "^1.17.0",
"is-glob": "^4.0.0",
"lodash": "^4.17.11",
"micromatch": "^3.1.10"
}
},
@ -7593,7 +7584,6 @@
"cli-width": "^2.0.0",
"external-editor": "^3.0.3",
"figures": "^2.0.0",
"lodash": "^4.17.12",
"mute-stream": "0.0.7",
"run-async": "^2.2.0",
"rxjs": "^6.4.0",
@ -8098,11 +8088,6 @@
"path-exists": "^3.0.0"
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lodash._reinterpolate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
@ -8641,7 +8626,6 @@
"get-stdin": "^4.0.1",
"glob": "^7.0.3",
"in-publish": "^2.0.0",
"lodash": "^4.17.15",
"meow": "^3.7.0",
"mkdirp": "^0.5.1",
"nan": "^2.13.2",
@ -10273,7 +10257,6 @@
"integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=",
"requires": {
"glob": "^7.0.0",
"lodash": "^4.0.0",
"scss-tokenizer": "^0.2.3",
"yargs": "^7.0.0"
},
@ -11221,7 +11204,6 @@
"integrity": "sha512-oGa2Hl7CQjfoaogtrOHEJroOcYILTx7BZWLGsJIlzoWmB2zmguhNfPJZsWPKYek/MgCxfco54gEi31d1uN2hFA==",
"requires": {
"ajv": "^6.10.2",
"lodash": "^4.17.14",
"slice-ansi": "^2.1.0",
"string-width": "^3.0.0"
},
@ -12096,15 +12078,9 @@
"integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==",
"requires": {
"http-parser-js": ">=0.4.0 <0.4.11",
"safe-buffer": ">=5.1.0",
"websocket-extensions": ">=0.1.1"
"safe-buffer": ">=5.1.0"
}
},
"websocket-extensions": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
"integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg=="
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@ -14113,7 +14089,6 @@
"browserify-rsa": "^4.0.0",
"create-hash": "^1.1.0",
"create-hmac": "^1.1.2",
"elliptic": "^6.0.0",
"inherits": "^2.0.1",
"parse-asn1": "^5.0.0"
}
@ -14530,8 +14505,7 @@
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
"integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
"requires": {
"bn.js": "^4.1.0",
"elliptic": "^6.0.0"
"bn.js": "^4.1.0"
}
},
"create-hash": {
@ -14996,20 +14970,6 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.377.tgz",
"integrity": "sha512-cm2WzMKf/3dW5+hNANKm8GAW6SwIWOqLTJ6GPCD0Bbw1qJ9Wzm9nmx9M+byzSsgw8CdCv5fb/wzLFqVS5h6QrA=="
},
"elliptic": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
"integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
}
},
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
@ -18344,9 +18304,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"lodash.sortby": {
"version": "4.7.0",
@ -21348,9 +21308,9 @@
}
},
"dom-align": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.10.2.tgz",
"integrity": "sha512-AYZUzLepy05E9bCY4ExoqHrrIlM49PEak9oF93JEFoibqKL0F7w5DLM70/rosLOawerWZ3MlepQcl+EmHskOyw=="
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.0.tgz",
"integrity": "sha512-YkoezQuhp3SLFGdOlr5xkqZ640iXrnHAwVYcDg8ZKRUtO7mSzSC2BA5V0VuyAwPSJA4CLIc6EDDJh4bEsD2+zA=="
},
"dom-closest": {
"version": "0.2.0",
@ -21526,9 +21486,9 @@
"dev": true
},
"elliptic": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
"integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"dev": true,
"requires": {
"bn.js": "^4.4.0",
@ -24417,6 +24377,11 @@
"is-extglob": "^2.1.1"
}
},
"is-mobile": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-2.2.1.tgz",
"integrity": "sha512-6zELsfVFr326eq2CI53yvqq6YBanOxKBybwDT+MbMS2laBnK6Ez8m5XHSuTQQbnKRfpDzCod1CMWW5q3wZYMvA=="
},
"is-number": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
@ -24784,9 +24749,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"lodash-es": {
"version": "4.17.15",
@ -24920,6 +24885,11 @@
"object-visit": "^1.0.0"
}
},
"material-colors": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
"integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg=="
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -25318,9 +25288,9 @@
"dev": true
},
"mutationobserver-shim": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz",
"integrity": "sha512-gciOLNN8Vsf7YzcqRjKzlAJ6y7e+B86u7i3KXes0xfxx/nfLmozlW1Vn+Sc9x3tPIePFgc1AeIFhtRgkqTjzDQ=="
"version": "0.3.7",
"resolved": "https://registry.npmjs.org/mutationobserver-shim/-/mutationobserver-shim-0.3.7.tgz",
"integrity": "sha512-oRIDTyZQU96nAiz2AQyngwx1e89iApl2hN5AOYwyxLUB47UYsU3Wv9lJWqH5y/QdiYkc5HQLi23ZNB3fELdHcQ=="
},
"mute-stream": {
"version": "0.0.8",
@ -26225,6 +26195,11 @@
"find-up": "^3.0.0"
}
},
"platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
},
"portfinder": {
"version": "1.0.25",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz",
@ -27028,9 +27003,9 @@
}
},
"rc-animate": {
"version": "2.10.2",
"resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-2.10.2.tgz",
"integrity": "sha512-cE/A7piAzoWFSgUD69NmmMraqCeqVBa51UErod8NS3LUEqWfppSVagHfa0qHAlwPVPiIBg3emRONyny3eiH0Dg==",
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-2.11.1.tgz",
"integrity": "sha512-1NyuCGFJG/0Y+9RKh5y/i/AalUCA51opyyS/jO2seELpgymZm2u9QV3xwODwEuzkmeQ1BDPxMLmYLcTJedPlkQ==",
"requires": {
"babel-runtime": "6.x",
"classnames": "^2.2.6",
@ -27042,9 +27017,9 @@
}
},
"rc-calendar": {
"version": "9.15.8",
"resolved": "https://registry.npmjs.org/rc-calendar/-/rc-calendar-9.15.8.tgz",
"integrity": "sha512-x3zVaZSRX7FkRNKw7nz3tutwrlIrU1aqMn5GtRUmlf84GnXLtd9fuuydxeNkFWfcHry3BPSto7+r9TK2al0h+g==",
"version": "9.15.11",
"resolved": "https://registry.npmjs.org/rc-calendar/-/rc-calendar-9.15.11.tgz",
"integrity": "sha512-qv0VXfAAnysMWJigxaP6se4bJHvr17D9qsLbi8BOpdgEocsS0RkgY1IUiFaOVYKJDy/EyLC447O02sV/y5YYBg==",
"requires": {
"babel-runtime": "6.x",
"classnames": "2.x",
@ -27081,9 +27056,9 @@
}
},
"rc-collapse": {
"version": "1.11.7",
"resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-1.11.7.tgz",
"integrity": "sha512-ge3EEzIFtrDGuPX4bxXdQqwb91JnPIdj3B+FU88yNOUeOroNuA2q9kVK+UatpQ1Eft5hNo/ICTDrVFi8+685ng==",
"version": "1.11.8",
"resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-1.11.8.tgz",
"integrity": "sha512-8EhfPyScTYljkbRuIoHniSwZagD5UPpZ3CToYgoNYWC85L2qCbPYF7+OaC713FOrIkp6NbfNqXsITNxmDAmxog==",
"requires": {
"classnames": "2.x",
"css-animation": "1.x",
@ -27095,23 +27070,22 @@
}
},
"rc-dialog": {
"version": "7.5.13",
"resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-7.5.13.tgz",
"integrity": "sha512-tmubIipW/qoCmRlHHV8tpepDaFhuhk+SeSFSyRhNKW4mYgflsEYQmYWilyCJHy6UzKl84bSyFvJskhc1z1Hniw==",
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-7.6.1.tgz",
"integrity": "sha512-KUKf+2eZ4YL+lnXMG3hR4ZtIhC9glfH27NtTVz3gcoDIPAf3uUvaXVRNoDCiSi+OGKLyIb/b6EoidFh6nQC5Wg==",
"requires": {
"babel-runtime": "6.x",
"rc-animate": "2.x",
"rc-util": "^4.8.1"
"rc-util": "^4.16.1"
}
},
"rc-drawer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-3.0.2.tgz",
"integrity": "sha512-oPScGXB/8/ov9gEFLxPH8RBv/9jLTZboZtyF/GgrrnCAvbFwUxXdELH6n6XIowmuDKKvTGIMgZdnao0T46Yv3A==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-3.1.3.tgz",
"integrity": "sha512-2z+RdxmzXyZde/1OhVMfDR1e/GBswFeWSZ7FS3Fdd0qhgVdpV1wSzILzzxRaT481ItB5hOV+e8pZT07vdJE8kg==",
"requires": {
"babel-runtime": "^6.26.0",
"classnames": "^2.2.6",
"rc-util": "^4.11.2",
"rc-util": "^4.16.1",
"react-lifecycles-compat": "^3.0.4"
}
},
@ -27157,9 +27131,9 @@
}
},
"rc-form": {
"version": "2.4.10",
"resolved": "https://registry.npmjs.org/rc-form/-/rc-form-2.4.10.tgz",
"integrity": "sha512-h6a5Nvn6fMe3BfLpIWwL2RUkfXs1tvtifblTgGgH0UfzGgiQ5M12jiMJaAXek7TDDBUw90/c5vlZ6wFZjW0IgQ==",
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/rc-form/-/rc-form-2.4.11.tgz",
"integrity": "sha512-8BL+FNlFLTOY/A5X6tU35GQJLSIpsmqpwn/tFAYQTczXc4dMJ33ggtH248Cum8+LS0jLTsJKG2L4Qp+1CkY+sA==",
"requires": {
"async-validator": "~1.11.3",
"babel-runtime": "6.x",
@ -27172,9 +27146,9 @@
}
},
"rc-hammerjs": {
"version": "0.6.9",
"resolved": "https://registry.npmjs.org/rc-hammerjs/-/rc-hammerjs-0.6.9.tgz",
"integrity": "sha512-4llgWO3RgLyVbEqUdGsDfzUDqklRlQW5VEhE3x35IvhV+w//VPRG34SBavK3D2mD/UaLKaohgU41V4agiftC8g==",
"version": "0.6.10",
"resolved": "https://registry.npmjs.org/rc-hammerjs/-/rc-hammerjs-0.6.10.tgz",
"integrity": "sha512-Vgh9qIudyN5CHRop4M+v+xUniQBFWXKrsJxQRVtJOi2xgRrCeI52/bkpaL5HWwUhqTK9Ayq0n7lYTItT6ld5rg==",
"requires": {
"babel-runtime": "6.x",
"hammerjs": "^2.0.8",
@ -27182,9 +27156,9 @@
}
},
"rc-input-number": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-4.5.1.tgz",
"integrity": "sha512-grO7/Lau7iv3NyHVyCajE1LuGLqGkG1tEAAZSwm9M0esYfrwXVSip4qhb5sF+8g6ACsiI20sOVLIihXuhSoifA==",
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-4.5.7.tgz",
"integrity": "sha512-99PrQ90sTOKyyj7eu0VzwxY17xQ+bwG1XTQd+bTwFQ+IOUkIw7L4qSAYxt58sVYL+Cw+bu/RAtT2IpT9yC2pCQ==",
"requires": {
"babel-runtime": "6.x",
"classnames": "^2.2.0",
@ -27194,9 +27168,9 @@
}
},
"rc-mentions": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-0.4.1.tgz",
"integrity": "sha512-XSJp6kcEPydUaM0I/gnxpXggiKgA5FjgFPKZCMQBDQJYUjXpQNyg5ogNkOJt1/4B2P7pwbYPZXgxP/30yZVahA==",
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-0.4.2.tgz",
"integrity": "sha512-DTZurQzacLXOfVuiHydGzqkq7cFMHXF18l2jZ9PhWUn2cqvOSY3W4osN0Pq29AOMOBpcxdZCzgc7Lb0r/bgkDw==",
"requires": {
"@ant-design/create-react-context": "^0.2.4",
"classnames": "^2.2.6",
@ -27207,9 +27181,9 @@
}
},
"rc-menu": {
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-7.5.3.tgz",
"integrity": "sha512-H/jUyGbJxZI/iuVdC6Iu9KHfz7tucoqK0Vn8ahDnv+ppc1PnKb4SkBbXn5LrmUyaj7thCBiaktBxVnUXSmNE2g==",
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-7.5.5.tgz",
"integrity": "sha512-4YJXJgrpUGEA1rMftXN7bDhrV5rPB8oBJoHqT+GVXtIWCanfQxEnM3fmhHQhatL59JoAFMZhJaNzhJIk4FUWCQ==",
"requires": {
"classnames": "2.x",
"dom-scroll-into-view": "1.x",
@ -27235,9 +27209,9 @@
}
},
"rc-pagination": {
"version": "1.20.11",
"resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-1.20.11.tgz",
"integrity": "sha512-2wKO5kO+ELx1/zlqTY8TwGBruzofi+1BcZ7Z4xalMlLbDMTuUU4FDljbBBP/n9D2llK+NtgWA619PMBhInozZw==",
"version": "1.20.15",
"resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-1.20.15.tgz",
"integrity": "sha512-/Xr4/3GOa1DtL8iCYl7qRUroEMrRDhZiiuHwcVFfSiwa9LYloMlUWcOJsnr8LN6A7rLPdm3/CHStUNeYd+2pKw==",
"requires": {
"babel-runtime": "6.x",
"classnames": "^2.2.6",
@ -27246,18 +27220,18 @@
}
},
"rc-progress": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-2.5.2.tgz",
"integrity": "sha512-ajI+MJkbBz9zYDuE9GQsY5gsyqPF7HFioZEDZ9Fmc+ebNZoiSeSJsTJImPFCg0dW/5WiRGUy2F69SX1aPtSJgA==",
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-2.5.3.tgz",
"integrity": "sha512-K2fa4CnqGehLZoMrdmBeZ86ONSTVcdk5FlqetbwJ3R/+42XfqhwQVOjWp2MH4P7XSQOMAGcNOy1SFfCP3415sg==",
"requires": {
"babel-runtime": "6.x",
"prop-types": "^15.5.8"
}
},
"rc-rate": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.5.0.tgz",
"integrity": "sha512-aXX5klRqbVZxvLghcKnLqqo7LvLVCHswEDteWsm5Gb7NBIPa1YKTcAbvb5SZ4Z4i4EeRoZaPwygRAWsQgGtbKw==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.5.1.tgz",
"integrity": "sha512-3iJkNJT8xlHklPCdeZtUZmJmRVUbr6AHRlfSsztfYTXVlHrv2TcPn3XkHsH+12j812WVB7gvilS2j3+ffjUHXg==",
"requires": {
"classnames": "^2.2.5",
"prop-types": "^15.5.8",
@ -27276,9 +27250,9 @@
}
},
"rc-select": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/rc-select/-/rc-select-9.2.1.tgz",
"integrity": "sha512-nW/Zr2OCgxN26OX8ff3xcO1wK0e1l5ixnEfyN15Rbdk7TNI/rIPJIjPCQAoihRpk9A2C/GH8pahjlvKV1Vj++g==",
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/rc-select/-/rc-select-9.2.3.tgz",
"integrity": "sha512-WhswxOMWiNnkXRbxyrj0kiIvyCfo/BaRPaYbsDetSIAU2yEDwKHF798blCP5u86KLOBKBvtxWLFCkSsQw1so5w==",
"requires": {
"babel-runtime": "^6.23.0",
"classnames": "2.x",
@ -27331,9 +27305,9 @@
}
},
"rc-table": {
"version": "6.9.5",
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-6.9.5.tgz",
"integrity": "sha512-STL6387A/izVh6r9F1WDiIIZ0QeubCdTgIlzMeGTSl/bXhB0VqjAZEikvoijPoauTjJIkIzVuQEIDjOhAWbpkQ==",
"version": "6.10.15",
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-6.10.15.tgz",
"integrity": "sha512-LAr0M/gqt+irOjvPNBLApmQ0CUHNOfKsEBhu1uIuB3OlN1ynA9z+sdoTQyNd9+8NSl0MYnQOOfhtLChAY7nU0A==",
"requires": {
"classnames": "^2.2.5",
"component-classes": "^1.2.6",
@ -27346,9 +27320,9 @@
}
},
"rc-tabs": {
"version": "9.6.7",
"resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-9.6.7.tgz",
"integrity": "sha512-OXbDOgaqv2MGK9QaDi6cdva6bNz3XGw+M9BHQpm1gTGmVQEGx5VcclDClH/3xobIzooxy8hrxg/s0rTlgDnC2w==",
"version": "9.7.0",
"resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-9.7.0.tgz",
"integrity": "sha512-kvmgp8/MfLzFZ06hWHignqomFQ5nF7BqKr5O1FfhE4VKsGrep52YSF/1MvS5oe0NPcI9XGNS2p751C5v6cYDpQ==",
"requires": {
"@ant-design/create-react-context": "^0.2.4",
"babel-runtime": "6.x",
@ -27364,9 +27338,9 @@
}
},
"rc-time-picker": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/rc-time-picker/-/rc-time-picker-3.7.2.tgz",
"integrity": "sha512-UVWO9HXGyZoM4I2THlJsEAFcZQz+tYwdcpoHXCEFZsRLz9L2+7vV4EMp9Wa3UrtzMFEt83qSAX/90dCJeKl9sg==",
"version": "3.7.3",
"resolved": "https://registry.npmjs.org/rc-time-picker/-/rc-time-picker-3.7.3.tgz",
"integrity": "sha512-Lv1Mvzp9fRXhXEnRLO4nW6GLNxUkfAZ3RsiIBsWjGjXXvMNjdr4BX/ayElHAFK0DoJqOhm7c5tjmIYpEOwcUXg==",
"requires": {
"classnames": "2.x",
"moment": "2.x",
@ -27387,9 +27361,9 @@
}
},
"rc-tree": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-2.1.3.tgz",
"integrity": "sha512-COvV65spQ6omrHBUhHRKqKNL5+ddXjlS+qWZchaL9FFuQNvjM5pjp9RnmMWK4fJJ5kBhhpLneh6wh9Vh3kSMXQ==",
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-2.1.4.tgz",
"integrity": "sha512-Xey794Iavgs8YldFlXcZLOhfcIhlX5Oz/yfKufknBXf2AlZCOkc7aHqSM9uTF7fBPtTGPhPxNEfOqHfY7b7xng==",
"requires": {
"@ant-design/create-react-context": "^0.2.4",
"classnames": "2.x",
@ -27401,51 +27375,27 @@
}
},
"rc-tree-select": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-2.9.1.tgz",
"integrity": "sha512-AfJQC1ZzaeH+Onmx84TtVLUL2guBZe7exA8XSfj1RRB1doDbYGTtybzpP3CEw/tuSftSRnz+iPt+iaxRTrgXRw==",
"version": "2.9.4",
"resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-2.9.4.tgz",
"integrity": "sha512-0HQkXAN4XbfBW20CZYh3G+V+VMrjX42XRtDCpyv6PDUm5vikC0Ob682ZBCVS97Ww2a5Hf6Ajmu0ahWEdIEpwhg==",
"requires": {
"classnames": "^2.2.1",
"dom-scroll-into-view": "^1.2.1",
"prop-types": "^15.5.8",
"raf": "^3.4.0",
"rc-animate": "^2.8.2",
"rc-tree": "~2.0.0",
"rc-trigger": "^3.0.0-rc.2",
"rc-tree": "~2.1.0",
"rc-trigger": "^3.0.0",
"rc-util": "^4.5.0",
"react-lifecycles-compat": "^3.0.4",
"shallowequal": "^1.0.2",
"warning": "^4.0.1"
},
"dependencies": {
"rc-tree": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-2.0.0.tgz",
"integrity": "sha512-DAT/jsbnFbHqG9Df9OaVG93CAVtTsJVnJiwKX+wqsG8TChpty3s6QX3zJZ+gBgjkq4ikLbu1kuFJtX63EKhSAA==",
"requires": {
"babel-runtime": "^6.23.0",
"classnames": "2.x",
"prop-types": "^15.5.8",
"rc-animate": "^2.6.0",
"rc-util": "^4.5.1",
"react-lifecycles-compat": "^3.0.4",
"warning": "^3.0.0"
},
"dependencies": {
"warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",
"integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=",
"requires": {
"loose-envify": "^1.0.0"
}
}
}
},
"rc-trigger": {
"version": "3.0.0-rc.3",
"resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-3.0.0-rc.3.tgz",
"integrity": "sha512-4vB6cpxcUdm2qO5VtB9q1TZz0MoWm9BzFLvGknulphGrl1qI6uxUsPDCvqnmujdpDdAKGGfjxntFpA7RtAwkFQ==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-3.0.0.tgz",
"integrity": "sha512-hQxbbJpo23E2QnYczfq3Ec5J5tVl2mUDhkqxrEsQAqk16HfADQg+iKNWzEYXyERSncdxfnzYuaBgy764mNRzTA==",
"requires": {
"babel-runtime": "6.x",
"classnames": "^2.2.6",
@ -27453,25 +27403,37 @@
"raf": "^3.4.0",
"rc-align": "^2.4.1",
"rc-animate": "^3.0.0-rc.1",
"rc-util": "^4.4.0"
"rc-util": "^4.15.7"
},
"dependencies": {
"rc-animate": {
"version": "3.0.0-rc.6",
"resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-3.0.0-rc.6.tgz",
"integrity": "sha512-oBLPpiT6Q4t6YvD/pkLcmofBP1p01TX0Otse8Q4+Mxt8J+VSDflLZGIgf62EwkvRwsQUkLPjZVFBsldnPKLzjg==",
"requires": {
"babel-runtime": "6.x",
"classnames": "^2.2.5",
"component-classes": "^1.2.6",
"fbjs": "^0.8.16",
"prop-types": "15.x",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-3.1.0.tgz",
"integrity": "sha512-8FsM+3B1H+0AyTyGggY6JyVldHTs1CyYT8CfTmG/nGHHXlecvSLeICJhcKgRLjUiQlctNnRtB1rwz79cvBVmrw==",
"requires": {
"@ant-design/css-animation": "^1.7.2",
"classnames": "^2.2.6",
"raf": "^3.4.0",
"rc-util": "^4.5.0",
"react-lifecycles-compat": "^3.0.4"
"rc-util": "^5.0.1"
},
"dependencies": {
"rc-util": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.0.4.tgz",
"integrity": "sha512-cd19RCrE0DJH6UcJ9+V3eaXA/5sNWyVKOKkWl8ZM2OqgNzVb8fv0obf/TkuvSN43tmTsgqY8k7OqpFYHhmef8g==",
"requires": {
"react-is": "^16.12.0",
"shallowequal": "^1.1.0"
}
}
}
}
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}
}
},
@ -27490,9 +27452,9 @@
}
},
"rc-upload": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-2.9.2.tgz",
"integrity": "sha512-USjuWpTRJl3my32G5woysTaGrAld+S4dvvZ9kW6RX/RkekfmLDjvWc5ho8Mj/+6B6/tDRJnyGyvMxMQNkW7cvw==",
"version": "2.9.4",
"resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-2.9.4.tgz",
"integrity": "sha512-WXt0HGxXyzLrPV6iec/96Rbl/6dyrAW8pKuY6wwD7yFYwfU5bjgKjv7vC8KNMJ6wzitFrZjnoiogNL3dF9dj3Q==",
"requires": {
"babel-runtime": "6.x",
"classnames": "^2.2.5",
@ -27501,15 +27463,22 @@
}
},
"rc-util": {
"version": "4.15.6",
"resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.15.6.tgz",
"integrity": "sha512-W6HB1gIn+xZLxmQfLkhMnAtaZY9RktcOH2I0Tbam4D4ZDFrkO33f3M7IolN0EPtLMpf4Mv/dEQNclY77/PtBpg==",
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.21.1.tgz",
"integrity": "sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==",
"requires": {
"add-dom-event-listener": "^1.1.0",
"babel-runtime": "6.x",
"prop-types": "^15.5.10",
"react-is": "^16.12.0",
"react-lifecycles-compat": "^3.0.4",
"shallowequal": "^1.1.0"
},
"dependencies": {
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}
}
},
"react": {
@ -27522,6 +27491,19 @@
"prop-types": "^15.6.2"
}
},
"react-color": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.1.tgz",
"integrity": "sha512-X5XpyJS6ncplZs74ak0JJoqPi+33Nzpv5RYWWxn17bslih+X7OlgmfpmGC1fNvdkK7/SGWYf1JJdn7D2n5gSuQ==",
"requires": {
"@icons/material": "^0.2.4",
"lodash": "^4.17.11",
"material-colors": "^1.2.1",
"prop-types": "^15.5.10",
"reactcss": "^1.2.0",
"tinycolor2": "^1.4.1"
}
},
"react-dom": {
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.11.0.tgz",
@ -27654,6 +27636,14 @@
"react-svg-core": "^3.0.3"
}
},
"reactcss": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
"integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==",
"requires": {
"lodash": "^4.0.1"
}
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@ -28648,9 +28638,9 @@
}
},
"shallow-equal": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.0.tgz",
"integrity": "sha512-Z21pVxR4cXsfwpMKMhCEIO1PCi5sp7KEp+CmOpBQ+E8GpHwKOw2sEzk7sgblM3d/j4z4gakoWEoPcjK0VJQogA=="
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="
},
"shallowequal": {
"version": "1.1.0",
@ -30490,9 +30480,9 @@
}
},
"websocket-extensions": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
"integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
"dev": true
},
"whatwg-fetch": {

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.2.0",
"version": "1.8.4",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
@ -47,22 +47,26 @@
"worker-loader": "^2.0.0"
},
"dependencies": {
"cvat-core": "file:../cvat-core",
"cvat-canvas": "file:../cvat-canvas",
"@types/platform": "^1.3.2",
"@types/react": "^16.9.2",
"@types/react-color": "^3.0.2",
"@types/react-dom": "^16.9.0",
"@types/react-redux": "^7.1.2",
"@types/react-router": "^5.0.5",
"@types/react-router-dom": "^5.1.0",
"@types/react-share": "^3.0.1",
"@types/redux-logger": "^3.0.7",
"antd": "^3.25.2",
"antd": "^3.26.17",
"copy-to-clipboard": "^3.2.0",
"cvat-canvas": "file:../cvat-canvas",
"cvat-core": "file:../cvat-core",
"dotenv-webpack": "^1.7.0",
"error-stack-parser": "^2.0.6",
"moment": "^2.24.0",
"platform": "^1.3.6",
"prop-types": "^15.7.2",
"react": "^16.9.0",
"react-color": "^2.18.1",
"react-dom": "^16.9.0",
"react-hotkeys": "^2.0.0",
"react-redux": "^7.1.1",

@ -8,7 +8,7 @@ import {
ActionCreator,
Store,
} from 'redux';
import { ThunkAction } from 'redux-thunk';
import { ThunkAction } from 'utils/redux';
import {
CombinedState,
@ -148,8 +148,6 @@ export enum AnnotationActionTypes {
GROUP_ANNOTATIONS_FAILED = 'GROUP_ANNOTATIONS_FAILED',
SPLIT_ANNOTATIONS_SUCCESS = 'SPLIT_ANNOTATIONS_SUCCESS',
SPLIT_ANNOTATIONS_FAILED = 'SPLIT_ANNOTATIONS_FAILED',
CHANGE_LABEL_COLOR_SUCCESS = 'CHANGE_LABEL_COLOR_SUCCESS',
CHANGE_LABEL_COLOR_FAILED = 'CHANGE_LABEL_COLOR_FAILED',
UPDATE_TAB_CONTENT_HEIGHT = 'UPDATE_TAB_CONTENT_HEIGHT',
COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR',
COLLAPSE_APPEARANCE = 'COLLAPSE_APPEARANCE',
@ -191,8 +189,7 @@ export enum AnnotationActionTypes {
SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED',
}
export function saveLogsAsync():
ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function saveLogsAsync(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>) => {
try {
await logger.save();
@ -236,7 +233,7 @@ export function switchZLayer(cur: number): AnyAction {
};
}
export function fetchAnnotationsAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function fetchAnnotationsAsync(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const {
@ -308,8 +305,7 @@ export function updateCanvasContextMenu(
};
}
export function removeAnnotationsAsync(sessionInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function removeAnnotationsAsync(sessionInstance: any): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
await sessionInstance.annotations.clear();
@ -333,8 +329,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
};
}
export function uploadJobAnnotationsAsync(job: any, loader: any, file: File):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function uploadJobAnnotationsAsync(job: any, loader: any, file: File): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const state: CombinedState = getStore().getState();
@ -404,8 +399,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
};
}
export function changeJobStatusAsync(jobInstance: any, status: string):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function changeJobStatusAsync(jobInstance: any, status: string): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const oldStatus = jobInstance.status;
try {
@ -435,8 +429,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
};
}
export function collectStatisticsAsync(sessionInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function collectStatisticsAsync(sessionInstance: any): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch({
@ -477,7 +470,7 @@ export function propagateObjectAsync(
objectState: any,
from: number,
to: number,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const copy = {
@ -490,6 +483,7 @@ export function propagateObjectAsync(
label: objectState.label,
zOrder: objectState.zOrder,
frame: from,
source: objectState.source,
};
await sessionInstance.logger.log(
@ -542,7 +536,7 @@ export function changePropagateFrames(frames: number): AnyAction {
}
export function removeObjectAsync(sessionInstance: any, objectState: any, force: boolean):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
await sessionInstance.logger.log(LogType.deleteObject, { count: 1 });
@ -659,7 +653,7 @@ export function switchPlay(playing: boolean): AnyAction {
}
export function changeFrameAsync(toFrame: number, fillBuffer?: boolean, frameStep?: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const state: CombinedState = getStore().getState();
const { instance: job } = state.annotation.job;
@ -751,7 +745,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}
export function undoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const state = getStore().getState();
@ -794,7 +788,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}
export function redoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const state = getStore().getState();
@ -906,25 +900,31 @@ export function confirmCanvasReady(): AnyAction {
};
}
export function closeJob(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const { jobInstance } = receiveAnnotationsParameters();
if (jobInstance) {
await jobInstance.task.close();
}
dispatch({
type: AnnotationActionTypes.CLOSE_JOB,
});
};
}
export function getJobAsync(
tid: number,
jid: number,
initialFrame: number,
initialFilters: string[],
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const state: CombinedState = getStore().getState();
const filters = initialFilters;
const { showAllInterpolationTracks } = state.settings.workspace;
// Check if already loaded job is different from asking one
if (state.annotation.job.instance && state.annotation.job.instance.id !== jid) {
dispatch({
type: AnnotationActionTypes.CLOSE_JOB,
});
}
dispatch({
type: AnnotationActionTypes.GET_JOB,
payload: {
@ -995,7 +995,7 @@ export function getJobAsync(
}
export function saveAnnotationsAsync(sessionInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const { filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters();
@ -1112,8 +1112,7 @@ export function splitTrack(enabled: boolean): AnyAction {
};
}
export function updateAnnotationsAsync(statesToUpdate: any[]):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function updateAnnotationsAsync(statesToUpdate: any[]): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
jobInstance,
@ -1158,7 +1157,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}
export function createAnnotationsAsync(sessionInstance: any, frame: number, statesToCreate: any[]):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
@ -1186,7 +1185,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}
export function mergeAnnotationsAsync(sessionInstance: any, frame: number, statesToMerge: any[]):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
@ -1224,7 +1223,7 @@ export function groupAnnotationsAsync(
sessionInstance: any,
frame: number,
statesToGroup: any[],
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
@ -1260,7 +1259,7 @@ export function groupAnnotationsAsync(
}
export function splitAnnotationsAsync(sessionInstance: any, frame: number, stateToSplit: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
try {
@ -1287,44 +1286,10 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
};
}
export function changeLabelColorAsync(
sessionInstance: any,
frameNumber: number,
label: any,
color: string,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
const updatedLabel = label;
updatedLabel.color = color;
const states = await sessionInstance.annotations
.get(frameNumber, showAllInterpolationTracks, filters);
const history = await sessionInstance.actions.get();
dispatch({
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS,
payload: {
label: updatedLabel,
history,
states,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_FAILED,
payload: {
error,
},
});
}
};
}
export function changeGroupColorAsync(
group: number,
color: string,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const state: CombinedState = getStore().getState();
const groupStates = state.annotation.annotations.states
@ -1342,7 +1307,7 @@ export function searchAnnotationsAsync(
sessionInstance: any,
frameFrom: number,
frameTo: number,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
@ -1361,7 +1326,7 @@ export function searchAnnotationsAsync(
};
}
export function pasteShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function pasteShapeAsync(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
canvas: {
@ -1420,7 +1385,7 @@ export function pasteShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction>
};
}
export function repeatDrawShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function repeatDrawShapeAsync(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
canvas: {
@ -1483,3 +1448,51 @@ export function repeatDrawShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAc
}
};
}
export function redrawShapeAsync(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
annotations: {
activatedStateID,
states,
},
canvas: {
instance: canvasInstance,
},
} = getStore().getState().annotation;
if (activatedStateID !== null) {
const [state] = states
.filter((_state: any): boolean => _state.clientID === activatedStateID);
if (state && state.objectType !== ObjectType.TAG) {
let activeControl = ActiveControl.CURSOR;
if (state.shapeType === ShapeType.RECTANGLE) {
activeControl = ActiveControl.DRAW_RECTANGLE;
} else if (state.shapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} else if (state.shapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (state.shapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
} else if (state.shapeType === ShapeType.CUBOID) {
activeControl = ActiveControl.DRAW_CUBOID;
}
dispatch({
type: AnnotationActionTypes.REPEAT_DRAW_SHAPE,
payload: {
activeControl,
},
});
canvasInstance.cancel();
canvasInstance.draw({
enabled: true,
redraw: activatedStateID,
shapeType: state.shapeType,
crosshair: state.shapeType === ShapeType.RECTANGLE,
});
}
}
};
}

@ -5,6 +5,7 @@
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { UserConfirmation } from 'components/register-page/register-form';
import getCore from 'cvat-core-wrapper';
import isReachable from 'utils/url-checker';
const cvat = getCore();
@ -20,9 +21,16 @@ export enum AuthActionTypes {
LOGOUT = 'LOGOUT',
LOGOUT_SUCCESS = 'LOGOUT_SUCCESS',
LOGOUT_FAILED = 'LOGOUT_FAILED',
CHANGE_PASSWORD = 'CHANGE_PASSWORD',
CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS',
CHANGE_PASSWORD_FAILED = 'CHANGE_PASSWORD_FAILED',
SWITCH_CHANGE_PASSWORD_DIALOG = 'SWITCH_CHANGE_PASSWORD_DIALOG',
LOAD_AUTH_ACTIONS = 'LOAD_AUTH_ACTIONS',
LOAD_AUTH_ACTIONS_SUCCESS = 'LOAD_AUTH_ACTIONS_SUCCESS',
LOAD_AUTH_ACTIONS_FAILED = 'LOAD_AUTH_ACTIONS_FAILED',
}
const authActions = {
export const authActions = {
authorizeSuccess: (user: any) => createAction(AuthActionTypes.AUTHORIZED_SUCCESS, { user }),
authorizeFailed: (error: any) => createAction(AuthActionTypes.AUTHORIZED_FAILED, { error }),
login: () => createAction(AuthActionTypes.LOGIN),
@ -34,6 +42,21 @@ const authActions = {
logout: () => createAction(AuthActionTypes.LOGOUT),
logoutSuccess: () => createAction(AuthActionTypes.LOGOUT_SUCCESS),
logoutFailed: (error: any) => createAction(AuthActionTypes.LOGOUT_FAILED, { error }),
changePassword: () => createAction(AuthActionTypes.CHANGE_PASSWORD),
changePasswordSuccess: () => createAction(AuthActionTypes.CHANGE_PASSWORD_SUCCESS),
changePasswordFailed: (error: any) => (
createAction(AuthActionTypes.CHANGE_PASSWORD_FAILED, { error })
),
switchChangePasswordDialog: (showChangePasswordDialog: boolean) => (
createAction(AuthActionTypes.SWITCH_CHANGE_PASSWORD_DIALOG, { showChangePasswordDialog })
),
loadServerAuthActions: () => createAction(AuthActionTypes.LOAD_AUTH_ACTIONS),
loadServerAuthActionsSuccess: (allowChangePassword: boolean) => (
createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_SUCCESS, { allowChangePassword })
),
loadServerAuthActionsFailed: (error: any) => (
createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED, { error })
),
};
export type AuthActions = ActionUnion<typeof authActions>;
@ -52,10 +75,10 @@ export const registerAsync = (
dispatch(authActions.register());
try {
await cvat.server.register(username, firstName, lastName, email, password1, password2, confirmations);
const users = await cvat.users.get({ self: true });
const user = await cvat.server.register(username, firstName, lastName, email, password1, password2,
confirmations);
dispatch(authActions.registerSuccess(users[0]));
dispatch(authActions.registerSuccess(user));
} catch (error) {
dispatch(authActions.registerFailed(error));
}
@ -99,3 +122,30 @@ export const authorizedAsync = (): ThunkAction => async (dispatch) => {
dispatch(authActions.authorizeFailed(error));
}
};
export const changePasswordAsync = (oldPassword: string,
newPassword1: string, newPassword2: string): ThunkAction => async (dispatch) => {
dispatch(authActions.changePassword());
try {
await cvat.server.changePassword(oldPassword, newPassword1, newPassword2);
dispatch(authActions.changePasswordSuccess());
} catch (error) {
dispatch(authActions.changePasswordFailed(error));
}
};
export const loadAuthActionsAsync = (): ThunkAction => async (dispatch) => {
dispatch(authActions.loadServerAuthActions());
try {
const promises: Promise<boolean>[] = [
isReachable(`${cvat.config.backendAPI}/auth/password/change`, 'OPTIONS'),
];
const [allowChangePassword] = await Promise.all(promises);
dispatch(authActions.loadServerAuthActionsSuccess(allowChangePassword));
} catch (error) {
dispatch(authActions.loadServerAuthActionsFailed(error));
}
};

@ -3,27 +3,14 @@
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import {
Model,
ModelType,
ModelFiles,
ActiveInference,
CombinedState,
} from 'reducers/interfaces';
import { Model, ActiveInference, RQStatus } from 'reducers/interfaces';
import getCore from 'cvat-core-wrapper';
export enum PreinstalledModels {
RCNN = 'RCNN Object Detector',
MaskRCNN = 'Mask RCNN Object Detector',
}
export enum ModelsActionTypes {
GET_MODELS = 'GET_MODELS',
GET_MODELS_SUCCESS = 'GET_MODELS_SUCCESS',
GET_MODELS_FAILED = 'GET_MODELS_FAILED',
DELETE_MODEL = 'DELETE_MODEL',
DELETE_MODEL_SUCCESS = 'DELETE_MODEL_SUCCESS',
DELETE_MODEL_FAILED = 'DELETE_MODEL_FAILED',
CREATE_MODEL = 'CREATE_MODEL',
CREATE_MODEL_SUCCESS = 'CREATE_MODEL_SUCCESS',
CREATE_MODEL_FAILED = 'CREATE_MODEL_FAILED',
@ -50,28 +37,6 @@ export const modelsActions = {
error,
},
),
deleteModelSuccess: (id: number) => createAction(
ModelsActionTypes.DELETE_MODEL_SUCCESS, {
id,
},
),
deleteModelFailed: (id: number, error: any) => createAction(
ModelsActionTypes.DELETE_MODEL_FAILED, {
error, id,
},
),
createModel: () => createAction(ModelsActionTypes.CREATE_MODEL),
createModelSuccess: () => createAction(ModelsActionTypes.CREATE_MODEL_SUCCESS),
createModelFailed: (error: any) => createAction(
ModelsActionTypes.CREATE_MODEL_FAILED, {
error,
},
),
createModelUpdateStatus: (status: string) => createAction(
ModelsActionTypes.CREATE_MODEL_STATUS_UPDATED, {
status,
},
),
fetchMetaFailed: (error: any) => createAction(ModelsActionTypes.FETCH_META_FAILED, { error }),
getInferenceStatusSuccess: (taskID: number, activeInference: ActiveInference) => createAction(
ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS, {
@ -96,7 +61,7 @@ export const modelsActions = {
taskID,
},
),
cancelInferenceFaild: (taskID: number, error: any) => createAction(
cancelInferenceFailed: (taskID: number, error: any) => createAction(
ModelsActionTypes.CANCEL_INFERENCE_FAILED, {
taskID,
error,
@ -113,361 +78,76 @@ export const modelsActions = {
export type ModelsActions = ActionUnion<typeof modelsActions>;
const core = getCore();
const baseURL = core.config.backendAPI.slice(0, -7);
export function getModelsAsync(): ThunkAction {
return async (dispatch, getState): Promise<void> => {
const state: CombinedState = getState();
const OpenVINO = state.plugins.list.AUTO_ANNOTATION;
const RCNN = state.plugins.list.TF_ANNOTATION;
const MaskRCNN = state.plugins.list.TF_SEGMENTATION;
return async (dispatch): Promise<void> => {
dispatch(modelsActions.getModels());
const models: Model[] = [];
try {
if (OpenVINO) {
const response = await core.server.request(
`${baseURL}/auto_annotation/meta/get`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: JSON.stringify([]),
},
);
for (const model of response.models) {
models.push({
id: model.id,
ownerID: model.owner,
primary: model.primary,
name: model.name,
uploadDate: model.uploadDate,
updateDate: model.updateDate,
labels: [...model.labels],
});
}
}
if (RCNN) {
models.push({
id: null,
ownerID: null,
primary: true,
name: PreinstalledModels.RCNN,
uploadDate: '',
updateDate: '',
labels: ['surfboard', 'car', 'skateboard', 'boat', 'clock',
'cat', 'cow', 'knife', 'apple', 'cup', 'tv',
'baseball_bat', 'book', 'suitcase', 'tennis_racket',
'stop_sign', 'couch', 'cell_phone', 'keyboard',
'cake', 'tie', 'frisbee', 'truck', 'fire_hydrant',
'snowboard', 'bed', 'vase', 'teddy_bear',
'toaster', 'wine_glass', 'traffic_light',
'broccoli', 'backpack', 'carrot', 'potted_plant',
'donut', 'umbrella', 'parking_meter', 'bottle',
'sandwich', 'motorcycle', 'bear', 'banana',
'person', 'scissors', 'elephant', 'dining_table',
'toothbrush', 'toilet', 'skis', 'bowl', 'sheep',
'refrigerator', 'oven', 'microwave', 'train',
'orange', 'mouse', 'laptop', 'bench', 'bicycle',
'fork', 'kite', 'zebra', 'baseball_glove', 'bus',
'spoon', 'horse', 'handbag', 'pizza', 'sports_ball',
'airplane', 'hair_drier', 'hot_dog', 'remote',
'sink', 'dog', 'bird', 'giraffe', 'chair',
],
});
}
if (MaskRCNN) {
models.push({
id: null,
ownerID: null,
primary: true,
name: PreinstalledModels.MaskRCNN,
uploadDate: '',
updateDate: '',
labels: ['BG', 'person', 'bicycle', 'car', 'motorcycle', 'airplane',
'bus', 'train', 'truck', 'boat', 'traffic light',
'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird',
'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear',
'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie',
'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball',
'kite', 'baseball bat', 'baseball glove', 'skateboard',
'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup',
'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza',
'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed',
'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote',
'keyboard', 'cell phone', 'microwave', 'oven', 'toaster',
'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors',
'teddy bear', 'hair drier', 'toothbrush',
],
});
}
const models = (await core.lambda.list())
.filter((model: Model) => ['detector', 'reid'].includes(model.type));
dispatch(modelsActions.getModelsSuccess(models));
} catch (error) {
dispatch(modelsActions.getModelsFailed(error));
return;
}
dispatch(modelsActions.getModelsSuccess(models));
};
}
export function deleteModelAsync(id: number): ThunkAction {
return async (dispatch): Promise<void> => {
try {
await core.server.request(`${baseURL}/auto_annotation/delete/${id}`, {
method: 'DELETE',
});
} catch (error) {
dispatch(modelsActions.deleteModelFailed(id, error));
return;
}
dispatch(modelsActions.deleteModelSuccess(id));
};
}
export function createModelAsync(name: string, files: ModelFiles, global: boolean): ThunkAction {
return async (dispatch): Promise<void> => {
async function checkCallback(id: string): Promise<void> {
try {
const data = await core.server.request(
`${baseURL}/auto_annotation/check/${id}`, {
method: 'GET',
},
);
switch (data.status) {
case 'failed':
dispatch(modelsActions.createModelFailed(
`Checking request has returned the "${data.status}" status. Message: ${data.error}`,
));
break;
case 'unknown':
dispatch(modelsActions.createModelFailed(
`Checking request has returned the "${data.status}" status.`,
));
break;
case 'finished':
dispatch(modelsActions.createModelSuccess());
break;
default:
if ('progress' in data) {
modelsActions.createModelUpdateStatus(data.progress);
}
setTimeout(checkCallback.bind(null, id), 1000);
}
} catch (error) {
dispatch(modelsActions.createModelFailed(error));
}
}
dispatch(modelsActions.createModel());
const data = new FormData();
data.append('name', name);
data.append('storage', typeof files.bin === 'string' ? 'shared' : 'local');
data.append('shared', global.toString());
Object.keys(files).reduce((acc, key: string): FormData => {
acc.append(key, files[key]);
return acc;
}, data);
try {
dispatch(modelsActions.createModelUpdateStatus('Request is beign sent..'));
const response = await core.server.request(
`${baseURL}/auto_annotation/create`, {
method: 'POST',
data,
contentType: false,
processData: false,
},
);
dispatch(modelsActions.createModelUpdateStatus('Request is being processed..'));
setTimeout(checkCallback.bind(null, response.id), 1000);
} catch (error) {
dispatch(modelsActions.createModelFailed(error));
}
};
}
interface InferenceMeta {
active: boolean;
taskID: number;
requestID: string;
modelType: ModelType;
}
const timers: any = {};
async function timeoutCallback(
url: string,
taskID: number,
modelType: ModelType,
function listen(
inferenceMeta: InferenceMeta,
dispatch: (action: ModelsActions) => void,
): Promise<void> {
try {
delete timers[taskID];
const response = await core.server.request(url, {
method: 'GET',
});
const activeInference: ActiveInference = {
status: response.status,
progress: +response.progress || 0,
error: response.error || response.stderr || '',
modelType,
};
if (activeInference.status === 'unknown') {
dispatch(modelsActions.getInferenceStatusFailed(
taskID,
new Error(
`Inference status for the task ${taskID} is unknown.`,
),
));
return;
}
if (activeInference.status === 'failed') {
): void {
const { taskID, requestID } = inferenceMeta;
core.lambda.listen(requestID, (status: RQStatus, progress: number, message: string) => {
if (status === RQStatus.failed || status === RQStatus.unknown) {
dispatch(modelsActions.getInferenceStatusFailed(
taskID,
new Error(
`Inference status for the task ${taskID} is failed. ${activeInference.error}`,
`Inference status for the task ${taskID} is ${status}. ${message}`,
),
));
return;
}
if (activeInference.status !== 'finished') {
timers[taskID] = setTimeout(
timeoutCallback.bind(
null,
url,
taskID,
modelType,
dispatch,
), 3000,
);
}
dispatch(modelsActions.getInferenceStatusSuccess(taskID, activeInference));
} catch (error) {
dispatch(modelsActions.getInferenceStatusFailed(taskID, new Error(
`Server request for the task ${taskID} was failed`,
)));
}
}
function subscribe(
inferenceMeta: InferenceMeta,
dispatch: (action: ModelsActions) => void,
): void {
if (!(inferenceMeta.taskID in timers)) {
let requestURL = `${baseURL}`;
if (inferenceMeta.modelType === ModelType.OPENVINO) {
requestURL = `${requestURL}/auto_annotation/check`;
} else if (inferenceMeta.modelType === ModelType.RCNN) {
requestURL = `${requestURL}/tensorflow/annotation/check/task`;
} else if (inferenceMeta.modelType === ModelType.MASK_RCNN) {
requestURL = `${requestURL}/tensorflow/segmentation/check/task`;
}
requestURL = `${requestURL}/${inferenceMeta.requestID}`;
timers[inferenceMeta.taskID] = setTimeout(
timeoutCallback.bind(
null,
requestURL,
inferenceMeta.taskID,
inferenceMeta.modelType,
dispatch,
),
);
}
dispatch(modelsActions.getInferenceStatusSuccess(taskID, {
status,
progress,
error: message,
id: requestID,
}));
}).catch((error: Error) => {
dispatch(modelsActions.getInferenceStatusFailed(taskID, {
status: 'unknown',
progress: 0,
error: error.toString(),
id: requestID,
}));
});
}
export function getInferenceStatusAsync(tasks: number[]): ThunkAction {
return async (dispatch, getState): Promise<void> => {
function parse(response: any, modelType: ModelType): InferenceMeta[] {
return Object.keys(response).map((key: string): InferenceMeta => ({
taskID: +key,
requestID: response[key].rq_id || key,
active: typeof (response[key].active) === 'undefined' ? ['queued', 'started']
.includes(response[key].status.toLowerCase()) : response[key].active,
modelType,
}));
}
const state: CombinedState = getState();
const OpenVINO = state.plugins.list.AUTO_ANNOTATION;
const RCNN = state.plugins.list.TF_ANNOTATION;
const MaskRCNN = state.plugins.list.TF_SEGMENTATION;
export function getInferenceStatusAsync(): ThunkAction {
return async (dispatch): Promise<void> => {
const dispatchCallback = (action: ModelsActions): void => {
dispatch(action);
};
try {
if (OpenVINO) {
const response = await core.server.request(
`${baseURL}/auto_annotation/meta/get`, {
method: 'POST',
data: JSON.stringify(tasks),
headers: {
'Content-Type': 'application/json',
},
},
);
parse(response.run, ModelType.OPENVINO)
.filter((inferenceMeta: InferenceMeta): boolean => inferenceMeta.active)
.forEach((inferenceMeta: InferenceMeta): void => {
subscribe(inferenceMeta, dispatchCallback);
});
}
if (RCNN) {
const response = await core.server.request(
`${baseURL}/tensorflow/annotation/meta/get`, {
method: 'POST',
data: JSON.stringify(tasks),
headers: {
'Content-Type': 'application/json',
},
},
);
parse(response, ModelType.RCNN)
.filter((inferenceMeta: InferenceMeta): boolean => inferenceMeta.active)
.forEach((inferenceMeta: InferenceMeta): void => {
subscribe(inferenceMeta, dispatchCallback);
});
}
if (MaskRCNN) {
const response = await core.server.request(
`${baseURL}/tensorflow/segmentation/meta/get`, {
method: 'POST',
data: JSON.stringify(tasks),
headers: {
'Content-Type': 'application/json',
},
},
);
parse(response, ModelType.MASK_RCNN)
.filter((inferenceMeta: InferenceMeta): boolean => inferenceMeta.active)
.forEach((inferenceMeta: InferenceMeta): void => {
subscribe(inferenceMeta, dispatchCallback);
});
}
const requests = await core.lambda.requests();
requests
.map((request: any): object => ({
taskID: +request.function.task,
requestID: request.id,
}))
.forEach((inferenceMeta: InferenceMeta): void => {
listen(inferenceMeta, dispatchCallback);
});
} catch (error) {
dispatch(modelsActions.fetchMetaFailed(error));
}
@ -477,37 +157,20 @@ export function getInferenceStatusAsync(tasks: number[]): ThunkAction {
export function startInferenceAsync(
taskInstance: any,
model: Model,
mapping: {
[index: string]: string;
},
cleanOut: boolean,
body: object,
): ThunkAction {
return async (dispatch): Promise<void> => {
try {
if (model.name === PreinstalledModels.RCNN) {
await core.server.request(
`${baseURL}/tensorflow/annotation/create/task/${taskInstance.id}`,
);
} else if (model.name === PreinstalledModels.MaskRCNN) {
await core.server.request(
`${baseURL}/tensorflow/segmentation/create/task/${taskInstance.id}`,
);
} else {
await core.server.request(
`${baseURL}/auto_annotation/start/${model.id}/${taskInstance.id}`, {
method: 'POST',
data: JSON.stringify({
reset: cleanOut,
labels: mapping,
}),
headers: {
'Content-Type': 'application/json',
},
},
);
}
const requestID: string = await core.lambda.run(taskInstance, model, body);
const dispatchCallback = (action: ModelsActions): void => {
dispatch(action);
};
dispatch(getInferenceStatusAsync([taskInstance.id]));
listen({
taskID: taskInstance.id,
requestID,
}, dispatchCallback);
} catch (error) {
dispatch(modelsActions.startInferenceFailed(taskInstance.id, error));
}
@ -518,30 +181,10 @@ export function cancelInferenceAsync(taskID: number): ThunkAction {
return async (dispatch, getState): Promise<void> => {
try {
const inference = getState().models.inferences[taskID];
if (inference) {
if (inference.modelType === ModelType.OPENVINO) {
await core.server.request(
`${baseURL}/auto_annotation/cancel/${taskID}`,
);
} else if (inference.modelType === ModelType.RCNN) {
await core.server.request(
`${baseURL}/tensorflow/annotation/cancel/task/${taskID}`,
);
} else if (inference.modelType === ModelType.MASK_RCNN) {
await core.server.request(
`${baseURL}/tensorflow/segmentation/cancel/task/${taskID}`,
);
}
if (timers[taskID]) {
clearTimeout(timers[taskID]);
delete timers[taskID];
}
}
await core.lambda.cancel(inference.id);
dispatch(modelsActions.cancelInferenceSuccess(taskID));
} catch (error) {
dispatch(modelsActions.cancelInferenceFaild(taskID, error));
dispatch(modelsActions.cancelInferenceFailed(taskID, error));
}
};
}

@ -8,7 +8,7 @@ import PluginChecker from 'utils/plugin-checker';
export enum PluginsActionTypes {
CHECK_PLUGINS = 'CHECK_PLUGINS',
CHECKED_ALL_PLUGINS = 'CHECKED_ALL_PLUGINS'
CHECKED_ALL_PLUGINS = 'CHECKED_ALL_PLUGINS',
}
type PluginObjects = Record<SupportedPlugins, boolean>;
@ -29,27 +29,20 @@ export function checkPluginsAsync(): ThunkAction {
dispatch(pluginActions.checkPlugins());
const plugins: PluginObjects = {
ANALYTICS: false,
AUTO_ANNOTATION: false,
GIT_INTEGRATION: false,
TF_ANNOTATION: false,
TF_SEGMENTATION: false,
REID: false,
DEXTR_SEGMENTATION: false,
};
const promises: Promise<boolean>[] = [
// check must return true/false with no exceptions
PluginChecker.check(SupportedPlugins.ANALYTICS),
PluginChecker.check(SupportedPlugins.AUTO_ANNOTATION),
PluginChecker.check(SupportedPlugins.GIT_INTEGRATION),
PluginChecker.check(SupportedPlugins.TF_ANNOTATION),
PluginChecker.check(SupportedPlugins.TF_SEGMENTATION),
PluginChecker.check(SupportedPlugins.DEXTR_SEGMENTATION),
PluginChecker.check(SupportedPlugins.REID),
];
const values = await Promise.all(promises);
[plugins.ANALYTICS, plugins.AUTO_ANNOTATION, plugins.GIT_INTEGRATION, plugins.TF_ANNOTATION,
plugins.TF_SEGMENTATION, plugins.DEXTR_SEGMENTATION, plugins.REID] = values;
[plugins.ANALYTICS, plugins.GIT_INTEGRATION,
plugins.DEXTR_SEGMENTATION] = values;
dispatch(pluginActions.checkedAllPlugins(plugins));
};
}

@ -32,6 +32,8 @@ export enum SettingsActionTypes {
SWITCH_AUTOMATIC_BORDERING = 'SWITCH_AUTOMATIC_BORDERING',
SWITCH_SHOWNIG_INTERPOLATED_TRACKS = 'SWITCH_SHOWNIG_INTERPOLATED_TRACKS',
SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS = 'SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS',
CHANGE_CANVAS_BACKGROUND_COLOR = 'CHANGE_CANVAS_BACKGROUND_COLOR',
SWITCH_SETTINGS_DIALOG = 'SWITCH_SETTINGS_DIALOG',
}
export function changeShapesOpacity(opacity: number): AnyAction {
@ -240,3 +242,21 @@ export function switchAutomaticBordering(automaticBordering: boolean): AnyAction
},
};
}
export function changeCanvasBackgroundColor(color: string): AnyAction {
return {
type: SettingsActionTypes.CHANGE_CANVAS_BACKGROUND_COLOR,
payload: {
color,
},
};
}
export function switchSettingsDialog(show?: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_SETTINGS_DIALOG,
payload: {
show,
},
};
}

@ -102,13 +102,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
const promises = array
.map((task): string => (task as any).frames.preview());
dispatch(
getInferenceStatusAsync(
array.map(
(task: any): number => task.id,
),
),
);
dispatch(getInferenceStatusAsync());
for (const promise of promises) {
try {
@ -350,10 +344,12 @@ function createTask(): AnyAction {
return action;
}
function createTaskSuccess(): AnyAction {
function createTaskSuccess(taskId: number): AnyAction {
const action = {
type: TasksActionTypes.CREATE_TASK_SUCCESS,
payload: {},
payload: {
taskId,
},
};
return action;
@ -439,10 +435,10 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
dispatch(createTask());
try {
await taskInstance.save((status: string): void => {
const savedTask = await taskInstance.save((status: string): void => {
dispatch(createTaskUpdateStatus(status));
});
dispatch(createTaskSuccess());
dispatch(createTaskSuccess(savedTask.id));
} catch (error) {
dispatch(createTaskFailed(error));
}

@ -4,7 +4,7 @@
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core-wrapper';
import { UserAgreement } from 'reducers/interfaces'
import { UserAgreement } from 'reducers/interfaces';
const core = getCore();
@ -16,10 +16,12 @@ export enum UserAgreementsActionTypes {
const userAgreementsActions = {
getUserAgreements: () => createAction(UserAgreementsActionTypes.GET_USER_AGREEMENTS),
getUserAgreementsSuccess: (userAgreements: UserAgreement[]) =>
createAction(UserAgreementsActionTypes.GET_USER_AGREEMENTS_SUCCESS, userAgreements),
getUserAgreementsFailed: (error: any) =>
createAction(UserAgreementsActionTypes.GET_USER_AGREEMENTS_FAILED, { error }),
getUserAgreementsSuccess: (userAgreements: UserAgreement[]) => (
createAction(UserAgreementsActionTypes.GET_USER_AGREEMENTS_SUCCESS, userAgreements)
),
getUserAgreementsFailed: (error: any) => (
createAction(UserAgreementsActionTypes.GET_USER_AGREEMENTS_FAILED, { error })
),
};
export type UserAgreementsActions = ActionUnion<typeof userAgreementsActions>;

@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 14 14" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M13.7735 2.04722L11.9536 0.227468C11.6503 -0.0758228 11.1603 -0.0758228 10.857 0.227468L8.43052 2.6538L6.92951 1.16845L5.83292 2.26496L6.93729 3.36925L0 10.3061V14H3.69419L10.6315 7.06318L11.7358 8.16748L12.8324 7.07096L11.3392 5.57784L13.7657 3.15151C14.0768 2.84044 14.0768 2.35051 13.7735 2.04722V2.04722ZM3.04867 12.4447L1.55545 10.9515L7.8239 4.68352L9.31712 6.17664L3.04867 12.4447Z"/>
</svg>

After

Width:  |  Height:  |  Size: 514 B

@ -0,0 +1,4 @@
<svg width="1em" height="1em" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M213 32L240 64H220V192H240L212.5 224L185 192H205V64H185L213 32Z" fill="black"/>
<path d="M32 64L33.1578 56.5899L24.5 55.2371V64H32ZM32 192H24.5V200.628L33.0444 199.427L32 192ZM160 174L161.044 181.427L167.5 180.519V174H160ZM160 84H167.5V77.5809L161.158 76.5899L160 84ZM24.5 64V192H39.5V64H24.5ZM33.0444 199.427L161.044 181.427L158.956 166.573L30.9556 184.573L33.0444 199.427ZM161.158 76.5899L33.1578 56.5899L30.8422 71.4101L158.842 91.4101L161.158 76.5899ZM167.5 174V84H152.5V174H167.5Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 658 B

@ -2,26 +2,27 @@
//
// SPDX-License-Identifier: MIT
$header-color: #D8D8D8;
$header-color: #d8d8d8;
$text-color: #303030;
$hover-menu-color: rgba(24,144,255,0.05);
$completed-progress-color: #61C200;
$inprogress-progress-color: #1890FF;
$pending-progress-color: #C1C1C1;
$hover-menu-color: rgba(24, 144, 255, 0.05);
$completed-progress-color: #61c200;
$inprogress-progress-color: #1890ff;
$pending-progress-color: #c1c1c1;
$border-color-1: #c3c3c3;
$border-color-2: #d9d9d9;
$border-color-3: #242424;
$border-color-hover: #40a9ff;
$background-color-1: white;
$background-color-2: #F1F1F1;
$background-color-2: #f1f1f1;
$transparent-color: rgba(0, 0, 0, 0);
$player-slider-color: #979797;
$player-buttons-color: #242424;
$danger-icon-color: #FF4136;
$info-icon-color: #0074D9;
$objects-bar-tabs-color: #BEBEBE;
$objects-bar-icons-color: #242424; // #6E6E6E
$active-object-item-background-color: #D8ECFF;
$slider-color: #1890FF;
$danger-icon-color: #ff4136;
$info-icon-color: #0074d9;
$objects-bar-tabs-color: #bebebe;
$objects-bar-icons-color: #242424; // #6e6e6e
$active-label-background-color: #d8ecff;
$object-item-border-color: #000;
$slider-color: #1890ff;
$monospaced-fonts-stack: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
$monospaced-fonts-stack: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;

@ -15,16 +15,11 @@ interface Props {
taskID: number;
taskMode: string;
bugTracker: string;
loaders: string[];
dumpers: string[];
loaders: any[];
dumpers: any[];
loadActivity: string | null;
dumpActivities: string[] | null;
exportActivities: string[] | null;
installedTFAnnotation: boolean;
installedTFSegmentation: boolean;
installedAutoAnnotation: boolean;
inferenceIsActive: boolean;
onClickMenu: (params: ClickParam, file?: File) => void;
@ -44,12 +39,7 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
taskID,
taskMode,
bugTracker,
installedAutoAnnotation,
installedTFAnnotation,
installedTFSegmentation,
inferenceIsActive,
dumpers,
loaders,
onClickMenu,
@ -58,9 +48,6 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
loadActivity,
} = props;
const renderModelRunner = installedAutoAnnotation
|| installedTFAnnotation || installedTFSegmentation;
let latestParams: ClickParam | null = null;
function onClickMenuWrapper(params: ClickParam | null, file?: File): void {
const copyParams = params || latestParams;
@ -137,17 +124,12 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
})
}
{!!bugTracker && <Menu.Item key={Actions.OPEN_BUG_TRACKER}>Open bug tracker</Menu.Item>}
{
renderModelRunner
&& (
<Menu.Item
disabled={inferenceIsActive}
key={Actions.RUN_AUTO_ANNOTATION}
>
Automatic annotation
</Menu.Item>
)
}
<Menu.Item
disabled={inferenceIsActive}
key={Actions.RUN_AUTO_ANNOTATION}
>
Automatic annotation
</Menu.Item>
<hr />
<Menu.Item key={Actions.DELETE_TASK}>Delete</Menu.Item>
</Menu>

@ -15,7 +15,7 @@ function isDefaultFormat(dumperName: string, taskMode: string): boolean {
interface Props {
taskMode: string;
menuKey: string;
dumpers: string[];
dumpers: any[];
dumpActivities: string[] | null;
}
@ -30,21 +30,24 @@ export default function DumpSubmenu(props: Props): JSX.Element {
return (
<Menu.SubMenu key={menuKey} title='Dump annotations'>
{
dumpers.map((dumper: string): JSX.Element => {
const pending = (dumpActivities || []).includes(dumper);
const isDefault = isDefaultFormat(dumper, taskMode);
return (
<Menu.Item
key={dumper}
disabled={pending}
className='cvat-menu-dump-submenu-item'
>
<Icon type='download' />
<Text strong={isDefault}>{dumper}</Text>
{pending && <Icon style={{ marginLeft: 10 }} type='loading' />}
</Menu.Item>
);
})
dumpers
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.map((dumper: any): JSX.Element => {
const pending = (dumpActivities || []).includes(dumper.name);
const disabled = !dumper.enabled || pending;
const isDefault = isDefaultFormat(dumper.name, taskMode);
return (
<Menu.Item
key={dumper.name}
disabled={disabled}
className='cvat-menu-dump-submenu-item'
>
<Icon type='download' />
<Text strong={isDefault} disabled={disabled}>{dumper.name}</Text>
{pending && <Icon style={{ marginLeft: 10 }} type='loading' />}
</Menu.Item>
);
})
}
</Menu.SubMenu>
);

@ -9,7 +9,7 @@ import Text from 'antd/lib/typography/Text';
interface Props {
menuKey: string;
exporters: string[];
exporters: any[];
exportActivities: string[] | null;
}
@ -23,20 +23,23 @@ export default function ExportSubmenu(props: Props): JSX.Element {
return (
<Menu.SubMenu key={menuKey} title='Export as a dataset'>
{
exporters.map((exporter: string): JSX.Element => {
const pending = (exportActivities || []).includes(exporter);
return (
<Menu.Item
key={exporter}
disabled={pending}
className='cvat-menu-export-submenu-item'
>
<Icon type='export' />
<Text>{exporter}</Text>
{pending && <Icon style={{ marginLeft: 10 }} type='loading' />}
</Menu.Item>
);
})
exporters
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.map((exporter: any): JSX.Element => {
const pending = (exportActivities || []).includes(exporter.name);
const disabled = !exporter.enabled || pending;
return (
<Menu.Item
key={exporter.name}
disabled={disabled}
className='cvat-menu-export-submenu-item'
>
<Icon type='export' />
<Text disabled={disabled}>{exporter.name}</Text>
{pending && <Icon style={{ marginLeft: 10 }} type='loading' />}
</Menu.Item>
);
})
}
</Menu.SubMenu>
);

@ -11,7 +11,7 @@ import Text from 'antd/lib/typography/Text';
interface Props {
menuKey: string;
loaders: string[];
loaders: any[];
loadActivity: string | null;
onFileUpload(file: File): void;
}
@ -27,34 +27,40 @@ export default function LoadSubmenu(props: Props): JSX.Element {
return (
<Menu.SubMenu key={menuKey} title='Upload annotations'>
{
loaders.map((_loader: string): JSX.Element => {
const [loader, accept] = _loader.split('::');
const pending = loadActivity === loader;
return (
<Menu.Item
key={loader}
disabled={!!loadActivity}
className='cvat-menu-load-submenu-item'
>
<Upload
accept={accept}
multiple={false}
showUploadList={false}
beforeUpload={(file: File): boolean => {
onFileUpload(file);
return false;
}}
loaders
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.map((loader: any): JSX.Element => {
const accept = loader.format
.split(',')
.map((x: string) => `.${x.trimStart()}`)
.join(', '); // add '.' to each extension in a list
const pending = loadActivity === loader.name;
const disabled = !loader.enabled || !!loadActivity;
return (
<Menu.Item
key={loader.name}
disabled={disabled}
className='cvat-menu-load-submenu-item'
>
<Button block type='link' disabled={!!loadActivity}>
<Icon type='upload' />
<Text>{loader}</Text>
{pending && <Icon style={{ marginLeft: 10 }} type='loading' />}
</Button>
</Upload>
<Upload
accept={accept}
multiple={false}
showUploadList={false}
beforeUpload={(file: File): boolean => {
onFileUpload(file);
return false;
}}
>
<Button block type='link' disabled={disabled}>
<Icon type='upload' />
<Text>{loader.name}</Text>
{pending && <Icon style={{ marginLeft: 10 }} type='loading' />}
</Button>
</Upload>
</Menu.Item>
);
})
</Menu.Item>
);
})
}
</Menu.SubMenu>
);

@ -5,14 +5,14 @@
@import '../../base.scss';
.ant-menu.cvat-actions-menu {
box-shadow: 0 0 17px rgba(0,0,0,0.2);
box-shadow: 0 0 17px rgba(0, 0, 0, 0.2);
> li:hover {
background-color: $hover-menu-color;
}
.ant-menu-submenu-title {
margin: 0px;
margin: 0;
width: 13em;
}
}
@ -30,10 +30,10 @@
}
.ant-menu-item.cvat-menu-load-submenu-item {
margin: 0px;
padding: 0px;
margin: 0;
padding: 0;
> span > .ant-upload {
> span > .ant-upload {
width: 100%;
height: 100%;

@ -4,6 +4,7 @@
import './styles.scss';
import React, { useEffect } from 'react';
import { useHistory } from 'react-router';
import Layout from 'antd/lib/layout';
import Spin from 'antd/lib/spin';
import Result from 'antd/lib/result';
@ -13,12 +14,14 @@ import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-ba
import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal';
import StandardWorkspaceComponent from './standard-workspace/standard-workspace';
import AttributeAnnotationWorkspace from './attribute-annotation-workspace/attribute-annotation-workspace';
import TagAnnotationWorkspace from './tag-annotation-workspace/tag-annotation-workspace';
interface Props {
job: any | null | undefined;
fetching: boolean;
getJob(): void;
saveLogs(): void;
closeJob(): void;
workspace: Workspace;
}
@ -27,10 +30,12 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
job,
fetching,
getJob,
closeJob,
saveLogs,
workspace,
} = props;
const history = useHistory();
useEffect(() => {
saveLogs();
const root = window.document.getElementById('root');
@ -43,6 +48,10 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
if (root) {
root.style.minHeight = '';
}
if (!history.location.pathname.includes('/jobs')) {
closeJob();
}
};
}, []);
@ -70,15 +79,21 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
<Layout.Header className='cvat-annotation-header'>
<AnnotationTopBarContainer />
</Layout.Header>
{ workspace === Workspace.STANDARD ? (
<Layout.Content>
{ workspace === Workspace.STANDARD && (
<Layout.Content style={{ height: '100%' }}>
<StandardWorkspaceComponent />
</Layout.Content>
) : (
<Layout.Content>
)}
{ workspace === Workspace.ATTRIBUTE_ANNOTATION && (
<Layout.Content style={{ height: '100%' }}>
<AttributeAnnotationWorkspace />
</Layout.Content>
)}
{ workspace === Workspace.TAG_ANNOTATION && (
<Layout.Content style={{ height: '100%' }}>
<TagAnnotationWorkspace />
</Layout.Content>
)}
<StatisticsModalContainer />
</Layout>
);

@ -147,7 +147,7 @@ function AnnotationsFiltersInput(props: StateToProps & DispatchToProps): JSX.Ele
placeholder={
underCursor ? (
<>
<Tooltip title='Click to open help'>
<Tooltip title='Click to open help' mouseLeaveDelay={0}>
<Icon
type='filter'
onClick={(e: React.MouseEvent) => {

@ -0,0 +1,214 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { Dispatch } from 'react';
import { AnyAction } from 'redux';
import { connect } from 'react-redux';
import Text from 'antd/lib/typography/Text';
import Radio, { RadioChangeEvent } from 'antd/lib/radio';
import Slider, { SliderValue } from 'antd/lib/slider';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
import Collapse from 'antd/lib/collapse';
import { ColorBy, CombinedState } from 'reducers/interfaces';
import {
collapseAppearance as collapseAppearanceAction,
updateTabContentHeight as updateTabContentHeightAction,
} from 'actions/annotation-actions';
import {
changeShapesColorBy as changeShapesColorByAction,
changeShapesOpacity as changeShapesOpacityAction,
changeSelectedShapesOpacity as changeSelectedShapesOpacityAction,
changeShapesBlackBorders as changeShapesBlackBordersAction,
changeShowBitmap as changeShowBitmapAction,
changeShowProjections as changeShowProjectionsAction,
} from 'actions/settings-actions';
interface StateToProps {
appearanceCollapsed: boolean;
colorBy: ColorBy;
opacity: number;
selectedOpacity: number;
blackBorders: boolean;
showBitmap: boolean;
showProjections: boolean;
}
interface DispatchToProps {
collapseAppearance(): void;
changeShapesColorBy(event: RadioChangeEvent): void;
changeShapesOpacity(event: SliderValue): void;
changeSelectedShapesOpacity(event: SliderValue): void;
changeShapesBlackBorders(event: CheckboxChangeEvent): void;
changeShowBitmap(event: CheckboxChangeEvent): void;
changeShowProjections(event: CheckboxChangeEvent): void;
}
export function computeHeight(): number {
const [sidebar] = window.document.getElementsByClassName('cvat-objects-sidebar');
const [appearance] = window.document.getElementsByClassName('cvat-objects-appearance-collapse');
const [tabs] = Array.from(
window.document.querySelectorAll('.cvat-objects-sidebar-tabs > .ant-tabs-card-bar'),
);
if (sidebar && appearance && tabs) {
const maxHeight = sidebar ? sidebar.clientHeight : 0;
const appearanceHeight = appearance ? appearance.clientHeight : 0;
const tabsHeight = tabs ? tabs.clientHeight : 0;
return maxHeight - appearanceHeight - tabsHeight;
}
return 0;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
appearanceCollapsed,
},
settings: {
shapes: {
colorBy,
opacity,
selectedOpacity,
blackBorders,
showBitmap,
showProjections,
},
},
} = state;
return {
appearanceCollapsed,
colorBy,
opacity,
selectedOpacity,
blackBorders,
showBitmap,
showProjections,
};
}
function mapDispatchToProps(dispatch: Dispatch<AnyAction>): DispatchToProps {
return {
collapseAppearance(): void {
dispatch(collapseAppearanceAction());
const [collapser] = window.document
.getElementsByClassName('cvat-objects-appearance-collapse');
if (collapser) {
const listener = (event: Event): void => {
if ((event as TransitionEvent).propertyName === 'height') {
const height = computeHeight();
dispatch(updateTabContentHeightAction(height));
collapser.removeEventListener('transitionend', listener);
}
};
collapser.addEventListener('transitionend', listener);
}
},
changeShapesColorBy(event: RadioChangeEvent): void {
dispatch(changeShapesColorByAction(event.target.value));
},
changeShapesOpacity(value: SliderValue): void {
dispatch(changeShapesOpacityAction(value as number));
},
changeSelectedShapesOpacity(value: SliderValue): void {
dispatch(changeSelectedShapesOpacityAction(value as number));
},
changeShapesBlackBorders(event: CheckboxChangeEvent): void {
dispatch(changeShapesBlackBordersAction(event.target.checked));
},
changeShowBitmap(event: CheckboxChangeEvent): void {
dispatch(changeShowBitmapAction(event.target.checked));
},
changeShowProjections(event: CheckboxChangeEvent): void {
dispatch(changeShowProjectionsAction(event.target.checked));
},
};
}
type Props = StateToProps & DispatchToProps;
function AppearanceBlock(props: Props): JSX.Element {
const {
appearanceCollapsed,
colorBy,
opacity,
selectedOpacity,
blackBorders,
showBitmap,
showProjections,
collapseAppearance,
changeShapesColorBy,
changeShapesOpacity,
changeSelectedShapesOpacity,
changeShapesBlackBorders,
changeShowBitmap,
changeShowProjections,
} = props;
return (
<Collapse
onChange={collapseAppearance}
activeKey={appearanceCollapsed ? [] : ['appearance']}
className='cvat-objects-appearance-collapse'
>
<Collapse.Panel
header={
<Text strong>Appearance</Text>
}
key='appearance'
>
<div className='cvat-objects-appearance-content'>
<Text type='secondary'>Color by</Text>
<Radio.Group value={colorBy} onChange={changeShapesColorBy}>
<Radio.Button value={ColorBy.LABEL}>{ColorBy.LABEL}</Radio.Button>
<Radio.Button value={ColorBy.INSTANCE}>{ColorBy.INSTANCE}</Radio.Button>
<Radio.Button value={ColorBy.GROUP}>{ColorBy.GROUP}</Radio.Button>
</Radio.Group>
<Text type='secondary'>Opacity</Text>
<Slider
onChange={changeShapesOpacity}
value={opacity}
min={0}
max={100}
/>
<Text type='secondary'>Selected opacity</Text>
<Slider
onChange={changeSelectedShapesOpacity}
value={selectedOpacity}
min={0}
max={100}
/>
<Checkbox
onChange={changeShapesBlackBorders}
checked={blackBorders}
>
Black borders
</Checkbox>
<Checkbox
onChange={changeShowBitmap}
checked={showBitmap}
>
Show bitmap
</Checkbox>
<Checkbox
onChange={changeShowProjections}
checked={showProjections}
>
Show projections
</Checkbox>
</div>
</Collapse.Panel>
</Collapse>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(React.memo(AppearanceBlock));

@ -5,22 +5,25 @@
import React, { useState, useEffect } from 'react';
import { GlobalHotKeys, ExtendedKeyMapOptions } from 'react-hotkeys';
import { connect } from 'react-redux';
import { Action } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import Layout, { SiderProps } from 'antd/lib/layout';
import { SelectValue } from 'antd/lib/select';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { Row, Col } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text';
import Icon from 'antd/lib/icon';
import { ThunkDispatch } from 'utils/redux';
import { Canvas } from 'cvat-canvas-wrapper';
import { LogType } from 'cvat-logger';
import {
activateObject as activateObjectAction,
updateAnnotationsAsync,
changeFrameAsync,
} from 'actions/annotation-actions';
import { CombinedState } from 'reducers/interfaces';
import { CombinedState, ObjectType } from 'reducers/interfaces';
import AnnotationsFiltersInput from 'components/annotation-page/annotations-filters-input';
import AppearanceBlock from 'components/annotation-page/appearance-block';
import ObjectButtonsContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-buttons';
import ObjectSwitcher from './object-switcher';
import AttributeSwitcher from './attribute-switcher';
import ObjectBasicsEditor from './object-basics-edtior';
@ -35,11 +38,15 @@ interface StateToProps {
jobInstance: any;
keyMap: Record<string, ExtendedKeyMapOptions>;
normalizedKeyMap: Record<string, string>;
canvasInstance: Canvas;
canvasIsReady: boolean;
curZLayer: number;
}
interface DispatchToProps {
activateObject(clientID: number | null, attrID: number | null): void;
updateAnnotations(statesToUpdate: any[]): void;
changeFrame(frame: number): void;
}
interface LabelAttrMap {
@ -53,11 +60,18 @@ function mapStateToProps(state: CombinedState): StateToProps {
activatedStateID,
activatedAttributeID,
states,
zLayer: {
cur,
},
},
job: {
instance: jobInstance,
labels,
},
canvas: {
instance: canvasInstance,
ready: canvasIsReady,
},
},
shortcuts: {
keyMap,
@ -73,10 +87,13 @@ function mapStateToProps(state: CombinedState): StateToProps {
states,
keyMap,
normalizedKeyMap,
canvasInstance,
canvasIsReady,
curZLayer: cur,
};
}
function mapDispatchToProps(dispatch: ThunkDispatch<CombinedState, {}, Action>): DispatchToProps {
function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps {
return {
activateObject(clientID: number, attrID: number): void {
dispatch(activateObjectAction(clientID, attrID));
@ -84,6 +101,9 @@ function mapDispatchToProps(dispatch: ThunkDispatch<CombinedState, {}, Action>):
updateAnnotations(states): void {
dispatch(updateAnnotationsAsync(states));
},
changeFrame(frame: number): void {
dispatch(changeFrameAsync(frame));
},
};
}
@ -95,11 +115,18 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
activatedAttributeID,
jobInstance,
updateAnnotations,
changeFrame,
activateObject,
keyMap,
normalizedKeyMap,
canvasInstance,
canvasIsReady,
curZLayer,
} = props;
const filteredStates = states.filter((state) => !state.outside
&& !state.hidden
&& state.zOrder <= curZLayer);
const [labelAttrMap, setLabelAttrMap] = useState(
labels.reduce((acc, label): LabelAttrMap => {
acc[label.id] = label.attributes.length ? label.attributes[0] : null;
@ -109,36 +136,52 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [activeObjectState] = activatedStateID === null
? [null] : states.filter((objectState: any): boolean => (
objectState.clientID === activatedStateID
));
const collapse = (): void => {
const [collapser] = window.document
.getElementsByClassName('attribute-annotation-sidebar');
if (collapser) {
collapser.addEventListener('transitionend', () => {
canvasInstance.fitCanvas();
}, { once: true });
}
setSidebarCollapsed(!sidebarCollapsed);
};
const indexes = filteredStates.map((state) => state.clientID);
const activatedIndex = indexes.indexOf(activatedStateID);
const activeObjectState = activatedStateID === null || activatedIndex === -1
? null : filteredStates[activatedIndex];
const activeAttribute = activeObjectState
? labelAttrMap[activeObjectState.label.id]
: null;
if (activeObjectState) {
const attribute = labelAttrMap[activeObjectState.label.id];
if (attribute && attribute.id !== activatedAttributeID) {
activateObject(activatedStateID, attribute ? attribute.id : null);
if (canvasIsReady) {
if (activeObjectState) {
const attribute = labelAttrMap[activeObjectState.label.id];
if (attribute && attribute.id !== activatedAttributeID) {
activateObject(activatedStateID, attribute ? attribute.id : null);
}
} else if (filteredStates.length) {
const attribute = labelAttrMap[filteredStates[0].label.id];
activateObject(filteredStates[0].clientID, attribute ? attribute.id : null);
}
} else if (states.length) {
const attribute = labelAttrMap[states[0].label.id];
activateObject(states[0].clientID, attribute ? attribute.id : null);
}
const nextObject = (step: number): void => {
if (states.length) {
const index = states.indexOf(activeObjectState);
if (filteredStates.length) {
const index = filteredStates.indexOf(activeObjectState);
let nextIndex = index + step;
if (nextIndex > states.length - 1) {
if (nextIndex > filteredStates.length - 1) {
nextIndex = 0;
} else if (nextIndex < 0) {
nextIndex = states.length - 1;
nextIndex = filteredStates.length - 1;
}
if (nextIndex !== index) {
const attribute = labelAttrMap[states[nextIndex].label.id];
activateObject(states[nextIndex].clientID, attribute ? attribute.id : null);
const attribute = labelAttrMap[filteredStates[nextIndex].label.id];
activateObject(filteredStates[nextIndex].clientID, attribute ? attribute.id : null);
}
}
};
@ -181,42 +224,74 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
collapsed: sidebarCollapsed,
};
const preventDefault = (event: KeyboardEvent | undefined): void => {
if (event) {
event.preventDefault();
}
};
const subKeyMap = {
NEXT_ATTRIBUTE: keyMap.NEXT_ATTRIBUTE,
PREVIOUS_ATTRIBUTE: keyMap.PREVIOUS_ATTRIBUTE,
NEXT_OBJECT: keyMap.NEXT_OBJECT,
PREVIOUS_OBJECT: keyMap.PREVIOUS_OBJECT,
SWITCH_LOCK: keyMap.SWITCH_LOCK,
SWITCH_OCCLUDED: keyMap.SWITCH_OCCLUDED,
NEXT_KEY_FRAME: keyMap.NEXT_KEY_FRAME,
PREV_KEY_FRAME: keyMap.PREV_KEY_FRAME,
};
const handlers = {
NEXT_ATTRIBUTE: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
preventDefault(event);
nextAttribute(1);
},
PREVIOUS_ATTRIBUTE: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
preventDefault(event);
nextAttribute(-1);
},
NEXT_OBJECT: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
preventDefault(event);
nextObject(1);
},
PREVIOUS_OBJECT: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
preventDefault(event);
nextObject(-1);
},
SWITCH_LOCK: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (activeObjectState) {
activeObjectState.lock = !activeObjectState.lock;
updateAnnotations([activeObjectState]);
}
},
SWITCH_OCCLUDED: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (activeObjectState && activeObjectState.objectType !== ObjectType.TAG) {
activeObjectState.occluded = !activeObjectState.occluded;
updateAnnotations([activeObjectState]);
}
},
NEXT_KEY_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (activeObjectState && activeObjectState.objectType === ObjectType.TRACK) {
const frame = typeof (activeObjectState.keyframes.next) === 'number'
? activeObjectState.keyframes.next : null;
if (frame !== null && canvasInstance.isAbleToChangeFrame()) {
changeFrame(frame);
}
}
},
PREV_KEY_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (activeObjectState && activeObjectState.objectType === ObjectType.TRACK) {
const frame = typeof (activeObjectState.keyframes.prev) === 'number'
? activeObjectState.keyframes.prev : null;
if (frame !== null && canvasInstance.isAbleToChangeFrame()) {
changeFrame(frame);
}
}
},
};
if (activeObjectState) {
@ -227,7 +302,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
className={`cvat-objects-sidebar-sider
ant-layout-sider-zero-width-trigger
ant-layout-sider-zero-width-trigger-left`}
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
onClick={collapse}
>
{sidebarCollapsed ? <Icon type='menu-fold' title='Show' />
: <Icon type='menu-unfold' title='Hide' />}
@ -242,15 +317,14 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
currentLabel={activeObjectState.label.name}
clientID={activeObjectState.clientID}
occluded={activeObjectState.occluded}
objectsCount={states.length}
currentIndex={states.indexOf(activeObjectState)}
objectsCount={filteredStates.length}
currentIndex={filteredStates.indexOf(activeObjectState)}
normalizedKeyMap={normalizedKeyMap}
nextObject={nextObject}
/>
<ObjectBasicsEditor
currentLabel={activeObjectState.label.name}
labels={labels}
occluded={activeObjectState.occluded}
changeLabel={(value: SelectValue): void => {
const labelName = value as string;
const [newLabel] = labels
@ -258,10 +332,12 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
activeObjectState.label = newLabel;
updateAnnotations([activeObjectState]);
}}
setOccluded={(event: CheckboxChangeEvent): void => {
activeObjectState.occluded = event.target.checked;
updateAnnotations([activeObjectState]);
}}
/>
<ObjectButtonsContainer
clientID={activeObjectState.clientID}
outsideDisabled
hiddenDisabled
keyframeDisabled
/>
{
activeAttribute
@ -276,6 +352,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
nextAttribute={nextAttribute}
/>
<AttributeEditor
clientID={activeObjectState.clientID}
attribute={activeAttribute}
currentValue={activeObjectState.attributes[activeAttribute.id]}
onChange={(value: string) => {
@ -300,12 +377,29 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
</div>
)
}
{ !sidebarCollapsed && <AppearanceBlock /> }
</Layout.Sider>
);
}
return (
<Layout.Sider {...siderProps}>
{/* eslint-disable-next-line */}
<span
className={`cvat-objects-sidebar-sider
ant-layout-sider-zero-width-trigger
ant-layout-sider-zero-width-trigger-left`}
onClick={collapse}
>
{sidebarCollapsed ? <Icon type='menu-fold' title='Show' />
: <Icon type='menu-unfold' title='Hide' />}
</span>
<Row className='cvat-objects-sidebar-filter-input'>
<Col>
<AnnotationsFiltersInput />
</Col>
</Row>
<div className='attribute-annotations-sidebar-not-found-wrapper'>
<Text strong>No objects found</Text>
</div>

@ -13,6 +13,7 @@ import Input from 'antd/lib/input';
import consts from 'consts';
interface InputElementParameters {
clientID: number;
attrID: number;
inputType: string;
values: string[];
@ -24,6 +25,7 @@ function renderInputElement(parameters: InputElementParameters): JSX.Element {
const {
inputType,
attrID,
clientID,
values,
currentValue,
onChange,
@ -103,7 +105,7 @@ function renderInputElement(parameters: InputElementParameters): JSX.Element {
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Input
autoFocus
key={attrID}
key={`${clientID}:${attrID}`}
defaultValue={currentValue}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
@ -257,13 +259,19 @@ function renderList(parameters: ListParameters): JSX.Element | null {
}
interface Props {
clientID: number;
attribute: any;
currentValue: string;
onChange(value: string): void;
}
function AttributeEditor(props: Props): JSX.Element {
const { attribute, currentValue, onChange } = props;
const {
attribute,
currentValue,
onChange,
clientID,
} = props;
const { inputType, values, id: attrID } = attribute;
return (
@ -271,6 +279,7 @@ function AttributeEditor(props: Props): JSX.Element {
{renderList({ values, inputType, onChange })}
<hr />
{renderInputElement({
clientID,
attrID,
inputType,
currentValue,

@ -27,17 +27,17 @@ function AttributeSwitcher(props: Props): JSX.Element {
const title = `${currentAttribute} [${currentIndex + 1}/${attributesCount}]`;
return (
<div className='attribute-annotation-sidebar-switcher'>
<Tooltip title={`Previous attribute ${normalizedKeyMap.PREVIOUS_ATTRIBUTE}`}>
<div className='attribute-annotation-sidebar-attribute-switcher'>
<Tooltip title={`Previous attribute ${normalizedKeyMap.PREVIOUS_ATTRIBUTE}`} mouseLeaveDelay={0}>
<Button disabled={attributesCount <= 1} onClick={() => nextAttribute(-1)}>
<Icon type='left' />
</Button>
</Tooltip>
<Tooltip title={title}>
<Tooltip title={title} mouseLeaveDelay={0}>
<Text className='cvat-text'>{currentAttribute}</Text>
<Text strong>{` [${currentIndex + 1}/${attributesCount}]`}</Text>
</Tooltip>
<Tooltip title={`Next attribute ${normalizedKeyMap.NEXT_ATTRIBUTE}`}>
<Tooltip title={`Next attribute ${normalizedKeyMap.NEXT_ATTRIBUTE}`} mouseLeaveDelay={0}>
<Button disabled={attributesCount <= 1} onClick={() => nextAttribute(1)}>
<Icon type='right' />
</Button>

@ -4,24 +4,15 @@
import React from 'react';
import Select, { SelectValue } from 'antd/lib/select';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
interface Props {
currentLabel: string;
labels: any[];
occluded: boolean;
setOccluded(event: CheckboxChangeEvent): void;
changeLabel(value: SelectValue): void;
}
function ObjectBasicsEditor(props: Props): JSX.Element {
const {
currentLabel,
occluded,
labels,
setOccluded,
changeLabel,
} = props;
const { currentLabel, labels, changeLabel } = props;
return (
<div className='attribute-annotation-sidebar-basics-editor'>
@ -35,7 +26,6 @@ function ObjectBasicsEditor(props: Props): JSX.Element {
</Select.Option>
))}
</Select>
<Checkbox checked={occluded} onChange={setOccluded}>Occluded</Checkbox>
</div>
);
}

@ -31,18 +31,18 @@ function ObjectSwitcher(props: Props): JSX.Element {
const title = `${currentLabel} ${clientID} [${currentIndex + 1}/${objectsCount}]`;
return (
<div className='attribute-annotation-sidebar-switcher'>
<Tooltip title={`Previous object ${normalizedKeyMap.PREVIOUS_OBJECT}`}>
<div className='attribute-annotation-sidebar-object-switcher'>
<Tooltip title={`Previous object ${normalizedKeyMap.PREVIOUS_OBJECT}`} mouseLeaveDelay={0}>
<Button disabled={objectsCount <= 1} onClick={() => nextObject(-1)}>
<Icon type='left' />
</Button>
</Tooltip>
<Tooltip title={title}>
<Tooltip title={title} mouseLeaveDelay={0}>
<Text className='cvat-text'>{currentLabel}</Text>
<Text className='cvat-text'>{` ${clientID} `}</Text>
<Text strong>{`[${currentIndex + 1}/${objectsCount}]`}</Text>
</Tooltip>
<Tooltip title={`Next object ${normalizedKeyMap.NEXT_OBJECT}`}>
<Tooltip title={`Next object ${normalizedKeyMap.NEXT_OBJECT}`} mouseLeaveDelay={0}>
<Button disabled={objectsCount <= 1} onClick={() => nextObject(1)}>
<Icon type='right' />
</Button>

@ -13,7 +13,8 @@
padding: 5px;
}
.attribute-annotation-sidebar-switcher {
.attribute-annotation-sidebar-object-switcher,
.attribute-annotation-sidebar-attribute-switcher {
display: flex;
align-items: center;
justify-content: space-between;
@ -36,7 +37,7 @@
align-items: center;
justify-content: space-between;
font-size: 18px;
margin: 10px 0px;
margin: 10px 0;
}
.attribute-annotations-sidebar-not-found-wrapper {
@ -45,7 +46,7 @@
}
.attribute-annotation-sidebar-attr-list-wrapper {
margin: 10px 0px 10px 10px;
margin: 10px 0 10px 10px;
}
.attribute-annotation-sidebar-attr-elem-wrapper {

@ -28,7 +28,7 @@ export default function CanvasContextMenu(props: Props): JSX.Element | null {
return ReactDOM.createPortal(
<div className='cvat-canvas-context-menu' style={{ top, left }}>
<ObjectItemContainer clientID={activatedStateID} />
<ObjectItemContainer key={activatedStateID} clientID={activatedStateID} />
</div>,
window.document.body,
);

@ -2,40 +2,131 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import Button from 'antd/lib/button';
import Tooltip from 'antd/lib/tooltip';
import { connect } from 'react-redux';
interface Props {
activatedStateID: number | null;
import { CombinedState, ContextMenuType } from 'reducers/interfaces';
import { updateAnnotationsAsync, updateCanvasContextMenu } from 'actions/annotation-actions';
interface StateToProps {
activatedState: any | null;
selectedPoint: number | null;
visible: boolean;
left: number;
top: number;
onPointDelete(): void;
left: number;
type: ContextMenuType;
}
export default function CanvasPointContextMenu(props: Props): JSX.Element | null {
function mapStateToProps(state: CombinedState): StateToProps {
const {
onPointDelete,
activatedStateID,
annotation: {
annotations: {
states,
activatedStateID,
},
canvas: {
contextMenu: {
visible,
top,
left,
type,
pointID: selectedPoint,
},
},
},
} = state;
return {
activatedState: activatedStateID === null
? null : states.filter((_state) => _state.clientID === activatedStateID)[0] || null,
selectedPoint,
visible,
left,
top,
type,
};
}
interface DispatchToProps {
onUpdateAnnotations(states: any[]): void;
onCloseContextMenu(): void;
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onUpdateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(states));
},
onCloseContextMenu(): void {
dispatch(updateCanvasContextMenu(false, 0, 0));
},
};
}
type Props = StateToProps & DispatchToProps;
function CanvasPointContextMenu(props: Props): React.ReactPortal | null {
const {
onCloseContextMenu,
onUpdateAnnotations,
activatedState,
visible,
type,
top,
left,
} = props;
if (!visible || activatedStateID === null) {
return null;
const [contextMenuFor, setContextMenuFor] = useState(activatedState);
if (activatedState !== contextMenuFor) {
setContextMenuFor(activatedState);
if (visible && type === ContextMenuType.CANVAS_SHAPE_POINT) {
onCloseContextMenu();
}
}
return ReactDOM.createPortal(
<div className='cvat-canvas-point-context-menu' style={{ top, left }}>
<Tooltip title='Delete point [Ctrl + dblclick]'>
<Button type='link' icon='delete' onClick={onPointDelete}>
Delete point
</Button>
</Tooltip>
</div>,
window.document.body,
);
const onPointDelete = (): void => {
const { selectedPoint } = props;
if (contextMenuFor && selectedPoint !== null) {
contextMenuFor.points = contextMenuFor.points.slice(0, selectedPoint * 2)
.concat(contextMenuFor.points.slice(selectedPoint * 2 + 2));
onUpdateAnnotations([contextMenuFor]);
onCloseContextMenu();
}
};
const onSetStartPoint = (): void => {
const { selectedPoint } = props;
if (contextMenuFor && selectedPoint !== null && contextMenuFor.shapeType === 'polygon') {
contextMenuFor.points = contextMenuFor.points.slice(selectedPoint * 2)
.concat(contextMenuFor.points.slice(0, selectedPoint * 2));
onUpdateAnnotations([contextMenuFor]);
onCloseContextMenu();
}
};
return visible && contextMenuFor && type === ContextMenuType.CANVAS_SHAPE_POINT
? (ReactDOM.createPortal(
<div className='cvat-canvas-point-context-menu' style={{ top, left }}>
<Tooltip title='Delete point [Ctrl + dblclick]' mouseLeaveDelay={0}>
<Button type='link' icon='delete' onClick={onPointDelete}>
Delete point
</Button>
</Tooltip>
{contextMenuFor && contextMenuFor.shapeType === 'polygon' && (
<Button type='link' icon='environment' onClick={onSetStartPoint}>
Set start point
</Button>
)}
</div>,
window.document.body,
)) : null;
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(CanvasPointContextMenu);

@ -58,13 +58,13 @@ interface Props {
contrastLevel: number;
saturationLevel: number;
resetZoom: boolean;
contextVisible: boolean;
contextType: ContextMenuType;
aamZoomMargin: number;
showObjectsTextAlways: boolean;
showAllInterpolationTracks: boolean;
workspace: Workspace;
automaticBordering: boolean;
keyMap: Record<string, ExtendedKeyMapOptions>;
canvasBackgroundColor: string;
switchableAutomaticBordering: boolean;
onSetupCanvas: () => void;
onDragCanvas: (enabled: boolean) => void;
@ -93,6 +93,7 @@ interface Props {
onChangeGridColor(color: GridColor): void;
onSwitchGrid(enabled: boolean): void;
onSwitchAutomaticBordering(enabled: boolean): void;
onFetchAnnotation(): void;
}
export default class CanvasWrapperComponent extends React.PureComponent<Props> {
@ -101,7 +102,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
automaticBordering,
showObjectsTextAlways,
canvasInstance,
curZLayer,
} = this.props;
// It's awful approach from the point of view React
@ -115,7 +115,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE,
displayAllText: showObjectsTextAlways,
});
canvasInstance.setZLayer(curZLayer);
this.initialSetup();
this.updateCanvas();
@ -137,6 +136,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
curZLayer,
resetZoom,
grid,
gridSize,
gridOpacity,
gridColor,
brightnessLevel,
@ -145,8 +145,11 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
workspace,
frameFetching,
showObjectsTextAlways,
showAllInterpolationTracks,
automaticBordering,
showProjections,
canvasBackgroundColor,
onFetchAnnotation,
} = this.props;
if (prevProps.showObjectsTextAlways !== showObjectsTextAlways
@ -161,6 +164,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
});
}
if (prevProps.showAllInterpolationTracks !== showAllInterpolationTracks) {
onFetchAnnotation();
}
if (prevProps.sidebarCollapsed !== sidebarCollapsed) {
const [sidebar] = window.document.getElementsByClassName('cvat-objects-sidebar');
if (sidebar) {
@ -179,6 +186,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
}
if (gridSize !== prevProps.gridSize) {
canvasInstance.grid(gridSize, gridSize);
}
if (gridOpacity !== prevProps.gridOpacity
|| gridColor !== prevProps.gridColor
|| grid !== prevProps.grid) {
@ -204,17 +215,15 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
}
if (prevProps.curZLayer !== curZLayer) {
canvasInstance.setZLayer(curZLayer);
}
if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) {
if (prevProps.annotations !== annotations
|| prevProps.frameData !== frameData
|| prevProps.curZLayer !== curZLayer) {
this.updateCanvas();
}
if (prevProps.frame !== frameData.number
&& resetZoom
&& workspace !== Workspace.ATTRIBUTE_ANNOTATION
&& ((resetZoom && workspace !== Workspace.ATTRIBUTE_ANNOTATION)
|| workspace === Workspace.TAG_ANNOTATION)
) {
canvasInstance.html().addEventListener('canvas.setup', () => {
canvasInstance.fit();
@ -222,7 +231,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
if (prevProps.opacity !== opacity || prevProps.blackBorders !== blackBorders
|| prevProps.selectedOpacity !== selectedOpacity || prevProps.colorBy !== colorBy) {
|| prevProps.selectedOpacity !== selectedOpacity || prevProps.colorBy !== colorBy
) {
this.updateShapesView();
}
@ -243,6 +253,13 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
}
if (prevProps.canvasBackgroundColor !== canvasBackgroundColor) {
const canvasWrapperElement = window.document.getElementsByClassName('cvat-canvas-container').item(0) as HTMLElement | null;
if (canvasWrapperElement) {
canvasWrapperElement.style.backgroundColor = canvasBackgroundColor;
}
}
this.activateOnCanvas();
}
@ -382,10 +399,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const {
activatedStateID,
onUpdateContextMenu,
contextType,
} = this.props;
if (contextType !== ContextMenuType.CANVAS_SHAPE_POINT) {
if (e.target && !(e.target as HTMLElement).classList.contains('svg_select_points')) {
onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY,
ContextMenuType.CANVAS_SHAPE);
}
@ -442,7 +458,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onActivateObject,
} = this.props;
if (workspace === Workspace.ATTRIBUTE_ANNOTATION) {
if (workspace !== Workspace.STANDARD) {
return;
}
@ -571,7 +587,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.fit();
}
}
if (activatedState.objectType !== ObjectType.TAG) {
if (activatedState && activatedState.objectType !== ObjectType.TAG) {
canvasInstance.activate(activatedStateID, activatedAttributeID);
}
const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`);
@ -616,16 +632,15 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
private updateCanvas(): void {
const {
curZLayer,
annotations,
frameData,
frameAngle,
canvasInstance,
} = this.props;
if (frameData !== null) {
canvasInstance.setup(frameData, annotations
.filter((e) => e.objectType !== ObjectType.TAG));
canvasInstance.rotate(frameAngle);
.filter((e) => e.objectType !== ObjectType.TAG), curZLayer);
}
}
@ -639,6 +654,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
brightnessLevel,
contrastLevel,
saturationLevel,
canvasBackgroundColor,
} = this.props;
// Size
@ -665,6 +681,11 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
+ `saturate(${saturationLevel / 100})`;
}
const canvasWrapperElement = window.document.getElementsByClassName('cvat-canvas-container').item(0) as HTMLElement | null;
if (canvasWrapperElement) {
canvasWrapperElement.style.backgroundColor = canvasBackgroundColor;
}
// Events
canvasInstance.html().addEventListener('canvas.setup', () => {
const { activatedStateID, activatedAttributeID } = this.props;
@ -855,7 +876,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
defaultValue={0}
onChange={(value: SliderValue): void => onSwitchZLayer(value as number)}
/>
<Tooltip title={`Add new layer ${maxZLayer + 1} and switch to it`}>
<Tooltip title={`Add new layer ${maxZLayer + 1} and switch to it`} mouseLeaveDelay={0}>
<Icon type='plus-circle' onClick={onAddZLayer} />
</Tooltip>
</div>

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

Loading…
Cancel
Save