Release 1.0.0 (#1335)

* Added button to cancel started automatic annotation (#1198)

* [WIP] Cuboid feature user guide (#1218)

* Initial cuboid description

* Added Gifs

* Added gifs  to descriptions

* Formatting fixes

* Codacy Fixes

* Az/fix annotation dump upload (#1229)

* fixed upload annotation in case of frame step != 1

* fixed upload annotation in case of attribute value is empty

* Setup tag forward to the state

* React UI: Added shortcuts (#1230)

* [Datumaro] Label remapping transform (#1233)

* Add label remapping transform

* Apply transforms before project saving

* Refactor voc converter

* [Datumaro] Optimize mask operations (#1232)

* Optimize mask to rle

* Optimize mask operations

* Fix dm format cmdline

* Use RLE masks in datumaro format

* Added tag support in new UI (without canvas drawing)

* merge fix

* Fixed copying/pasting actions

* Deleted unused objects

* [Datumaro] Dataset format auto detection (#1242)

* Add dataset format detection

* Add auto format detection for import

* Split VOC extractor

* Some debian package manager tweaks (#1235)

* Some debian package manager tweaks

By default, Ubuntu or Debian based "apt" or "apt-get" system installs recommended but not suggested packages . 

By passing "--no-install-recommends" option, the user lets apt-get know not to consider recommended packages as a dependency to install.

This results in smaller downloads and installation of packages .

Refer to blog at [Ubuntu Blog](https://ubuntu.com/blog/we-reduced-our-docker-images-by-60-with-no-install-recommends) .

* doc: fix description of attribute CVAT XML format (#1168)

* Fixed security issues in Datumaro (#1244)

* Fixed security issues reported by bandit.
* Fixed voc_format extractor
* Sorted requirements, added a comment, removed nosec for exec.

* Fix copying and creating tags

* fixed git sync app (#1247)

* fixed git sync app

* removed shell=True for subprocess call

* Fixed tags color changing and hiding

* Fixed filters with tags

* wip

* PR fixed

* Styles fixed

* PR fixed

* Update develop from release-0.6.0 branch (#1266)

* temp

* React UI: Attribute annotation mode (#1255)

* Done main work

* Fixed mount/unmount for canvas wrapper

* Refactoring, added filters

* Added missed file

* Removed unnecessary useEffect

* Removed extra code

* Max 9 attributes, inputNumber -> Input in aam

* Added blur

* Renamed component

* Fixed condition when validate number attribute

* Some minor fixes

* Fixed hotkeys config

* Fixed canvas zoom

* Improved behaviour of number & text

* Fixed attributes switching order

* Fix tags

* Fixed interval

* Installation issues for development environment (#1280)

* Installation issues

* Added ffmpeg

* Bump acorn from 6.3.0 to 6.4.1 in /cvat-ui (#1270)

* Bump acorn from 6.3.0 to 6.4.1 in /cvat-ui

Bumps [acorn](https://github.com/acornjs/acorn) from 6.3.0 to 6.4.1.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/6.3.0...6.4.1)

Signed-off-by: dependabot[bot] <support@github.com>

* Updated CHANGELOG.md

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Boris Sekachev <boris.sekachev@yandex.ru>

* Bump acorn from 6.2.1 to 6.4.1 in /cvat-canvas (#1281)

Bumps [acorn](https://github.com/acornjs/acorn) from 6.2.1 to 6.4.1.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/6.2.1...6.4.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Use source label map for voc export (#1276)

* Use source label map for voc export

* Add line to changelog

* [Datumaro] Fix frame matching in video annotations import (#1274)

* Add extra frame matching way for videos

* Add line to changelog

* [Datumaro] Allow empty COCO dataset export (#1272)

* Allow empty dataset export in coco

* Add line to changelog

Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* [Datumaro] Fix occluded and z_order attributes export (#1271)

* Fix occluded and z_order attributes export

* Add line to changelog

Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* Fix LabelMe format (#1260)

* Fix labelme filenames

* Change module path

* Add tests for LabelMe

* Update test

* Fix test

* Add line in changelog

* Added point deletion context menu

* React UI: Added logging (#1288)

* OpenVino 2020 (#1269)

* added support for OpenVINO 2020

* fixed dextr and tf_annotation

Co-authored-by: Andrey Zhavoronkov <andrey.zhavoronkov@intel.com>

* fixed point context menu for rectangles

* Add recursive importers (#1290)

* [Datumaro] MOT format (#1289)

* Add mot format base

* Add mot format

* Extract common code

* [Datumaro] LabelMe format (#1293)

* Little refactoring

* Add LabelMe format

* [Datumaro] Update LabelMe format (#1296)

* Little refactoring

* Add LabelMe format

* Add usernames

* Update tests

* Add extractor test

* Add information about v0.6.1 release.

* React UI: Better exception handling (#1297)

* Fixed context menu on ubuntu

* Fixed deleting of the latest point

* fixes

* Fix attributes with spaces in names (#1305)

* fixed PR

* [Datumaro] Fix image merging (#1301)

* Always merge images for own dataset

* Fix codacy

* Validation for frame input value

* Fixed UI fail when write characters in auto save interval input

* Fixed input numbers in player settings

* Fixed ui failing in propagate confirmation

* Fixed latest input numbers, removed extra code, fixed typings

* Fix navigation

* Added undopoint in editing

* Fixed: Could not receive frame (after merge on the latest frame)

* Removed extra action dispatching

* Which -> button property

* Fixed: Inconsistent labels between UI and CLI/API

* Fixed resize on right mouse button

* Fixed create object URL after first save, fixed URL itself

* Undo/redo returns frame where was a change (as it was done in previous version)

* Fixed unit tests

* [Datumaro] Extract common extractor functionality (#1319)

* Extract common extractor functionality

* Simplify coco extractor

* Fix tfrecord

* Fix AWS deployment  (#1316)

* Don't use antd less (big memory consumtion during the build process)
* Fix AWS deployment guide
* fix a problem with proxy and long domain names
* remove sass loader for antd
* Removed less and less-loader.
* Simplified webpack config.

* Data streaming using chunks (#1007)

Huge feature (200+ commits from different developers). It completely changes layout of data (please expect very long DB migration process if you have a lot of tasks). The primary idea is to send data as zip chunks (e.g. 36 images in one chunk) or encoded video chunks and decode them on the client side. It helps to solve the problem with latency  when you try to view a separate frame in the UI quickly (play mode).
Another important feature of the patch is to provide access to the original images. Thus for annotations the client uses compressed chunks but if you want to export a dataset Datumaro will use original chunks (but video will be decoded with original quality and encoded with maximum/optimal quality in any case).

* Shortcuts keymaps moved to state

* Titles for objects in side menu

* Fixed bug in menu

* Titles in attribute annotations mode

* Controls panel titles

* Titles for object list header

* Minor fixes

* Added tooltips in top bar

* Added settings tooltip

* Optimized patch

* Typos

* Fix a problem with known hosts inside git app (cannot clone a repo from github.com) (#1330)

* Fixed zOrder range computing in case when there are tags

* Small preview and progress (#1331)

* Reduce preview size (untested)

* Fix tests

* Improve media readers (untested)

* fixed migration

* fixed frame provider

* fixed preview save

* fixed stop frame

* handle duration == None

* codacy

* added missed import

* unified iteration over frames for media readers and fixed corner case when user specify stop_frame = 0

Co-authored-by: Nikita Manovich <nikita.manovich@intel.com>

* Move annotation formats to dataset manager (#1256)

* Move formats to dataset manager

* Unify datataset export and anno export implementations

* Add track_id to TrackedShape, export tracked shapes

* Replace MOT format

* Replace LabelMe format

* Add new formats to dm

* Add dm tests

* Extend TrackedShape

* Enable dm test in CI

* Fix tests

* Add import

* Fix tests

* Fix mot track ids

* Fix mot format

* Update attribute logic in labelme tests

* Use common code in yolo

* Put datumaro in path in settings

* Expect labels file in MOT next to annotations file

* Add MOT format description

* Add import

* Add labelme format description

* Linter fix

* Linter fix2

* Compare attributes ordered

* Update docs

* Update tests

* Az/fix migration (#1333)

* fixed migration because readers interface changed

* fixed tests

* Fixed escape in draw

* Insert multiple shapes

* Added dialog window with some help info about filters

* Typos in doc

* Add missing information into changelog.

* Add information for next release + update version.

* Updated changelog

* Updated changelog

* Special behaviour for the attribute value __undefined__

* Fixed license year

* No break space const

* Updated changelog

* Fixed year in license headers

* Implementation of bitmap in client

* Updated changelog

* Z-layer support

* Fixed settings after reopen a job

* Do not show invisible objects on bitmap

* Fix point interpolation (#1344)

* Extend formats tests with different track types

* Add unordered list comparison

* Skip empty list comparison

* fix

* fix

* Reproduce problem

* Fix point interpolation for single point

* undo rest api refactor

* Added button to reset color settings

* Updated changelog

* Added option to display shape text always

* Updated changelog

* Hidden/outside fix

* Fixed screen scaling

* fixed dump error after moving format files (#1342)

* fixed dump error after moving format files

* updated changelog

* Az/fix dextr (#1348)

* Fixed dextr_segmentation app

* updated changelog

Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* Disabled option by default

* Fixed typos in interface

* Increase preview size till 256, 256.

Previous preview size was not optimal and led to a blurred image
due to too small size.

* Add a line into CHANGELOG.md about the change.

* Refactor frame provider (#1355)

* Refactor frame provider

* fix

* Add CODEOWNERS file (#1360)

* Add CODEOWNERS file

* Removed the outdated file. See https://github.com/opencv/cvat/graphs/contributors

* Change codeowners

* Add pull request and issue templates (#1359)

* Add initial version of pull request template

* Fix links

* Fix codacy issues

* Slightly improve titles of sections

* Add a note about strikethough for the checklist.

* Fix progress of a pull request (each checkbox is an issue)

* Add the license header, checkboxes about the license.

* Updated the license

* Update the license to met https://github.com/licensee/licensee/blob/master/vendor/choosealicense.com/_licenses/mit.txt restrictions.

* Fix the pull request template name

* Make explaination text as comments (it will be visible when you edit the PR message)

* Add initial version of the issue template.

* Batch of fixes (#1370)

* Some margins were change to paddings

* Removed extra selected

* Fix: added outside shapes when merge polyshapes

* Fixed double scroll bars

* Updated canvas table

* Fixed setup methodf

* Disabled change frame during drag, resize and editing

* Fixed: hidden points are visible

* Fixed: Merge is allowed for points, but clicks on points conflict with frame dragging logic

* Fixed: do not filter removed objects

* Updated CHANGELOG.md

* Couple of headers updated

* Added missed fields in exception logs (#1372)

* Simplified codeowners file (#1376)

* Fixed points visibility when go between frames

* React UI: Added message when share is empty or not mounted (#1373)

* Added message when share is empty or not mounted

* Updated changelog

* Update CHANGELOG.md

Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* Save full image paths in coco (#1381)

* Add chunk iterator cache to frame provider (#1367)

* Add chunk iterator cache

* fix

* Update item "Creating an annotation task" in User Guide (#1363)

* Az/fix remote files (#1392)

* fixed task creation from remote files

* Update CHANGELOG.md

* [Datumaro] Fix COCO keypoint export bug (#1388)

* Instructions on using HTTPS (#1357)

* Fix label comparison in voc format (#1382)

* Update item "Interface of the annotation tool" in User Guide (#1386)

* React UI: Batch of fixes (#1383)

* Fixed: cannot read property 'set' of undefined

* Fixed UI failing: save during drag/resize

* Fixed multiple saving (shortcut sticking)

* Undo/redo fixed

* Allowed one interpolated point

* Fixed API reaction when repository synchronization is failed

* Updated changelog

* Update item "Basic navigation" in User Guide and bug fix in user_guide.md (#1395)

* Fix git app paths (#1400)

* changed paths for the git repos

* Update CHANGELOG.md

* updated licence header

* React UI: Displaying public ssh keys in UI (#1375)

* Updated changelog

* Typos

* Batch of fixes (#1403)

* Fixed bug when job cannot be opened

* Fixed bug when deactivated shape is still highlighted

* Fixed Error: 'AttributeError: 'tuple' object has no attribute 'read'

* Fixed: wrong semi-automatic segmentation near edges of an image

* Updated changelog

Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* React UI: Semi-automatic segmentation (#1398)

* implemented checked

* Implemented plugin

* Added dialog windows

* Updated changelo

* Added cancel request

* React UI: Automatic bordering for polygons and polylines during drawing/editing (#1394)

* Fixed: cannot read property 'set' of undefined

* Fixed UI failing: save during drag/resize

* Fixed multiple saving (shortcut sticking)

* Undo/redo fixed

* Allowed one interpolated point

* Fixed API reaction when repository synchronization is failed

* Updated changelog

* Auto bordering feature

* Some fixes, added shortcuts

* Fixed draw when start with one of supporting point

* React UI: batch of fixes (#1404)

* React UI has become a primary UI
* Temporary disabled cuboid in cvat-core

* Fixed: Failed to execute removChild.. (#1405)

* Update CHANGELOG.md

* Updated CHANGELOG (new version will be 1.0.0-beta.1)

* Update CHANGELOG.md (next version is 1.0.0-beta.2)

* Updated version of CVAT server till beta.2

* Fixed auto annotation, tf annotation and auto segmentation apps (#1409)

* fixed code that uses FrameProvider, as the interface has changed

* Update CHANGELOG.md

* Update item "Types of shapes" (#1401)

* React UI: ReID algorithm (#1406)

* Initial commit

* Connected storage

* Added core API method

* Done implementation

* Removed rule

* Removed double cancel

* Updated changelog

* Fixed: Cannot read property toFixed of undefined

* Update CHANGELOG.md

* Improve PR template (#1427)

* Simplified PR template.

* Remove a new line to make codacy happy.

* fix: OSError:broken data stream (#1430)

* [Datumaro] Fix duplicating keypoints in COCO export (#1435)

* React UI: Fixed typos in remove annotations confirmation (#1450)

* React UI: Batch of fixes (#1445)

* Hide functionality (H) doesn't work
* The highlighted attribute doesn't correspond to the chosen attribute in AAM
* Inconvinient image shaking while drawing a polygon (hold Alt key during drawing/editing/grouping to drag an image)
* Filter property "shape" doesn't work and extra operator in description
* Block of text information doesn't disappear after deactivating for locked shapes
* Annotation uploading fails in annotation view
* UI freezes after canceling pasting with escape

Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* Update item "Annotation mode (basics)" in User Guide (#1412)

* Update item "annotation mode (basics)" in User Guide

* Replacing a gif017 with an image

* React UI: Added client versioning (#1448)

* Adjusted antd import

* Wrapped core and canvas

* Added versioning

* Updated changelog, adjusted installation guide a bit

* Update item "Interpolation mode (basics)" in User Guide (#1455)

* Update item interpolation mode in user guide

fix typos and contents

* Fix a typo

* Fix a typo in the filename

Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* React UI: batch of fixes (#1462)

* CVAT new UI: add arrows on a mouse cursor
* Delete point bug (in new UI)

* fix auto annotation to not eat all RAM (#1328)

Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* Fixed versioning for node 8 (#1466)

* Update items from AAM (basics) to Vocabulary in User Guide (#1459)

* Update items from AAM (basics) to Vocabulary
* Delete unused images and gif

* Fix apache startup (#1467)

* Fix Network Error after PC Restart (#1035)
* Update CHANGELOG.md

* Remove deprecated utils (#1477)

* removed deprecated convert scripts

* updated changelog

* Fixed 'Open task' button doesn't work (#1474)

* Fixed uploading track annotations for multi-segment tasks (#1396)

* fixed uploading annotation for overlapped segments

* fixed dump of tracks for multisegment task

* Update CHANGELOG.md

* fixed comments

* fixed comments

* Update cvat/apps/engine/data_manager.py

Co-Authored-By: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* drop start shapes with outside==True for splitted track

* code cleanup

* fixed typo

* fix

Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* React UI: cuboids (#1451)

* Update changelog.

* Update CHANGELOG and the version of CVAT server.

* session.annotations.put() returns indexes of added objects (#1493)

* session.annotations.put() returns indexes of added objects

* Updated changelog

* Updated README.md files (added info about versioning) (#1490)

* Updated README.md files (added info about versioning)

* Typos

* Add coverage for python (#1483)

* Fix coverage merging (#1504)

* Updating instructions to serve Swagger documentation  (#1502)

* Update items Workspace and Types of shapes in User Guide (#1497)

* Merge annotations and dataset_manager apps (#1352)

* Fix coverage measurement (#1516)

* fixed linter issues and store credentials cookie in the session object (#1526)

* fixed issues

* fixed issues and stored credentials cookies inside the session

* fixed tests

* React UI: batch of fixes (#1525)

* Update item Settings in User Guide (#1508)

* React UI: cookie policy drawer (#1511)

* fixed linter issues (#1538)

* fixed false tag activation (#1541)

* Added item with npm package version increasing to the PR template (#1542)

* [Datumaro] Fix coco import conflict with labels (#1548)

* [Datumaro] Change alignment in mask parsing (#1547)

* Include empty images in exported annotations (#1479)

* Update item Bottom panel in User Guide (#1509)

* Update item Bottom panel in User Guide

rename the "Bottom panel" to the "Top panel"

* Fixed typos, remove trailing spaces in user_guide.md

* Fix user_guide.md and update image051.jpg

* Ability to configure user agreements for the register user form (#1464)

* Update item Side panel in User guide (#1513)

* Update item Side panel in User guide

* Removed trailing spaces in an Object sidebar item

* Update cvat/apps/documentation/user_guide.md

* fix typo

* missing image correction

Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* Layout styles fixes

* React UI: cuboid interpolation and cuboid drawing from rectangles (#1560)

* Added backend cuboid interpolation and cuboid drawing from rectangles
* Added CHANELOG.md
* Fixed cuboid front edges stroke width
* PR fixes

* Fixed auto_segmentation app (#1562)

* disabled tf eager execution for auto_segmentation
* Update CHANGELOG.md

* Add VOC grayscale masks test and documentation (#1576)

* Add a test for unpainted masks
* Update format documentation

* [Datumaro] Fix mask to polygons warning (#1581)

* Fix message, add test
* update changelog

* Fix cuboid conversion (#1577)

* Fix cuboid conversion

* update changelog

Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* [Datumaro] Simplify log level setting (#1583)

* Simplify loglevel setting

* update changelog

* Fixed git synchronization (#1582)

* fixed git synchronization

* Update CHANGELOG.md

Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* Fixed dextr, fixed moving of the canvas (#1573)

* Fixed dextr, fixed moving of the canvas

* Updated CONTRIBUTUNG.md, updated version

Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* Added the ability to configure custom pageViewHit (may useful for web analytics) (#1566)

* added the ability to configure custom pageViewHit (may useful for web analytics)
* updated version and changelog
* fixed comments
* cvat-ui minor v++
* subscribe on history updates in the root component

* updated Online Demo section of Readme (#1588)

* updated Online Demo section of Readme

* Change content of "online demo" section

Co-authored-by: Nikita Manovich <nikita.manovich@intel.com>

* Fixed task creation for videos with uneven dimensions. (#1594)

* used yuv420p format for compressed and original chunks

* updated changelog

* added settings to reduce access to analytics component (#1592)

* added settings to reduce access to analytics component

* updated CHANGELOG

* fixed typo

* Add item Controls sidebar in User Guide (#1510)

* Update text, images and gif in user_guide.md (#1558)

* Update user_guide.md, images and gif (#1556)

Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>

* update user_guide.md, images and gifs

* Delete gif013

* add gif013 with correct name

* Use z_order as a class property (#1589)

* Use z_order as a class property

* Fix z_order use in voc

* Update changelog

* Update item Annotation with polylines (#1596)

* update user_guide.md and images

* fix uppercase letters in images path in user_guide.md and remove trailing spaces

* delete images and gifs containing uppercase letters in the name

* add images with correct names

* fix image paths in user_guide.md

* Delete image133

* add image133 with correct name

* Fix example yaml format (#1603)

* delete duplicate item in user_guide.md (#1607)

* update user_guide.md and add image (#1604)

* update gifs, images and user_guide.md (#1605)

* fix analytics permissions (#1608)

* Update item Annotation with cuboids (#1598)

* Update item Annotation with polygons in User guide v2 (#1612)

* update user_guide.md
* update images and gifs

* Update Filter, Analytics and Shortcuts items in User Guide (#1606)

* update user_guide.md and images

* fix user_guide.md

* delete unused image

* delete unused images

* Fix duplicate item in User Guide (#1617)

* remove item Annotation with Auto Segmentation

* fix link in user_guide.md

* delete unused images

* Slightly improve changelog

* Update CVAT version

Co-authored-by: Boris Sekachev <40690378+bsekachev@users.noreply.github.com>
Co-authored-by: Tritin Truong <truongtritin98@gmail.com>
Co-authored-by: Andrey Zhavoronkov <41117609+azhavoro@users.noreply.github.com>
Co-authored-by: Dmitry Kalinin <dmitry.kalinin@intel.com>
Co-authored-by: zhiltsov-max <zhiltsov.max35@gmail.com>
Co-authored-by: Pratik Raj <Rajpratik71@gmail.com>
Co-authored-by: Mathis Chenuet <artdevelopp@hotmail.fr>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Boris Sekachev <boris.sekachev@yandex.ru>
Co-authored-by: Ben Hoff <hoff.benjamin.k@gmail.com>
Co-authored-by: Andrey Zhavoronkov <andrey.zhavoronkov@intel.com>
Co-authored-by: TOsmanov <54434686+TOsmanov@users.noreply.github.com>
Co-authored-by: ranko r sredojevic <radoye@users.noreply.github.com>
Co-authored-by: Thomas Albrecht <thomas.albrecht@gmx.net>
Co-authored-by: Johannes222 <johannes.halaoui@alumni.fh-aachen.de>
Co-authored-by: Gururaj Jeerge <gururaj@orangepro.in>
Co-authored-by: timurx.osmanov <timurx.osmanov@intel.com>
Co-authored-by: YutaYamazaki <37947061+yutayamazaki@users.noreply.github.com>
main
Nikita Manovich 6 years ago committed by GitHub
parent 034268e87d
commit 07de7141ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,5 @@
exclude_paths:
- '**/3rdparty/**'
- '**/engine/js/cvat-core.min.js'
- '**/engine/js/unzip_imgs.js'
- CHANGELOG.md

@ -0,0 +1,40 @@
[run]
branch = true
# relative_files = true # does not work?
source =
datumaro/datumaro/
cvat/apps/
utils/cli/
omit =
datumaro/datumaro/__main__.py
datumaro/datumaro/version.py
cvat/settings/*
*/tests/*
*/test_*
*/_test_*
*/migrations/*
[report]
# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover
# Don't complain about missing debug-only code:
def __repr__
if\s+[\w\.()]+\.isEnabledFor\(log\.DEBUG\):
# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
# Don't complain if non-runnable code isn't run:
if 0:
if __name__ == .__main__.:
# don't fail on the code that can be found
ignore_errors = true
skip_empty = true

39
.github/CODEOWNERS vendored

@ -0,0 +1,39 @@
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence, they will
# be requested for review when someone opens a pull request.
* @nmanovic
# Order is important; the last matching pattern takes the most
# precedence. When someone opens a pull request that only
# modifies components below, only the list of owners and not
# the global owner(s) will be requested for a review.
# Component: Server
/cvat/ @nmanovic
# Component: CVAT UI
/cvat-ui/ @bsekachev
/cvat-data/ @azhavoro
/cvat-canvas/ @bsekachev
/cvat-core/ @bsekachev
# Component: Datumaro
/datumaro/ @zhiltsov-max
/cvat/apps/dataset_manager/ @zhiltsov-max
# Advanced components (e.g. OpenVINO)
/components/ @azhavoro
# Infrastructure
Dockerfile* @azhavoro
docker-compose* @azhavoro
.* @azhavoro
*.conf @azhavoro
*.sh @azhavoro
/cvat_proxy/ @azhavoro
/tests/ @azhavoro
/utils/ @azhavoro
/LICENSE @nmanovic
/.github/ @nmanovic

@ -34,6 +34,8 @@ If you're unsure about any of these, don't hesitate to ask. We're here to help!
- [ ] I have added tests to cover my changes
- [ ] I have linked related issues ([read github docs](
https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword))
- [ ] I have increased versions of npm packages if it is necessary ([cvat-canvas](https://github.com/opencv/cvat/tree/develop/cvat-canvas#versioning),
[cvat-core](https://github.com/opencv/cvat/tree/develop/cvat-core#versioning), [cvat-data](https://github.com/opencv/cvat/tree/develop/cvat-data#versioning) and [cvat-ui](https://github.com/opencv/cvat/tree/develop/cvat-ui#versioning))
### License

6
.gitignore vendored

@ -19,8 +19,14 @@ docker-compose.override.yml
__pycache__
*.pyc
._*
.coverage
# Ignore development npm files
node_modules
# Ignore npm logs file
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store

@ -8,10 +8,19 @@ python:
services:
- docker
env:
- CONTAINER_COVERAGE_DATA_DIR="/coverage_data"
HOST_COVERAGE_DATA_DIR="${TRAVIS_BUILD_DIR}"
before_script:
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml build
- chmod a+rwx ${HOST_COVERAGE_DATA_DIR}
script:
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'python3 manage.py test cvat/apps utils/cli'
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'python3 manage.py test datumaro/'
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-core && npm install && npm run test && npm run coveralls'
# 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'
after_success:
# https://coveralls-python.readthedocs.io/en/latest/usage/multilang.html
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'ln -s ${CONTAINER_COVERAGE_DATA_DIR}/.git . && ln -s ${CONTAINER_COVERAGE_DATA_DIR}/.coverage . && ln -s ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json . && coveralls --merge=coverage.json'

@ -33,5 +33,6 @@
"./datumaro",
],
"licenser.license": "Custom",
"licenser.customHeader": "Copyright (C) @YEAR@ Intel Corporation\n\nSPDX-License-Identifier: MIT"
"licenser.customHeader": "Copyright (C) @YEAR@ Intel Corporation\n\nSPDX-License-Identifier: MIT",
"files.trimTrailingWhitespace": true
}

@ -4,6 +4,139 @@ 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.0.0] - 2020-05-29
### Added
- cvat-ui: cookie policy drawer for login page (<https://github.com/opencv/cvat/pull/1511>)
- `datumaro_project` export format (<https://github.com/opencv/cvat/pull/1352>)
- Ability to configure user agreements for the user registration form (<https://github.com/opencv/cvat/pull/1464>)
- Cuboid interpolation and cuboid drawing from rectangles (<https://github.com/opencv/cvat/pull/1560>)
- Ability to configure custom pageViewHit, which can be useful for web analytics integration (<https://github.com/opencv/cvat/pull/1566>)
- Ability to configure access to the analytics page based on roles (<https://github.com/opencv/cvat/pull/1592>)
### Changed
- Downloaded file name in annotations export became more informative (<https://github.com/opencv/cvat/pull/1352>)
- Added auto trimming for trailing whitespaces style enforcement (<https://github.com/opencv/cvat/pull/1352>)
- REST API: updated `GET /task/<id>/annotations`: parameters are `format`, `filename` (now optional), `action` (optional) (<https://github.com/opencv/cvat/pull/1352>)
- REST API: removed `dataset/formats`, changed format of `annotation/formats` (<https://github.com/opencv/cvat/pull/1352>)
- Exported annotations are stored for N hours instead of indefinitely (<https://github.com/opencv/cvat/pull/1352>)
- Formats: CVAT format now accepts ZIP and XML (<https://github.com/opencv/cvat/pull/1352>)
- Formats: COCO format now accepts ZIP and JSON (<https://github.com/opencv/cvat/pull/1352>)
- Formats: most of formats renamed, no extension in title (<https://github.com/opencv/cvat/pull/1352>)
- Formats: definitions are changed, are not stored in DB anymore (<https://github.com/opencv/cvat/pull/1352>)
- cvat-core: session.annotations.put() now returns ids of added objects (<https://github.com/opencv/cvat/pull/1493>)
- Images without annotations now also included in dataset/annotations export (<https://github.com/opencv/cvat/issues/525>)
### Removed
- `annotation` application is replaced with `dataset_manager` (<https://github.com/opencv/cvat/pull/1352>)
- `_DATUMARO_INIT_LOGLEVEL` env. variable is removed in favor of regular `--loglevel` cli parameter (<https://github.com/opencv/cvat/pull/1583>)
### Fixed
- Categories for empty projects with no sources are taken from own dataset (<https://github.com/opencv/cvat/pull/1352>)
- Added directory removal on error during `extract` command (<https://github.com/opencv/cvat/pull/1352>)
- Added debug error message on incorrect XPath (<https://github.com/opencv/cvat/pull/1352>)
- Exporting frame stepped task (<https://github.com/opencv/cvat/issues/1294, https://github.com/opencv/cvat/issues/1334>)
- Fixed broken command line interface for `cvat` export format in Datumaro (<https://github.com/opencv/cvat/issues/1494>)
- Updated Rest API document, Swagger document serving instruction issue (<https://github.com/opencv/cvat/issues/1495>)
- Fixed cuboid occluded view (<https://github.com/opencv/cvat/pull/1500>)
- Non-informative lock icon (<https://github.com/opencv/cvat/pull/1434>)
- Sidebar in AAM has no hide/show button (<https://github.com/opencv/cvat/pull/1420>)
- Task/Job buttons has no "Open in new tab" option (<https://github.com/opencv/cvat/pull/1419>)
- Delete point context menu option has no shortcut hint (<https://github.com/opencv/cvat/pull/1416>)
- Fixed issue with unnecessary tag activation in cvat-canvas (<https://github.com/opencv/cvat/issues/1540>)
- Fixed an issue with large number of instances in instance mask (<https://github.com/opencv/cvat/issues/1539>)
- Fixed full COCO dataset import error with conflicting labels in keypoints and detection (<https://github.com/opencv/cvat/pull/1548>)
- Fixed COCO keypoints skeleton parsing and saving (<https://github.com/opencv/cvat/issues/1539>)
- `tf.placeholder() is not compatible with eager execution` exception for auto_segmentation (<https://github.com/opencv/cvat/pull/1562>)
- Canvas cannot be moved with move functionality on left mouse key (<https://github.com/opencv/cvat/pull/1573>)
- Deep extreme cut request is sent when draw any shape with Make AI polygon option enabled (<https://github.com/opencv/cvat/pull/1573>)
- Fixed an error when exporting a task with cuboids to any format except CVAT (<https://github.com/opencv/cvat/pull/1577>)
- Synchronization with remote git repo (<https://github.com/opencv/cvat/pull/1582>)
- 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>)
- Permission group whitelist check for analytics view (<https://github.com/opencv/cvat/pull/1608>)
## [1.0.0-beta.2] - 2020-04-30
### Added
- Re-Identification algorithm to merging bounding boxes automatically to the new UI (<https://github.com/opencv/cvat/pull/1406>)
- Methods ``import`` and ``export`` to import/export raw annotations for Job and Task in ``cvat-core`` (<https://github.com/opencv/cvat/pull/1406>)
- Versioning of client packages (``cvat-core``, ``cvat-canvas``, ``cvat-ui``). Initial versions are set to 1.0.0 (<https://github.com/opencv/cvat/pull/1448>)
- Cuboids feature was migrated from old UI to new one. (<https://github.com/opencv/cvat/pull/1451>)
### Removed
- Annotation convertation utils, currently supported natively via Datumaro framework (https://github.com/opencv/cvat/pull/1477)
### Fixed
- Auto annotation, TF annotation and Auto segmentation apps (https://github.com/opencv/cvat/pull/1409)
- Import works with truncated images now: "OSError:broken data stream" on corrupt images (https://github.com/opencv/cvat/pull/1430)
- Hide functionality (H) doesn't work (<https://github.com/opencv/cvat/pull/1445>)
- The highlighted attribute doesn't correspond to the chosen attribute in AAM (<https://github.com/opencv/cvat/pull/1445>)
- Inconvinient image shaking while drawing a polygon (hold Alt key during drawing/editing/grouping to drag an image) (<https://github.com/opencv/cvat/pull/1445>)
- Filter property "shape" doesn't work and extra operator in description (<https://github.com/opencv/cvat/pull/1445>)
- Block of text information doesn't disappear after deactivating for locked shapes (<https://github.com/opencv/cvat/pull/1445>)
- Annotation uploading fails in annotation view (<https://github.com/opencv/cvat/pull/1445>)
- UI freezes after canceling pasting with escape (<https://github.com/opencv/cvat/pull/1445>)
- Duplicating keypoints in COCO export (https://github.com/opencv/cvat/pull/1435)
- CVAT new UI: add arrows on a mouse cursor (<https://github.com/opencv/cvat/pull/1391>)
- Delete point bug (in new UI) (<https://github.com/opencv/cvat/pull/1440>)
- Fix apache startup after PC restart (https://github.com/opencv/cvat/pull/1467)
- Open task button doesn't work (https://github.com/opencv/cvat/pull/1474)
## [1.0.0-beta.1] - 2020-04-15
### Added
- Special behaviour for attribute value ``__undefined__`` (invisibility, no shortcuts to be set in AAM)
- Dialog window with some helpful information about using filters
- Ability to display a bitmap in the new UI
- Button to reset colors settings (brightness, saturation, contrast) in the new UI
- Option to display shape text always
- Dedicated message with clarifications when share is unmounted (https://github.com/opencv/cvat/pull/1373)
- Ability to create one tracked point (https://github.com/opencv/cvat/pull/1383)
- Ability to draw/edit polygons and polylines with automatic bordering feature (https://github.com/opencv/cvat/pull/1394)
- Tutorial: instructions for CVAT over HTTPS
- Deep extreme cut (semi-automatic segmentation) to the new UI (https://github.com/opencv/cvat/pull/1398)
### Changed
- Increase preview size of a task till 256, 256 on the server
- Public ssh-keys are displayed in a dedicated window instead of console when create a task with a repository
- React UI is the primary UI
### Fixed
- Cleaned up memory in Auto Annotation to enable long running tasks on videos
- New shape is added when press ``esc`` when drawing instead of cancellation
- Dextr segmentation doesn't work.
- `FileNotFoundError` during dump after moving format files
- CVAT doesn't append outside shapes when merge polyshapes in old UI
- Layout sometimes shows double scroll bars on create task, dashboard and settings pages
- UI fails after trying to change frame during resizing, dragging, editing
- Hidden points (or outsided) are visible after changing a frame
- Merge is allowed for points, but clicks on points conflict with frame dragging logic
- Removed objects are visible for search
- Add missed task_id and job_id fields into exception logs for the new UI (https://github.com/opencv/cvat/pull/1372)
- UI fails when annotations saving occurs during drag/resize/edit (https://github.com/opencv/cvat/pull/1383)
- Multiple savings when hold Ctrl+S (a lot of the same copies of events were sent with the same working time) (https://github.com/opencv/cvat/pull/1383)
- UI doesn't have any reaction when git repos synchronization failed (https://github.com/opencv/cvat/pull/1383)
- Bug when annotations cannot be saved after (delete - save - undo - save) (https://github.com/opencv/cvat/pull/1383)
- VOC format exports Upper case labels correctly in lower case (https://github.com/opencv/cvat/pull/1379)
- Fixed polygon exporting bug in COCO dataset (https://github.com/opencv/cvat/issues/1387)
- Task creation from remote files (https://github.com/opencv/cvat/pull/1392)
- Job cannot be opened in some cases when the previous job was failed during opening (https://github.com/opencv/cvat/issues/1403)
- Deactivated shape is still highlighted on the canvas (https://github.com/opencv/cvat/issues/1403)
- AttributeError: 'tuple' object has no attribute 'read' in ReID algorithm (https://github.com/opencv/cvat/issues/1403)
- Wrong semi-automatic segmentation near edges of an image (https://github.com/opencv/cvat/issues/1403)
- Git repos paths (https://github.com/opencv/cvat/pull/1400)
- Uploading annotations for tasks with multiple jobs (https://github.com/opencv/cvat/pull/1396)
## [1.0.0-alpha] - 2020-03-31
### Added
- Data streaming using chunks (https://github.com/opencv/cvat/pull/1007)
- New UI: showing file names in UI (https://github.com/opencv/cvat/pull/1311)
- New UI: delete a point from context menu (https://github.com/opencv/cvat/pull/1292)
### Fixed
- Git app cannot clone a repository (https://github.com/opencv/cvat/pull/1330)
- New UI: preview position in task details (https://github.com/opencv/cvat/pull/1312)
- AWS deployment (https://github.com/opencv/cvat/pull/1316)
## [0.6.1] - 2020-03-21
### Changed
- VOC task export now does not use official label map by default, but takes one
@ -43,6 +176,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- React & Redux & Antd based dashboard
- Yolov3 interpretation script fix and changes to mapping.json
- YOLO format support ([#1151](https://github.com/opencv/cvat/pull/1151))
- Added support for OpenVINO 2020
### Fixed
- Exception in Git plugin [#826](https://github.com/opencv/cvat/issues/826)

@ -12,52 +12,62 @@ patches and features.
Next steps should work on clear Ubuntu 18.04.
- Install necessary dependencies:
```sh
$ sudo apt-get update && sudo apt-get --no-install-recommends install -y ffmpeg build-essential nodejs npm curl redis-server python3-dev python3-pip python3-venv libldap2-dev libsasl2-dev
```
- Install necessary dependencies:
```sh
$ sudo apt-get update && sudo apt-get --no-install-recommends install -y ffmpeg build-essential curl redis-server python3-dev python3-pip python3-venv libldap2-dev libsasl2-dev
```
Also please make sure that you have installed ffmpeg with all necessary libav* libraries and pkg-config package.
```sh
# Node and npm (you can use default versions of these packages from apt (8.*, 3.*), but we would recommend to use newer versions)
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs
# General dependencies
sudo apt-get install -y pkg-config
# Library components
sudo apt-get install -y \
libavformat-dev libavcodec-dev libavdevice-dev \
libavutil-dev libswscale-dev libswresample-dev libavfilter-dev
```
See [PyAV Dependencies installation guide](http://docs.mikeboers.com/pyav/develop/overview/installation.html#dependencies)
for details.
- Install [Visual Studio Code](https://code.visualstudio.com/docs/setup/linux#_debian-and-ubuntu-based-distributions)
for development
- Install CVAT on your local host:
```sh
git clone https://github.com/opencv/cvat
cd cvat && mkdir logs keys
python3 -m venv .env
. .env/bin/activate
pip install -U pip wheel setuptools
pip install -r cvat/requirements/development.txt
pip install -r datumaro/requirements.txt
python manage.py migrate
python manage.py collectstatic
```
- Create a super user for CVAT:
```sh
$ python manage.py createsuperuser
Username (leave blank to use 'django'): ***
Email address: ***
Password: ***
Password (again): ***
```
- Install npm packages for UI and start UI debug server (run the following command from CVAT root directory):
```sh
npm install && \
cd cvat-core && npm install && \
cd ../cvat-canvas && npm install && \
cd ../cvat-ui && npm install && npm start
```
- Open new terminal (Ctrl + Shift + T), run Visual Studio Code from the virtual environment
```sh
cd .. && source .env/bin/activate && code
```
```sh
git clone https://github.com/opencv/cvat
cd cvat && mkdir logs keys
python3 -m venv .env
. .env/bin/activate
pip install -U pip wheel setuptools
pip install -r cvat/requirements/development.txt
pip install -r datumaro/requirements.txt
python manage.py migrate
python manage.py collectstatic
```
- Create a super user for CVAT:
```sh
$ python manage.py createsuperuser
Username (leave blank to use 'django'): ***
Email address: ***
Password: ***
Password (again): ***
```
- Install npm packages for UI and start UI debug server (run the following command from CVAT root directory):
```sh
cd cvat-core && npm install && \
cd ../cvat-ui && npm install && npm start
```
- Open new terminal (Ctrl + Shift + T), run Visual Studio Code from the virtual environment
```sh
cd .. && source .env/bin/activate && code
```
- Install followig vscode extensions:
- [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
@ -65,6 +75,7 @@ cd ../cvat-ui && npm install && npm start
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
- [vscode-remark-lint](https://marketplace.visualstudio.com/items?itemName=drewbourne.vscode-remark-lint)
- [licenser](https://marketplace.visualstudio.com/items?itemName=ymotongpoo.licenser)
- [Trailing Spaces](https://marketplace.visualstudio.com/items?itemName=shardulm94.trailing-spaces)
- Reload Visual Studio Code from virtual environment

@ -1,38 +0,0 @@
# Core support team
- **[Nikita Manovich](https://github.com/nmanovic)**
* Project lead
* Developer
* Author and maintainer
- **[Boris Sekachev](https://github.com/bsekachev)**
* Primary developer
* Author and maintainer
- **[Andrey Zhavoronkov](https://github.com/azhavoro)**
* Developer
* Author and maintainer
# Contributors
- **[Victor Salimonov](https://github.com/VikTorSalimonov)**
* Documentation, screencasts
- **[Dmitry Sidnev](https://github.com/DmitriySidnev)**
* [convert_to_coco.py](utils/coco) - an utility for converting annotation from CVAT to COCO data annotation format
- **[Sebastián Yonekura](https://github.com/syonekura)**
* [convert_to_voc.py](utils/voc) - an utility for converting CVAT XML to PASCAL VOC data annotation format.
- **[ITLab Team](https://github.com/itlab-vision/cvat):**
**[Vasily Danilin](https://github.com/DanVev)**,
**[Eugene Shashkin](https://github.com/EvgenyShashkin)**,
**[Dmitry Silenko](https://github.com/DimaSilenko)**,
**[Alina Bykovskaya](https://github.com/alinaut)**,
**[Yanina Koltushkina](https://github.com/YaniKolt)**
* Integrating CI tools as Travis CI, Codacy and Coveralls.io

@ -21,20 +21,31 @@ ENV DJANGO_CONFIGURATION=${DJANGO_CONFIGURATION}
# Install necessary apt packages
RUN apt-get update && \
apt-get install -yq \
apt-get --no-install-recommends install -yq \
software-properties-common && \
add-apt-repository ppa:mc3man/xerus-media -y && \
add-apt-repository ppa:mc3man/gstffmpeg-keep -y && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
apache2 \
apache2-dev \
apt-utils \
build-essential \
libapache2-mod-xsendfile \
supervisor \
ffmpeg \
gstreamer0.10-ffmpeg \
libavcodec-dev \
libavdevice-dev \
libavfilter-dev \
libavformat-dev \
libavutil-dev \
libldap2-dev \
libswresample-dev \
libswscale-dev \
libldap2-dev \
libsasl2-dev \
pkg-config \
python3-dev \
python3-pip \
tzdata \
@ -44,24 +55,25 @@ RUN apt-get update && \
poppler-utils \
curl && \
curl https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \
apt-get install -y git-lfs && git lfs install && \
if [ -z ${socks_proxy} ]; then \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30\"" >> ${HOME}/.bashrc; \
else \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ProxyCommand='nc -X 5 -x ${socks_proxy} %h %p'\"" >> ${HOME}/.bashrc; \
fi && \
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 && \
ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \
add-apt-repository --remove ppa:mc3man/gstffmpeg-keep -y && \
add-apt-repository --remove ppa:mc3man/xerus-media -y && \
rm -rf /var/lib/apt/lists/*
rm -rf /var/lib/apt/lists/* && \
echo 'application/wasm wasm' >> /etc/mime.types
# Add a non-root user
ENV USER=${USER}
ENV HOME /home/${USER}
WORKDIR ${HOME}
RUN adduser --shell /bin/bash --disabled-password --gecos "" ${USER}
RUN adduser --shell /bin/bash --disabled-password --gecos "" ${USER} && \
if [ -z ${socks_proxy} ]; then \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30\"" >> ${HOME}/.bashrc; \
else \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ProxyCommand='nc -X 5 -x ${socks_proxy} %h %p'\"" >> ${HOME}/.bashrc; \
fi
COPY components /tmp/components
@ -121,6 +133,7 @@ COPY ssh ${HOME}/.ssh
COPY utils ${HOME}/utils
COPY cvat/ ${HOME}/cvat
COPY cvat-core/ ${HOME}/cvat-core
COPY cvat-data/ ${HOME}/cvat-data
COPY tests ${HOME}/tests
COPY datumaro/ ${HOME}/datumaro

@ -7,12 +7,21 @@ 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 - && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
apt-utils \
build-essential \
google-chrome-stable \
nodejs && \
nodejs \
python3-dev \
ruby \
&& \
rm -rf /var/lib/apt/lists/*;
RUN python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt
RUN python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt && \
python3 -m pip install --no-cache-dir coveralls
RUN gem install coveralls-lcov
COPY .coveragerc .
# RUN all commands below as 'django' user
USER ${USER}
@ -29,4 +38,4 @@ RUN mkdir -p tests && cd tests && npm install \
qunit; \
echo "export PATH=~/tests/node_modules/.bin:${PATH}" >> ~/.bashrc;
ENTRYPOINT []
ENTRYPOINT []

@ -4,20 +4,26 @@ ARG http_proxy
ARG https_proxy
ARG no_proxy
ARG socks_proxy
ARG PUBLIC_INSTANCE
ARG WA_PAGE_VIEW_HIT
ENV TERM=xterm \
http_proxy=${http_proxy} \
https_proxy=${https_proxy} \
no_proxy=${no_proxy} \
socks_proxy=${socks_proxy}
ENV LANG='C.UTF-8' \
socks_proxy=${socks_proxy} \
LANG='C.UTF-8' \
LC_ALL='C.UTF-8'
# Install dependencies
COPY cvat-core/package*.json /tmp/cvat-core/
COPY cvat-canvas/package*.json /tmp/cvat-canvas/
COPY cvat-ui/package*.json /tmp/cvat-ui/
COPY cvat-data/package*.json /tmp/cvat-data/
# Install cvat-data dependencies
WORKDIR /tmp/cvat-data/
RUN npm install
# Install cvat-core dependencies
WORKDIR /tmp/cvat-core/
@ -32,6 +38,7 @@ WORKDIR /tmp/cvat-ui/
RUN npm install
# Build source code
COPY cvat-data/ /tmp/cvat-data/
COPY cvat-core/ /tmp/cvat-core/
COPY cvat-canvas/ /tmp/cvat-canvas/
COPY cvat-ui/ /tmp/cvat-ui/
@ -39,5 +46,6 @@ RUN npm run build
FROM nginx:stable-alpine
# Replace default.conf configuration to remove unnecessary rules
RUN sed -i "s/}/application\/wasm wasm;\n}/g" /etc/nginx/mime.types
COPY cvat-ui/react_nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=cvat-ui /tmp/cvat-ui/dist /usr/share/nginx/html/

@ -7,7 +7,11 @@
[![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.
CVAT is free, online, interactive video and image annotation
tool for computer vision. It is being used by our team to
annotate million of objects with different properties. Many UI
and UX decisions are based on feedbacks from professional data annotation team.
Try it online [cvat.org](https://cvat.org).
![CVAT screenshot](cvat/apps/documentation/static/documentation/images/cvat.jpg)
@ -34,37 +38,50 @@ CVAT is free, online, interactive video and image annotation tool for computer v
## Supported annotation formats
Format selection is possible after clicking on the Upload annotation / Dump annotation button.
[Datumaro](datumaro/README.md) dataset framework allows additional dataset transformations
via its command line tool.
Format selection is possible after clicking on the Upload annotation
and Dump annotation buttons. [Datumaro](datumaro/README.md) dataset
framework allows additional dataset transformations
via its command line tool and Python library.
| Annotation format | Dumper | Loader |
| Annotation format | Import | Export |
| ------------------------------------------------------------------------------------------ | ------ | ------ |
| [CVAT XML v1.1 for images](cvat/apps/documentation/xml_format.md#annotation) | X | X |
| [CVAT XML v1.1 for a video](cvat/apps/documentation/xml_format.md#interpolation) | X | X |
| [Pascal VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| [CVAT for images](cvat/apps/documentation/xml_format.md#annotation) | X | X |
| [CVAT for a video](cvat/apps/documentation/xml_format.md#interpolation) | X | X |
| [Datumaro](datumaro/README.md) | | X |
| [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| Segmentation masks from [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| [YOLO](https://pjreddie.com/darknet/yolo/) | X | X |
| [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X |
| PNG class mask + instance mask as in [Pascal VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tf_records) | X | X |
| [MOT](https://motchallenge.net/) | X | X |
| [LabelMe](http://labelme.csail.mit.edu/Release3.0) | 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/)
## Online Demo
## Online demo: [cvat.org](https://cvat.org)
[Onepanel](https://www.onepanel.io/) has added CVAT as an environment into their platform and a running demo of CVAT can be accessed at [CVAT Public Demo](https://c.onepanel.io/onepanel-demo/projects/cvat-public-demo/workspaces?utm_source=cvat).
This is an online demo with the latest version of the annotation tool.
Try it online without local installation. Only own or assigned tasks
are visible to users.
If you have any questions, please contact Onepanel directly at support@onepanel.io. If you are in the Onepanel application, you can also use the chat icon in the bottom right corner.
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
- Uploaded data is limited to 500Mb
## REST API
Automatically generated Swagger documentation for Django REST API is
available on ``<cvat_origin>/api/swagger`` (default: ``localhost:8080/api/swagger``).
available on ``<cvat_origin>/api/swagger``
(default: ``localhost:8080/api/swagger``).
Swagger documentation is visiable on allowed hostes, Update environement variable in docker-compose.yml file with cvat hosted machine IP or domain name. Example - ``ALLOWED_HOSTS: 'localhost, 127.0.0.1'``)
## LICENSE

@ -13,10 +13,10 @@
sudo add-apt-repository ppa:graphics-drivers/ppa
sudo apt-get update
sudo apt-cache search nvidia-* # find latest nvidia driver
sudo apt-get install nvidia-* # install the nvidia driver
sudo apt-get install mesa-common-dev
sudo apt-get install freeglut3-dev
sudo apt-get install nvidia-modprobe
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.

@ -22,7 +22,7 @@ cd /tmp/components/openvino
tar -xzf `ls | grep "openvino_toolkit"`
cd `ls -d */ | grep "openvino_toolkit"`
apt-get update && apt-get install -y sudo cpio && \
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

@ -4,6 +4,13 @@
The CVAT module written in TypeScript language.
It presents a canvas to viewing, drawing and editing of annotations.
## Versioning
If you make changes in this package, please do following:
- After not important changes (typos, backward compatible bug fixes, refactoring) do: ``npm version patch``
- After changing API (backward compatible new features) do: ``npm version minor``
- After changing API (changes that break backward compatibility) do: ``npm version major``
## Commands
- Building of the module from sources in the ```dist``` directory:
@ -12,13 +19,6 @@ npm run build
npm run build -- --mode=development # without a minification
```
- Updating of a module version:
```bash
npm version patch # updated after minor fixes
npm version minor # updated after major changes which don't affect API compatibility with previous versions
npm version major # updated after major changes which affect API compatibility with previous versions
```
## Using
Canvas itself handles:
@ -37,10 +37,34 @@ Canvas itself handles:
EXTREME_POINTS = 'By 4 points'
}
enum CuboidDrawingMethod {
CLASSIC = 'From rectangle',
CORNER_POINTS = 'By 4 points',
}
enum Mode {
IDLE = 'idle',
DRAG = 'drag',
RESIZE = 'resize',
DRAW = 'draw',
EDIT = 'edit',
MERGE = 'merge',
SPLIT = 'split',
GROUP = 'group',
DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'zoom_canvas',
}
interface Configuration {
displayAllText?: boolean;
undefinedAttrValue?: string;
}
interface DrawData {
enabled: boolean;
shapeType?: string;
rectDrawingMethod?: RectDrawingMethod;
cuboidDrawingMethod?: CuboidDrawingMethod;
numberOfPoints?: number;
initialState?: any;
crosshair?: boolean;
@ -86,10 +110,13 @@ Canvas itself handles:
select(objectState: any): void;
fitCanvas(): void;
bitmap(enabled: boolean): void;
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
mode(): Mode;
cancel(): void;
configure(configuration: Configuration): void;
}
```
@ -128,6 +155,11 @@ Standard JS events are used.
- canvas.dragstop
- canvas.zoomstart
- canvas.zoomstop
- canvas.zoom
- canvas.fit
- canvas.dragshape => {id: number}
- canvas.resizeshape => {id: number}
- canvas.contextmenu => { mouseEvent: MouseEvent, objectState: ObjectState, pointID: number }
```
### WEB
@ -135,7 +167,8 @@ Standard JS events are used.
// Create an instance of a canvas
const canvas = new window.canvas.Canvas();
console.log('Version', window.canvas.CanvasVersion);
console.log('Version ', window.canvas.CanvasVersion);
console.log('Current mode is ', window.canvas.mode());
// Put canvas to a html container
htmlContainer.appendChild(canvas.html());
@ -153,21 +186,25 @@ Standard JS events are used.
## API Reaction
| | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING | DRAG | ZOOM |
|--------------|------|----------|-----------|---------|---------|---------|------|------|
| html() | + | + | + | + | + | + | + | + |
| setup() | + | + | + | + | + | - | + | + |
| activate() | + | - | - | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + | + | + |
| draw() | + | - | - | - | - | - | - | - |
| split() | + | - | + | - | - | - | - | - |
| group() | + | + | - | - | - | - | - | - |
| merge() | + | - | - | - | + | - | - | - |
| fitCanvas() | + | + | + | + | + | + | + | + |
| dragCanvas() | + | - | - | - | - | - | + | - |
| zoomCanvas() | + | - | - | - | - | - | - | + |
| cancel() | - | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + |
| | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS |
|--------------|------|-------|-------|------|-------|------|------|--------|-------------|-------------|
| html() | + | + | + | + | + | + | + | + | + | + |
| setup() | + | + | + | + | + | +/- | +/- | +/- | + | + |
| activate() | + | - | - | - | - | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + | + | + | + | + |
| draw() | + | - | - | - | - | - | - | - | - | - |
| split() | + | - | + | - | - | - | - | - | - | - |
| group() | + | + | - | - | - | - | - | - | - | - |
| merge() | + | - | - | - | + | - | - | - | - | - |
| fitCanvas() | + | + | + | + | + | + | + | + | + | + |
| dragCanvas() | + | - | - | - | - | - | + | - | - | + |
| zoomCanvas() | + | - | - | - | - | - | - | + | + | - |
| cancel() | - | + | + | + | + | + | + | + | + | + |
| configure() | + | + | + | + | + | + | + | + | + | + |
| bitmap() | + | + | + | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + | + | + |
You can call setup() during editing, dragging, and resizing only to update objects, not to change a frame.

@ -1,6 +1,6 @@
{
"name": "cvat-canvas",
"version": "0.5.2",
"version": "1.1.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -337,6 +337,189 @@
"@babel/plugin-syntax-async-generators": "^7.2.0"
}
},
"@babel/plugin-proposal-class-properties": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz",
"integrity": "sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==",
"dev": true,
"requires": {
"@babel/helper-create-class-features-plugin": "^7.8.3",
"@babel/helper-plugin-utils": "^7.8.3"
},
"dependencies": {
"@babel/code-frame": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
"integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
"dev": true,
"requires": {
"@babel/highlight": "^7.8.3"
}
},
"@babel/generator": {
"version": "7.8.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.7.tgz",
"integrity": "sha512-DQwjiKJqH4C3qGiyQCAExJHoZssn49JTMJgZ8SANGgVFdkupcUhLOdkAeoC6kmHZCPfoDG5M0b6cFlSN5wW7Ew==",
"dev": true,
"requires": {
"@babel/types": "^7.8.7",
"jsesc": "^2.5.1",
"lodash": "^4.17.13",
"source-map": "^0.5.0"
}
},
"@babel/helper-create-class-features-plugin": {
"version": "7.8.6",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.6.tgz",
"integrity": "sha512-klTBDdsr+VFFqaDHm5rR69OpEQtO2Qv8ECxHS1mNhJJvaHArR6a1xTf5K/eZW7eZpJbhCx3NW1Yt/sKsLXLblg==",
"dev": true,
"requires": {
"@babel/helper-function-name": "^7.8.3",
"@babel/helper-member-expression-to-functions": "^7.8.3",
"@babel/helper-optimise-call-expression": "^7.8.3",
"@babel/helper-plugin-utils": "^7.8.3",
"@babel/helper-replace-supers": "^7.8.6",
"@babel/helper-split-export-declaration": "^7.8.3"
}
},
"@babel/helper-function-name": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz",
"integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==",
"dev": true,
"requires": {
"@babel/helper-get-function-arity": "^7.8.3",
"@babel/template": "^7.8.3",
"@babel/types": "^7.8.3"
}
},
"@babel/helper-get-function-arity": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz",
"integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==",
"dev": true,
"requires": {
"@babel/types": "^7.8.3"
}
},
"@babel/helper-member-expression-to-functions": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz",
"integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==",
"dev": true,
"requires": {
"@babel/types": "^7.8.3"
}
},
"@babel/helper-optimise-call-expression": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz",
"integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==",
"dev": true,
"requires": {
"@babel/types": "^7.8.3"
}
},
"@babel/helper-plugin-utils": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz",
"integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==",
"dev": true
},
"@babel/helper-replace-supers": {
"version": "7.8.6",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz",
"integrity": "sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==",
"dev": true,
"requires": {
"@babel/helper-member-expression-to-functions": "^7.8.3",
"@babel/helper-optimise-call-expression": "^7.8.3",
"@babel/traverse": "^7.8.6",
"@babel/types": "^7.8.6"
}
},
"@babel/helper-split-export-declaration": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz",
"integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==",
"dev": true,
"requires": {
"@babel/types": "^7.8.3"
}
},
"@babel/highlight": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
"integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
"dev": true,
"requires": {
"chalk": "^2.0.0",
"esutils": "^2.0.2",
"js-tokens": "^4.0.0"
}
},
"@babel/parser": {
"version": "7.8.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz",
"integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==",
"dev": true
},
"@babel/template": {
"version": "7.8.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz",
"integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.8.3",
"@babel/parser": "^7.8.6",
"@babel/types": "^7.8.6"
}
},
"@babel/traverse": {
"version": "7.8.6",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz",
"integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.8.3",
"@babel/generator": "^7.8.6",
"@babel/helper-function-name": "^7.8.3",
"@babel/helper-split-export-declaration": "^7.8.3",
"@babel/parser": "^7.8.6",
"@babel/types": "^7.8.6",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.13"
}
},
"@babel/types": {
"version": "7.8.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz",
"integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}
}
},
"@babel/plugin-proposal-dynamic-import": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz",
@ -1567,9 +1750,9 @@
"dev": true
},
"aws4": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.0.tgz",
"integrity": "sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==",
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==",
"dev": true
},
"babel-loader": {
@ -1939,28 +2122,6 @@
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
"dev": true
},
"cacache": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.3.tgz",
"integrity": "sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==",
"dev": true,
"requires": {
"bluebird": "^3.5.5",
"chownr": "^1.1.1",
"figgy-pudding": "^3.5.1",
"glob": "^7.1.4",
"graceful-fs": "^4.1.15",
"lru-cache": "^5.1.1",
"mississippi": "^3.0.0",
"mkdirp": "^0.5.1",
"move-concurrently": "^1.0.1",
"promise-inflight": "^1.0.1",
"rimraf": "^2.6.3",
"ssri": "^6.0.1",
"unique-filename": "^1.1.1",
"y18n": "^4.0.0"
}
},
"cache-base": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
@ -2628,9 +2789,9 @@
}
},
"css-loader": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.2.0.tgz",
"integrity": "sha512-QTF3Ud5H7DaZotgdcJjGMvyDj5F3Pn1j/sC6VBEOVp94cbwqyIBdcs/quzj4MC1BKQSrTpQznegH/5giYbhnCQ==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz",
"integrity": "sha512-jYq4zdZT0oS0Iykt+fqnzVLRIeiPWhka+7BqPn+oSIpWJAHak5tmB/WZrJ2a21JhCeFyNnnlroSl8c+MtVndzA==",
"dev": true,
"requires": {
"camelcase": "^5.3.1",
@ -2638,24 +2799,41 @@
"icss-utils": "^4.1.1",
"loader-utils": "^1.2.3",
"normalize-path": "^3.0.0",
"postcss": "^7.0.17",
"postcss": "^7.0.23",
"postcss-modules-extract-imports": "^2.0.0",
"postcss-modules-local-by-default": "^3.0.2",
"postcss-modules-scope": "^2.1.0",
"postcss-modules-scope": "^2.1.1",
"postcss-modules-values": "^3.0.0",
"postcss-value-parser": "^4.0.0",
"schema-utils": "^2.0.0"
"postcss-value-parser": "^4.0.2",
"schema-utils": "^2.6.0"
},
"dependencies": {
"postcss": {
"version": "7.0.27",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz",
"integrity": "sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
"source-map": "^0.6.1",
"supports-color": "^6.1.0"
}
},
"schema-utils": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.2.0.tgz",
"integrity": "sha512-5EwsCNhfFTZvUreQhx/4vVQpJ/lnCAkgoIHLhSpp4ZirE+4hzFvdJi0FMub6hxbFVBJYSpeVVmon+2e7uEGRrA==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz",
"integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==",
"dev": true,
"requires": {
"ajv": "^6.10.2",
"ajv-keywords": "^3.4.1"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
@ -4755,13 +4933,13 @@
}
},
"globule": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.3.0.tgz",
"integrity": "sha512-YlD4kdMqRCQHrhVdonet4TdRtv1/sZKepvoxNT4Nrhrp5HI8XFfc8kFlGlBn2myBo80aGp8Eft259mbcUJhgSg==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.3.1.tgz",
"integrity": "sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==",
"dev": true,
"requires": {
"glob": "~7.1.1",
"lodash": "~4.17.10",
"lodash": "~4.17.12",
"minimatch": "~3.0.2"
}
},
@ -5149,6 +5327,12 @@
"integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
"dev": true
},
"infer-owner": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
"integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
"dev": true
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -5378,13 +5562,10 @@
"dev": true
},
"is-finite": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
"integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
"dev": true,
"requires": {
"number-is-nan": "^1.0.0"
}
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz",
"integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
@ -5584,9 +5765,9 @@
"dev": true
},
"js-base64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz",
"integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz",
"integrity": "sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ==",
"dev": true
},
"js-levenshtein": {
@ -6363,9 +6544,9 @@
}
},
"node-sass": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.0.tgz",
"integrity": "sha512-W1XBrvoJ1dy7VsvTAS5q1V45lREbTlZQqFbiHb3R3OTTCma0XBtuG6xZ6Z4506nR4lmHPTqVRwxT6KgtWC97CA==",
"version": "4.13.1",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.1.tgz",
"integrity": "sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw==",
"dev": true,
"requires": {
"async-foreach": "^0.1.3",
@ -7381,9 +7562,9 @@
}
},
"postcss-modules-scope": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz",
"integrity": "sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.1.tgz",
"integrity": "sha512-OXRUPecnHCg8b9xWvldG/jUpRIGPNRka0r4D4j0ESUU2/5IOnpsjfPPmDprM3Ih8CgZ8FXjWqaniK5v4rWt3oQ==",
"dev": true,
"requires": {
"postcss": "^7.0.6",
@ -7633,9 +7814,9 @@
"dev": true
},
"psl": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.6.0.tgz",
"integrity": "sha512-SYKKmVel98NCOYXpkwUqZqh0ahZeeKfmisiLIcEZdsb+WbLv02g/dI5BUmZnIyOe7RzZtLax81nnb2HbvC2tzA==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz",
"integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==",
"dev": true
},
"pstree.remy": {
@ -8010,9 +8191,9 @@
}
},
"request": {
"version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"dev": true,
"requires": {
"aws-sign2": "~0.7.0",
@ -8022,7 +8203,7 @@
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.0",
"har-validator": "~5.1.3",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
@ -8032,7 +8213,7 @@
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.4.3",
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
},
@ -8402,22 +8583,22 @@
}
},
"sass-loader": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.0.tgz",
"integrity": "sha512-+qeMu563PN7rPdit2+n5uuYVR0SSVwm0JsOUsaJXzgYcClWSlmX0iHDnmeOobPkf5kUglVot3QS6SyLyaQoJ4w==",
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.2.tgz",
"integrity": "sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ==",
"dev": true,
"requires": {
"clone-deep": "^4.0.1",
"loader-utils": "^1.2.3",
"neo-async": "^2.6.1",
"schema-utils": "^2.1.0",
"schema-utils": "^2.6.1",
"semver": "^6.3.0"
},
"dependencies": {
"schema-utils": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.1.tgz",
"integrity": "sha512-0WXHDs1VDJyo+Zqs9TKLKyD/h7yDpHUhEFsM2CzkICFdoX1av+GBq/J2xRTFfsQO5kBfhZzANf2VcIm84jqDbg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz",
"integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==",
"dev": true,
"requires": {
"ajv": "^6.10.2",
@ -8523,12 +8704,6 @@
}
}
},
"serialize-javascript": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.7.0.tgz",
"integrity": "sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA==",
"dev": true
},
"serve-index": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
@ -9401,28 +9576,66 @@
}
},
"terser-webpack-plugin": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.3.0.tgz",
"integrity": "sha512-W2YWmxPjjkUcOWa4pBEv4OP4er1aeQJlSo2UhtCFQCuRXEHjOFscO8VyWHj9JLlA0RzQb8Y2/Ta78XZvT54uGg==",
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
"integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
"dev": true,
"requires": {
"cacache": "^11.3.2",
"find-cache-dir": "^2.0.0",
"cacache": "^12.0.2",
"find-cache-dir": "^2.1.0",
"is-wsl": "^1.1.0",
"loader-utils": "^1.2.3",
"schema-utils": "^1.0.0",
"serialize-javascript": "^1.7.0",
"serialize-javascript": "^2.1.2",
"source-map": "^0.6.1",
"terser": "^4.0.0",
"webpack-sources": "^1.3.0",
"terser": "^4.1.2",
"webpack-sources": "^1.4.0",
"worker-farm": "^1.7.0"
},
"dependencies": {
"cacache": {
"version": "12.0.3",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz",
"integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==",
"dev": true,
"requires": {
"bluebird": "^3.5.5",
"chownr": "^1.1.1",
"figgy-pudding": "^3.5.1",
"glob": "^7.1.4",
"graceful-fs": "^4.1.15",
"infer-owner": "^1.0.3",
"lru-cache": "^5.1.1",
"mississippi": "^3.0.0",
"mkdirp": "^0.5.1",
"move-concurrently": "^1.0.1",
"promise-inflight": "^1.0.1",
"rimraf": "^2.6.3",
"ssri": "^6.0.1",
"unique-filename": "^1.1.1",
"y18n": "^4.0.0"
}
},
"serialize-javascript": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
"integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"webpack-sources": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz",
"integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==",
"dev": true,
"requires": {
"source-list-map": "^2.0.0",
"source-map": "~0.6.1"
}
}
}
},
@ -9548,21 +9761,13 @@
}
},
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"dev": true,
"requires": {
"psl": "^1.1.24",
"punycode": "^1.4.1"
},
"dependencies": {
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
}
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
},
"trim-newlines": {

@ -1,6 +1,6 @@
{
"name": "cvat-canvas",
"version": "0.5.2",
"version": "1.1.1",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts",
"scripts": {
@ -19,23 +19,24 @@
"devDependencies": {
"@babel/cli": "^7.5.5",
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/preset-env": "^7.5.5",
"@babel/preset-typescript": "^7.3.3",
"@types/node": "^12.6.8",
"@typescript-eslint/eslint-plugin": "^1.13.0",
"@typescript-eslint/parser": "^1.13.0",
"babel-loader": "^8.0.6",
"css-loader": "^3.2.0",
"css-loader": "^3.4.2",
"dts-bundle-webpack": "^1.0.2",
"eslint": "^6.1.0",
"eslint-config-airbnb-typescript": "^4.0.1",
"eslint-config-typescript-recommended": "^1.4.17",
"eslint-plugin-import": "^2.18.2",
"node-sass": "^4.13.0",
"node-sass": "^4.13.1",
"nodemon": "^1.19.1",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"sass-loader": "^8.0.0",
"sass-loader": "^8.0.2",
"style-loader": "^1.0.0",
"typescript": "^3.5.3",
"webpack": "^4.36.1",

@ -103,11 +103,54 @@ polyline.cvat_canvas_shape_splitting {
stroke-dasharray: 5;
}
.cvat_canvas_shape .svg_select_points, .cvat_canvas_shape .cvat_canvas_cuboid_projections {
stroke-dasharray: none;
}
.cvat_canvas_autoborder_point {
opacity: 0.55;
}
.cvat_canvas_autoborder_point:hover {
opacity: 1;
fill: red;
}
.cvat_canvas_autoborder_point:active {
opacity: 0.55;
fill: red;
}
.cvat_canvas_autoborder_point_direction {
fill: blueviolet;
}
.svg_select_boundingRect {
opacity: 0;
pointer-events: none;
}
.svg_select_points_lb:hover, .svg_select_points_rt:hover {
cursor: nesw-resize;
}
.svg_select_points_lt:hover, .svg_select_points_rb:hover {
cursor: nwse-resize;
}
.svg_select_points_l:hover, .svg_select_points_r:hover,
.svg_select_points_ew:hover {
cursor: ew-resize;
}
.svg_select_points_t:hover, .svg_select_points_b:hover {
cursor: ns-resize;
}
.cvat_canvas_shape_draggable:hover {
cursor: move;
}
#cvat_canvas_wrapper {
width: calc(100% - 10px);
height: calc(100% - 10px);
@ -152,6 +195,16 @@ polyline.cvat_canvas_shape_splitting {
box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75);
}
#cvat_canvas_bitmap {
pointer-events: none;
position: absolute;
z-index: 4;
background: black;
width: 100%;
height: 100%;
box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75);
}
#cvat_canvas_grid {
position: absolute;
z-index: 2;

@ -0,0 +1,301 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
import { Geometry } from './canvasModel';
interface TransformedShape {
points: string;
color: string;
}
export interface AutoborderHandler {
autoborder(enabled: boolean, currentShape?: SVG.Shape, ignoreCurrent?: boolean): void;
transform(geometry: Geometry): void;
updateObjects(): void;
}
export class AutoborderHandlerImpl implements AutoborderHandler {
private currentShape: SVG.Shape | null;
private ignoreCurrent: boolean;
private frameContent: SVGSVGElement;
private enabled: boolean;
private scale: number;
private groups: SVGGElement[];
private auxiliaryGroupID: number | null;
private auxiliaryClicks: number[];
private listeners: Record<number, Record<number, {
click: (event: MouseEvent) => void;
dblclick: (event: MouseEvent) => void;
}>>;
public constructor(frameContent: SVGSVGElement) {
this.frameContent = frameContent;
this.ignoreCurrent = false;
this.currentShape = null;
this.enabled = false;
this.scale = 1;
this.groups = [];
this.auxiliaryGroupID = null;
this.auxiliaryClicks = [];
this.listeners = {};
}
private removeMarkers(): void {
this.groups.forEach((group: SVGGElement): void => {
const groupID = group.dataset.groupId;
Array.from(group.children)
.forEach((circle: SVGCircleElement, pointID: number): void => {
circle.removeEventListener('click', this.listeners[+groupID][pointID].click);
circle.removeEventListener('dblclick', this.listeners[+groupID][pointID].click);
circle.remove();
});
group.remove();
});
this.groups = [];
this.auxiliaryGroupID = null;
this.auxiliaryClicks = [];
this.listeners = {};
}
private release(): void {
this.removeMarkers();
this.enabled = false;
this.currentShape = null;
}
private addPointToCurrentShape(x: number, y: number): void {
const array: number[][] = (this.currentShape as any).array().valueOf();
array.pop();
// need to append twice (specific of the library)
array.push([x, y]);
array.push([x, y]);
const paintHandler = this.currentShape.remember('_paintHandler');
paintHandler.drawCircles();
paintHandler.set.members.forEach((el: SVG.Circle): void => {
el.attr('stroke-width', 1 / this.scale).attr('r', 2.5 / this.scale);
});
(this.currentShape as any).plot(array);
}
private resetAuxiliaryShape(): void {
if (this.auxiliaryGroupID !== null) {
while (this.auxiliaryClicks.length > 0) {
const resetID = this.auxiliaryClicks.pop();
this.groups[this.auxiliaryGroupID]
.children[resetID].classList.remove('cvat_canvas_autoborder_point_direction');
}
}
this.auxiliaryClicks = [];
this.auxiliaryGroupID = null;
}
// convert each shape to group of clicable points
// save all groups
private drawMarkers(transformedShapes: TransformedShape[]): void {
const svgNamespace = 'http://www.w3.org/2000/svg';
this.groups = transformedShapes
.map((shape: TransformedShape, groupID: number): SVGGElement => {
const group = document.createElementNS(svgNamespace, 'g');
group.setAttribute('data-group-id', `${groupID}`);
this.listeners[groupID] = this.listeners[groupID] || {};
const circles = shape.points.split(/\s/).map((
point: string, pointID: number, points: string[],
): SVGCircleElement => {
const [x, y] = point.split(',');
const circle = document.createElementNS(svgNamespace, 'circle');
circle.classList.add('cvat_canvas_autoborder_point');
circle.setAttribute('fill', shape.color);
circle.setAttribute('stroke', 'black');
circle.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.scale}`);
circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
circle.setAttribute('r', `${consts.BASE_POINT_SIZE / this.scale}`);
const click = (event: MouseEvent): void => {
event.stopPropagation();
// another shape was clicked
if (this.auxiliaryGroupID !== null
&& this.auxiliaryGroupID !== groupID
) {
this.resetAuxiliaryShape();
}
this.auxiliaryGroupID = groupID;
// up clicked group for convenience
this.frameContent.appendChild(group);
if (this.auxiliaryClicks[1] === pointID) {
// the second point was clicked twice
this.addPointToCurrentShape(+x, +y);
this.resetAuxiliaryShape();
return;
}
// the first point can not be clicked twice
// just ignore such a click if it is
if (this.auxiliaryClicks[0] !== pointID) {
this.auxiliaryClicks.push(pointID);
} else {
return;
}
// it is the first click
if (this.auxiliaryClicks.length === 1) {
const handler = this.currentShape.remember('_paintHandler');
// draw and remove initial point just to initialize data structures
if (!handler || !handler.startPoint) {
(this.currentShape as any).draw('point', event);
(this.currentShape as any).draw('undo');
}
this.addPointToCurrentShape(+x, +y);
// is is the second click
} else if (this.auxiliaryClicks.length === 2) {
circle.classList.add('cvat_canvas_autoborder_point_direction');
// it is the third click
} else {
// sign defines bypass direction
const landmarks = this.auxiliaryClicks;
const sign = Math.sign(landmarks[2] - landmarks[0])
* Math.sign(landmarks[1] - landmarks[0])
* Math.sign(landmarks[2] - landmarks[1]);
// go via a polygon and get vertexes
// the first vertex has been already drawn
const way = [];
for (let i = landmarks[0] + sign; ; i += sign) {
if (i < 0) {
i = points.length - 1;
} else if (i === points.length) {
i = 0;
}
way.push(points[i]);
if (i === this.auxiliaryClicks[this.auxiliaryClicks.length - 1]) {
// put the last element twice
// specific of svg.draw.js
// way.push(points[i]);
break;
}
}
// remove the latest cursor position from drawing array
for (const wayPoint of way) {
const [_x, _y] = wayPoint.split(',')
.map((coordinate: string): number => +coordinate);
this.addPointToCurrentShape(_x, _y);
}
this.resetAuxiliaryShape();
}
};
const dblclick = (event: MouseEvent): void => {
event.stopPropagation();
};
this.listeners[groupID][pointID] = {
click,
dblclick,
};
circle.addEventListener('mousedown', this.listeners[groupID][pointID].click);
circle.addEventListener('dblclick', this.listeners[groupID][pointID].click);
return circle;
});
group.append(...circles);
return group;
});
this.frameContent.append(...this.groups);
}
public updateObjects(): void {
if (!this.enabled) return;
this.removeMarkers();
const currentClientID = this.currentShape.node.dataset.originClientId;
const shapes = Array.from(this.frameContent.getElementsByClassName('cvat_canvas_shape'));
const transformedShapes = shapes.map((shape: HTMLElement): TransformedShape | null => {
const color = shape.getAttribute('fill');
const clientID = shape.getAttribute('clientID');
if (color === null || clientID === null) return null;
if (+clientID === +currentClientID) {
return null;
}
let points = '';
if (shape.tagName === 'polyline' || shape.tagName === 'polygon') {
points = shape.getAttribute('points');
} else if (shape.tagName === 'rect') {
const x = +shape.getAttribute('x');
const y = +shape.getAttribute('y');
const width = +shape.getAttribute('width');
const height = +shape.getAttribute('height');
if (Number.isNaN(x) || Number.isNaN(y) || Number.isNaN(x) || Number.isNaN(x)) {
return null;
}
points = `${x},${y} ${x + width},${y} ${x + width},${y + height} ${x},${y + height}`;
} else if (shape.tagName === 'g') {
const polylineID = shape.dataset.polylineId;
const polyline = this.frameContent.getElementById(polylineID);
if (polyline && polyline.getAttribute('points')) {
points = polyline.getAttribute('points');
} else {
return null;
}
}
return {
color,
points: points.trim(),
};
}).filter((state: TransformedShape | null): boolean => state !== null);
this.drawMarkers(transformedShapes);
}
public autoborder(
enabled: boolean,
currentShape?: SVG.Shape,
ignoreCurrent: boolean = false,
): void {
if (enabled && !this.enabled && currentShape) {
this.enabled = true;
this.currentShape = currentShape;
this.ignoreCurrent = ignoreCurrent;
this.updateObjects();
} else {
this.release();
}
}
public transform(geometry: Geometry): void {
this.scale = geometry.scale;
this.groups.forEach((group: SVGGElement): void => {
Array.from(group.children).forEach((circle: SVGCircleElement): void => {
circle.setAttribute('r', `${consts.BASE_POINT_SIZE / this.scale}`);
circle.setAttribute('stroke-width', `${consts.BASE_STROKE_WIDTH / this.scale}`);
});
});
}
}

@ -11,6 +11,8 @@ import {
CanvasModel,
CanvasModelImpl,
RectDrawingMethod,
CuboidDrawingMethod,
Configuration,
} from './canvasModel';
import {
@ -49,11 +51,13 @@ interface Canvas {
select(objectState: any): void;
fitCanvas(): void;
bitmap(enable: boolean): void;
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
mode(): void;
mode(): Mode;
cancel(): void;
configure(configuration: Configuration): void;
}
class CanvasImpl implements Canvas {
@ -86,6 +90,10 @@ class CanvasImpl implements Canvas {
);
}
public bitmap(enable: boolean): void {
this.model.bitmap(enable);
}
public dragCanvas(enable: boolean): void {
this.model.dragCanvas(enable);
}
@ -141,11 +149,17 @@ class CanvasImpl implements Canvas {
public cancel(): void {
this.model.cancel();
}
public configure(configuration: Configuration): void {
this.model.configure(configuration);
}
}
export {
CanvasImpl as Canvas,
CanvasVersion,
Configuration,
RectDrawingMethod,
CuboidDrawingMethod,
Mode as CanvasMode,
};

@ -36,7 +36,6 @@ export interface CanvasController {
enableDrag(x: number, y: number): void;
drag(x: number, y: number): void;
disableDrag(): void;
fit(): void;
}

@ -9,6 +9,12 @@ export interface Size {
height: number;
}
export interface Image {
renderWidth: number;
renderHeight: number;
imageData: ImageData | CanvasImageSource;
}
export interface Position {
x: number;
y: number;
@ -40,10 +46,23 @@ export enum RectDrawingMethod {
EXTREME_POINTS = 'By 4 points'
}
export enum CuboidDrawingMethod {
CLASSIC = 'From rectangle',
CORNER_POINTS = 'By 4 points',
}
export interface Configuration {
autoborders?: boolean;
displayAllText?: boolean;
undefinedAttrValue?: string;
showProjections?: boolean;
}
export interface DrawData {
enabled: boolean;
shapeType?: string;
rectDrawingMethod?: RectDrawingMethod;
cuboidDrawingMethod?: CuboidDrawingMethod;
numberOfPoints?: number;
initialState?: any;
crosshair?: boolean;
@ -92,8 +111,10 @@ export enum UpdateReasons {
GROUP = 'group',
SELECT = 'select',
CANCEL = 'cancel',
BITMAP = 'bitmap',
DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'ZOOM_CANVAS',
ZOOM_CANVAS = 'zoom_canvas',
CONFIG_UPDATED = 'config_updated',
}
export enum Mode {
@ -110,7 +131,8 @@ export enum Mode {
}
export interface CanvasModel {
readonly image: HTMLImageElement | null;
readonly imageBitmap: boolean;
readonly image: Image | null;
readonly objects: any[];
readonly zLayer: number | null;
readonly gridSize: Size;
@ -120,6 +142,7 @@ export interface CanvasModel {
readonly mergeData: MergeData;
readonly splitData: SplitData;
readonly groupData: GroupData;
readonly configuration: Configuration;
readonly selected: any;
geometry: Geometry;
mode: Mode;
@ -142,9 +165,11 @@ export interface CanvasModel {
select(objectState: any): void;
fitCanvas(width: number, height: number): void;
bitmap(enabled: boolean): void;
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
configure(configuration: Configuration): void;
cancel(): void;
}
@ -153,7 +178,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
activeElement: ActiveElement;
angle: number;
canvasSize: Size;
image: HTMLImageElement | null;
configuration: Configuration;
imageBitmap: boolean;
image: Image | null;
imageID: number | null;
imageOffset: number;
imageSize: Size;
@ -185,6 +212,12 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
height: 0,
width: 0,
},
configuration: {
displayAllText: false,
autoborders: false,
undefinedAttrValue: '',
},
imageBitmap: false,
image: null,
imageID: null,
imageOffset: 0,
@ -271,6 +304,11 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.OBJECTS_UPDATED);
}
public bitmap(enabled: boolean): void {
this.data.imageBitmap = enabled;
this.notify(UpdateReasons.BITMAP);
}
public dragCanvas(enable: boolean): void {
if (enable && this.data.mode !== Mode.IDLE) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
@ -298,6 +336,12 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public setup(frameData: any, objectStates: any[]): 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}`);
}
}
if (frameData.number === this.data.imageID) {
this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED);
@ -310,7 +354,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.image = null;
this.notify(UpdateReasons.IMAGE_CHANGED);
},
).then((data: HTMLImageElement): void => {
).then((data: Image): void => {
if (frameData.number !== this.data.imageID) {
// already another image
return;
@ -331,6 +375,12 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public activate(clientID: number | null, attributeID: number | null): void {
if (this.data.activeElement.clientID === clientID
&& this.data.activeElement.attributeID === attributeID
) {
return;
}
if (this.data.mode !== Mode.IDLE && clientID !== null) {
// Exception or just return?
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
@ -479,10 +529,33 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.selected = null;
}
public configure(configuration: Configuration): void {
if (typeof (configuration.displayAllText) !== 'undefined') {
this.data.configuration.displayAllText = configuration.displayAllText;
}
if (typeof (configuration.showProjections) !== 'undefined') {
this.data.configuration.showProjections = configuration.showProjections;
}
if (typeof (configuration.autoborders) !== 'undefined') {
this.data.configuration.autoborders = configuration.autoborders;
}
if (typeof (configuration.undefinedAttrValue) !== 'undefined') {
this.data.configuration.undefinedAttrValue = configuration.undefinedAttrValue;
}
this.notify(UpdateReasons.CONFIG_UPDATED);
}
public cancel(): void {
this.notify(UpdateReasons.CANCEL);
}
public get configuration(): Configuration {
return { ...this.data.configuration };
}
public get geometry(): Geometry {
return {
angle: this.data.angle,
@ -516,7 +589,11 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return this.data.zLayer;
}
public get image(): HTMLImageElement | null {
public get imageBitmap(): boolean {
return this.data.imageBitmap;
}
public get image(): Image | null {
return this.data.image;
}

@ -16,6 +16,7 @@ import { MergeHandler, MergeHandlerImpl } from './mergeHandler';
import { SplitHandler, SplitHandlerImpl } from './splitHandler';
import { GroupHandler, GroupHandlerImpl } from './groupHandler';
import { ZoomHandler, ZoomHandlerImpl } from './zoomHandler';
import { AutoborderHandler, AutoborderHandlerImpl } from './autoborderHandler';
import consts from './consts';
import {
translateToSVG,
@ -23,6 +24,7 @@ import {
pointsToArray,
displayShapeSize,
ShapeSizeElement,
DrawnState,
} from './shared';
import {
CanvasModel,
@ -36,6 +38,7 @@ import {
GroupData,
Mode,
Size,
Configuration,
} from './canvasModel';
export interface CanvasView {
@ -47,6 +50,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private text: SVGSVGElement;
private adoptedText: SVG.Container;
private background: HTMLCanvasElement;
private bitmap: HTMLCanvasElement;
private grid: SVGSVGElement;
private content: SVGSVGElement;
private adoptedContent: SVG.Container;
@ -56,7 +60,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private controller: CanvasController;
private svgShapes: Record<number, SVG.Shape>;
private svgTexts: Record<number, SVG.Text>;
private drawnStates: Record<number, any>;
private drawnStates: Record<number, DrawnState>;
private geometry: Geometry;
private drawHandler: DrawHandler;
private editHandler: EditHandler;
@ -64,7 +68,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
private splitHandler: SplitHandler;
private groupHandler: GroupHandler;
private zoomHandler: ZoomHandler;
private autoborderHandler: AutoborderHandler;
private activeElement: ActiveElement;
private configuration: Configuration;
private set mode(value: Mode) {
this.controller.mode = value;
@ -74,7 +80,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.controller.mode;
}
private onDrawDone(data: object, continueDraw?: boolean): void {
private onDrawDone(data: object | null, duration: number, continueDraw?: boolean): void {
if (data) {
const { zLayer } = this.controller;
const event: CustomEvent = new CustomEvent('canvas.drawn', {
@ -87,11 +93,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
zOrder: zLayer || 0,
},
continue: continueDraw,
duration,
},
});
this.canvas.dispatchEvent(event);
} else {
} else if (!continueDraw) {
const event: CustomEvent = new CustomEvent('canvas.canceled', {
bubbles: false,
cancelable: true,
@ -100,12 +107,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.dispatchEvent(event);
}
if (continueDraw) {
this.drawHandler.draw(
this.controller.drawData,
this.geometry,
);
} else {
if (!continueDraw) {
this.mode = Mode.IDLE;
this.controller.draw({
enabled: false,
@ -137,12 +139,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.mode = Mode.IDLE;
}
private onMergeDone(objects: any[]): void {
private onMergeDone(objects: any[]| null, duration?: number): void {
if (objects) {
const event: CustomEvent = new CustomEvent('canvas.merged', {
bubbles: false,
cancelable: true,
detail: {
duration,
states: objects,
},
});
@ -264,9 +267,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
y + height / 2,
]);
const canvasOffset = this.canvas.getBoundingClientRect();
const [cx, cy] = [
this.canvas.clientWidth / 2 + this.canvas.offsetLeft,
this.canvas.clientHeight / 2 + this.canvas.offsetTop,
this.canvas.clientWidth / 2 + canvasOffset.left,
this.canvas.clientHeight / 2 + canvasOffset.top,
];
const dragged = {
@ -282,7 +286,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
private moveCanvas(): void {
for (const obj of [this.background, this.grid, this.loadingAnimation]) {
for (const obj of [this.background, this.grid, this.bitmap]) {
obj.style.top = `${this.geometry.top}px`;
obj.style.left = `${this.geometry.left}px`;
}
@ -300,7 +304,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private transformCanvas(): void {
// Transform canvas
for (const obj of [this.background, this.grid, this.loadingAnimation, this.content]) {
for (const obj of [this.background, this.grid, this.content, this.bitmap]) {
obj.style.transform = `scale(${this.geometry.scale}) rotate(${this.geometry.angle}deg)`;
}
@ -352,10 +356,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
// Transform handlers
this.drawHandler.transform(this.geometry);
this.editHandler.transform(this.geometry);
this.autoborderHandler.transform(this.geometry);
}
private resizeCanvas(): void {
for (const obj of [this.background, this.grid, this.loadingAnimation]) {
for (const obj of [this.background, this.grid, this.bitmap]) {
obj.style.width = `${this.geometry.image.width}px`;
obj.style.height = `${this.geometry.image.height}px`;
}
@ -379,6 +384,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
created.push(state);
} else {
const drawnState = this.drawnStates[state.clientID];
// object has been changed or changed frame for a track
if (drawnState.updated !== state.updated || drawnState.frame !== state.frame) {
updated.push(state);
}
@ -389,35 +395,41 @@ export class CanvasViewImpl implements CanvasView, Listener {
.filter((id: number): boolean => !newIDs.includes(id))
.map((id: number): any => this.drawnStates[id]);
if (deleted.length || updated.length || created.length) {
if (this.activeElement.clientID !== null) {
this.deactivate();
}
if (this.activeElement.clientID !== null) {
this.deactivate();
}
for (const state of deleted) {
if (state.clientID in this.svgTexts) {
this.svgTexts[state.clientID].remove();
}
for (const state of deleted) {
if (state.clientID in this.svgTexts) {
this.svgTexts[state.clientID].remove();
this.svgShapes[state.clientID].off('click.canvas');
this.svgShapes[state.clientID].remove();
delete this.drawnStates[state.clientID];
}
this.svgShapes[state.clientID].off('click.canvas');
this.svgShapes[state.clientID].remove();
delete this.drawnStates[state.clientID];
}
this.addObjects(created, translate);
this.updateObjects(updated, translate);
this.sortObjects();
this.addObjects(created, translate);
this.updateObjects(updated, translate);
this.sortObjects();
if (this.controller.activeElement.clientID !== null) {
const { clientID } = this.controller.activeElement;
if (states.map((state: any): number => state.clientID).includes(clientID)) {
this.activate(this.controller.activeElement);
if (this.controller.activeElement.clientID !== null) {
const { clientID } = this.controller.activeElement;
if (states.map((state: any): number => state.clientID).includes(clientID)) {
this.activate(this.controller.activeElement);
}
}
this.autoborderHandler.updateObjects();
}
}
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 {
const pointID = Array.prototype.indexOf
@ -428,6 +440,22 @@ export class CanvasViewImpl implements CanvasView, Listener {
.filter((_state: any): boolean => (
_state.clientID === self.activeElement.clientID
));
if (state.shapeType === 'rectangle') {
e.preventDefault();
return;
}
if (state.shapeType === 'cuboid') {
if (e.shiftKey) {
const points = translate(pointsToArray((e.target as any)
.parentElement.parentElement.instance.attr('points')));
self.onEditDone(
state,
points,
);
e.preventDefault();
return;
}
}
if (e.ctrlKey) {
const { points } = state;
self.onEditDone(
@ -453,6 +481,27 @@ export class CanvasViewImpl implements CanvasView, Listener {
e.preventDefault();
}
function contextMenuHandler(e: MouseEvent): void {
const pointID = Array.prototype.indexOf
.call(((e.target as HTMLElement).parentElement as HTMLElement).children, e.target);
if (self.activeElement.clientID !== null) {
const [state] = self.controller.objects
.filter((_state: any): boolean => (
_state.clientID === self.activeElement.clientID
));
self.canvas.dispatchEvent(new CustomEvent('canvas.contextmenu', {
bubbles: false,
cancelable: true,
detail: {
mouseEvent: e,
objectState: state,
pointID,
},
}));
}
e.preventDefault();
}
if (value) {
(shape as any).selectize(value, {
deepSelect: true,
@ -475,6 +524,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
});
circle.on('dblclick', dblClickHandler);
circle.on('contextmenu', contextMenuHandler);
circle.addClass('cvat_canvas_selected_point');
});
@ -484,6 +534,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
});
circle.off('dblclick', dblClickHandler);
circle.off('contextmenu', contextMenuHandler);
circle.removeClass('cvat_canvas_selected_point');
});
@ -512,6 +563,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
clientID: null,
attributeID: null,
};
this.configuration = model.configuration;
this.mode = Mode.IDLE;
// Create HTML elements
@ -520,6 +572,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.text = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.adoptedText = (SVG.adopt((this.text as any as HTMLElement)) as SVG.Container);
this.background = window.document.createElement('canvas');
this.bitmap = window.document.createElement('canvas');
// window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.grid = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
@ -564,6 +617,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.text.setAttribute('id', 'cvat_canvas_text_content');
this.background.setAttribute('id', 'cvat_canvas_background');
this.content.setAttribute('id', 'cvat_canvas_content');
this.bitmap.setAttribute('id', 'cvat_canvas_bitmap');
this.bitmap.style.display = 'none';
// Setup wrappers
this.canvas.setAttribute('id', 'cvat_canvas_wrapper');
@ -579,6 +634,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.appendChild(this.loadingAnimation);
this.canvas.appendChild(this.text);
this.canvas.appendChild(this.background);
this.canvas.appendChild(this.bitmap);
this.canvas.appendChild(this.grid);
this.canvas.appendChild(this.content);
@ -586,14 +642,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
const self = this;
// Setup API handlers
this.autoborderHandler = new AutoborderHandlerImpl(
this.content,
);
this.drawHandler = new DrawHandlerImpl(
this.onDrawDone.bind(this),
this.adoptedContent,
this.adoptedText,
this.autoborderHandler,
);
this.editHandler = new EditHandlerImpl(
this.onEditDone.bind(this),
this.adoptedContent,
this.autoborderHandler,
);
this.mergeHandler = new MergeHandlerImpl(
this.onMergeDone.bind(this),
@ -625,8 +686,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
});
this.content.addEventListener('mousedown', (event): void => {
if ([1, 2].includes(event.which)) {
if (![Mode.ZOOM_CANVAS, Mode.GROUP].includes(this.mode) || event.which === 2) {
if ([0, 1].includes(event.button)) {
if ([Mode.IDLE, Mode.DRAG_CANVAS, Mode.MERGE, Mode.SPLIT].includes(this.mode)
|| event.button === 1 || event.altKey
) {
self.controller.enableDrag(event.clientX, event.clientY);
}
}
@ -676,17 +739,48 @@ export class CanvasViewImpl implements CanvasView, Listener {
public notify(model: CanvasModel & Master, reason: UpdateReasons): void {
this.geometry = this.controller.geometry;
if (reason === UpdateReasons.IMAGE_CHANGED) {
if (reason === UpdateReasons.CONFIG_UPDATED) {
const { activeElement } = this;
this.deactivate();
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);
} else if (reason === UpdateReasons.BITMAP) {
const { imageBitmap } = model;
if (imageBitmap) {
this.bitmap.style.display = '';
this.redrawBitmap();
} else {
this.bitmap.style.display = 'none';
}
} else if (reason === UpdateReasons.IMAGE_CHANGED) {
const { image } = model;
if (!image) {
this.loadingAnimation.classList.remove('cvat_canvas_hidden');
} else {
this.loadingAnimation.classList.add('cvat_canvas_hidden');
const ctx = this.background.getContext('2d');
this.background.setAttribute('width', `${image.width}px`);
this.background.setAttribute('height', `${image.height}px`);
this.background.setAttribute('width', `${image.renderWidth}px`);
this.background.setAttribute('height', `${image.renderHeight}px`);
if (ctx) {
ctx.drawImage(image, 0, 0);
if (image.imageData instanceof ImageData) {
ctx.scale(image.renderWidth / image.imageData.width,
image.renderHeight / image.imageData.height);
ctx.putImageData(image.imageData, 0, 0);
// Transformation matrix must not affect the putImageData() method.
// By this reason need to redraw the image to apply scale.
// https://www.w3.org/TR/2dcontext/#dom-context-2d-putimagedata
ctx.drawImage(this.background, 0, 0);
} else {
ctx.drawImage(image.imageData, 0, 0);
}
}
this.moveCanvas();
this.resizeCanvas();
@ -700,6 +794,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
} else if ([UpdateReasons.IMAGE_ZOOMED, UpdateReasons.IMAGE_FITTED].includes(reason)) {
this.moveCanvas();
this.transformCanvas();
if (reason === UpdateReasons.IMAGE_FITTED) {
this.canvas.dispatchEvent(new CustomEvent('canvas.fit', {
bubbles: false,
cancelable: true,
}));
}
} else if (reason === UpdateReasons.IMAGE_MOVED) {
this.moveCanvas();
} else if ([UpdateReasons.OBJECTS_UPDATED, UpdateReasons.SET_Z_LAYER].includes(reason)) {
@ -725,7 +825,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (object) {
const bbox: SVG.BBox = object.bbox();
this.onFocusRegion(bbox.x - padding, bbox.y - padding,
bbox.width + padding, bbox.height + padding);
bbox.width + padding * 2, bbox.height + padding * 2);
}
} else if (reason === UpdateReasons.SHAPE_ACTIVATED) {
this.activate(this.controller.activeElement);
@ -832,12 +932,96 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.mode = Mode.IDLE;
this.canvas.style.cursor = '';
}
if (model.imageBitmap
&& [UpdateReasons.IMAGE_CHANGED,
UpdateReasons.OBJECTS_UPDATED,
UpdateReasons.SET_Z_LAYER,
].includes(reason)
) {
this.redrawBitmap();
}
}
public html(): HTMLDivElement {
return this.canvas;
}
private redrawBitmap(): void {
const width = +this.background.style.width.slice(0, -2);
const height = +this.background.style.height.slice(0, -2);
this.bitmap.setAttribute('width', `${width}px`);
this.bitmap.setAttribute('height', `${height}px`);
const states = this.controller.objects;
const ctx = this.bitmap.getContext('2d');
if (ctx) {
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, width, height);
for (const state of states) {
if (state.hidden || state.outside) continue;
ctx.fillStyle = 'white';
if (['rectangle', 'polygon', 'cuboid'].includes(state.shapeType)) {
let points = [];
if (state.shapeType === 'rectangle') {
points = [
state.points[0], // xtl
state.points[1], // ytl
state.points[2], // xbr
state.points[1], // ytl
state.points[2], // xbr
state.points[3], // ybr
state.points[0], // xtl
state.points[3], // ybr
];
} else if (state.shapeType === 'cuboid') {
points = [
state.points[0],
state.points[1],
state.points[4],
state.points[5],
state.points[8],
state.points[9],
state.points[12],
state.points[13],
];
} else {
points = [...state.points];
}
ctx.beginPath();
ctx.moveTo(points[0], points[1]);
for (let i = 0; i < points.length; i += 2) {
ctx.lineTo(points[i], points[i + 1]);
}
ctx.closePath();
ctx.fill();
}
if (state.shapeType === 'cuboid') {
for (let i = 0; i < 5; i++) {
const points = [
state.points[(0 + i * 4) % 16],
state.points[(1 + i * 4) % 16],
state.points[(2 + i * 4) % 16],
state.points[(3 + i * 4) % 16],
state.points[(6 + i * 4) % 16],
state.points[(7 + i * 4) % 16],
state.points[(4 + i * 4) % 16],
state.points[(5 + i * 4) % 16],
];
ctx.beginPath();
ctx.moveTo(points[0], points[1]);
for (let j = 0; j < points.length; j += 2) {
ctx.lineTo(points[j], points[j + 1]);
}
ctx.closePath();
ctx.fill();
}
}
}
}
}
private saveState(state: any): void {
this.drawnStates[state.clientID] = {
clientID: state.clientID,
@ -850,6 +1034,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
attributes: { ...state.attributes },
zOrder: state.zOrder,
pinned: state.pinned,
updated: state.updated,
frame: state.frame,
};
}
@ -857,31 +1043,44 @@ export class CanvasViewImpl implements CanvasView, Listener {
for (const state of states) {
const { clientID } = state;
const drawnState = this.drawnStates[clientID];
const shape = this.svgShapes[state.clientID];
const text = this.svgTexts[state.clientID];
const isInvisible = state.hidden || state.outside;
if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) {
const none = state.hidden || state.outside;
if (state.shapeType === 'points') {
this.svgShapes[clientID].remember('_selectHandler').nested
.style('display', none ? 'none' : '');
if (isInvisible) {
(state.shapeType === 'points' ? shape.remember('_selectHandler').nested : shape)
.style('display', 'none');
if (text) {
text.addClass('cvat_canvas_hidden');
}
} else {
this.svgShapes[clientID].style('display', none ? 'none' : '');
(state.shapeType === 'points' ? shape.remember('_selectHandler').nested : shape)
.style('display', '');
if (text) {
text.removeClass('cvat_canvas_hidden');
this.updateTextPosition(
text,
shape,
);
}
}
}
if (drawnState.zOrder !== state.zOrder) {
if (state.shapeType === 'points') {
this.svgShapes[clientID].remember('_selectHandler').nested
shape.remember('_selectHandler').nested
.attr('data-z-order', state.zOrder);
} else {
this.svgShapes[clientID].attr('data-z-order', state.zOrder);
shape.attr('data-z-order', state.zOrder);
}
}
if (drawnState.occluded !== state.occluded) {
if (state.occluded) {
this.svgShapes[clientID].addClass('cvat_canvas_shape_occluded');
shape.addClass('cvat_canvas_shape_occluded');
} else {
this.svgShapes[clientID].removeClass('cvat_canvas_shape_occluded');
shape.removeClass('cvat_canvas_shape_occluded');
}
}
@ -891,7 +1090,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.activate(activeElement);
}
if (state.points
if (state.points.length !== drawnState.points.length || state.points
.some((p: number, id: number): boolean => p !== drawnState.points[id])
) {
const translatedPoints: number[] = translate(state.points);
@ -899,7 +1098,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (state.shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = translatedPoints;
this.svgShapes[clientID].attr({
shape.attr({
x: xtl,
y: ytl,
width: xbr - xtl,
@ -915,21 +1114,22 @@ export class CanvasViewImpl implements CanvasView, Listener {
return `${acc}${val},`;
}, '',
);
(this.svgShapes[clientID] as any).clear();
this.svgShapes[clientID].attr('points', stringified);
if (state.shapeType !== 'cuboid') {
(shape as any).clear();
}
shape.attr('points', stringified);
if (state.shapeType === 'points') {
this.selectize(false, this.svgShapes[clientID]);
this.setupPoints(this.svgShapes[clientID] as SVG.PolyLine, state);
if (state.shapeType === 'points' && !isInvisible) {
this.selectize(false, shape);
this.setupPoints(shape as SVG.PolyLine, state);
}
}
}
for (const attrID of Object.keys(state.attributes)) {
if (state.attributes[attrID] !== drawnState.attributes[attrID]) {
const text = this.svgTexts[state.clientID];
if (state.attributes[attrID] !== drawnState.attributes[+attrID]) {
if (text) {
const [span] = this.svgTexts[state.clientID].node
const [span] = text.node
.querySelectorAll(`[attrID="${attrID}"]`) as any as SVGTSpanElement[];
if (span && span.textContent) {
const prefix = span.textContent.split(':').slice(0, -1).join(':');
@ -944,6 +1144,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
private addObjects(states: any[], translate: (points: number[]) => number[]): void {
const { displayAllText } = this.configuration;
for (const state of states) {
if (state.objectType === 'tag') {
this.addTag(state);
@ -975,6 +1177,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
} 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);
}
}
@ -987,6 +1192,14 @@ export class CanvasViewImpl implements CanvasView, Listener {
},
}));
});
if (displayAllText) {
this.svgTexts[state.clientID] = this.addText(state);
this.updateTextPosition(
this.svgTexts[state.clientID],
this.svgShapes[state.clientID],
);
}
}
this.saveState(state);
@ -1014,13 +1227,34 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.content.prepend(...sorted.map((pair): SVGElement => pair[0]));
}
private deactivate(): void {
private deactivateAttribute(): void {
const { clientID, attributeID } = this.activeElement;
if (clientID !== null && attributeID !== null) {
const text = this.svgTexts[clientID];
if (text) {
const [span] = text.node
.querySelectorAll(`[attrID="${attributeID}"]`) as any as SVGTSpanElement[];
if (span) {
span.style.fill = '';
}
}
this.activeElement = {
...this.activeElement,
attributeID: null,
};
}
}
private deactivateShape(): void {
if (this.activeElement.clientID !== null) {
const { displayAllText } = this.configuration;
const { clientID } = this.activeElement;
const drawnState = this.drawnStates[clientID];
const shape = this.svgShapes[clientID];
shape.removeClass('cvat_canvas_shape_activated');
shape.removeClass('cvat_canvas_shape_draggable');
if (!drawnState.pinned) {
(shape as any).off('dragstart');
@ -1032,14 +1266,18 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.selectize(false, shape);
}
if (drawnState.shapeType === 'cuboid') {
(shape as any).attr('projections', false);
}
(shape as any).off('resizestart');
(shape as any).off('resizing');
(shape as any).off('resizedone');
(shape as any).resize(false);
(shape as any).resize('stop');
// TODO: Hide text only if it is hidden by settings
const text = this.svgTexts[clientID];
if (text) {
if (text && !displayAllText) {
text.remove();
delete this.svgTexts[clientID];
}
@ -1047,29 +1285,34 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.sortObjects();
this.activeElement = {
...this.activeElement,
clientID: null,
attributeID: null,
};
}
}
private activate(activeElement: ActiveElement): void {
// Check if other element have been already activated
if (this.activeElement.clientID !== null) {
// Check if it is the same element
if (this.activeElement.clientID === activeElement.clientID) {
return;
}
private deactivate(): void {
this.deactivateAttribute();
this.deactivateShape();
}
// Deactivate previous element
this.deactivate();
}
private activateAttribute(clientID: number, attributeID: number): void {
const text = this.svgTexts[clientID];
if (text) {
const [span] = text.node
.querySelectorAll(`[attrID="${attributeID}"]`) as any as SVGTSpanElement[];
if (span) {
span.style.fill = 'red';
}
const { clientID } = activeElement;
if (clientID === null) {
return;
this.activeElement = {
...this.activeElement,
attributeID,
};
}
}
private activateShape(clientID: number): void {
const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID);
@ -1082,8 +1325,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
return;
}
this.activeElement = { ...activeElement };
const shape = this.svgShapes[clientID];
let text = this.svgTexts[clientID];
if (!text) {
text = this.addText(state);
@ -1106,7 +1349,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.content.append(shape.node);
}
const { showProjections } = this.configuration;
if (state.shapeType === 'cuboid' && showProjections) {
(shape as any).attr('projections', true);
}
if (!state.pinned) {
shape.addClass('cvat_canvas_shape_draggable');
(shape as any).draggable().on('dragstart', (): void => {
this.mode = Mode.DRAG;
if (text) {
@ -1134,6 +1383,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
).map((x: number): number => x - offset);
this.drawnStates[state.clientID].points = points;
this.canvas.dispatchEvent(new CustomEvent('canvas.dragshape', {
bubbles: false,
cancelable: true,
detail: {
id: state.clientID,
},
}));
this.onEditDone(state, points);
}
});
@ -1184,6 +1440,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
).map((x: number): number => x - offset);
this.drawnStates[state.clientID].points = points;
this.canvas.dispatchEvent(new CustomEvent('canvas.resizeshape', {
bubbles: false,
cancelable: true,
detail: {
id: state.clientID,
},
}));
this.onEditDone(state, points);
}
});
@ -1197,8 +1460,37 @@ export class CanvasViewImpl implements CanvasView, Listener {
}));
}
private activate(activeElement: ActiveElement): void {
// Check if another element have been already activated
if (this.activeElement.clientID !== null) {
if (this.activeElement.clientID !== activeElement.clientID) {
// Deactivate previous shape and attribute
this.deactivate();
} else if (this.activeElement.attributeID !== activeElement.attributeID) {
this.deactivateAttribute();
}
}
const { clientID, attributeID } = activeElement;
if (clientID !== null && this.activeElement.clientID !== clientID) {
this.activateShape(clientID);
this.activeElement = {
...this.activeElement,
clientID,
};
}
if (clientID !== null
&& attributeID !== null
&& this.activeElement.attributeID !== attributeID
) {
this.activateAttribute(clientID, attributeID);
}
}
// Update text position after corresponding box has been moved, resized, etc.
private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void {
if (text.node.style.display === 'none') return; // wrong transformation matrix
let box = (shape.node as any).getBBox();
// Translate the whole box to the client coordinate system
@ -1237,6 +1529,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
private addText(state: any): SVG.Text {
const { undefinedAttrValue } = this.configuration;
const { label, clientID, attributes } = state;
const attrNames = label.attributes.reduce((acc: any, val: any): void => {
acc[val.id] = val.name;
@ -1246,7 +1539,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.adoptedText.text((block): void => {
block.tspan(`${label.name} ${clientID}`).style('text-transform', 'uppercase');
for (const attrID of Object.keys(attributes)) {
block.tspan(`${attrNames[attrID]}: ${attributes[attrID]}`).attr({
const value = attributes[attrID] === undefinedAttrValue
? '' : attributes[attrID];
block.tspan(`${attrNames[attrID]}: ${value}`).attr({
attrID,
dy: '1em',
x: 0,
@ -1326,6 +1621,30 @@ export class CanvasViewImpl implements CanvasView, Listener {
return polyline;
}
private addCuboid(points: string, state: any): any {
const cube = (this.adoptedContent as any).cube(points)
.fill(state.color).attr({
clientID: state.clientID,
'color-rendering': 'optimizeQuality',
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'data-z-order': state.zOrder,
}).addClass('cvat_canvas_shape');
if (state.occluded) {
cube.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside) {
cube.style('display', 'none');
}
return cube;
}
private setupPoints(basicPolyline: SVG.PolyLine, state: any): any {
this.selectize(true, basicPolyline);
@ -1333,12 +1652,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
.addClass('cvat_canvas_shape').attr({
clientID: state.clientID,
id: `cvat_canvas_shape_${state.clientID}`,
'data-polyline-id': basicPolyline.attr('id'),
'data-z-order': state.zOrder,
});
group.on('click.canvas', (event: MouseEvent): void => {
// Need to redispatch the event on another element
basicPolyline.fire(new MouseEvent('click', event));
// redispatch event to canvas to be able merge points clicking them
this.content.dispatchEvent(new MouseEvent('click', event));
});
group.bbox = basicPolyline.bbox.bind(basicPolyline);

@ -10,6 +10,10 @@ const AREA_THRESHOLD = 9;
const SIZE_THRESHOLD = 3;
const POINTS_STROKE_WIDTH = 1.5;
const POINTS_SELECTED_STROKE_WIDTH = 4;
const MIN_EDGE_LENGTH = 3;
const CUBOID_ACTIVE_EDGE_STROKE_WIDTH = 2.5;
const CUBOID_UNACTIVE_EDGE_STROKE_WIDTH = 1.75;
const UNDEFINED_ATTRIBUTE_VALUE = '__undefined__';
export default {
BASE_STROKE_WIDTH,
@ -20,4 +24,8 @@ export default {
SIZE_THRESHOLD,
POINTS_STROKE_WIDTH,
POINTS_SELECTED_STROKE_WIDTH,
MIN_EDGE_LENGTH,
CUBOID_ACTIVE_EDGE_STROKE_WIDTH,
CUBOID_UNACTIVE_EDGE_STROKE_WIDTH,
UNDEFINED_ATTRIBUTE_VALUE,
};

@ -0,0 +1,494 @@
/* eslint-disable func-names */
/* eslint-disable no-underscore-dangle */
/* eslint-disable curly */
/*
* Copyright (C) 2020 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
import consts from './consts';
export interface Point {
x: number;
y: number;
}
export enum Orientation {
LEFT = 'left',
RIGHT = 'right',
}
function line(p1: Point, p2: Point): number[] {
const a = p1.y - p2.y;
const b = p2.x - p1.x;
const c = b * p1.y + a * p1.x;
return [a, b, c];
}
function intersection(
p1: Point, p2: Point, p3: Point, p4: Point,
): Point | null {
const L1 = line(p1, p2);
const L2 = line(p3, p4);
const D = L1[0] * L2[1] - L1[1] * L2[0];
const Dx = L1[2] * L2[1] - L1[1] * L2[2];
const Dy = L1[0] * L2[2] - L1[2] * L2[0];
let x = null;
let y = null;
if (D !== 0) {
x = Dx / D;
y = Dy / D;
return { x, y };
}
return null;
}
export class Equation {
private a: number;
private b: number;
private c: number;
public constructor(p1: Point, p2: Point) {
this.a = p1.y - p2.y;
this.b = p2.x - p1.x;
this.c = this.b * p1.y + this.a * p1.x;
}
// get the line equation in actual coordinates
public getY(x: number): number {
return (this.c - this.a * x) / this.b;
}
}
export class Figure {
private indices: number[];
private allPoints: Point[];
public constructor(indices: number[], points: Point[]) {
this.indices = indices;
this.allPoints = points;
}
public get points(): Point[] {
const points = [];
for (const index of this.indices) {
points.push(this.allPoints[index]);
}
return points;
}
// sets the point for a given edge, points must be given in
// array form in the same ordering as the getter
// if you only need to update a subset of the points,
// simply put null for the points you want to keep
public set points(newPoints) {
const oldPoints = this.allPoints;
for (let i = 0; i < newPoints.length; i += 1) {
if (newPoints[i] !== null) {
oldPoints[this.indices[i]] = { x: newPoints[i].x, y: newPoints[i].y };
}
}
}
}
export class Edge extends Figure {
public getEquation(): Equation {
return new Equation(this.points[0], this.points[1]);
}
}
export class CuboidModel {
public points: Point[];
private fr: Edge;
private fl: Edge;
private dr: Edge;
private dl: Edge;
private ft: Edge;
private rt: Edge;
private lt: Edge;
private dt: Edge;
private fb: Edge;
private rb: Edge;
private lb: Edge;
private db: Edge;
public edgeList: Edge[];
private front: Figure;
private right: Figure;
private dorsal: Figure;
private left: Figure;
private top: Figure;
private bot: Figure;
public facesList: Figure[];
public vpl: Point | null;
public vpr: Point | null;
public orientation: Orientation;
public constructor(points?: Point[]) {
this.points = points;
this.initEdges();
this.initFaces();
this.updateVanishingPoints(false);
this.buildBackEdge(false);
this.updatePoints();
this.updateOrientation();
}
public getPoints(): Point[] {
return this.points;
}
public setPoints(points: (Point | null)[]): void {
points.forEach((point: Point | null, i: number): void => {
if (point !== null) {
this.points[i].x = point.x;
this.points[i].y = point.y;
}
});
}
public updateOrientation(): void {
if (this.dl.points[0].x > this.fl.points[0].x) {
this.orientation = Orientation.LEFT;
} else {
this.orientation = Orientation.RIGHT;
}
}
public updatePoints(): void {
// making sure that the edges are vertical
this.fr.points[0].x = this.fr.points[1].x;
this.fl.points[0].x = this.fl.points[1].x;
this.dr.points[0].x = this.dr.points[1].x;
this.dl.points[0].x = this.dl.points[1].x;
}
public computeSideEdgeConstraints(edge: any): any {
const midLength = this.fr.points[1].y - this.fr.points[0].y - 1;
const minY = edge.points[1].y - midLength;
const maxY = edge.points[0].y + midLength;
const y1 = edge.points[0].y;
const y2 = edge.points[1].y;
const miny1 = y2 - midLength;
const maxy1 = y2 - consts.MIN_EDGE_LENGTH;
const miny2 = y1 + consts.MIN_EDGE_LENGTH;
const maxy2 = y1 + midLength;
return {
constraint: {
minY,
maxY,
},
y1Range: {
max: maxy1,
min: miny1,
},
y2Range: {
max: maxy2,
min: miny2,
},
};
}
// boolean value parameter controls which edges should be used to recalculate vanishing points
private updateVanishingPoints(buildright: boolean): void {
let leftEdge = [];
let rightEdge = [];
let midEdge = [];
if (buildright) {
leftEdge = this.fr.points;
rightEdge = this.dl.points;
midEdge = this.fl.points;
} else {
leftEdge = this.fl.points;
rightEdge = this.dr.points;
midEdge = this.fr.points;
}
this.vpl = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
this.vpr = intersection(rightEdge[0], midEdge[0], rightEdge[1], midEdge[1]);
if (this.vpl === null) {
// shift the edge slightly to avoid edge case
leftEdge[0].y -= 0.001;
leftEdge[0].x += 0.001;
leftEdge[1].x += 0.001;
this.vpl = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
}
if (this.vpr === null) {
// shift the edge slightly to avoid edge case
rightEdge[0].y -= 0.001;
rightEdge[0].x -= 0.001;
rightEdge[1].x -= 0.001;
this.vpr = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
}
}
private initEdges(): void {
this.fl = new Edge([0, 1], this.points);
this.fr = new Edge([2, 3], this.points);
this.dr = new Edge([4, 5], this.points);
this.dl = new Edge([6, 7], this.points);
this.ft = new Edge([0, 2], this.points);
this.lt = new Edge([0, 6], this.points);
this.rt = new Edge([2, 4], this.points);
this.dt = new Edge([6, 4], this.points);
this.fb = new Edge([1, 3], this.points);
this.lb = new Edge([1, 7], this.points);
this.rb = new Edge([3, 5], this.points);
this.db = new Edge([7, 5], this.points);
this.edgeList = [this.fl, this.fr, this.dl, this.dr, this.ft, this.lt,
this.rt, this.dt, this.fb, this.lb, this.rb, this.db];
}
private initFaces(): void {
this.front = new Figure([0, 1, 3, 2], this.points);
this.right = new Figure([2, 3, 5, 4], this.points);
this.dorsal = new Figure([4, 5, 7, 6], this.points);
this.left = new Figure([6, 7, 1, 0], this.points);
this.top = new Figure([0, 2, 4, 6], this.points);
this.bot = new Figure([1, 3, 5, 7], this.points);
this.facesList = [this.front, this.right, this.dorsal, this.left];
}
private buildBackEdge(buildright: boolean): void {
this.updateVanishingPoints(buildright);
let leftPoints = [];
let rightPoints = [];
let topIndex = 0;
let botIndex = 0;
if (buildright) {
leftPoints = this.dl.points;
rightPoints = this.fr.points;
topIndex = 4;
botIndex = 5;
} else {
leftPoints = this.dr.points;
rightPoints = this.fl.points;
topIndex = 6;
botIndex = 7;
}
const vpLeft = this.vpl;
const vpRight = this.vpr;
let p1 = intersection(vpLeft, leftPoints[0], vpRight, rightPoints[0]);
let p2 = intersection(vpLeft, leftPoints[1], vpRight, rightPoints[1]);
if (p1 === null) {
p1 = { x: p2.x, y: vpLeft.y };
} else if (p2 === null) {
p2 = { x: p1.x, y: vpLeft.y };
}
this.points[topIndex] = { x: p1.x, y: p1.y };
this.points[botIndex] = { x: p2.x, y: p2.y };
// Making sure that the vertical edges stay vertical
this.updatePoints();
}
}
function sortPointsClockwise(points: any[]): any[] {
points.sort((a, b): number => a.y - b.y);
// Get center y
const cy = (points[0].y + points[points.length - 1].y) / 2;
// Sort from right to left
points.sort((a, b): number => b.x - a.x);
// Get center x
const cx = (points[0].x + points[points.length - 1].x) / 2;
// Center point
const center = {
x: cx,
y: cy,
};
// Starting angle used to reference other angles
let startAng: number | undefined;
points.forEach((point): void => {
let ang = Math.atan2(point.y - center.y, point.x - center.x);
if (!startAng) {
startAng = ang;
// ensure that all points are clockwise of the start point
} else if (ang < startAng) {
ang += Math.PI * 2;
}
// eslint-disable-next-line no-param-reassign
point.angle = ang; // add the angle to the point
});
// first sort clockwise
points.sort((a, b): number => a.angle - b.angle);
return points.reverse();
}
function setupCuboidPoints(points: Point[]): any[] {
let left;
let right;
let left2;
let right2;
let p1;
let p2;
let p3;
let p4;
const height = Math.abs(points[0].x - points[1].x)
< Math.abs(points[1].x - points[2].x)
? Math.abs(points[1].y - points[0].y)
: Math.abs(points[1].y - points[2].y);
// seperate into left and right point
// we pick the first and third point because we know assume they will be on
// opposite corners
if (points[0].x < points[2].x) {
[left,, right] = points;
} else {
[right,, left] = points;
}
// get other 2 points using the given height
if (left.y < right.y) {
left2 = { x: left.x, y: left.y + height };
right2 = { x: right.x, y: right.y - height };
} else {
left2 = { x: left.x, y: left.y - height };
right2 = { x: right.x, y: right.y + height };
}
// get the vector for the last point relative to the previous point
const vec = {
x: points[3].x - points[2].x,
y: points[3].y - points[2].y,
};
if (left.y < left2.y) {
p1 = left;
p2 = left2;
} else {
p1 = left2;
p2 = left;
}
if (right.y < right2.y) {
p3 = right;
p4 = right2;
} else {
p3 = right2;
p4 = right;
}
const p5 = { x: p3.x + vec.x, y: p3.y + vec.y + 0.1 };
const p6 = { x: p4.x + vec.x, y: p4.y + vec.y - 0.1 };
const p7 = { x: p1.x + vec.x, y: p1.y + vec.y + 0.1 };
const p8 = { x: p2.x + vec.x, y: p2.y + vec.y - 0.1 };
p1.y += 0.1;
return [p1, p2, p3, p4, p5, p6, p7, p8];
}
export function cuboidFrom4Points(flattenedPoints: any[]): any[] {
const points: Point[] = [];
for (let i = 0; i < 4; i++) {
const [x, y] = flattenedPoints.slice(i * 2, i * 2 + 2);
points.push({ x, y });
}
const unsortedPlanePoints = points.slice(0, 3);
function rotate(array: any[], times: number): void{
let t = times;
while (t--) {
const temp = array.shift();
array.push(temp);
}
}
const plane2 = {
p1: points[0],
p2: points[0],
p3: points[0],
p4: points[0],
};
// completing the plane
const vector = {
x: points[2].x - points[1].x,
y: points[2].y - points[1].y,
};
// sorting the first plane
unsortedPlanePoints.push({
x: points[0].x + vector.x,
y: points[0].y + vector.y,
});
const sortedPlanePoints = sortPointsClockwise(unsortedPlanePoints);
let leftIndex = 0;
for (let i = 0; i < 4; i++) {
leftIndex = sortedPlanePoints[i].x < sortedPlanePoints[leftIndex].x ? i : leftIndex;
}
rotate(sortedPlanePoints, leftIndex);
const plane1 = {
p1: sortedPlanePoints[0],
p2: sortedPlanePoints[1],
p3: sortedPlanePoints[2],
p4: sortedPlanePoints[3],
};
const vec = {
x: points[3].x - points[2].x,
y: points[3].y - points[2].y,
};
// determine the orientation
const angle = Math.atan2(vec.y, vec.x);
// making the other plane
plane2.p1 = { x: plane1.p1.x + vec.x, y: plane1.p1.y + vec.y };
plane2.p2 = { x: plane1.p2.x + vec.x, y: plane1.p2.y + vec.y };
plane2.p3 = { x: plane1.p3.x + vec.x, y: plane1.p3.y + vec.y };
plane2.p4 = { x: plane1.p4.x + vec.x, y: plane1.p4.y + vec.y };
let cuboidPoints;
// right
if (Math.abs(angle) < Math.PI / 2 - 0.1) {
cuboidPoints = setupCuboidPoints(points);
// left
} else if (Math.abs(angle) > Math.PI / 2 + 0.1) {
cuboidPoints = setupCuboidPoints(points);
// down
} else if (angle > 0) {
cuboidPoints = [
plane1.p1, plane2.p1, plane1.p2, plane2.p2,
plane1.p3, plane2.p3, plane1.p4, plane2.p4,
];
cuboidPoints[0].y += 0.1;
cuboidPoints[4].y += 0.1;
// up
} else {
cuboidPoints = [
plane2.p1, plane1.p1, plane2.p2, plane1.p2,
plane2.p3, plane1.p3, plane2.p4, plane1.p4,
];
cuboidPoints[0].y += 0.1;
cuboidPoints[4].y += 0.1;
}
return cuboidPoints.reduce((arr: number[], point: any): number[] => {
arr.push(point.x);
arr.push(point.y);
return arr;
}, []);
}

@ -3,16 +3,10 @@
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
import 'svg.draw.js';
import './svg.patch';
import {
DrawData,
Geometry,
RectDrawingMethod,
} from './canvasModel';
import { AutoborderHandler } from './autoborderHandler';
import {
translateToSVG,
displayShapeSize,
@ -22,8 +16,19 @@ import {
BBox,
Box,
} from './shared';
import consts from './consts';
import {
DrawData,
Geometry,
RectDrawingMethod,
Configuration,
CuboidDrawingMethod,
} from './canvasModel';
import { cuboidFrom4Points } from './cuboid';
export interface DrawHandler {
configurate(configuration: Configuration): void;
draw(drawData: DrawData, geometry: Geometry): void;
transform(geometry: Geometry): void;
cancel(): void;
@ -31,7 +36,8 @@ export interface DrawHandler {
export class DrawHandlerImpl implements DrawHandler {
// callback is used to notify about creating new shape
private onDrawDone: (data: object, continueDraw?: boolean) => void;
private onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void;
private startTimestamp: number;
private canvas: SVG.Container;
private text: SVG.Container;
private cursorPosition: {
@ -44,11 +50,14 @@ export class DrawHandlerImpl implements DrawHandler {
};
private drawData: DrawData;
private geometry: Geometry;
private autoborderHandler: AutoborderHandler;
private autobordersEnabled: boolean;
// we should use any instead of SVG.Shape because svg plugins cannot change declared interface
// so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist
private drawInstance: any;
private initialized: boolean;
private canceled: boolean;
private pointsGroup: SVG.G | null;
private shapeSizeElement: ShapeSizeElement;
@ -99,6 +108,84 @@ export class DrawHandlerImpl implements DrawHandler {
};
}
private getFinalCuboidCoordinates(targetPoints: number[]): {
points: number[];
box: Box;
} {
const { offset } = this.geometry;
let points = targetPoints;
const box = {
xtl: 0,
ytl: 0,
xbr: Number.MAX_SAFE_INTEGER,
ybr: Number.MAX_SAFE_INTEGER,
};
const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height;
const cuboidOffsets = [];
const minCuboidOffset = {
d: Number.MAX_SAFE_INTEGER,
dx: 0,
dy: 0,
};
for (let i = 0; i < points.length - 1; i += 2) {
const [x, y] = points.slice(i);
if (x >= offset && x <= offset + frameWidth
&& y >= offset && y <= offset + frameHeight) continue;
let xOffset = 0;
let yOffset = 0;
if (x < offset) {
xOffset = offset - x;
} else if (x > offset + frameWidth) {
xOffset = offset + frameWidth - x;
}
if (y < offset) {
yOffset = offset - y;
} else if (y > offset + frameHeight) {
yOffset = offset + frameHeight - y;
}
cuboidOffsets.push([xOffset, yOffset]);
}
if (cuboidOffsets.length === points.length / 2) {
cuboidOffsets.forEach((offsetCoords: number[]): void => {
if (Math.sqrt((offsetCoords[0] ** 2) + (offsetCoords[1] ** 2))
< minCuboidOffset.d) {
minCuboidOffset.d = Math.sqrt((offsetCoords[0] ** 2) + (offsetCoords[1] ** 2));
[minCuboidOffset.dx, minCuboidOffset.dy] = offsetCoords;
}
});
points = points.map((coord: number, i: number): number => {
const finalCoord = coord + (i % 2 === 0 ? minCuboidOffset.dx : minCuboidOffset.dy);
if (i % 2 === 0) {
box.xtl = Math.max(box.xtl, finalCoord);
box.xbr = Math.min(box.xbr, finalCoord);
} else {
box.ytl = Math.max(box.ytl, finalCoord);
box.ybr = Math.min(box.ybr, finalCoord);
}
return finalCoord;
});
}
return {
points: points.map((coord: number): number => coord - offset),
box,
};
}
private addCrosshair(): void {
const { x, y } = this.cursorPosition;
this.crosshair = {
@ -125,9 +212,9 @@ export class DrawHandlerImpl implements DrawHandler {
return;
}
this.autoborderHandler.autoborder(false);
this.initialized = false;
this.canvas.off('mousedown.draw');
this.canvas.off('mouseup.draw');
this.canvas.off('mousemove.draw');
this.canvas.off('click.draw');
@ -141,13 +228,15 @@ export class DrawHandlerImpl implements DrawHandler {
// Or when no drawn points, but we call cancel() drawing
// We check if it is activated with remember function
if (this.drawInstance.remember('_paintHandler')) {
if (this.drawData.shapeType !== 'rectangle') {
if (this.drawData.shapeType !== 'rectangle'
&& this.drawData.cuboidDrawingMethod !== CuboidDrawingMethod.CLASSIC) {
// Check for unsaved drawn shapes
this.drawInstance.draw('done');
}
// Clear drawing
this.drawInstance.draw('stop');
}
this.drawInstance.off();
this.drawInstance.remove();
this.drawInstance = null;
@ -160,6 +249,8 @@ export class DrawHandlerImpl implements DrawHandler {
if (this.crosshair) {
this.removeCrosshair();
}
this.onDrawDone(null);
}
private initDrawing(): void {
@ -174,13 +265,14 @@ export class DrawHandlerImpl implements DrawHandler {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType } = this.drawData;
this.cancel();
this.release();
if (this.canceled) return;
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone({
shapeType,
points: [xtl, ytl, xbr, ybr],
});
}, Date.now() - this.startTimestamp);
}
}).on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
@ -213,7 +305,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone({
shapeType,
points: [xtl, ytl, xbr, ybr],
});
}, Date.now() - this.startTimestamp);
}
}
}).on('undopoint', (): void => {
@ -226,22 +318,21 @@ export class DrawHandlerImpl implements DrawHandler {
}
private drawPolyshape(): void {
let size = this.drawData.numberOfPoints;
const sizeDecrement = function sizeDecrement(): void {
if (!--size) {
let size = this.drawData.shapeType === 'cuboid' ? 4 : this.drawData.numberOfPoints;
const sizeDecrement = (): void => {
if (--size === 0) {
this.drawInstance.draw('done');
}
}.bind(this);
};
if (this.drawData.numberOfPoints) {
this.drawInstance.on('drawstart', sizeDecrement);
this.drawInstance.on('drawpoint', sizeDecrement);
this.drawInstance.on('undopoint', (): number => size++);
}
this.drawInstance.on('drawstart', sizeDecrement);
this.drawInstance.on('drawpoint', sizeDecrement);
this.drawInstance.on('undopoint', (): number => size++);
// Add ability to cancel the latest drawn point
this.canvas.on('mousedown.draw', (e: MouseEvent): void => {
if (e.which === 3) {
if (e.button === 2) {
e.stopPropagation();
e.preventDefault();
this.drawInstance.draw('undo');
@ -289,18 +380,19 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance.on('drawdone', (e: CustomEvent): void => {
const targetPoints = pointsToArray((e.target as SVGElement).getAttribute('points'));
const { points, box } = this.getFinalPolyshapeCoordinates(targetPoints);
const { shapeType } = this.drawData;
this.cancel();
const { points, box } = shapeType === 'cuboid' ? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
this.release();
if (this.canceled) return;
if (shapeType === 'polygon'
&& ((box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD)
&& points.length >= 3 * 2) {
this.onDrawDone({
shapeType,
points,
});
}, Date.now() - this.startTimestamp);
} else if (shapeType === 'polyline'
&& ((box.xbr - box.xtl) >= consts.SIZE_THRESHOLD
|| (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD)
@ -308,13 +400,20 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone({
shapeType,
points,
});
}, Date.now() - this.startTimestamp);
} else if (shapeType === 'points'
&& (e.target as any).getAttribute('points') !== '0,0') {
this.onDrawDone({
shapeType,
points,
});
}, Date.now() - this.startTimestamp);
// TODO: think about correct constraign for cuboids
} else if (shapeType === 'cuboid'
&& points.length === 4 * 2) {
this.onDrawDone({
shapeType,
points: cuboidFrom4Points(points),
}, Date.now() - this.startTimestamp);
}
});
}
@ -326,6 +425,9 @@ export class DrawHandlerImpl implements DrawHandler {
});
this.drawPolyshape();
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, false);
}
}
private drawPolyline(): void {
@ -336,6 +438,9 @@ export class DrawHandlerImpl implements DrawHandler {
});
this.drawPolyshape();
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, false);
}
}
private drawPoints(): void {
@ -348,6 +453,37 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawPolyshape();
}
private drawCuboidBy4Points(): void {
this.drawInstance = (this.canvas as any).polyline()
.addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.drawPolyshape();
}
private drawCuboid(): void {
this.drawInstance = this.canvas.rect();
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;
this.release();
if (this.canceled) return;
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
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]),
}, Date.now() - this.startTimestamp);
}
}).on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
}).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
}
private pastePolyshape(): void {
this.drawInstance.on('done', (e: CustomEvent): void => {
const targetPoints = this.drawInstance
@ -355,8 +491,13 @@ export class DrawHandlerImpl implements DrawHandler {
.split(/[,\s]/g)
.map((coord: string): number => +coord);
const { points } = this.getFinalPolyshapeCoordinates(targetPoints);
this.release();
const { points } = this.drawData.initialState.shapeType === 'cuboid' ? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
this.onDrawDone({
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
@ -365,7 +506,7 @@ export class DrawHandlerImpl implements DrawHandler {
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
}, e.detail.originalEvent.ctrlKey);
}, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey);
});
}
@ -396,7 +537,10 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance.on('done', (e: CustomEvent): void => {
const bbox = this.drawInstance.node.getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
this.release();
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
this.onDrawDone({
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
@ -405,7 +549,7 @@ export class DrawHandlerImpl implements DrawHandler {
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
}, e.detail.originalEvent.ctrlKey);
}, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey);
});
}
@ -428,6 +572,15 @@ export class DrawHandlerImpl implements DrawHandler {
this.pastePolyshape();
}
private pasteCuboid(points: string): void {
this.drawInstance = (this.canvas as any).cube(points).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'face-stroke': 'black',
});
this.pasteShape();
this.pastePolyshape();
}
private pastePoints(initialPoints: string): void {
function moveShape(
shape: SVG.PolyLine,
@ -481,54 +634,23 @@ export class DrawHandlerImpl implements DrawHandler {
}
private setupPasteEvents(): void {
let mouseX: number | null = null;
let mouseY: number | null = null;
this.canvas.on('mousedown.draw', (e: MouseEvent): void => {
if (e.which === 1) {
mouseX = e.clientX;
mouseY = e.clientY;
}
});
this.canvas.on('mouseup.draw', (e: MouseEvent): void => {
const threshold = 10; // px
if (e.which === 1) {
if (Math.sqrt( // l2 distance < threshold
((mouseX - e.clientX) ** 2)
+ ((mouseY - e.clientY) ** 2),
) < threshold) {
this.drawInstance.fire('done', { originalEvent: e });
}
if (e.button === 0 && !e.altKey) {
this.drawInstance.fire('done', { originalEvent: e });
}
});
}
private setupDrawEvents(): void {
let initialized = false;
let mouseX: number | null = null;
let mouseY: number | null = null;
this.canvas.on('mousedown.draw', (e: MouseEvent): void => {
if (e.which === 1) {
mouseX = e.clientX;
mouseY = e.clientY;
}
});
this.canvas.on('mouseup.draw', (e: MouseEvent): void => {
const threshold = 10; // px
if (e.which === 1) {
if (Math.sqrt( // l2 distance < threshold
((mouseX - e.clientX) ** 2)
+ ((mouseY - e.clientY) ** 2),
) < threshold) {
if (!initialized) {
this.drawInstance.draw(e, { snapToGrid: 0.1 });
initialized = true;
} else {
this.drawInstance.draw(e);
}
if (e.button === 0 && !e.altKey) {
if (!initialized) {
this.drawInstance.draw(e, { snapToGrid: 0.1 });
initialized = true;
} else {
this.drawInstance.draw(e);
}
}
});
@ -559,6 +681,8 @@ export class DrawHandlerImpl implements DrawHandler {
this.pastePolyline(stringifiedPoints);
} else if (this.drawData.shapeType === 'points') {
this.pastePoints(stringifiedPoints);
} else if (this.drawData.shapeType === 'cuboid') {
this.pasteCuboid(stringifiedPoints);
}
}
this.setupPasteEvents();
@ -579,22 +703,35 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawPolyline();
} else if (this.drawData.shapeType === 'points') {
this.drawPoints();
} else if (this.drawData.shapeType === 'cuboid') {
if (this.drawData.cuboidDrawingMethod === CuboidDrawingMethod.CORNER_POINTS) {
this.drawCuboidBy4Points();
} else {
this.drawCuboid();
this.shapeSizeElement = displayShapeSize(this.canvas, this.text);
}
}
this.setupDrawEvents();
}
this.startTimestamp = Date.now();
this.initialized = true;
}
public constructor(
onDrawDone: (data: object, continueDraw?: boolean) => void,
onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void,
canvas: SVG.Container,
text: SVG.Container,
autoborderHandler: AutoborderHandler,
) {
this.autoborderHandler = autoborderHandler;
this.autobordersEnabled = false;
this.startTimestamp = Date.now();
this.onDrawDone = onDrawDone;
this.canvas = canvas;
this.text = text;
this.initialized = false;
this.canceled = false;
this.drawData = null;
this.geometry = null;
this.crosshair = null;
@ -618,6 +755,19 @@ export class DrawHandlerImpl implements DrawHandler {
});
}
public configurate(configuration: Configuration): void {
if (typeof (configuration.autoborders) === 'boolean') {
this.autobordersEnabled = configuration.autoborders;
if (this.drawInstance) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, false);
} else {
this.autoborderHandler.autoborder(false);
}
}
}
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
@ -668,17 +818,18 @@ export class DrawHandlerImpl implements DrawHandler {
this.geometry = geometry;
if (drawData.enabled) {
this.canceled = false;
this.drawData = drawData;
this.initDrawing();
this.startDraw();
} else {
this.cancel();
this.release();
this.drawData = drawData;
}
}
public cancel(): void {
this.canceled = true;
this.release();
this.onDrawDone(null);
}
}

@ -6,29 +6,27 @@ import * as SVG from 'svg.js';
import 'svg.select.js';
import consts from './consts';
import {
translateFromSVG,
pointsToArray,
} from './shared';
import {
EditData,
Geometry,
} from './canvasModel';
import { translateFromSVG, pointsToArray } from './shared';
import { EditData, Geometry, Configuration } from './canvasModel';
import { AutoborderHandler } from './autoborderHandler';
export interface EditHandler {
edit(editData: EditData): void;
transform(geometry: Geometry): void;
configurate(configuration: Configuration): void;
cancel(): void;
}
export class EditHandlerImpl implements EditHandler {
private onEditDone: (state: any, points: number[]) => void;
private autoborderHandler: AutoborderHandler;
private geometry: Geometry;
private canvas: SVG.Container;
private editData: EditData;
private editedShape: SVG.Shape;
private editLine: SVG.PolyLine;
private clones: SVG.Polygon[];
private autobordersEnabled: boolean;
private startEdit(): void {
// get started coordinates
@ -77,6 +75,8 @@ export class EditHandlerImpl implements EditHandler {
(this.editLine as any).addClass('cvat_canvas_shape_drawing').style({
'pointer-events': 'none',
'fill-opacity': 0,
}).attr({
'data-origin-client-id': this.editData.state.clientID,
}).on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry);
lastDrawnPoint.x = e.detail.event.clientX;
@ -89,27 +89,20 @@ export class EditHandlerImpl implements EditHandler {
}
this.setupEditEvents();
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.editLine, true);
}
}
private setupEditEvents(): void {
let mouseX: number | null = null;
let mouseY: number | null = null;
this.canvas.on('mousedown.edit', (e: MouseEvent): void => {
if (e.which === 1) {
mouseX = e.clientX;
mouseY = e.clientY;
}
});
this.canvas.on('mouseup.edit', (e: MouseEvent): void => {
const threshold = 10; // px
if (e.which === 1) {
if (Math.sqrt( // l2 distance < threshold
((mouseX - e.clientX) ** 2)
+ ((mouseY - e.clientY) ** 2),
) < threshold) {
(this.editLine as any).draw('point', e);
if (e.button === 0 && !e.altKey) {
(this.editLine as any).draw('point', e);
} else if (e.button === 2 && this.editLine) {
if (this.editData.state.shapeType === 'points'
|| this.editLine.attr('points').split(' ').length > 2
) {
(this.editLine as any).draw('undo');
}
}
});
@ -183,7 +176,6 @@ export class EditHandlerImpl implements EditHandler {
// We do not need these events any more
this.canvas.off('mousedown.edit');
this.canvas.off('mouseup.edit');
this.canvas.off('mousemove.edit');
(this.editLine as any).draw('stop');
@ -265,8 +257,8 @@ export class EditHandlerImpl implements EditHandler {
private release(): void {
this.canvas.off('mousedown.edit');
this.canvas.off('mouseup.edit');
this.canvas.off('mousemove.edit');
this.autoborderHandler.autoborder(false);
if (this.editedShape) {
this.setupPoints(false);
@ -308,7 +300,10 @@ export class EditHandlerImpl implements EditHandler {
public constructor(
onEditDone: (state: any, points: number[]) => void,
canvas: SVG.Container,
autoborderHandler: AutoborderHandler,
) {
this.autoborderHandler = autoborderHandler;
this.autobordersEnabled = false;
this.onEditDone = onEditDone;
this.canvas = canvas;
this.editData = null;
@ -337,6 +332,19 @@ export class EditHandlerImpl implements EditHandler {
this.onEditDone(null, null);
}
public configurate(configuration: Configuration): void {
if (typeof (configuration.autoborders) === 'boolean') {
this.autobordersEnabled = configuration.autoborders;
if (this.editLine) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.editLine, true);
} else {
this.autoborderHandler.autoborder(false);
}
}
}
}
public transform(geometry: Geometry): void {
this.geometry = geometry;

@ -15,8 +15,9 @@ export interface MergeHandler {
export class MergeHandlerImpl implements MergeHandler {
// callback is used to notify about merging end
private onMergeDone: (objects: any[]) => void;
private onMergeDone: (objects: any[] | null, duration?: number) => void;
private onFindObject: (event: MouseEvent) => void;
private startTimestamp: number;
private canvas: SVG.Container;
private initialized: boolean;
private statesToBeMerged: any[]; // are being merged
@ -57,6 +58,7 @@ export class MergeHandlerImpl implements MergeHandler {
private initMerging(): void {
this.canvas.node.addEventListener('click', this.onFindObject);
this.startTimestamp = Date.now();
this.initialized = true;
}
@ -66,7 +68,7 @@ export class MergeHandlerImpl implements MergeHandler {
this.release();
if (statesToBeMerged.length > 1) {
this.onMergeDone(statesToBeMerged);
this.onMergeDone(statesToBeMerged, Date.now() - this.startTimestamp);
} else {
this.onMergeDone(null);
// here is a cycle
@ -77,12 +79,13 @@ export class MergeHandlerImpl implements MergeHandler {
}
public constructor(
onMergeDone: (objects: any[]) => void,
onMergeDone: (objects: any[] | null, duration?: number) => void,
onFindObject: (event: MouseEvent) => void,
canvas: SVG.Container,
) {
this.onMergeDone = onMergeDone;
this.onFindObject = onFindObject;
this.startTimestamp = Date.now();
this.canvas = canvas;
this.statesToBeMerged = [];
this.highlightedShapes = {};

@ -25,6 +25,25 @@ export interface BBox {
y: number;
}
interface Point {
x: number;
y: number;
}
export interface DrawnState {
clientID: number;
outside?: boolean;
occluded?: boolean;
hidden?: boolean;
lock: boolean;
shapeType: string;
points?: number[];
attributes: Record<number, string>;
zOrder?: number;
pinned?: boolean;
updated: number;
frame: number;
}
// Translate point array from the canvas coordinate system
// to the coordinate system of a client
export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] {
@ -100,3 +119,26 @@ 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 parsePoints(stringified: string): Point[] {
return stringified.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 {
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);
}

@ -9,6 +9,16 @@ import 'svg.resize.js';
import 'svg.select.js';
import 'svg.draw.js';
import consts from './consts';
import {
Point,
Equation,
CuboidModel,
Orientation,
Edge,
} from './cuboid';
import { parsePoints, stringifyPoints, clamp } from './shared';
// Update constructor
const originalDraw = SVG.Element.prototype.draw;
SVG.Element.prototype.draw = function constructor(...args: any): any {
@ -16,7 +26,9 @@ SVG.Element.prototype.draw = function constructor(...args: any): any {
if (!handler) {
originalDraw.call(this, ...args);
handler = this.remember('_paintHandler');
if (!handler.set) {
// There is use case (drawing a single point when handler is created and destructed immediately in one stack)
// So, we need to check if handler still exists
if (handler && !handler.set) {
handler.set = new SVG.Set();
}
} else {
@ -161,6 +173,11 @@ SVG.Element.prototype.resize = function constructor(...args: any): any {
if (!handler) {
originalResize.call(this, ...args);
handler = this.remember('_resizeHandler');
handler.resize = function(e: any) {
if (e.detail.event.button === 0) {
return handler.constructor.prototype.resize.call(this, e);
}
}
handler.update = function(e: any) {
this.m = this.el.node.getScreenCTM().inverse();
return handler.constructor.prototype.update.call(this, e);
@ -174,3 +191,873 @@ SVG.Element.prototype.resize = function constructor(...args: any): any {
for (const key of Object.keys(originalResize)) {
SVG.Element.prototype.resize[key] = originalResize[key];
}
enum EdgeIndex {
FL = 1,
FR = 2,
DR = 3,
DL = 4,
}
function getEdgeIndex(cuboidPoint: number): EdgeIndex {
switch (cuboidPoint) {
case 0:
case 1:
return EdgeIndex.FL;
case 2:
case 3:
return EdgeIndex.FR;
case 4:
case 5:
return EdgeIndex.DR;
default:
return EdgeIndex.DL;
}
}
function getTopDown(edgeIndex: EdgeIndex): number[] {
switch (edgeIndex) {
case EdgeIndex.FL:
return [0, 1];
case EdgeIndex.FR:
return [2, 3];
case EdgeIndex.DR:
return [4, 5];
default:
return [6, 7];
}
}
(SVG as any).Cube = SVG.invent({
create: 'g',
inherit: SVG.G,
extend: {
constructorMethod(points: string) {
this.cuboidModel = new CuboidModel(parsePoints(points));
this.setupFaces();
this.setupEdges();
this.setupProjections();
this.hideProjections();
this._attr('points', points);
return this;
},
setupFaces() {
this.bot = this.polygon(this.cuboidModel.bot.points);
this.top = this.polygon(this.cuboidModel.top.points);
this.right = this.polygon(this.cuboidModel.right.points);
this.left = this.polygon(this.cuboidModel.left.points);
this.dorsal = this.polygon(this.cuboidModel.dorsal.points);
this.face = this.polygon(this.cuboidModel.front.points);
},
setupProjections() {
this.ftProj = this.line(this.updateProjectionLine(this.cuboidModel.ft.getEquation(),
this.cuboidModel.ft.points[0], this.cuboidModel.vpl));
this.fbProj = this.line(this.updateProjectionLine(this.cuboidModel.fb.getEquation(),
this.cuboidModel.ft.points[0], this.cuboidModel.vpl));
this.rtProj = this.line(this.updateProjectionLine(this.cuboidModel.rt.getEquation(),
this.cuboidModel.rt.points[1], this.cuboidModel.vpr));
this.rbProj = this.line(this.updateProjectionLine(this.cuboidModel.rb.getEquation(),
this.cuboidModel.rb.points[1], this.cuboidModel.vpr));
this.ftProj.stroke({ color: '#C0C0C0' }).addClass('cvat_canvas_cuboid_projections');
this.fbProj.stroke({ color: '#C0C0C0' }).addClass('cvat_canvas_cuboid_projections');
this.rtProj.stroke({ color: '#C0C0C0' }).addClass('cvat_canvas_cuboid_projections');
this.rbProj.stroke({ color: '#C0C0C0' }).addClass('cvat_canvas_cuboid_projections');
},
setupEdges() {
this.frontLeftEdge = this.line(this.cuboidModel.fl.points);
this.frontRightEdge = this.line(this.cuboidModel.fr.points);
this.dorsalRightEdge = this.line(this.cuboidModel.dr.points);
this.dorsalLeftEdge = this.line(this.cuboidModel.dl.points);
this.frontTopEdge = this.line(this.cuboidModel.ft.points);
this.rightTopEdge = this.line(this.cuboidModel.rt.points);
this.frontBotEdge = this.line(this.cuboidModel.fb.points);
this.rightBotEdge = this.line(this.cuboidModel.rb.points);
},
setupGrabPoints(circleType: Function | string) {
const viewModel = this.cuboidModel;
const circle = typeof circleType === 'function' ? circleType : this.circle;
this.flCenter = circle(0, 0).addClass('svg_select_points').addClass('svg_select_points_l');
this.frCenter = circle(0, 0).addClass('svg_select_points').addClass('svg_select_points_r');
this.ftCenter = circle(0, 0).addClass('svg_select_points').addClass('svg_select_points_t');
this.fbCenter = circle(0, 0).addClass('svg_select_points').addClass('svg_select_points_b');
this.drCenter = circle(0, 0).addClass('svg_select_points').addClass('svg_select_points_ew');
this.dlCenter = circle(0, 0).addClass('svg_select_points').addClass('svg_select_points_ew');
const grabPoints = this.getGrabPoints();
const edges = this.getEdges();
for (let i = 0; i < grabPoints.length; i += 1) {
const edge = edges[i];
const cx = (edge.attr('x2') + edge.attr('x1')) / 2;
const cy = (edge.attr('y2') + edge.attr('y1')) / 2;
grabPoints[i].center(cx, cy);
}
if (viewModel.orientation === Orientation.LEFT) {
this.dlCenter.hide();
} else {
this.drCenter.hide();
}
},
showProjections() {
if (this.projectionLineEnable) {
this.ftProj.show();
this.fbProj.show();
this.rtProj.show();
this.rbProj.show();
}
},
hideProjections() {
this.ftProj.hide();
this.fbProj.hide();
this.rtProj.hide();
this.rbProj.hide();
},
getEdges() {
const arr = [];
arr.push(this.frontLeftEdge);
arr.push(this.frontRightEdge);
arr.push(this.dorsalRightEdge);
arr.push(this.frontTopEdge);
arr.push(this.frontBotEdge);
arr.push(this.dorsalLeftEdge);
arr.push(this.rightTopEdge);
arr.push(this.rightBotEdge);
return arr;
},
getGrabPoints() {
const arr = [];
arr.push(this.flCenter);
arr.push(this.frCenter);
arr.push(this.drCenter);
arr.push(this.ftCenter);
arr.push(this.fbCenter);
arr.push(this.dlCenter);
return arr;
},
updateProjectionLine(equation: Equation, source: Point, direction: Point) {
const x1 = source.x;
const y1 = equation.getY(x1);
const x2 = direction.x;
const y2 = equation.getY(x2);
return [[x1, y1], [x2, y2]];
},
selectize(value: boolean, options: object) {
this.face.selectize(value, options);
if (this.cuboidModel.orientation === Orientation.LEFT) {
this.dorsalLeftEdge.selectize(false, options);
this.dorsalRightEdge.selectize(value, options);
} else {
this.dorsalRightEdge.selectize(false, options);
this.dorsalLeftEdge.selectize(value, options);
}
if (value === false) {
this.getGrabPoints().forEach((point: SVG.Element) => {point && point.remove()});
} else {
this.setupGrabPoints(this.face.remember('_selectHandler').drawPoint.bind(
{nested: this, options: this.face.remember('_selectHandler').options}
));
// setup proper classes for selection points for proper cursor
Array.from(this.face.remember('_selectHandler').nested.node.children)
.forEach((point: SVG.LinkedHTMLElement, i: number) => {
point.classList.add(`svg_select_points_${['lt', 'lb', 'rb', 'rt'][i]}`)
});
if (this.cuboidModel.orientation === Orientation.LEFT) {
Array.from(this.dorsalRightEdge.remember('_selectHandler').nested.node.children)
.forEach((point: SVG.LinkedHTMLElement, i: number) => {
point.classList.add(`svg_select_points_${['t', 'b'][i]}`);
point.ondblclick = (e: MouseEvent) => {
if (e.shiftKey) {
this.resetPerspective()
}
};
});
} else {
Array.from(this.dorsalLeftEdge.remember('_selectHandler').nested.node.children)
.forEach((point: SVG.LinkedHTMLElement, i: number) => {
point.classList.add(`svg_select_points_${['t', 'b'][i]}`);
point.ondblclick = (e: MouseEvent) => {
if (e.shiftKey) {
this.resetPerspective()
}
};
});
}
}
return this;
},
resize(value?: string | object) {
this.face.resize(value);
if (value === 'stop') {
this.dorsalRightEdge.resize(value);
this.dorsalLeftEdge.resize(value);
this.face.off('resizing').off('resizedone').off('resizestart');
this.dorsalRightEdge.off('resizing').off('resizedone').off('resizestart');
this.dorsalLeftEdge.off('resizing').off('resizedone').off('resizestart');
this.getGrabPoints().forEach((point: SVG.Element) => {
if (point) {
point.off('dragstart');
point.off('dragmove');
point.off('dragend');
}
})
return;
}
function getResizedPointIndex(event: CustomEvent): number {
const { target } = event.detail.event.detail.event;
const { parentElement } = target;
return Array
.from(parentElement.children)
.indexOf(target);
}
let resizedCubePoint: null | number = null;
const accumulatedOffset: Point = {
x: 0,
y: 0,
};
this.face.on('resizestart', (event: CustomEvent) => {
accumulatedOffset.x = 0;
accumulatedOffset.y = 0;
const resizedFacePoint = getResizedPointIndex(event);
resizedCubePoint = [0, 1].includes(resizedFacePoint) ? resizedFacePoint
: 5 - resizedFacePoint; // 2,3 -> 3,2
this.fire(new CustomEvent('resizestart', event));
}).on('resizing', (event: CustomEvent) => {
let { dx, dy } = event.detail;
let dxPortion = dx - accumulatedOffset.x;
let dyPortion = dy - accumulatedOffset.y;
accumulatedOffset.x += dxPortion;
accumulatedOffset.y += dyPortion;
const edge = getEdgeIndex(resizedCubePoint);
const [edgeTopIndex, edgeBottomIndex] = getTopDown(edge);
let cuboidPoints = this.cuboidModel.getPoints();
let x1 = cuboidPoints[edgeTopIndex].x + dxPortion;
let x2 = cuboidPoints[edgeBottomIndex].x + dxPortion;
if (edge === EdgeIndex.FL
&& (cuboidPoints[2].x - (cuboidPoints[0].x + dxPortion) < consts.MIN_EDGE_LENGTH)
) {
x1 = cuboidPoints[edgeTopIndex].x;
x2 = cuboidPoints[edgeBottomIndex].x;
} else if (edge === EdgeIndex.FR
&& (cuboidPoints[2].x + dxPortion - cuboidPoints[0].x < consts.MIN_EDGE_LENGTH)
) {
x1 = cuboidPoints[edgeTopIndex].x;
x2 = cuboidPoints[edgeBottomIndex].x;
}
const y1 = this.cuboidModel.ft.getEquation().getY(x1);
const y2 = this.cuboidModel.fb.getEquation().getY(x2);
const topPoint = { x: x1, y: y1 };
const botPoint = { x: x2, y: y2 };
if (edge === 1) {
this.cuboidModel.fl.points = [topPoint, botPoint];
} else {
this.cuboidModel.fr.points = [topPoint, botPoint];
}
this.updateViewAndVM(edge === EdgeIndex.FR);
cuboidPoints = this.cuboidModel.getPoints();
const midPointUp = { ...cuboidPoints[edgeTopIndex] };
const midPointDown = { ...cuboidPoints[edgeBottomIndex] };
(edgeTopIndex === resizedCubePoint ? midPointUp : midPointDown).y += dyPortion;
if (midPointDown.y - midPointUp.y > consts.MIN_EDGE_LENGTH) {
const topPoints = this.computeHeightFace(midPointUp, edge);
const bottomPoints = this.computeHeightFace(midPointDown, edge);
this.cuboidModel.top.points = topPoints;
this.cuboidModel.bot.points = bottomPoints;
this.updateViewAndVM(false);
}
this.face.plot(this.cuboidModel.front.points);
this.fire(new CustomEvent('resizing', event));
}).on('resizedone', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});
function computeSideEdgeConstraints(edge: Edge, fr: Edge) {
const midLength = fr.points[1].y - fr.points[0].y - 1;
const minY = edge.points[1].y - midLength;
const maxY = edge.points[0].y + midLength;
const y1 = edge.points[0].y;
const y2 = edge.points[1].y;
const miny1 = y2 - midLength;
const maxy1 = y2 - consts.MIN_EDGE_LENGTH;
const miny2 = y1 + consts.MIN_EDGE_LENGTH;
const maxy2 = y1 + midLength;
return {
constraint: {
minY,
maxY,
},
y1Range: {
max: maxy1,
min: miny1,
},
y2Range: {
max: maxy2,
min: miny2,
},
};
}
function setupDorsalEdge(edge: SVG.Line, orientation: Orientation) {
edge.on('resizestart', (event: CustomEvent) => {
accumulatedOffset.x = 0;
accumulatedOffset.y = 0;
resizedCubePoint = getResizedPointIndex(event) + (orientation === Orientation.LEFT ? 4 : 6);
this.fire(new CustomEvent('resizestart', event));
}).on('resizing', (event: CustomEvent) => {
let { dy } = event.detail;
let dyPortion = dy - accumulatedOffset.y;
accumulatedOffset.y += dyPortion;
const edge = getEdgeIndex(resizedCubePoint);
const [edgeTopIndex, edgeBottomIndex] = getTopDown(edge);
let cuboidPoints = this.cuboidModel.getPoints();
if (!event.detail.event.shiftKey) {
cuboidPoints = this.cuboidModel.getPoints();
const midPointUp = { ...cuboidPoints[edgeTopIndex] };
const midPointDown = { ...cuboidPoints[edgeBottomIndex] };
(edgeTopIndex === resizedCubePoint ? midPointUp : midPointDown).y += dyPortion;
if (midPointDown.y - midPointUp.y > consts.MIN_EDGE_LENGTH) {
const topPoints = this.computeHeightFace(midPointUp, edge);
const bottomPoints = this.computeHeightFace(midPointDown, edge);
this.cuboidModel.top.points = topPoints;
this.cuboidModel.bot.points = bottomPoints;
}
} else {
const midPointUp = { ...cuboidPoints[edgeTopIndex] };
const midPointDown = { ...cuboidPoints[edgeBottomIndex] };
(edgeTopIndex === resizedCubePoint ? midPointUp : midPointDown).y += dyPortion;
const dorselEdge = (orientation === Orientation.LEFT ? this.cuboidModel.dr : this.cuboidModel.dl);
const constraints = computeSideEdgeConstraints(dorselEdge, this.cuboidModel.fr);
midPointUp.y = clamp(midPointUp.y, constraints.y1Range.min, constraints.y1Range.max);
midPointDown.y = clamp(midPointDown.y, constraints.y2Range.min, constraints.y2Range.max);
dorselEdge.points = [midPointUp, midPointDown];
this.updateViewAndVM(edge === EdgeIndex.DL);
}
this.updateViewAndVM(false);
this.face.plot(this.cuboidModel.front.points);
this.fire(new CustomEvent('resizing', event));
}).on('resizedone', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});
}
if (this.cuboidModel.orientation === Orientation.LEFT) {
this.dorsalRightEdge.resize(value);
setupDorsalEdge.call(this, this.dorsalRightEdge, this.cuboidModel.orientation);
} else {
this.dorsalLeftEdge.resize(value);
setupDorsalEdge.call(this, this.dorsalLeftEdge, this.cuboidModel.orientation);
}
function horizontalEdgeControl(updatingFace: any, midX: number, midY: number) {
const leftPoints = this.updatedEdge(
this.cuboidModel.fl.points[0],
{x: midX, y: midY},
this.cuboidModel.vpl,
);
const rightPoints = this.updatedEdge(
this.cuboidModel.dr.points[0],
{x: midX, y: midY},
this.cuboidModel.vpr,
);
updatingFace.points = [leftPoints, {x: midX, y: midY}, rightPoints, null];
}
this.drCenter.draggable((x: number) => {
let xStatus;
if (this.drCenter.cx() < this.cuboidModel.fr.points[0].x) {
xStatus = x < this.cuboidModel.fr.points[0].x - consts.MIN_EDGE_LENGTH
&& x > this.cuboidModel.vpr.x + consts.MIN_EDGE_LENGTH;
} else {
xStatus = x > this.cuboidModel.fr.points[0].x + consts.MIN_EDGE_LENGTH
&& x < this.cuboidModel.vpr.x - consts.MIN_EDGE_LENGTH;
}
return { x: xStatus, y: this.drCenter.attr('y1') };
}).on('dragstart', ((event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => {
this.dorsalRightEdge.center(this.drCenter.cx(), this.drCenter.cy());
const x = this.dorsalRightEdge.attr('x1');
const y1 = this.cuboidModel.rt.getEquation().getY(x);
const y2 = this.cuboidModel.rb.getEquation().getY(x);
const topPoint = { x, y: y1 };
const botPoint = { x, y: y2 };
this.cuboidModel.dr.points = [topPoint, botPoint];
this.updateViewAndVM();
this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});
this.dlCenter.draggable((x: number) => {
let xStatus;
if (this.dlCenter.cx() < this.cuboidModel.fl.points[0].x) {
xStatus = x < this.cuboidModel.fl.points[0].x - consts.MIN_EDGE_LENGTH
&& x > this.cuboidModel.vpr.x + consts.MIN_EDGE_LENGTH;
} else {
xStatus = x > this.cuboidModel.fl.points[0].x + consts.MIN_EDGE_LENGTH
&& x < this.cuboidModel.vpr.x - consts.MIN_EDGE_LENGTH;
}
return { x: xStatus, y: this.dlCenter.attr('y1') };
}).on('dragstart', ((event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => {
this.dorsalLeftEdge.center(this.dlCenter.cx(), this.dlCenter.cy());
const x = this.dorsalLeftEdge.attr('x1');
const y1 = this.cuboidModel.lt.getEquation().getY(x);
const y2 = this.cuboidModel.lb.getEquation().getY(x);
const topPoint = { x, y: y1 };
const botPoint = { x, y: y2 };
this.cuboidModel.dl.points = [topPoint, botPoint];
this.updateViewAndVM(true);
this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});;
this.flCenter.draggable((x: number) => {
const vpX = this.flCenter.cx() - this.cuboidModel.vpl.x > 0 ? this.cuboidModel.vpl.x : 0;
return { x: x < this.cuboidModel.fr.points[0].x && x > vpX + consts.MIN_EDGE_LENGTH };
}).on('dragstart', ((event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => {
this.frontLeftEdge.center(this.flCenter.cx(), this.flCenter.cy());
const x = this.frontLeftEdge.attr('x1');
const y1 = this.cuboidModel.ft.getEquation().getY(x);
const y2 = this.cuboidModel.fb.getEquation().getY(x);
const topPoint = { x, y: y1 };
const botPoint = { x, y: y2 };
this.cuboidModel.fl.points = [topPoint, botPoint];
this.updateViewAndVM();
this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});
this.frCenter.draggable((x: number) => {
return { x: x > this.cuboidModel.fl.points[0].x, y: this.frCenter.attr('y1') };
}).on('dragstart', ((event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => {
this.frontRightEdge.center(this.frCenter.cx(), this.frCenter.cy());
const x = this.frontRightEdge.attr('x1');
const y1 = this.cuboidModel.ft.getEquation().getY(x);
const y2 = this.cuboidModel.fb.getEquation().getY(x);
const topPoint = { x, y: y1 };
const botPoint = { x, y: y2 };
this.cuboidModel.fr.points = [topPoint, botPoint];
this.updateViewAndVM(true);
this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});
this.ftCenter.draggable((x: number, y: number) => {
return { x: x === this.ftCenter.cx(), y: y < this.fbCenter.cy() - consts.MIN_EDGE_LENGTH };
}).on('dragstart', ((event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => {
this.frontTopEdge.center(this.ftCenter.cx(), this.ftCenter.cy());
horizontalEdgeControl.call(this, this.cuboidModel.top, this.frontTopEdge.attr('x2'), this.frontTopEdge.attr('y2'));
this.updateViewAndVM();
this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});
this.fbCenter.draggable((x: number, y: number) => {
return { x: x === this.fbCenter.cx(), y: y > this.ftCenter.cy() + consts.MIN_EDGE_LENGTH };
}).on('dragstart', ((event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => {
this.frontBotEdge.center(this.fbCenter.cx(), this.fbCenter.cy());
horizontalEdgeControl.call(this, this.cuboidModel.bot, this.frontBotEdge.attr('x2'), this.frontBotEdge.attr('y2'));
this.updateViewAndVM();
this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});
return this;
},
draggable(value: any, constraint: any) {
const { cuboidModel } = this;
const faces = [this.face, this.right, this.dorsal, this.left]
const accumulatedOffset: Point = {
x: 0,
y: 0,
};
if (value === false) {
faces.forEach((face: any) => {
face.draggable(false);
face.off('dragstart');
face.off('dragmove');
face.off('dragend');
})
return
}
this.face.draggable().on('dragstart', (event: CustomEvent) => {
accumulatedOffset.x = 0;
accumulatedOffset.y = 0;
this.fire(new CustomEvent('dragstart', event));
}).on('dragmove', (event: CustomEvent) => {
const dx = event.detail.p.x - event.detail.handler.startPoints.point.x;
const dy = event.detail.p.y - event.detail.handler.startPoints.point.y;
let dxPortion = dx - accumulatedOffset.x;
let dyPortion = dy - accumulatedOffset.y;
accumulatedOffset.x += dxPortion;
accumulatedOffset.y += dyPortion;
this.dmove(dxPortion, dyPortion);
this.fire(new CustomEvent('dragmove', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('dragend', event));
})
this.left.draggable((x: number, y: number) => ({
x: x < Math.min(cuboidModel.dr.points[0].x,
cuboidModel.fr.points[0].x) - consts.MIN_EDGE_LENGTH, y
})).on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('dragstart', event));
}).on('dragmove', (event: CustomEvent) => {
this.cuboidModel.left.points = parsePoints(this.left.attr('points'));
this.updateViewAndVM();
this.fire(new CustomEvent('dragmove', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('dragend', event));
});
this.dorsal.draggable().on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('dragstart', event));
}).on('dragmove', (event: CustomEvent) => {
this.cuboidModel.dorsal.points = parsePoints(this.dorsal.attr('points'));
this.updateViewAndVM();
this.fire(new CustomEvent('dragmove', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('dragend', event));
});
this.right.draggable((x: number, y: number) => ({
x: x > Math.min(cuboidModel.dl.points[0].x,
cuboidModel.fl.points[0].x) + consts.MIN_EDGE_LENGTH, y
})).on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('dragstart', event));
}).on('dragmove', (event: CustomEvent) => {
this.cuboidModel.right.points = parsePoints(this.right.attr('points'));
this.updateViewAndVM(true);
this.fire(new CustomEvent('dragmove', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('dragend', event));
});
return this;
},
_attr: SVG.Element.prototype.attr,
attr(a: any, v: any, n: any) {
if ((a === 'fill' || a === 'stroke' || a === 'face-stroke')
&& v !== undefined) {
this._attr(a, v, n);
this.paintOrientationLines();
} else if (a === 'points' && typeof v === 'string') {
const points = parsePoints(v);
this.cuboidModel.setPoints(points);
this.updateViewAndVM();
} else if (a === 'projections') {
this._attr(a, v, n);
if (v === true) {
this.ftProj.show();
this.fbProj.show();
this.rtProj.show();
this.rbProj.show();
} else {
this.ftProj.hide();
this.fbProj.hide();
this.rtProj.hide();
this.rbProj.hide();
}
} else if (a === 'stroke-width' && typeof v === "number") {
this._attr(a, v, n);
this.updateThickness();
} else if (a === 'data-z-order' && typeof v !== 'undefined') {
this._attr(a, v, n);
[this.face, this.left, this.dorsal, this.right, ...this.getEdges(), ...this.getGrabPoints()]
.forEach((el) => {if (el) el.attr(a, v, n)})
} else {
return this._attr(a ,v, n);
}
return this;
},
updateThickness() {
const edges = [this.frontLeftEdge, this.frontRightEdge, this.frontTopEdge, this.frontBotEdge]
const width = this.attr('stroke-width');
edges.forEach((edge: SVG.Element) => {
edge.attr('stroke-width', width * (this.strokeOffset || consts.CUBOID_UNACTIVE_EDGE_STROKE_WIDTH));
});
this.on('mouseover', () => {
edges.forEach((edge: SVG.Element) => {
this.strokeOffset = this.node.classList.contains('cvat_canvas_shape_activated')
? consts.CUBOID_ACTIVE_EDGE_STROKE_WIDTH : consts.CUBOID_UNACTIVE_EDGE_STROKE_WIDTH;
edge.attr('stroke-width', width * this.strokeOffset);
})
}).on('mouseout', () => {
edges.forEach((edge: SVG.Element) => {
this.strokeOffset = consts.CUBOID_UNACTIVE_EDGE_STROKE_WIDTH;
edge.attr('stroke-width', width * this.strokeOffset);
})
});
},
paintOrientationLines() {
const fillColor = this.attr('fill');
const strokeColor = this.attr('stroke');
const selectedColor = this.attr('face-stroke') || '#b0bec5';
this.frontTopEdge.stroke({ color: selectedColor });
this.frontLeftEdge.stroke({ color: selectedColor });
this.frontBotEdge.stroke({ color: selectedColor });
this.frontRightEdge.stroke({ color: selectedColor });
this.rightTopEdge.stroke({ color: strokeColor });
this.rightBotEdge.stroke({ color: strokeColor });
this.dorsalRightEdge.stroke({ color: strokeColor });
this.dorsalLeftEdge.stroke({ color: strokeColor });
this.bot.stroke({ color: strokeColor })
.fill({ color: fillColor });
this.top.stroke({ color: strokeColor })
.fill({ color: fillColor });
this.face.stroke({ color: strokeColor, width: 0 })
.fill({ color: fillColor });
this.right.stroke({ color: strokeColor })
.fill({ color: fillColor });
this.dorsal.stroke({ color: strokeColor })
.fill({ color: fillColor });
this.left.stroke({ color: strokeColor })
.fill({ color: fillColor });
},
dmove(dx: number, dy: number) {
this.cuboidModel.points.forEach((point: Point) => {
point.x += dx;
point.y += dy;
});
this.updateViewAndVM();
},
x(x?: number) {
if (typeof x === 'number') {
const { x: xInitial } = this.bbox();
this.dmove(x - xInitial, 0);
return this;
} else {
return this.bbox().x;
}
},
y(y?: number) {
if (typeof y === 'number') {
const { y: yInitial } = this.bbox();
this.dmove(0, y - yInitial);
return this;
} else {
return this.bbox().y;
}
},
resetPerspective(){
if (this.cuboidModel.orientation === Orientation.LEFT) {
const edgePoints = this.cuboidModel.dl.points;
const constraints = this.cuboidModel.computeSideEdgeConstraints(this.cuboidModel.dl);
edgePoints[0].y = constraints.y1Range.min;
this.cuboidModel.dl.points = [edgePoints[0],edgePoints[1]];
this.updateViewAndVM(true);
} else {
const edgePoints = this.cuboidModel.dr.points;
const constraints = this.cuboidModel.computeSideEdgeConstraints(this.cuboidModel.dr);
edgePoints[0].y = constraints.y1Range.min;
this.cuboidModel.dr.points = [edgePoints[0],edgePoints[1]];
this.updateViewAndVM();
}
},
updateViewAndVM(build: boolean) {
this.cuboidModel.updateOrientation();
this.cuboidModel.buildBackEdge(build);
this.updateView();
// to correct getting of points in resizedone, dragdone
this._attr('points', this.cuboidModel
.getPoints()
.reduce((acc: string, point: Point): string => `${acc} ${point.x},${point.y}`, '').trim());
},
computeHeightFace(point: Point, index: number) {
switch (index) {
// fl
case 1: {
const p2 = this.updatedEdge(this.cuboidModel.fr.points[0], point, this.cuboidModel.vpl);
const p3 = this.updatedEdge(this.cuboidModel.dr.points[0], p2, this.cuboidModel.vpr);
const p4 = this.updatedEdge(this.cuboidModel.dl.points[0], point, this.cuboidModel.vpr);
return [point, p2, p3, p4];
}
// fr
case 2: {
const p1 = this.updatedEdge(this.cuboidModel.fl.points[0], point, this.cuboidModel.vpl);
const p3 = this.updatedEdge(this.cuboidModel.dr.points[0], point, this.cuboidModel.vpr);
const p4 = this.updatedEdge(this.cuboidModel.dl.points[0], p3, this.cuboidModel.vpr);
return [p1, point, p3, p4];
}
// dr
case 3: {
const p2 = this.updatedEdge(this.cuboidModel.dl.points[0], point, this.cuboidModel.vpl);
const p3 = this.updatedEdge(this.cuboidModel.fr.points[0], point, this.cuboidModel.vpr);
const p4 = this.updatedEdge(this.cuboidModel.fl.points[0], p2, this.cuboidModel.vpr);
return [p4, p3, point, p2];
}
// dl
case 4: {
const p2 = this.updatedEdge(this.cuboidModel.dr.points[0], point, this.cuboidModel.vpl);
const p3 = this.updatedEdge(this.cuboidModel.fl.points[0], point, this.cuboidModel.vpr);
const p4 = this.updatedEdge(this.cuboidModel.fr.points[0], p2, this.cuboidModel.vpr);
return [p3, p4, p2, point];
}
default: {
return [null, null, null, null];
}
}
},
updatedEdge(target: Point, base: Point, pivot: Point) {
const targetX = target.x;
const line = new Equation(pivot, base);
const newY = line.getY(targetX);
return { x: targetX, y: newY };
},
updateView() {
this.updateFaces();
this.updateEdges();
this.updateProjections();
this.updateGrabPoints();
},
updateFaces() {
const viewModel = this.cuboidModel;
this.bot.plot(viewModel.bot.points);
this.top.plot(viewModel.top.points);
this.right.plot(viewModel.right.points);
this.dorsal.plot(viewModel.dorsal.points);
this.left.plot(viewModel.left.points);
this.face.plot(viewModel.front.points);
},
updateEdges() {
const viewModel = this.cuboidModel;
this.frontLeftEdge.plot(viewModel.fl.points);
this.frontRightEdge.plot(viewModel.fr.points);
this.dorsalRightEdge.plot(viewModel.dr.points);
this.dorsalLeftEdge.plot(viewModel.dl.points);
this.frontTopEdge.plot(viewModel.ft.points);
this.rightTopEdge.plot(viewModel.rt.points);
this.frontBotEdge.plot(viewModel.fb.points);
this.rightBotEdge.plot(viewModel.rb.points);
},
updateProjections() {
const viewModel = this.cuboidModel;
this.ftProj.plot(this.updateProjectionLine(viewModel.ft.getEquation(),
viewModel.ft.points[0], viewModel.vpl));
this.fbProj.plot(this.updateProjectionLine(viewModel.fb.getEquation(),
viewModel.ft.points[0], viewModel.vpl));
this.rtProj.plot(this.updateProjectionLine(viewModel.rt.getEquation(),
viewModel.rt.points[1], viewModel.vpr));
this.rbProj.plot(this.updateProjectionLine(viewModel.rb.getEquation(),
viewModel.rt.points[1], viewModel.vpr));
},
updateGrabPoints() {
const centers = this.getGrabPoints();
const edges = this.getEdges();
for (let i = 0; i < centers.length; i += 1) {
const edge = edges[i];
if (centers[i]) centers[i].center(edge.cx(), edge.cy());
}
},
},
construct: {
cube(points: string) {
return this.put(new (SVG as any).Cube()).constructorMethod(points);
},
},
});

@ -23,10 +23,12 @@ const nodeConfig = {
},
module: {
rules: [{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: ['@babel/plugin-proposal-class-properties'],
presets: [
['@babel/preset-env'],
['@babel/typescript'],
@ -35,14 +37,20 @@ const nodeConfig = {
},
},
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader']
test: /\.(css|scss)$/,
exclude: /node_modules/,
use: ['style-loader', {
loader: 'css-loader',
options: {
importLoaders: 2,
},
}, 'postcss-loader', 'sass-loader']
}],
},
plugins: [
new DtsBundleWebpack({
name: 'cvat-canvas.node',
main: 'dist/declaration/canvas.d.ts',
main: 'dist/declaration/src/typescript/canvas.d.ts',
out: '../cvat-canvas.node.d.ts',
}),
]
@ -70,10 +78,12 @@ const webConfig = {
},
module: {
rules: [{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: ['@babel/plugin-proposal-class-properties'],
presets: [
['@babel/preset-env', {
targets: '> 2.5%', // https://github.com/browserslist/browserslist
@ -97,7 +107,7 @@ const webConfig = {
plugins: [
new DtsBundleWebpack({
name: 'cvat-canvas',
main: 'dist/declaration/canvas.d.ts',
main: 'dist/declaration/src/typescript/canvas.d.ts',
out: '../cvat-canvas.d.ts',
}),
]

@ -4,6 +4,13 @@
This CVAT module is a clien-side JavaScipt library to management of objects, frames, logs, etc.
It contains the core logic of the Computer Vision Annotation Tool.
## Versioning
If you make changes in this package, please do following:
- After not important changes (typos, backward compatible bug fixes, refactoring) do: ``npm version patch``
- After changing API (backward compatible new features) do: ``npm version minor``
- After changing API (changes that break backward compatibility) do: ``npm version major``
### Commands
- Dependencies installation

@ -1,6 +1,6 @@
{
"name": "cvat-core.js",
"version": "0.5.2",
"name": "cvat-core",
"version": "2.0.1",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {
@ -34,12 +34,15 @@
"dependencies": {
"axios": "^0.18.0",
"browser-or-node": "^1.2.1",
"cvat-data": "../cvat-data",
"detect-browser": "^5.0.0",
"error-stack-parser": "^2.0.2",
"form-data": "^2.5.0",
"jest-config": "^24.8.0",
"js-cookie": "^2.2.0",
"jsonpath": "^1.0.2",
"platform": "^1.3.5",
"store": "^2.0.12"
"store": "^2.0.12",
"worker-loader": "^2.0.0"
}
}

@ -12,9 +12,8 @@
class Loader {
constructor(initialData) {
const data = {
display_name: initialData.display_name,
format: initialData.format,
handler: initialData.handler,
name: initialData.name,
format: initialData.ext,
version: initialData.version,
};
@ -27,7 +26,7 @@
* @readonly
* @instance
*/
get: () => data.display_name,
get: () => data.name,
},
format: {
/**
@ -39,16 +38,6 @@
*/
get: () => data.format,
},
handler: {
/**
* @name handler
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.handler,
},
version: {
/**
* @name version
@ -71,9 +60,8 @@
class Dumper {
constructor(initialData) {
const data = {
display_name: initialData.display_name,
format: initialData.format,
handler: initialData.handler,
name: initialData.name,
format: initialData.ext,
version: initialData.version,
};
@ -86,7 +74,7 @@
* @readonly
* @instance
*/
get: () => data.display_name,
get: () => data.name,
},
format: {
/**
@ -98,16 +86,6 @@
*/
get: () => data.format,
},
handler: {
/**
* @name handler
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.handler,
},
version: {
/**
* @name version
@ -127,108 +105,41 @@
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class AnnotationFormat {
class AnnotationFormats {
constructor(initialData) {
const data = {
created_date: initialData.created_date,
updated_date: initialData.updated_date,
id: initialData.id,
owner: initialData.owner,
name: initialData.name,
handler_file: initialData.handler_file,
exporters: initialData.exporters.map((el) => new Dumper(el)),
importers: initialData.importers.map((el) => new Loader(el)),
};
data.dumpers = initialData.dumpers.map((el) => new Dumper(el));
data.loaders = initialData.loaders.map((el) => new Loader(el));
// Now all fields are readonly
Object.defineProperties(this, {
id: {
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => data.id,
},
owner: {
/**
* @name owner
* @type {integer}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => data.owner,
},
name: {
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => data.name,
},
createdDate: {
/**
* @name createdDate
* @type {string}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => data.created_date,
},
updatedDate: {
/**
* @name updatedDate
* @type {string}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => data.updated_date,
},
handlerFile: {
/**
* @name handlerFile
* @type {string}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => data.handler_file,
},
loaders: {
/**
* @name loaders
* @type {module:API.cvat.classes.Loader[]}
* @memberof module:API.cvat.classes.AnnotationFormat
* @memberof module:API.cvat.classes.AnnotationFormats
* @readonly
* @instance
*/
get: () => [...data.loaders],
get: () => [...data.importers],
},
dumpers: {
/**
* @name dumpers
* @type {module:API.cvat.classes.Dumper[]}
* @memberof module:API.cvat.classes.AnnotationFormat
* @memberof module:API.cvat.classes.AnnotationFormats
* @readonly
* @instance
*/
get: () => [...data.dumpers],
get: () => [...data.exporters],
},
});
}
}
module.exports = {
AnnotationFormat,
AnnotationFormats,
Loader,
Dumper,
};

@ -13,10 +13,12 @@
PolygonShape,
PolylineShape,
PointsShape,
CuboidShape,
RectangleTrack,
PolygonTrack,
PolylineTrack,
PointsTrack,
CuboidTrack,
Track,
Shape,
Tag,
@ -58,6 +60,9 @@
case 'points':
shapeModel = new PointsShape(shapeData, clientID, color, injection);
break;
case 'cuboid':
shapeModel = new CuboidShape(shapeData, clientID, color, injection);
break;
default:
throw new DataError(
`An unexpected type of shape "${type}"`,
@ -87,6 +92,9 @@
case 'points':
trackModel = new PointsTrack(trackData, clientID, color, injection);
break;
case 'cuboid':
trackModel = new CuboidTrack(trackData, clientID, color, injection);
break;
default:
throw new DataError(
`An unexpected type of track "${type}"`,
@ -317,7 +325,7 @@
// Push outside shape after each annotation shape
// Any not outside shape rewrites it
if (!((object.frame + 1) in keyframes)) {
if (!((object.frame + 1) in keyframes) && object.frame + 1 <= this.stopFrame) {
keyframes[object.frame + 1] = JSON
.parse(JSON.stringify(keyframes[object.frame]));
keyframes[object.frame + 1].outside = true;
@ -427,7 +435,10 @@
for (const object of objectsForMerge) {
object.removed = true;
}
}, [...objectsForMerge.map((object) => object.clientID), trackModel.clientID]);
}, [
...objectsForMerge
.map((object) => object.clientID), trackModel.clientID,
], objectStates[0].frame);
}
split(objectState, frame) {
@ -522,7 +533,7 @@
object.removed = true;
prevTrack.removed = false;
nextTrack.removed = false;
}, [object.clientID, prevTrack.clientID, nextTrack.clientID]);
}, [object.clientID, prevTrack.clientID, nextTrack.clientID], frame);
}
group(objectStates, reset) {
@ -554,7 +565,7 @@
objectsForGroup.forEach((object, idx) => {
object.group = redoGroups[idx];
});
}, objectsForGroup.map((object) => object.clientID));
}, objectsForGroup.map((object) => object.clientID), objectStates[0].frame);
return groupIdx;
}
@ -588,6 +599,10 @@
shape: 0,
track: 0,
},
cuboid: {
shape: 0,
track: 0,
},
tags: 0,
manually: 0,
interpolated: 0,
@ -790,7 +805,9 @@
importedArray.forEach((object) => {
object.removed = false;
});
}, importedArray.map((object) => object.clientID));
}, importedArray.map((object) => object.clientID), objectStates[0].frame);
return importedArray.map((value) => value.clientID);
}
select(objectStates, x, y) {
@ -802,7 +819,9 @@
let minimumState = null;
for (const state of objectStates) {
checkObjectType('object state', state, null, ObjectState);
if (state.outside || state.hidden) continue;
if (state.outside || state.hidden || state.objectType === ObjectType.TAG) {
continue;
}
const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') {
@ -810,9 +829,9 @@
'The object has not been saved yet. Call annotations.put([state]) before',
);
}
const distance = object.constructor.distance(state.points, x, y);
if (distance !== null && (minimumDistance === null || distance < minimumDistance)) {
if (distance !== null && (minimumDistance === null
|| distance < minimumDistance)) {
minimumDistance = distance;
minimumState = state;
}
@ -866,8 +885,10 @@
// In particular consider first and last frame as keyframes for all frames
const statesData = [].concat(
(frame in this.shapes ? this.shapes[frame] : [])
.filter((shape) => !shape.removed)
.map((shape) => shape.get(frame)),
(frame in this.tags ? this.tags[frame] : [])
.filter((tag) => !tag.removed)
.map((tag) => tag.get(frame)),
);
const tracks = Object.values(this.tracks)
@ -875,7 +896,7 @@
frame in track.shapes
|| frame === frameFrom
|| frame === frameTo
));
)).filter((track) => !track.removed);
statesData.push(
...tracks.map((track) => track.get(frame))
.filter((state) => !state.outside),

@ -8,14 +8,17 @@
*/
const jsonpath = require('jsonpath');
const { AttributeType } = require('./enums');
const {
AttributeType,
ObjectType,
} = require('./enums');
const { ArgumentError } = require('./exceptions');
class AnnotationsFilter {
constructor() {
// eslint-disable-next-line security/detect-unsafe-regex
this.operatorRegex = /(==|!=|<=|>=|>|<|~=)(?=(?:[^"]*(["])[^"]*\2)*[^"]*$)/g;
this.operatorRegex = /(==|!=|<=|>=|>|<)(?=(?:[^"]*(["])[^"]*\2)*[^"]*$)/g;
}
// Method splits expression by operators that are outside of any brackets
@ -165,18 +168,21 @@ class AnnotationsFilter {
let xbr = Number.MIN_SAFE_INTEGER;
let ytl = Number.MAX_SAFE_INTEGER;
let ybr = Number.MIN_SAFE_INTEGER;
let [width, height] = [null, null];
if (state.objectType !== ObjectType.TAG) {
state.points.forEach((coord, idx) => {
if (idx % 2) { // y
ytl = Math.min(ytl, coord);
ybr = Math.max(ybr, coord);
} else { // x
xtl = Math.min(xtl, coord);
xbr = Math.max(xbr, coord);
}
});
[width, height] = [xbr - xtl, ybr - ytl];
}
state.points.forEach((coord, idx) => {
if (idx % 2) { // y
ytl = Math.min(ytl, coord);
ybr = Math.max(ybr, coord);
} else { // x
xtl = Math.min(xtl, coord);
xbr = Math.max(xbr, coord);
}
});
const [width, height] = [xbr - xtl, ybr - ytl];
const attributes = {};
Object.keys(state.attributes).reduce((acc, key) => {
const attr = labelAttributes[key];
@ -198,7 +204,7 @@ class AnnotationsFilter {
serverID: state.serverID,
clientID: state.clientID,
type: state.objectType,
shape: state.objectShape,
shape: state.shapeType,
occluded: state.occluded,
};
});

@ -12,17 +12,18 @@ class AnnotationHistory {
get() {
return {
undo: this._undo.map((undo) => undo.action),
redo: this._redo.map((redo) => redo.action),
undo: this._undo.map((undo) => [undo.action, undo.frame]),
redo: this._redo.map((redo) => [redo.action, redo.frame]),
};
}
do(action, undo, redo, clientIDs) {
do(action, undo, redo, clientIDs, frame) {
const actionItem = {
clientIDs,
action,
undo,
redo,
frame,
};
this._undo = this._undo.slice(-MAX_HISTORY_LENGTH + 1);

@ -69,6 +69,12 @@
`Points must have at least 1 points, but got ${points.length / 2}`,
);
}
} else if (shapeType === ObjectShape.CUBOID) {
if (points.length / 2 !== 8) {
throw new DataError(
`Points must have exact 8 points, but got ${points.length / 2}`,
);
}
} else {
throw new ArgumentError(
`Unknown value of shapeType has been recieved ${shapeType}`,
@ -109,6 +115,35 @@
return area >= MIN_SHAPE_AREA;
}
function fitPoints(shapeType, points, maxX, maxY) {
const fittedPoints = [];
for (let i = 0; i < points.length - 1; i += 2) {
const x = points[i];
const y = points[i + 1];
checkObjectType('coordinate', x, 'number', null);
checkObjectType('coordinate', y, 'number', null);
fittedPoints.push(
Math.clamp(x, 0, maxX),
Math.clamp(y, 0, maxY),
);
}
return shapeType === ObjectShape.CUBOID ? points : fittedPoints;
}
function checkOutside(points, width, height) {
let inside = false;
for (let i = 0; i < points.length - 1; i += 2) {
const [x, y] = points.slice(i);
inside = inside || (x >= 0 && x <= width && y >= 0 && y <= height);
}
return !inside;
}
function validateAttributeValue(value, attr) {
const { values } = attr;
const type = attr.inputType;
@ -178,46 +213,52 @@
injection.groups.max = Math.max(injection.groups.max, this.group);
}
_saveLock(lock) {
_saveLock(lock, frame) {
const undoLock = this.lock;
const redoLock = lock;
this.history.do(HistoryActions.CHANGED_LOCK, () => {
this.lock = undoLock;
this.updated = Date.now();
}, () => {
this.lock = redoLock;
}, [this.clientID]);
this.updated = Date.now();
}, [this.clientID], frame);
this.lock = lock;
}
_saveColor(color) {
_saveColor(color, frame) {
const undoColor = this.color;
const redoColor = color;
this.history.do(HistoryActions.CHANGED_COLOR, () => {
this.color = undoColor;
this.updated = Date.now();
}, () => {
this.color = redoColor;
}, [this.clientID]);
this.updated = Date.now();
}, [this.clientID], frame);
this.color = color;
}
_saveHidden(hidden) {
_saveHidden(hidden, frame) {
const undoHidden = this.hidden;
const redoHidden = hidden;
this.history.do(HistoryActions.CHANGED_HIDDEN, () => {
this.hidden = undoHidden;
this.updated = Date.now();
}, () => {
this.hidden = redoHidden;
}, [this.clientID]);
this.updated = Date.now();
}, [this.clientID], frame);
this.hidden = hidden;
}
_saveLabel(label) {
_saveLabel(label, frame) {
const undoLabel = this.label;
const redoLabel = label;
const undoAttributes = { ...this.attributes };
@ -229,13 +270,15 @@
this.history.do(HistoryActions.CHANGED_LABEL, () => {
this.label = undoLabel;
this.attributes = undoAttributes;
this.updated = Date.now();
}, () => {
this.label = redoLabel;
this.attributes = redoAttributes;
}, [this.clientID]);
this.updated = Date.now();
}, [this.clientID], frame);
}
_saveAttributes(attributes) {
_saveAttributes(attributes, frame) {
const undoAttributes = { ...this.attributes };
for (const attrID of Object.keys(attributes)) {
@ -246,9 +289,11 @@
this.history.do(HistoryActions.CHANGED_ATTRIBUTES, () => {
this.attributes = undoAttributes;
this.updated = Date.now();
}, () => {
this.attributes = redoAttributes;
}, [this.clientID]);
this.updated = Date.now();
}, [this.clientID], frame);
}
_validateStateBeforeSave(frame, data, updated) {
@ -286,20 +331,9 @@
checkNumberOfPoints(this.shapeType, data.points);
// cut points
const { width, height } = this.frameMeta[frame];
for (let i = 0; i < data.points.length - 1; i += 2) {
const x = data.points[i];
const y = data.points[i + 1];
checkObjectType('coordinate', x, 'number', null);
checkObjectType('coordinate', y, 'number', null);
fittedPoints = fitPoints(this.shapeType, data.points, width, height);
fittedPoints.push(
Math.clamp(x, 0, width),
Math.clamp(y, 0, height),
);
}
if (!checkShapeArea(this.shapeType, fittedPoints)) {
if ((!checkShapeArea(this.shapeType, fittedPoints)) || checkOutside(fittedPoints, width, height)) {
fittedPoints = [];
}
}
@ -361,22 +395,25 @@
updateTimestamp(updated) {
const anyChanges = updated.label || updated.attributes || updated.points
|| updated.outside || updated.occluded || updated.keyframe
|| updated.zOrder;
|| updated.zOrder || updated.hidden || updated.lock || updated.pinned;
if (anyChanges) {
this.updated = Date.now();
}
}
delete(force) {
delete(frame, force) {
if (!this.lock || force) {
this.removed = true;
this.history.do(HistoryActions.REMOVED_OBJECT, () => {
this.serverID = undefined;
this.removed = false;
this.updated = Date.now();
}, () => {
this.removed = true;
}, [this.clientID]);
this.updated = Date.now();
}, [this.clientID], frame);
}
return this.removed;
@ -392,15 +429,17 @@
this.shapeType = null;
}
_savePinned(pinned) {
_savePinned(pinned, frame) {
const undoPinned = this.pinned;
const redoPinned = pinned;
this.history.do(HistoryActions.CHANGED_PINNED, () => {
this.pinned = undoPinned;
this.updated = Date.now();
}, () => {
this.pinned = redoPinned;
}, [this.clientID]);
this.updated = Date.now();
}, [this.clientID], frame);
this.pinned = pinned;
}
@ -483,41 +522,47 @@
};
}
_savePoints(points) {
_savePoints(points, frame) {
const undoPoints = this.points;
const redoPoints = points;
this.history.do(HistoryActions.CHANGED_POINTS, () => {
this.points = undoPoints;
this.updated = Date.now();
}, () => {
this.points = redoPoints;
}, [this.clientID]);
this.updated = Date.now();
}, [this.clientID], frame);
this.points = points;
}
_saveOccluded(occluded) {
_saveOccluded(occluded, frame) {
const undoOccluded = this.occluded;
const redoOccluded = occluded;
this.history.do(HistoryActions.CHANGED_OCCLUDED, () => {
this.occluded = undoOccluded;
this.updated = Date.now();
}, () => {
this.occluded = redoOccluded;
}, [this.clientID]);
this.updated = Date.now();
}, [this.clientID], frame);
this.occluded = occluded;
}
_saveZOrder(zOrder) {
_saveZOrder(zOrder, frame) {
const undoZOrder = this.zOrder;
const redoZOrder = zOrder;
this.history.do(HistoryActions.CHANGED_ZORDER, () => {
this.zOrder = undoZOrder;
this.updated = Date.now();
}, () => {
this.zOrder = redoZOrder;
}, [this.clientID]);
this.updated = Date.now();
}, [this.clientID], frame);
this.zOrder = zOrder;
}
@ -538,39 +583,39 @@
// Now when all fields are validated, we can apply them
if (updated.label) {
this._saveLabel(data.label);
this._saveLabel(data.label, frame);
}
if (updated.attributes) {
this._saveAttributes(data.attributes);
this._saveAttributes(data.attributes, frame);
}
if (updated.points && fittedPoints.length) {
this._savePoints(fittedPoints);
this._savePoints(fittedPoints, frame);
}
if (updated.occluded) {
this._saveOccluded(data.occluded);
this._saveOccluded(data.occluded, frame);
}
if (updated.zOrder) {
this._saveZOrder(data.zOrder);
this._saveZOrder(data.zOrder, frame);
}
if (updated.lock) {
this._saveLock(data.lock);
this._saveLock(data.lock, frame);
}
if (updated.pinned) {
this._savePinned(data.pinned);
this._savePinned(data.pinned, frame);
}
if (updated.color) {
this._saveColor(data.color);
this._saveColor(data.color, frame);
}
if (updated.hidden) {
this._saveHidden(data.hidden);
this._saveHidden(data.hidden, frame);
}
this.updateTimestamp(updated);
@ -745,7 +790,7 @@
return result;
}
_saveLabel(label) {
_saveLabel(label, frame) {
const undoLabel = this.label;
const redoLabel = label;
const undoAttributes = {
@ -777,16 +822,18 @@
for (const mutable of undoAttributes.mutable) {
this.shapes[mutable.frame].attributes = mutable.attributes;
}
this.updated = Date.now();
}, () => {
this.label = redoLabel;
this.attributes = redoAttributes.unmutable;
for (const mutable of redoAttributes.mutable) {
this.shapes[mutable.frame].attributes = mutable.attributes;
}
}, [this.clientID]);
this.updated = Date.now();
}, [this.clientID], frame);
}
_saveAttributes(frame, attributes) {
_saveAttributes(attributes, frame) {
const current = this.get(frame);
const labelAttributes = this.label.attributes
.reduce((accumulator, value) => {
@ -853,12 +900,14 @@
} else if (redoShape) {
delete this.shapes[frame];
}
this.updated = Date.now();
}, () => {
this.attributes = redoAttributes;
if (redoShape) {
this.shapes[frame] = redoShape;
}
}, [this.clientID]);
this.updated = Date.now();
}, [this.clientID], frame);
}
_appendShapeActionToHistory(actionType, frame, undoShape, redoShape) {
@ -868,16 +917,18 @@
} else {
this.shapes[frame] = undoShape;
}
this.updated = Date.now();
}, () => {
if (!redoShape) {
delete this.shapes[frame];
} else {
this.shapes[frame] = redoShape;
}
}, [this.clientID]);
this.updated = Date.now();
}, [this.clientID], frame);
}
_savePoints(frame, points) {
_savePoints(points, frame) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
@ -921,7 +972,7 @@
);
}
_saveOccluded(frame, occluded) {
_saveOccluded(occluded, frame) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
@ -943,7 +994,7 @@
);
}
_saveZOrder(frame, zOrder) {
_saveZOrder(zOrder, frame) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
@ -1007,27 +1058,27 @@
const fittedPoints = this._validateStateBeforeSave(frame, data, updated);
if (updated.label) {
this._saveLabel(data.label);
this._saveLabel(data.label, frame);
}
if (updated.lock) {
this._saveLock(data.lock);
this._saveLock(data.lock, frame);
}
if (updated.pinned) {
this._savePinned(data.pinned);
this._savePinned(data.pinned, frame);
}
if (updated.color) {
this._saveColor(data.color);
this._saveColor(data.color, frame);
}
if (updated.hidden) {
this._saveHidden(data.hidden);
this._saveHidden(data.hidden, frame);
}
if (updated.points && fittedPoints.length) {
this._savePoints(frame, fittedPoints);
this._savePoints(fittedPoints, frame);
}
if (updated.outside) {
@ -1035,15 +1086,15 @@
}
if (updated.occluded) {
this._saveOccluded(frame, data.occluded);
this._saveOccluded(data.occluded, frame);
}
if (updated.zOrder) {
this._saveZOrder(frame, data.zOrder);
this._saveZOrder(data.zOrder, frame);
}
if (updated.attributes) {
this._saveAttributes(frame, data.attributes);
this._saveAttributes(data.attributes, frame);
}
if (updated.keyframe) {
@ -1139,6 +1190,7 @@
attributes: { ...this.attributes },
label: this.label,
group: this.groupObject,
color: this.color,
updated: this.updated,
frame,
};
@ -1160,15 +1212,19 @@
// Now when all fields are validated, we can apply them
if (updated.label) {
this._saveLabel(data.label);
this._saveLabel(data.label, frame);
}
if (updated.attributes) {
this._saveAttributes(data.attributes);
this._saveAttributes(data.attributes, frame);
}
if (updated.lock) {
this._saveLock(data.lock);
this._saveLock(data.lock, frame);
}
if (updated.color) {
this._saveColor(data.color, frame);
}
this.updateTimestamp(updated);
@ -1346,6 +1402,127 @@
}
}
class CuboidShape extends Shape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.CUBOID;
this.pinned = false;
checkNumberOfPoints(this.shapeType, this.points);
}
static makeHull(geoPoints) {
// Returns the convex hull, assuming that each points[i] <= points[i + 1].
function makeHullPresorted(points) {
if (points.length <= 1) return points.slice();
// Andrew's monotone chain algorithm. Positive y coordinates correspond to 'up'
// as per the mathematical convention, instead of 'down' as per the computer
// graphics convention. This doesn't affect the correctness of the result.
const upperHull = [];
for (let i = 0; i < points.length; i += 1) {
const p = points[`${i}`];
while (upperHull.length >= 2) {
const q = upperHull[upperHull.length - 1];
const r = upperHull[upperHull.length - 2];
if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) upperHull.pop();
else break;
}
upperHull.push(p);
}
upperHull.pop();
const lowerHull = [];
for (let i = points.length - 1; i >= 0; i -= 1) {
const p = points[`${i}`];
while (lowerHull.length >= 2) {
const q = lowerHull[lowerHull.length - 1];
const r = lowerHull[lowerHull.length - 2];
if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) lowerHull.pop();
else break;
}
lowerHull.push(p);
}
lowerHull.pop();
if (upperHull.length
=== 1 && lowerHull.length
=== 1 && upperHull[0].x
=== lowerHull[0].x && upperHull[0].y
=== lowerHull[0].y) return upperHull;
return upperHull.concat(lowerHull);
}
function POINT_COMPARATOR(a, b) {
if (a.x < b.x) return -1;
if (a.x > b.x) return +1;
if (a.y < b.y) return -1;
if (a.y > b.y) return +1;
return 0;
}
const newPoints = geoPoints.slice();
newPoints.sort(POINT_COMPARATOR);
return makeHullPresorted(newPoints);
}
static contain(points, x, y) {
function isLeft(P0, P1, P2) {
return ((P1.x - P0.x) * (P2.y - P0.y) - (P2.x - P0.x) * (P1.y - P0.y));
}
points = CuboidShape.makeHull(points);
let wn = 0;
for (let i = 0; i < points.length; i += 1) {
const p1 = points[`${i}`];
const p2 = points[i + 1] || points[0];
if (p1.y <= y) {
if (p2.y > y) {
if (isLeft(p1, p2, { x, y }) > 0) {
wn += 1;
}
}
} else if (p2.y < y) {
if (isLeft(p1, p2, { x, y }) < 0) {
wn -= 1;
}
}
}
return wn !== 0;
}
static distance(actualPoints, x, y) {
const points = [];
for (let i = 0; i < 16; i += 2) {
points.push({ x: actualPoints[i], y: actualPoints[i + 1] });
}
if (!CuboidShape.contain(points, x, y)) return null;
let minDistance = Number.MAX_SAFE_INTEGER;
for (let i = 0; i < points.length; i += 1) {
const p1 = points[`${i}`];
const p2 = points[i + 1] || points[0];
// perpendicular from point to straight length
const distance = (Math.abs((p2.y - p1.y) * x
- (p2.x - p1.x) * y + p2.x * p1.y - p2.y * p1.x))
/ Math.sqrt(Math.pow(p2.y - p1.y, 2) + Math.pow(p2.x - p1.x, 2));
// check if perpendicular belongs to the straight segment
const a = Math.pow(p1.x - x, 2) + Math.pow(p1.y - y, 2);
const b = Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2);
const c = Math.pow(p2.x - x, 2) + Math.pow(p2.y - y, 2);
if (distance < minDistance && (a + b - c) >= 0 && (c + b - a) >= 0) {
minDistance = distance;
}
}
return minDistance;
}
}
class RectangleTrack extends Track {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
@ -1357,20 +1534,15 @@
}
interpolatePosition(leftPosition, rightPosition, offset) {
const positionOffset = [
rightPosition.points[0] - leftPosition.points[0],
rightPosition.points[1] - leftPosition.points[1],
rightPosition.points[2] - leftPosition.points[2],
rightPosition.points[3] - leftPosition.points[3],
];
return { // xtl, ytl, xbr, ybr
points: [
leftPosition.points[0] + positionOffset[0] * offset,
leftPosition.points[1] + positionOffset[1] * offset,
leftPosition.points[2] + positionOffset[2] * offset,
leftPosition.points[3] + positionOffset[3] * offset,
],
const positionOffset = leftPosition.points.map((point, index) => (
rightPosition.points[index] - point
))
return {
points: leftPosition.points.map((point ,index) => (
point + positionOffset[index] * offset
)),
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
@ -1795,20 +1967,50 @@
}
}
class CuboidTrack extends Track {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.CUBOID;
this.pinned = false;
for (const shape of Object.values(this.shapes)) {
checkNumberOfPoints(this.shapeType, shape.points);
}
}
interpolatePosition(leftPosition, rightPosition, offset) {
const positionOffset = leftPosition.points.map((point, index) => (
rightPosition.points[index] - point
))
return {
points: leftPosition.points.map((point ,index) => (
point + positionOffset[index] * offset
)),
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
}
}
RectangleTrack.distance = RectangleShape.distance;
PolygonTrack.distance = PolygonShape.distance;
PolylineTrack.distance = PolylineShape.distance;
PointsTrack.distance = PointsShape.distance;
CuboidTrack.distance = CuboidShape.distance;
module.exports = {
RectangleShape,
PolygonShape,
PolylineShape,
PointsShape,
CuboidShape,
RectangleTrack,
PolygonTrack,
PolylineTrack,
PointsTrack,
CuboidTrack,
Track,
Shape,
Tag,

@ -17,7 +17,7 @@
const {
Loader,
Dumper,
} = require('./annotation-format.js');
} = require('./annotation-formats.js');
const {
ScriptingError,
DataError,
@ -247,6 +247,32 @@
return result;
}
function importAnnotations(session, data) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.import(data);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function exportAnnotations(session) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.export();
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
async function exportDataset(session, format) {
if (!(format instanceof String || typeof format === 'string')) {
throw new ArgumentError(
@ -332,6 +358,8 @@
selectObject,
uploadAnnotations,
dumpAnnotations,
importAnnotations,
exportAnnotations,
exportDataset,
undoActions,
redoActions,

@ -26,7 +26,7 @@
} = require('./enums');
const User = require('./user');
const { AnnotationFormat } = require('./annotation-format.js');
const { AnnotationFormats } = require('./annotation-formats.js');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
@ -66,18 +66,18 @@
cvat.server.formats.implementation = async () => {
const result = await serverProxy.server.formats();
return result.map((el) => new AnnotationFormat(el));
return new AnnotationFormats(result);
};
cvat.server.datasetFormats.implementation = async () => {
const result = await serverProxy.server.datasetFormats();
cvat.server.userAgreements.implementation = async () => {
const result = await serverProxy.server.userAgreements();
return result;
};
cvat.server.register.implementation = async (username, firstName, lastName,
email, password1, password2) => {
email, password1, password2, userConfirmations) => {
await serverProxy.server.register(username, firstName, lastName, email,
password1, password2);
password1, password2, userConfirmations);
};
cvat.server.login.implementation = async (username, password) => {

@ -14,7 +14,8 @@
function build() {
const PluginRegistry = require('./plugins');
const User = require('./user');
const loggerStorage = require('./logger-storage');
const Log = require('./log');
const ObjectState = require('./object-state');
const Statistics = require('./statistics');
const { Job, Task } = require('./session');
@ -41,6 +42,7 @@ function build() {
ServerError,
} = require('./exceptions');
const User = require('./user');
const pjson = require('../package.json');
const config = require('./config');
@ -107,7 +109,7 @@ function build() {
* @method formats
* @async
* @memberof module:API.cvat.server
* @returns {module:API.cvat.classes.AnnotationFormat[]}
* @returns {module:API.cvat.classes.AnnotationFormats}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
@ -117,20 +119,21 @@ function build() {
return result;
},
/**
* Method returns available dataset export formats
* @method exportFormats
* Method returns user agreements that the user must accept
* @method userAgreements
* @async
* @memberof module:API.cvat.server
* @returns {module:String[]}
* @returns {Object[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async datasetFormats() {
const result = await PluginRegistry
.apiWrapper(cvat.server.datasetFormats);
return result;
async userAgreements() {
const result = await PluginRegistry
.apiWrapper(cvat.server.userAgreements);
return result;
},
/**
* Method allows to register on a server
* @method register
* @async
@ -141,13 +144,14 @@ function build() {
* @param {string} email A email address for the new account
* @param {string} password1 A password for the new account
* @param {string} password2 The confirmation password for the new account
* @param {Object} userConfirmations An user confirmations of terms of use if needed
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async register(username, firstName, lastName, email, password1, password2) {
async register(username, firstName, lastName, email, password1, password2, userConfirmations) {
const result = await PluginRegistry
.apiWrapper(cvat.server.register, username, firstName,
lastName, email, password1, password2);
lastName, email, password1, password2, userConfirmations);
return result;
},
/**
@ -419,6 +423,53 @@ function build() {
return result;
},
},
/**
* Namespace to working with logs
* @namespace logger
* @memberof module:API.cvat
*/
/**
* Method to logger configuration
* @method configure
* @memberof module:API.cvat.logger
* @param {function} isActiveChecker - callback to know if logger
* should increase working time or not
* @param {object} userActivityCallback - container for a callback <br>
* Logger put here a callback to update user activity timer <br>
* You can call it outside
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/**
* Append log to a log collection <br>
* Durable logs will have been added after "close" method is called for them <br>
* Ignore rules exist for some logs (e.g. zoomImage, changeAttribute) <br>
* Payload of ignored logs are shallowly combined to previous logs of the same type
* @method log
* @memberof module:API.cvat.logger
* @param {module:API.cvat.enums.LogType | string} type - log type
* @param {Object} [payload = {}] - any other data that will be appended to the log
* @param {boolean} [wait = false] - specifies if log is durable
* @returns {module:API.cvat.classes.Log}
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/**
* Save accumulated logs on a server
* @method save
* @memberof module:API.cvat.logger
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
* @instance
* @async
*/
logger: loggerStorage,
/**
* Namespace contains some changeable configurations
* @namespace config
@ -432,12 +483,6 @@ function build() {
* @property {string} proxy Axios proxy settings.
* For more details please read <a href="https://github.com/axios/axios"> here </a>
* @memberof module:API.cvat.config
* @property {integer} taskID this value is displayed in a logs if available
* @memberof module:API.cvat.config
* @property {integer} jobID this value is displayed in a logs if available
* @memberof module:API.cvat.config
* @property {integer} clientID read only auto-generated
* value which is displayed in a logs
* @memberof module:API.cvat.config
*/
get backendAPI() {
@ -452,21 +497,6 @@ function build() {
set proxy(value) {
config.proxy = value;
},
get taskID() {
return config.taskID;
},
set taskID(value) {
config.taskID = value;
},
get jobID() {
return config.jobID;
},
set jobID(value) {
config.jobID = value;
},
get clientID() {
return config.clientID;
},
},
/**
* Namespace contains some library information e.g. api version
@ -524,6 +554,7 @@ function build() {
Task,
User,
Job,
Log,
Attribute,
Label,
Statistics,

@ -6,7 +6,4 @@
module.exports = {
backendAPI: 'http://localhost:7000/api/v1',
proxy: false,
taskID: undefined,
jobID: undefined,
clientID: +Date.now().toString().substr(-6),
};

@ -0,0 +1,33 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
const Axios = require('axios');
Axios.defaults.withCredentials = true;
Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN';
Axios.defaults.xsrfCookieName = 'csrftoken';
onmessage = (e) => {
Axios.get(e.data.url, e.data.config)
.then((response) => {
postMessage({
responseData: response.data,
id: e.data.id,
isSuccess: true,
});
})
.catch((error) => {
postMessage({
id: e.data.id,
error,
isSuccess: false,
});
});
};

@ -93,6 +93,7 @@
* @property {string} POLYGON 'polygon'
* @property {string} POLYLINE 'polyline'
* @property {string} POINTS 'points'
* @property {string} CUBOID 'cuboid'
* @readonly
*/
const ObjectShape = Object.freeze({
@ -100,71 +101,80 @@
POLYGON: 'polygon',
POLYLINE: 'polyline',
POINTS: 'points',
CUBOID: 'cuboid',
});
/**
* Event types
* @enum {number}
* Logger event types
* @enum {string}
* @name LogType
* @memberof module:API.cvat.enums
* @property {number} pasteObject 0
* @property {number} changeAttribute 1
* @property {number} dragObject 2
* @property {number} deleteObject 3
* @property {number} pressShortcut 4
* @property {number} resizeObject 5
* @property {number} sendLogs 6
* @property {number} saveJob 7
* @property {number} jumpFrame 8
* @property {number} drawObject 9
* @property {number} changeLabel 10
* @property {number} sendTaskInfo 11
* @property {number} loadJob 12
* @property {number} moveImage 13
* @property {number} zoomImage 14
* @property {number} lockObject 15
* @property {number} mergeObjects 16
* @property {number} copyObject 17
* @property {number} propagateObject 18
* @property {number} undoAction 19
* @property {number} redoAction 20
* @property {number} sendUserActivity 21
* @property {number} sendException 22
* @property {number} changeFrame 23
* @property {number} debugInfo 24
* @property {number} fitImage 25
* @property {number} rotateImage 26
* @property {string} loadJob Load job
* @property {string} saveJob Save job
* @property {string} restoreJob Restore job
* @property {string} uploadAnnotations Upload annotations
* @property {string} sendUserActivity Send user activity
* @property {string} sendException Send exception
* @property {string} sendTaskInfo Send task info
* @property {string} drawObject Draw object
* @property {string} pasteObject Paste object
* @property {string} copyObject Copy object
* @property {string} propagateObject Propagate object
* @property {string} dragObject Drag object
* @property {string} resizeObject Resize object
* @property {string} deleteObject Delete object
* @property {string} lockObject Lock object
* @property {string} mergeObjects Merge objects
* @property {string} changeAttribute Change attribute
* @property {string} changeLabel Change label
* @property {string} changeFrame Change frame
* @property {string} moveImage Move image
* @property {string} zoomImage Zoom image
* @property {string} fitImage Fit image
* @property {string} rotateImage Rotate image
* @property {string} undoAction Undo action
* @property {string} redoAction Redo action
* @property {string} pressShortcut Press shortcut
* @property {string} debugInfo Debug info
* @readonly
*/
const LogType = {
pasteObject: 0,
changeAttribute: 1,
dragObject: 2,
deleteObject: 3,
pressShortcut: 4,
resizeObject: 5,
sendLogs: 6,
saveJob: 7,
jumpFrame: 8,
drawObject: 9,
changeLabel: 10,
sendTaskInfo: 11,
loadJob: 12,
moveImage: 13,
zoomImage: 14,
lockObject: 15,
mergeObjects: 16,
copyObject: 17,
propagateObject: 18,
undoAction: 19,
redoAction: 20,
sendUserActivity: 21,
sendException: 22,
changeFrame: 23,
debugInfo: 24,
fitImage: 25,
rotateImage: 26,
};
const LogType = Object.freeze({
loadJob: 'Load job',
saveJob: 'Save job',
restoreJob: 'Restore job',
uploadAnnotations: 'Upload annotations',
sendUserActivity: 'Send user activity',
sendException: 'Send exception',
sendTaskInfo: 'Send task info',
drawObject: 'Draw object',
pasteObject: 'Paste object',
copyObject: 'Copy object',
propagateObject: 'Propagate object',
dragObject: 'Drag object',
resizeObject: 'Resize object',
deleteObject: 'Delete object',
lockObject: 'Lock object',
mergeObjects: 'Merge objects',
changeAttribute: 'Change attribute',
changeLabel: 'Change label',
changeFrame: 'Change frame',
moveImage: 'Move image',
zoomImage: 'Zoom image',
fitImage: 'Fit image',
rotateImage: 'Rotate image',
undoAction: 'Undo action',
redoAction: 'Redo action',
pressShortcut: 'Press shortcut',
debugInfo: 'Debug info',
});
/**
* Types of actions with annotations
@ -208,7 +218,6 @@
/**
* Array of hex colors
* @type {module:API.cvat.classes.Loader[]} values
* @name colors
* @memberof module:API.cvat.enums
* @type {string[]}

@ -9,14 +9,14 @@
*/
(() => {
const cvatData = require('cvat-data');
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions');
const { isBrowser, isNode } = require('browser-or-node');
const { Exception, ArgumentError, DataError } = require('./exceptions');
// This is the frames storage
const frameDataCache = {};
const frameCache = {};
/**
* Class provides meta information about specific frame and frame itself
@ -24,8 +24,28 @@
* @hideconstructor
*/
class FrameData {
constructor(width, height, tid, number) {
constructor({
width,
height,
name,
taskID,
frameNumber,
startFrame,
stopFrame,
decodeForward,
}) {
Object.defineProperties(this, Object.freeze({
/**
* @name filename
* @type {string}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
filename: {
value: name,
writable: false,
},
/**
* @name width
* @type {integer}
@ -49,7 +69,7 @@
writable: false,
},
tid: {
value: tid,
value: taskID,
writable: false,
},
/**
@ -60,7 +80,19 @@
* @instance
*/
number: {
value: number,
value: frameNumber,
writable: false,
},
startFrame: {
value: startFrame,
writable: false,
},
stopFrame: {
value: stopFrame,
writable: false,
},
decodeForward: {
value: decodeForward,
writable: false,
},
}));
@ -86,42 +118,419 @@
}
FrameData.prototype.data.implementation = async function (onServerRequest) {
return new Promise(async (resolve, reject) => {
try {
if (this.number in frameCache[this.tid]) {
resolve(frameCache[this.tid][this.number]);
} else {
onServerRequest();
const frame = await serverProxy.frames.getData(this.tid, this.number);
if (isNode) {
frameCache[this.tid][this.number] = global.Buffer.from(frame, 'binary').toString('base64');
resolve(frameCache[this.tid][this.number]);
} else if (isBrowser) {
const reader = new FileReader();
reader.onload = () => {
const image = new Image(frame.width, frame.height);
image.onload = () => {
frameCache[this.tid][this.number] = image;
resolve(frameCache[this.tid][this.number]);
};
image.src = reader.result;
};
reader.readAsDataURL(frame);
return new Promise((resolve, reject) => {
const resolveWrapper = (data) => {
this._data = {
imageData: data,
renderWidth: this.width,
renderHeight: this.height,
};
return resolve(this._data);
};
if (this._data) {
resolve(this._data);
return;
}
const { provider } = frameDataCache[this.tid];
const { chunkSize } = frameDataCache[this.tid];
const start = parseInt(this.number / chunkSize, 10) * chunkSize;
const stop = Math.min(
this.stopFrame,
(parseInt(this.number / chunkSize, 10) + 1) * chunkSize - 1,
);
const chunkNumber = Math.floor(this.number / chunkSize);
const onDecodeAll = async (frameNumber) => {
if (frameDataCache[this.tid].activeChunkRequest
&& chunkNumber === frameDataCache[this.tid].activeChunkRequest.chunkNumber) {
const callbackArray = frameDataCache[this.tid].activeChunkRequest.callbacks;
for (let i = callbackArray.length - 1; i >= 0; --i) {
if (callbackArray[i].frameNumber === frameNumber) {
const callback = callbackArray[i];
callbackArray.splice(i, 1);
callback.resolve(await provider.frame(callback.frameNumber));
}
}
if (callbackArray.length === 0) {
frameDataCache[this.tid].activeChunkRequest = null;
}
}
};
const rejectRequestAll = () => {
if (frameDataCache[this.tid].activeChunkRequest
&& chunkNumber === frameDataCache[this.tid].activeChunkRequest.chunkNumber) {
for (const r of frameDataCache[this.tid].activeChunkRequest.callbacks) {
r.reject(r.frameNumber);
}
frameDataCache[this.tid].activeChunkRequest = null;
}
} catch (exception) {
reject(exception);
};
const makeActiveRequest = () => {
const taskDataCache = frameDataCache[this.tid];
const activeChunk = taskDataCache.activeChunkRequest;
activeChunk.request = serverProxy.frames.getData(this.tid,
activeChunk.chunkNumber).then((chunk) => {
frameDataCache[this.tid].activeChunkRequest.completed = true;
if (!taskDataCache.nextChunkRequest) {
provider.requestDecodeBlock(chunk,
taskDataCache.activeChunkRequest.start,
taskDataCache.activeChunkRequest.stop,
taskDataCache.activeChunkRequest.onDecodeAll,
taskDataCache.activeChunkRequest.rejectRequestAll);
}
}).catch((exception) => {
if (exception instanceof Exception) {
reject(exception);
} else {
reject(new Exception(exception.message));
}
}).finally(() => {
if (taskDataCache.nextChunkRequest) {
if (taskDataCache.activeChunkRequest) {
for (const r of taskDataCache.activeChunkRequest.callbacks) {
r.reject(r.frameNumber);
}
}
taskDataCache.activeChunkRequest = taskDataCache.nextChunkRequest;
taskDataCache.nextChunkRequest = null;
makeActiveRequest();
}
});
};
if (isNode) {
resolve('Dummy data');
} else if (isBrowser) {
provider.frame(this.number).then((frame) => {
if (frame === null) {
onServerRequest();
const activeRequest = frameDataCache[this.tid].activeChunkRequest;
if (!provider.isChunkCached(start, stop)) {
if (!activeRequest
|| (activeRequest
&& activeRequest.completed
&& activeRequest.chunkNumber !== chunkNumber)) {
if (activeRequest && activeRequest.rejectRequestAll) {
activeRequest.rejectRequestAll();
}
frameDataCache[this.tid].activeChunkRequest = {
request: null,
chunkNumber,
start,
stop,
onDecodeAll,
rejectRequestAll,
completed: false,
callbacks: [{
resolve: resolveWrapper,
reject,
frameNumber: this.number,
}],
};
makeActiveRequest();
} else if (activeRequest.chunkNumber === chunkNumber) {
if (!activeRequest.onDecodeAll
&& !activeRequest.rejectRequestAll) {
activeRequest.onDecodeAll = onDecodeAll;
activeRequest.rejectRequestAll = rejectRequestAll;
}
activeRequest.callbacks.push({
resolve: resolveWrapper,
reject,
frameNumber: this.number,
});
} else {
if (frameDataCache[this.tid].nextChunkRequest) {
const { callbacks } = frameDataCache[this.tid].nextChunkRequest;
for (const r of callbacks) {
r.reject(r.frameNumber);
}
}
frameDataCache[this.tid].nextChunkRequest = {
request: null,
chunkNumber,
start,
stop,
onDecodeAll,
rejectRequestAll,
completed: false,
callbacks: [{
resolve: resolveWrapper,
reject,
frameNumber: this.number,
}],
};
}
} else {
activeRequest.callbacks.push({
resolve: resolveWrapper,
reject,
frameNumber: this.number,
});
provider.requestDecodeBlock(null, start, stop,
onDecodeAll, rejectRequestAll);
}
} else {
if (this.number % chunkSize > chunkSize / 4
&& provider.decodedBlocksCacheSize > 1
&& this.decodeForward
&& !provider.isNextChunkExists(this.number)) {
const nextChunkNumber = Math.floor(this.number / chunkSize) + 1;
if (nextChunkNumber * chunkSize < this.stopFrame) {
provider.setReadyToLoading(nextChunkNumber);
const nextStart = nextChunkNumber * chunkSize;
const nextStop = (nextChunkNumber + 1) * chunkSize - 1;
if (!provider.isChunkCached(nextStart, nextStop)) {
if (!frameDataCache[this.tid].activeChunkRequest) {
frameDataCache[this.tid].activeChunkRequest = {
request: null,
chunkNumber: nextChunkNumber,
start: nextStart,
stop: nextStop,
onDecodeAll: null,
rejectRequestAll: null,
completed: false,
callbacks: [],
};
makeActiveRequest();
}
} else {
provider.requestDecodeBlock(null, nextStart, nextStop,
null, null);
}
}
}
resolveWrapper(frame);
}
}).catch((exception) => {
if (exception instanceof Exception) {
reject(exception);
} else {
reject(new Exception(exception.message));
}
});
}
});
};
function getFrameMeta(taskID, frame) {
const { meta, mode } = frameDataCache[taskID];
let size = null;
if (mode === 'interpolation') {
[size] = meta.frames;
} else if (mode === 'annotation') {
if (frame >= meta.size) {
throw new ArgumentError(
`Meta information about frame ${frame} can't be received from the server`,
);
} else {
size = meta.frames[frame];
}
} else {
throw new DataError(
`Invalid mode is specified ${mode}`,
);
}
return size;
}
class FrameBuffer {
constructor(size, chunkSize, stopFrame, taskID) {
this._size = size;
this._buffer = {};
this._requestedChunks = {};
this._chunkSize = chunkSize;
this._stopFrame = stopFrame;
this._activeFillBufferRequest = false;
this._taskID = taskID;
}
getFreeBufferSize() {
let requestedFrameCount = 0;
for (const chunk of Object.values(this._requestedChunks)) {
requestedFrameCount += chunk.requestedFrames.size;
}
return this._size - Object.keys(this._buffer).length - requestedFrameCount;
}
requestOneChunkFrames(chunkIdx) {
return new Promise((resolve, reject) => {
this._requestedChunks[chunkIdx] = {
...this._requestedChunks[chunkIdx],
resolve,
reject,
};
for (const frame of this._requestedChunks[chunkIdx].requestedFrames.entries()) {
const requestedFrame = frame[1];
const frameMeta = getFrameMeta(this._taskID, requestedFrame);
const frameData = new FrameData({
...frameMeta,
taskID: this._taskID,
frameNumber: requestedFrame,
startFrame: frameDataCache[this._taskID].startFrame,
stopFrame: frameDataCache[this._taskID].stopFrame,
decodeForward: false,
});
frameData.data().then(() => {
if (!(chunkIdx in this._requestedChunks)
|| !this._requestedChunks[chunkIdx].requestedFrames.has(requestedFrame)) {
reject(chunkIdx);
} else {
this._requestedChunks[chunkIdx].requestedFrames.delete(requestedFrame);
this._requestedChunks[chunkIdx].buffer[requestedFrame] = frameData;
if (this._requestedChunks[chunkIdx].requestedFrames.size === 0) {
const bufferedframes = Object.keys(
this._requestedChunks[chunkIdx].buffer,
).map((f) => +f);
this._requestedChunks[chunkIdx].resolve(new Set(bufferedframes));
}
}
}).catch(() => {
reject(chunkIdx);
});
}
});
}
fillBuffer(startFrame, frameStep = 1, count = null) {
const freeSize = this.getFreeBufferSize();
const requestedFrameCount = count ? count * frameStep : freeSize * frameStep;
const stopFrame = Math.min(startFrame + requestedFrameCount, this._stopFrame + 1);
for (let i = startFrame; i < stopFrame; i += frameStep) {
const chunkIdx = Math.floor(i / this._chunkSize);
if (!(chunkIdx in this._requestedChunks)) {
this._requestedChunks[chunkIdx] = {
requestedFrames: new Set(),
resolve: null,
reject: null,
buffer: {},
};
}
this._requestedChunks[chunkIdx].requestedFrames.add(i);
}
let bufferedFrames = new Set();
// Need to decode chunks in sequence
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
for (const chunkIdx in this._requestedChunks) {
if (Object.prototype.hasOwnProperty.call(this._requestedChunks, chunkIdx)) {
try {
const chunkFrames = await this.requestOneChunkFrames(chunkIdx);
if (chunkIdx in this._requestedChunks) {
bufferedFrames = new Set([...bufferedFrames, ...chunkFrames]);
this._buffer = {
...this._buffer,
...this._requestedChunks[chunkIdx].buffer,
};
delete this._requestedChunks[chunkIdx];
if (Object.keys(this._requestedChunks).length === 0) {
resolve(bufferedFrames);
}
} else {
reject(chunkIdx);
break;
}
} catch (error) {
reject(error);
break;
}
}
}
});
}
async makeFillRequest(start, step, count = null) {
if (!this._activeFillBufferRequest) {
this._activeFillBufferRequest = true;
try {
await this.fillBuffer(start, step, count);
this._activeFillBufferRequest = false;
} catch (error) {
if (typeof (error) === 'number' && error in this._requestedChunks) {
this._activeFillBufferRequest = false;
}
throw error;
}
}
}
async require(frameNumber, taskID, fillBuffer, frameStep) {
for (const frame in this._buffer) {
if (frame < frameNumber
|| frame >= frameNumber + this._size * frameStep) {
delete this._buffer[frame];
}
}
this._required = frameNumber;
const frameMeta = getFrameMeta(taskID, frameNumber);
let frame = new FrameData({
...frameMeta,
taskID,
frameNumber,
startFrame: frameDataCache[taskID].startFrame,
stopFrame: frameDataCache[taskID].stopFrame,
decodeForward: !fillBuffer,
});
if (frameNumber in this._buffer) {
frame = this._buffer[frameNumber];
delete this._buffer[frameNumber];
const cachedFrames = this.cachedFrames();
if (fillBuffer && !this._activeFillBufferRequest
&& this._size > this._chunkSize
&& cachedFrames.length < (this._size * 3) / 4) {
const maxFrame = cachedFrames ? Math.max(...cachedFrames) : frameNumber;
if (maxFrame < this._stopFrame) {
this.makeFillRequest(maxFrame + 1, frameStep).catch((e) => {
if (e !== 'not needed') {
throw e;
}
});
}
}
} else if (fillBuffer) {
this.clear();
await this.makeFillRequest(frameNumber, frameStep, fillBuffer ? null : 1);
frame = this._buffer[frameNumber];
} else {
this.clear();
}
return frame;
}
clear() {
for (const chunkIdx in this._requestedChunks) {
if (Object.prototype.hasOwnProperty.call(this._requestedChunks, chunkIdx)
&& this._requestedChunks[chunkIdx].reject) {
this._requestedChunks[chunkIdx].reject('not needed');
}
}
this._activeFillBufferRequest = false;
this._requestedChunks = {};
this._buffer = {};
}
cachedFrames() {
return Object.keys(this._buffer).map((f) => +f);
}
}
async function getPreview(taskID) {
return new Promise(async (resolve, reject) => {
try {
// Just go to server and get preview (no any cache)
const result = await serverProxy.frames.getPreview(taskID);
return new Promise((resolve, reject) => {
// Just go to server and get preview (no any cache)
serverProxy.frames.getPreview(taskID).then((result) => {
if (isNode) {
resolve(global.Buffer.from(result, 'binary').toString('base64'));
} else if (isBrowser) {
@ -131,48 +540,75 @@
};
reader.readAsDataURL(result);
}
} catch (error) {
}).catch((error) => {
reject(error);
}
});
});
}
async function getFrame(taskID, mode, frame) {
async function getFrame(taskID, chunkSize, chunkType, mode, frame,
startFrame, stopFrame, isPlaying, step) {
if (!(taskID in frameDataCache)) {
const blockType = chunkType === 'video' ? cvatData.BlockType.MP4VIDEO
: cvatData.BlockType.ARCHIVE;
const meta = await serverProxy.frames.getMeta(taskID);
const mean = meta.frames.reduce((a, b) => a + b.width * b.height, 0)
/ meta.frames.length;
const stdDev = Math.sqrt(meta.frames.map(
(x) => Math.pow(x.width * x.height - mean, 2),
).reduce((a, b) => a + b) / meta.frames.length);
// limit of decoded frames cache by 2GB
const decodedBlocksCacheSize = Math.floor(2147483648 / (mean + stdDev) / 4 / chunkSize)
|| 1;
frameDataCache[taskID] = {
meta: await serverProxy.frames.getMeta(taskID),
meta,
chunkSize,
mode,
startFrame,
stopFrame,
provider: new cvatData.FrameProvider(
blockType, chunkSize, Math.max(decodedBlocksCacheSize, 9),
decodedBlocksCacheSize, 1,
),
frameBuffer: new FrameBuffer(
Math.min(180, decodedBlocksCacheSize * chunkSize),
chunkSize,
stopFrame,
taskID,
),
decodedBlocksCacheSize,
activeChunkRequest: null,
nextChunkRequest: null,
};
frameCache[taskID] = {};
const frameMeta = getFrameMeta(taskID, frame);
// actual only for video chunks
frameDataCache[taskID].provider.setRenderSize(frameMeta.width, frameMeta.height);
}
if (!(frame in frameDataCache[taskID])) {
let size = null;
if (mode === 'interpolation') {
[size] = frameDataCache[taskID].meta;
} else if (mode === 'annotation') {
if (frame >= frameDataCache[taskID].meta.length) {
throw new ArgumentError(
`Meta information about frame ${frame} can't be received from the server`,
);
} else {
size = frameDataCache[taskID].meta[frame];
}
} else {
throw new ArgumentError(
`Invalid mode is specified ${mode}`,
);
}
return frameDataCache[taskID].frameBuffer.require(frame, taskID, isPlaying, step);
}
frameDataCache[taskID][frame] = new FrameData(size.width, size.height, taskID, frame);
function getRanges(taskID) {
if (!(taskID in frameDataCache)) {
return {
decoded: [],
buffered: [],
};
}
return frameDataCache[taskID][frame];
return {
decoded: frameDataCache[taskID].provider.cachedFrames,
buffered: frameDataCache[taskID].frameBuffer.cachedFrames(),
};
}
module.exports = {
FrameData,
getFrame,
getRanges,
getPreview,
};
})();

@ -0,0 +1,247 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/* global
require:false
*/
const { detect } = require('detect-browser');
const PluginRegistry = require('./plugins');
const { ArgumentError } = require('./exceptions');
const { LogType } = require('./enums');
/**
* Class representing a single log
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Log {
constructor(logType, payload) {
this.onCloseCallback = null;
this.type = logType;
this.payload = { ...payload };
this.time = new Date();
}
onClose(callback) {
this.onCloseCallback = callback;
}
validatePayload() {
if (typeof (this.payload) !== 'object') {
throw new ArgumentError('Payload must be an object');
}
try {
JSON.stringify(this.payload);
} catch (error) {
const message = `Log payload must be JSON serializable. ${error.toString()}`;
throw new ArgumentError(message);
}
}
dump() {
const payload = { ...this.payload };
const body = {
name: this.type,
time: this.time.toISOString(),
};
for (const field of ['client_id', 'job_id', 'task_id', 'is_active']) {
if (field in payload) {
body[field] = payload[field];
delete payload[field];
}
}
return {
...body,
payload,
};
}
/**
* Method saves a durable log in a storage <br>
* Note then you can call close() multiple times <br>
* Log duration will be computed based on the latest call <br>
* All payloads will be shallowly combined (all top level properties will exist)
* @method close
* @memberof module:API.cvat.classes.Log
* @param {object} [payload] part of payload can be added when close a log
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async close(payload = {}) {
const result = await PluginRegistry
.apiWrapper.call(this, Log.prototype.close, payload);
return result;
}
}
Log.prototype.close.implementation = function (payload) {
this.payload.duration = Date.now() - this.time.getTime();
this.payload = { ...this.payload, ...payload };
if (this.onCloseCallback) {
this.onCloseCallback();
}
};
class LogWithCount extends Log {
validatePayload() {
Log.prototype.validatePayload.call(this);
if (!Number.isInteger(this.payload.count) || this.payload.count < 1) {
const message = `The field "count" is required for "${this.type}" log`
+ 'It must be a positive integer';
throw new ArgumentError(message);
}
}
}
class LogWithObjectsInfo extends Log {
validatePayload() {
const generateError = (name, range) => {
const message = `The field "${name}" is required for "${this.type}" log. ${range}`;
throw new ArgumentError(message);
};
if (!Number.isInteger(this.payload['track count']) || this.payload['track count'] < 0) {
generateError('track count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['tag count']) || this.payload['tag count'] < 0) {
generateError('tag count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['object count']) || this.payload['object count'] < 0) {
generateError('object count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['frame count']) || this.payload['frame count'] < 1) {
generateError('frame count', 'It must be an integer not less than 1');
}
if (!Number.isInteger(this.payload['box count']) || this.payload['box count'] < 0) {
generateError('box count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['polygon count']) || this.payload['polygon count'] < 0) {
generateError('polygon count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['polyline count']) || this.payload['polyline count'] < 0) {
generateError('polyline count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['points count']) || this.payload['points count'] < 0) {
generateError('points count', 'It must be an integer not less than 0');
}
}
}
class LogWithWorkingTime extends Log {
validatePayload() {
Log.prototype.validatePayload.call(this);
if (!('working_time' in this.payload)
|| !typeof (this.payload.working_time) === 'number'
|| this.payload.working_time < 0
) {
const message = `The field "working_time" is required for ${this.type} log. `
+ 'It must be a number not less than 0';
throw new ArgumentError(message);
}
}
}
class LogWithExceptionInfo extends Log {
validatePayload() {
Log.prototype.validatePayload.call(this);
if (typeof (this.payload.message) !== 'string') {
const message = `The field "message" is required for ${this.type} log. `
+ 'It must be a string';
throw new ArgumentError(message);
}
if (typeof (this.payload.filename) !== 'string') {
const message = `The field "filename" is required for ${this.type} log. `
+ 'It must be a string';
throw new ArgumentError(message);
}
if (typeof (this.payload.line) !== 'number') {
const message = `The field "line" is required for ${this.type} log. `
+ 'It must be a number';
throw new ArgumentError(message);
}
if (typeof (this.payload.column) !== 'number') {
const message = `The field "column" is required for ${this.type} log. `
+ 'It must be a number';
throw new ArgumentError(message);
}
if (typeof (this.payload.stack) !== 'string') {
const message = `The field "stack" is required for ${this.type} log. `
+ 'It must be a string';
throw new ArgumentError(message);
}
}
dump() {
let body = super.dump();
const payload = body.payload;
const client = detect();
body = {
...body,
message: payload.message,
filename: payload.filename,
line: payload.line,
column: payload.column,
stack: payload.stack,
system: client.os,
client: client.name,
version: client.version,
};
delete payload.message;
delete payload.filename;
delete payload.line;
delete payload.column;
delete payload.stack;
return body;
}
}
function logFactory(logType, payload) {
const logsWithCount = [
LogType.deleteObject, LogType.mergeObjects, LogType.copyObject,
LogType.undoAction, LogType.redoAction,
];
if (logsWithCount.includes(logType)) {
return new LogWithCount(logType, payload);
}
if ([LogType.sendTaskInfo, LogType.loadJob, LogType.uploadAnnotations].includes(logType)) {
return new LogWithObjectsInfo(logType, payload);
}
if (logType === LogType.sendUserActivity) {
return new LogWithWorkingTime(logType, payload);
}
if (logType === LogType.sendException) {
return new LogWithExceptionInfo(logType, payload);
}
return new Log(logType, payload);
}
module.exports = logFactory;

@ -0,0 +1,177 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/* global
require:false
*/
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const logFactory = require('./log');
const { ArgumentError } = require('./exceptions');
const { LogType } = require('./enums');
const WORKING_TIME_THRESHOLD = 100000; // ms, 1.66 min
class LoggerStorage {
constructor() {
this.clientID = Date.now().toString().substr(-6);
this.lastLogTime = Date.now();
this.workingTime = 0;
this.collection = [];
this.ignoreRules = {}; // by event
this.isActiveChecker = null;
this.ignoreRules[LogType.zoomImage] = {
lastLog: null,
timeThreshold: 1000,
ignore(previousLog) {
return Date.now() - previousLog.time < this.timeThreshold;
},
};
this.ignoreRules[LogType.changeAttribute] = {
lastLog: null,
ignore(previousLog, currentPayload) {
return currentPayload.object_id === previousLog.payload.object_id
&& currentPayload.id === previousLog.payload.id;
},
};
}
updateWorkingTime() {
if (!this.isActiveChecker || this.isActiveChecker()) {
const lastLogTime = Date.now();
const diff = lastLogTime - this.lastLogTime;
this.workingTime += diff < WORKING_TIME_THRESHOLD ? diff : 0;
this.lastLogTime = lastLogTime;
}
}
async configure(isActiveChecker, activityHelper) {
const result = await PluginRegistry
.apiWrapper.call(
this, LoggerStorage.prototype.configure,
isActiveChecker, activityHelper,
);
return result;
}
async log(logType, payload = {}, wait = false) {
const result = await PluginRegistry
.apiWrapper.call(this, LoggerStorage.prototype.log, logType, payload, wait);
return result;
}
async save() {
const result = await PluginRegistry
.apiWrapper.call(this, LoggerStorage.prototype.save);
return result;
}
}
LoggerStorage.prototype.configure.implementation = function (
isActiveChecker,
userActivityCallback,
) {
if (typeof (isActiveChecker) !== 'function') {
throw new ArgumentError('isActiveChecker argument must be callable');
}
if (!Array.isArray(userActivityCallback)) {
throw new ArgumentError('userActivityCallback argument must be an array');
}
this.isActiveChecker = () => !!isActiveChecker();
userActivityCallback.push(this.updateWorkingTime.bind(this));
};
LoggerStorage.prototype.log.implementation = function (logType, payload, wait) {
if (typeof (payload) !== 'object') {
throw new ArgumentError('Payload must be an object');
}
if (typeof (wait) !== 'boolean') {
throw new ArgumentError('Payload must be an object');
}
if (logType in this.ignoreRules) {
const ignoreRule = this.ignoreRules[logType];
const { lastLog } = ignoreRule;
if (lastLog && ignoreRule.ignore(lastLog, payload)) {
lastLog.payload = {
...lastLog.payload,
...payload,
};
this.updateWorkingTime();
return ignoreRule.lastLog;
}
}
const logPayload = { ...payload };
logPayload.client_id = this.clientID;
if (this.isActiveChecker) {
logPayload.is_active = this.isActiveChecker();
}
const log = logFactory(logType, { ...logPayload });
if (logType in this.ignoreRules) {
this.ignoreRules[logType].lastLog = log;
}
const pushEvent = () => {
this.updateWorkingTime();
log.validatePayload();
log.onClose(null);
this.collection.push(log);
};
if (log.type === LogType.sendException) {
serverProxy.server.exception(log.dump()).catch(() => {
pushEvent();
});
return log;
}
if (wait) {
log.onClose(pushEvent);
} else {
pushEvent();
}
return log;
};
LoggerStorage.prototype.save.implementation = async function () {
const collectionToSend = [...this.collection];
const lastLog = this.collection[this.collection.length - 1];
const logPayload = {};
logPayload.client_id = this.clientID;
logPayload.working_time = this.workingTime;
if (this.isActiveChecker) {
logPayload.is_active = this.isActiveChecker();
}
if (lastLog && lastLog.type === LogType.sendTaskInfo) {
logPayload.job_id = lastLog.payload.job_id;
logPayload.task_id = lastLog.payload.task_id;
}
const userActivityLog = logFactory(LogType.sendUserActivity, logPayload);
collectionToSend.push(userActivityLog);
await serverProxy.logs.save(collectionToSend.map((log) => log.dump()));
for (const rule of Object.values(this.ignoreRules)) {
rule.lastLog = null;
}
this.collection = [];
this.workingTime = 0;
this.lastLogTime = Date.now();
};
module.exports = new LoggerStorage();

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

@ -398,14 +398,16 @@
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @param {integer} frame current frame number
* @param {boolean} [force=false] delete object even if it is locked
* @async
* @returns {boolean} true if object has been deleted
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async delete(force = false) {
async delete(frame, force = false) {
const result = await PluginRegistry
.apiWrapper.call(this, ObjectState.prototype.delete, force);
.apiWrapper.call(this, ObjectState.prototype.delete, frame, force);
return result;
}
}
@ -420,9 +422,13 @@
};
// Delete element from a collection which contains it
ObjectState.prototype.delete.implementation = async function (force) {
ObjectState.prototype.delete.implementation = async function (frame, force) {
if (this.__internal && this.__internal.delete) {
return this.__internal.delete(force);
if (!Number.isInteger(+frame) || +frame < 0) {
throw new ArgumentError('Frame argument must be a non negative integer');
}
return this.__internal.delete(frame, force);
}
return false;

@ -14,6 +14,7 @@
} = require('./exceptions');
const store = require('store');
const config = require('./config');
const DownloadWorker = require('./download.worker');
function generateError(errorData) {
if (errorData.response) {
@ -26,12 +27,66 @@
return new ServerError(message, 0);
}
class WorkerWrappedAxios {
constructor() {
const worker = new DownloadWorker();
const requests = {};
let requestId = 0;
worker.onmessage = (e) => {
if (e.data.id in requests) {
if (e.data.isSuccess) {
requests[e.data.id].resolve(e.data.responseData);
} else {
requests[e.data.id].reject(e.data.error);
}
delete requests[e.data.id];
}
};
worker.onerror = (e) => {
if (e.data.id in requests) {
requests[e.data.id].reject(e);
delete requests[e.data.id];
}
};
function getRequestId() {
return requestId++;
}
async function get(url, requestConfig) {
return new Promise((resolve, reject) => {
const newRequestId = getRequestId();
requests[newRequestId] = {
resolve,
reject,
};
worker.postMessage({
url,
config: requestConfig,
id: newRequestId,
});
});
}
Object.defineProperties(this, Object.freeze({
get: {
value: get,
writable: false,
},
}));
}
}
class ServerProxy {
constructor() {
const Axios = require('axios');
Axios.defaults.withCredentials = true;
Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN';
Axios.defaults.xsrfCookieName = 'csrftoken';
const workerAxios = new WorkerWrappedAxios();
let token = store.get('token');
if (token) {
@ -99,23 +154,23 @@
return response.data;
}
async function datasetFormats() {
const { backendAPI } = config;
async function userAgreements() {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/server/dataset/formats`, {
response = await Axios.get(`${backendAPI}/restrictions/user-agreements`, {
proxy: config.proxy,
});
response = JSON.parse(response.data);
} catch (errorData) {
throw generateError(errorData);
}
return response;
return response.data;
}
async function register(username, firstName, lastName, email, password1, password2) {
async function register(username, firstName, lastName, email, password1, password2, confirmations) {
let response = null;
try {
const data = JSON.stringify({
@ -125,6 +180,7 @@
email,
password1,
password2,
confirmations,
});
response = await Axios.post(`${config.backendAPI}/auth/register`, data, {
proxy: config.proxy,
@ -275,7 +331,7 @@
});
}
async function createTask(taskData, files, onUpdate) {
async function createTask(taskSpec, taskDataSpec, onUpdate) {
const { backendAPI } = config;
async function wait(id) {
@ -315,12 +371,14 @@
});
}
const batchOfFiles = new FormData();
for (const key in files) {
if (Object.prototype.hasOwnProperty.call(files, key)) {
for (let i = 0; i < files[key].length; i++) {
batchOfFiles.append(`${key}[${i}]`, files[key][i]);
}
const taskData = new FormData();
for (const [key, value] of Object.entries(taskDataSpec)) {
if (Array.isArray(value)) {
value.forEach((element, idx) => {
taskData.append(`${key}[${idx}]`, element);
});
} else {
taskData.set(key, value);
}
}
@ -328,7 +386,7 @@
onUpdate('The task is being created on the server..');
try {
response = await Axios.post(`${backendAPI}/tasks`, JSON.stringify(taskData), {
response = await Axios.post(`${backendAPI}/tasks`, JSON.stringify(taskSpec), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
@ -340,7 +398,7 @@
onUpdate('The data is being uploaded to the server..');
try {
await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, batchOfFiles, {
await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, taskData, {
proxy: config.proxy,
});
} catch (errorData) {
@ -435,8 +493,7 @@
let response = null;
try {
// TODO: change 0 frame to preview
response = await Axios.get(`${backendAPI}/tasks/${tid}/frames/0`, {
response = await Axios.get(`${backendAPI}/tasks/${tid}/data?type=preview`, {
proxy: config.proxy,
responseType: 'blob',
});
@ -451,20 +508,23 @@
return response.data;
}
async function getData(tid, frame) {
async function getData(tid, chunk) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/tasks/${tid}/frames/${frame}`, {
proxy: config.proxy,
responseType: 'blob',
});
response = await workerAxios.get(
`${backendAPI}/tasks/${tid}/data?type=chunk&number=${chunk}&quality=compressed`,
{
proxy: config.proxy,
responseType: 'arraybuffer',
},
);
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
return response;
}
async function getMeta(tid) {
@ -472,7 +532,7 @@
let response = null;
try {
response = await Axios.get(`${backendAPI}/tasks/${tid}/frames/meta`, {
response = await Axios.get(`${backendAPI}/tasks/${tid}/data/meta`, {
proxy: config.proxy,
});
} catch (errorData) {
@ -558,9 +618,12 @@
// Session is 'task' or 'job'
async function dumpAnnotations(id, name, format) {
const { backendAPI } = config;
const filename = name.replace(/\//g, '_');
const baseURL = `${backendAPI}/tasks/${id}/annotations/${encodeURIComponent(filename)}`;
const baseURL = `${backendAPI}/tasks/${id}/annotations`;
let query = `format=${encodeURIComponent(format)}`;
if (name) {
const filename = name.replace(/\//g, '_');
query += `&filename=${encodeURIComponent(filename)}`;
}
let url = `${baseURL}?${query}`;
return new Promise((resolve, reject) => {
@ -584,19 +647,34 @@
});
}
async function saveLogs(logs) {
const { backendAPI } = config;
try {
await Axios.post(`${backendAPI}/server/logs`, JSON.stringify(logs), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}
Object.defineProperties(this, Object.freeze({
server: {
value: Object.freeze({
about,
share,
formats,
datasetFormats,
exception,
login,
logout,
authorized,
register,
request: serverRequest,
userAgreements,
}),
writable: false,
},
@ -646,6 +724,13 @@
}),
writable: false,
},
logs: {
value: Object.freeze({
save: saveLogs,
}),
writable: false,
},
}));
}
}

@ -9,8 +9,9 @@
(() => {
const PluginRegistry = require('./plugins');
const loggerStorage = require('./logger-storage');
const serverProxy = require('./server-proxy');
const { getFrame, getPreview } = require('./frames');
const { getFrame, getRanges, getPreview } = require('./frames');
const { ArgumentError } = require('./exceptions');
const { TaskStatus } = require('./enums');
const { Label } = require('./labels');
@ -38,9 +39,9 @@
return result;
},
async dump(name, dumper) {
async dump(dumper, name = null) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.dump, name, dumper);
.apiWrapper.call(this, prototype.annotations.dump, dumper, name);
return result;
},
@ -96,6 +97,18 @@
return result;
},
async import(data) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.import, data);
return result;
},
async export() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.export);
return result;
},
async exportDataset(format) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.exportDataset, format);
@ -112,9 +125,14 @@
}),
frames: Object.freeze({
value: {
async get(frame) {
async get(frame, isPlaying = false, step = 1) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.frames.get, frame);
.apiWrapper.call(this, prototype.frames.get, frame, isPlaying, step);
return result;
},
async ranges() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.frames.ranges);
return result;
},
async preview() {
@ -125,16 +143,11 @@
},
writable: true,
}),
logs: Object.freeze({
logger: Object.freeze({
value: {
async put(logType, details) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.logs.put, logType, details);
return result;
},
async save(onUpdate) {
async log(logType, payload = {}, wait = false) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.logs.save, onUpdate);
.apiWrapper.call(this, prototype.logger.log, logType, payload, wait);
return result;
},
},
@ -242,8 +255,8 @@
* Method always dumps annotations for a whole task.
* @method dump
* @memberof Session.annotations
* @param {string} name - a name of a file with annotations
* @param {module:API.cvat.classes.Dumper} dumper - a dumper
* @param {string} [name = null] - a name of a file with annotations
* which will be used to dump
* @returns {string} URL which can be used in order to get a dump file
* @throws {module:API.cvat.exceptions.PluginError}
@ -267,6 +280,7 @@
* @method put
* @memberof Session.annotations
* @param {module:API.cvat.classes.ObjectState[]} data
* @returns {number[]} identificators of added objects
* array of objects on the specific frame
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.DataError}
@ -390,6 +404,28 @@
* @throws {module:API.cvat.exceptions.PluginError}
* @instance
*/
/**
*
* Import raw data in a collection
* @method import
* @memberof Session.annotations
* @param {Object} data
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
* @async
*/
/**
*
* Export a collection as a row data
* @method export
* @memberof Session.annotations
* @returns {Object} data
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
* @async
*/
/**
* Export as a dataset.
* Method builds a dataset in the specified format.
@ -420,8 +456,10 @@
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.DataError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/**
* Get the first frame of a task for preview
* @method preview
@ -434,35 +472,39 @@
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/**
* Returns the ranges of cached frames
* @method ranges
* @memberof Session.frames
* @returns {Array.<string>}
* @instance
* @async
*/
/**
* Namespace is used for an interaction with logs
* @namespace logs
* @namespace logger
* @memberof Session
*/
/**
* Append log to a log collection.
* Continue logs will have been added after "close" method is called
* @method put
* @memberof Session.logs
* @param {module:API.cvat.enums.LogType} type a type of a log
* @param {boolean} continuous log is a continuous log
* @param {Object} details any others data which will be append to log data
* Create a log and add it to a log collection <br>
* Durable logs will be added after "close" method is called for them <br>
* The fields "task_id" and "job_id" automatically added when add logs
* throught a task or a job <br>
* Ignore rules exist for some logs (e.g. zoomImage, changeAttribute) <br>
* Payload of ignored logs are shallowly combined to previous logs of the same type
* @method log
* @memberof Session.logger
* @param {module:API.cvat.enums.LogType | string} type - log type
* @param {Object} [payload = {}] - any other data that will be appended to the log
* @param {boolean} [wait = false] - specifies if log is durable
* @returns {module:API.cvat.classes.Log}
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/**
* Save accumulated logs on a server
* @method save
* @memberof Session.logs
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
* @instance
* @async
*/
/**
* Namespace is used for an interaction with actions
@ -513,6 +555,8 @@
* @returns {HistoryActions}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @returns {Array.<Array.<string|number>>}
* array of pairs [action name, frame number]
* @instance
* @async
*/
@ -686,6 +730,8 @@
search: Object.getPrototypeOf(this).annotations.search.bind(this),
upload: Object.getPrototypeOf(this).annotations.upload.bind(this),
select: Object.getPrototypeOf(this).annotations.select.bind(this),
import: Object.getPrototypeOf(this).annotations.import.bind(this),
export: Object.getPrototypeOf(this).annotations.export.bind(this),
statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this),
hasUnsavedChanges: Object.getPrototypeOf(this)
.annotations.hasUnsavedChanges.bind(this),
@ -700,8 +746,13 @@
this.frames = {
get: Object.getPrototypeOf(this).frames.get.bind(this),
ranges: Object.getPrototypeOf(this).frames.ranges.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
};
this.logger = {
log: Object.getPrototypeOf(this).logger.log.bind(this),
};
}
/**
@ -759,6 +810,10 @@
start_frame: undefined,
stop_frame: undefined,
frame_filter: undefined,
data_chunk_size: undefined,
data_compressed_chunk_type: undefined,
data_original_chunk_type: undefined,
use_zip_chunks: undefined,
};
for (const property in data) {
@ -996,6 +1051,24 @@
data.image_quality = quality;
},
},
/**
* @name useZipChunks
* @type {boolean}
* @memberof module:API.cvat.classes.Task
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
useZipChunks: {
get: () => data.use_zip_chunks,
set: (useZipChunks) => {
if (typeof (useZipChunks) !== 'boolean') {
throw new ArgumentError(
'Value must be a boolean',
);
}
data.use_zip_chunks = useZipChunks;
},
},
/**
* After task has been created value can be appended only.
* @name labels
@ -1177,6 +1250,21 @@
data.frame_filter = filter;
},
},
dataChunkSize: {
get: () => data.data_chunk_size,
set: (chunkSize) => {
if (typeof (chunkSize) !== 'number' || chunkSize < 1) {
throw new ArgumentError(
`Chunk size value must be a positive number. But value ${chunkSize} has been got.`,
);
}
data.data_chunk_size = chunkSize;
},
},
dataChunkType: {
get: () => data.data_compressed_chunk_type,
},
}));
// When we call a function, for example: task.annotations.get()
@ -1194,6 +1282,8 @@
search: Object.getPrototypeOf(this).annotations.search.bind(this),
upload: Object.getPrototypeOf(this).annotations.upload.bind(this),
select: Object.getPrototypeOf(this).annotations.select.bind(this),
import: Object.getPrototypeOf(this).annotations.import.bind(this),
export: Object.getPrototypeOf(this).annotations.export.bind(this),
statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this),
hasUnsavedChanges: Object.getPrototypeOf(this)
.annotations.hasUnsavedChanges.bind(this),
@ -1210,8 +1300,13 @@
this.frames = {
get: Object.getPrototypeOf(this).frames.get.bind(this),
ranges: Object.getPrototypeOf(this).frames.ranges.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
};
this.logger = {
log: Object.getPrototypeOf(this).logger.log.bind(this),
};
}
/**
@ -1270,6 +1365,8 @@
annotationsStatistics,
uploadAnnotations,
dumpAnnotations,
importAnnotations,
exportAnnotations,
exportDataset,
undoActions,
redoActions,
@ -1297,7 +1394,7 @@
);
};
Job.prototype.frames.get.implementation = async function (frame) {
Job.prototype.frames.get.implementation = async function (frame, isPlaying, step) {
if (!Number.isInteger(frame) || frame < 0) {
throw new ArgumentError(
`Frame must be a positive integer. Got: "${frame}"`,
@ -1310,13 +1407,25 @@
);
}
const frameData = await getFrame(this.task.id, this.task.mode, frame);
const frameData = await getFrame(
this.task.id,
this.task.dataChunkSize,
this.task.dataChunkType,
this.task.mode,
frame,
this.startFrame,
this.stopFrame,
isPlaying,
step,
);
return frameData;
};
Job.prototype.frames.preview.implementation = async function () {
const frameData = await getPreview(this.task.id);
return frameData;
Job.prototype.frames.ranges.implementation = async function () {
const rangesData = await getRanges(
this.task.id,
);
return rangesData;
};
// TODO: Check filter for annotations
@ -1422,7 +1531,17 @@
return result;
};
Job.prototype.annotations.dump.implementation = async function (name, dumper) {
Job.prototype.annotations.import.implementation = function (data) {
const result = importAnnotations(this, data);
return result;
};
Job.prototype.annotations.export.implementation = function () {
const result = exportAnnotations(this);
return result;
};
Job.prototype.annotations.dump.implementation = async function (dumper, name) {
const result = await dumpAnnotations(this, name, dumper);
return result;
};
@ -1452,6 +1571,11 @@
return result;
};
Job.prototype.logger.log.implementation = async function (logType, payload, wait) {
const result = await this.task.logger.log(logType, { ...payload, job_id: this.id }, wait);
return result;
};
Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) {
// TODO: Add ability to change an owner and an assignee
if (typeof (this.id) !== 'undefined') {
@ -1468,39 +1592,44 @@
return this;
}
const taskData = {
const taskSpec = {
name: this.name,
labels: this.labels.map((el) => el.toJSON()),
image_quality: this.imageQuality,
z_order: Boolean(this.zOrder),
};
if (typeof (this.bugTracker) !== 'undefined') {
taskData.bug_tracker = this.bugTracker;
taskSpec.bug_tracker = this.bugTracker;
}
if (typeof (this.segmentSize) !== 'undefined') {
taskData.segment_size = this.segmentSize;
taskSpec.segment_size = this.segmentSize;
}
if (typeof (this.overlap) !== 'undefined') {
taskData.overlap = this.overlap;
taskSpec.overlap = this.overlap;
}
const taskDataSpec = {
client_files: this.clientFiles,
server_files: this.serverFiles,
remote_files: this.remoteFiles,
image_quality: this.imageQuality,
use_zip_chunks: this.useZipChunks,
};
if (typeof (this.startFrame) !== 'undefined') {
taskData.start_frame = this.startFrame;
taskDataSpec.start_frame = this.startFrame;
}
if (typeof (this.stopFrame) !== 'undefined') {
taskData.stop_frame = this.stopFrame;
taskDataSpec.stop_frame = this.stopFrame;
}
if (typeof (this.frameFilter) !== 'undefined') {
taskData.frame_filter = this.frameFilter;
taskDataSpec.frame_filter = this.frameFilter;
}
if (typeof (this.dataChunkSize) !== 'undefined') {
taskDataSpec.chunk_size = this.dataChunkSize;
}
const taskFiles = {
client_files: this.clientFiles,
server_files: this.serverFiles,
remote_files: this.remoteFiles,
};
const task = await serverProxy.tasks.createTask(taskData, taskFiles, onUpdate);
const task = await serverProxy.tasks.createTask(taskSpec, taskDataSpec, onUpdate);
return new Task(task);
};
@ -1509,7 +1638,7 @@
return result;
};
Task.prototype.frames.get.implementation = async function (frame) {
Task.prototype.frames.get.implementation = async function (frame, isPlaying, step) {
if (!Number.isInteger(frame) || frame < 0) {
throw new ArgumentError(
`Frame must be a positive integer. Got: "${frame}"`,
@ -1522,10 +1651,32 @@
);
}
const result = await getFrame(this.id, this.mode, frame);
const result = await getFrame(
this.id,
this.dataChunkSize,
this.dataChunkType,
this.mode,
frame,
0,
this.size - 1,
isPlaying,
step,
);
return result;
};
Job.prototype.frames.preview.implementation = async function () {
const frameData = await getPreview(this.task.id);
return frameData;
};
Task.prototype.frames.ranges.implementation = async function () {
const rangesData = await getRanges(
this.id,
);
return rangesData;
};
Task.prototype.frames.preview.implementation = async function () {
const frameData = await getPreview(this.id);
return frameData;
@ -1634,11 +1785,21 @@
return result;
};
Task.prototype.annotations.dump.implementation = async function (name, dumper) {
Task.prototype.annotations.dump.implementation = async function (dumper, name) {
const result = await dumpAnnotations(this, name, dumper);
return result;
};
Task.prototype.annotations.import.implementation = function (data) {
const result = importAnnotations(this, data);
return result;
};
Task.prototype.annotations.export.implementation = function () {
const result = exportAnnotations(this);
return result;
};
Task.prototype.annotations.exportDataset.implementation = async function (format) {
const result = await exportDataset(this, format);
return result;
@ -1663,4 +1824,9 @@
const result = getActions(this);
return result;
};
Task.prototype.logger.log.implementation = async function (logType, payload, wait) {
const result = await loggerStorage.log(logType, { ...payload, task_id: this.id }, wait);
return result;
};
})();

@ -34,6 +34,10 @@
* tracks: 19,
* shapes: 20,
* },
* cuboids: {
* tracks: 21,
* shapes: 22,
* },
* tags: 66,
* manually: 186,
* interpolated: 500,
@ -69,6 +73,10 @@
* tracks: 19,
* shapes: 20,
* },
* cuboids: {
* tracks: 21,
* shapes: 22,
* },
* tags: 66,
* manually: 186,
* interpolated: 500,

@ -25,7 +25,7 @@ describe('Feature: get annotations', () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
expect(Array.isArray(annotations)).toBeTruthy();
expect(annotations).toHaveLength(11);
expect(annotations).toHaveLength(12);
for (const state of annotations) {
expect(state).toBeInstanceOf(window.cvat.classes.ObjectState);
}
@ -88,8 +88,10 @@ describe('Feature: put annotations', () => {
zOrder: 0,
});
await task.annotations.put([state]);
const indexes = await task.annotations.put([state]);
annotations = await task.annotations.get(1);
expect(indexes).toBeInstanceOf(Array);
expect(indexes).toHaveLength(1);
expect(annotations).toHaveLength(length + 1);
});
@ -108,7 +110,9 @@ describe('Feature: put annotations', () => {
zOrder: 0,
});
await job.annotations.put([state]);
const indexes = await job.annotations.put([state]);
expect(indexes).toBeInstanceOf(Array);
expect(indexes).toHaveLength(1);
annotations = await job.annotations.get(5);
expect(annotations).toHaveLength(length + 1);
});
@ -128,7 +132,9 @@ describe('Feature: put annotations', () => {
zOrder: 0,
});
await task.annotations.put([state]);
const indexes = await task.annotations.put([state]);
expect(indexes).toBeInstanceOf(Array);
expect(indexes).toHaveLength(1);
annotations = await task.annotations.get(1);
expect(annotations).toHaveLength(length + 1);
});
@ -148,7 +154,9 @@ describe('Feature: put annotations', () => {
zOrder: 0,
});
await job.annotations.put([state]);
const indexes = await job.annotations.put([state]);
expect(indexes).toBeInstanceOf(Array);
expect(indexes).toHaveLength(1);
annotations = await job.annotations.get(5);
expect(annotations).toHaveLength(length + 1);
});
@ -367,7 +375,7 @@ describe('Feature: save annotations', () => {
const annotations = await task.annotations.get(0);
expect(task.annotations.hasUnsavedChanges()).toBe(false);
await annotations[0].delete();
await annotations[0].delete(0);
expect(task.annotations.hasUnsavedChanges()).toBe(true);
await task.annotations.save();
expect(task.annotations.hasUnsavedChanges()).toBe(false);
@ -413,7 +421,7 @@ describe('Feature: save annotations', () => {
const annotations = await job.annotations.get(0);
expect(job.annotations.hasUnsavedChanges()).toBe(false);
await annotations[0].delete();
await annotations[0].delete(0);
expect(job.annotations.hasUnsavedChanges()).toBe(true);
await job.annotations.save();
expect(job.annotations.hasUnsavedChanges()).toBe(false);
@ -436,7 +444,7 @@ describe('Feature: save annotations', () => {
return result;
};
await annotations[0].delete();
await annotations[0].delete(0);
await job.annotations.save();
serverProxy.annotations.updateAnnotations = oldImplementation;
@ -692,7 +700,7 @@ describe('Feature: get statistics', () => {
await task.annotations.clear(true);
const statistics = await task.annotations.statistics();
expect(statistics).toBeInstanceOf(window.cvat.classes.Statistics);
expect(statistics.total.total).toBe(29);
expect(statistics.total.total).toBe(30);
});
test('get statistics from a job', async () => {
@ -719,6 +727,9 @@ describe('Feature: select object', () => {
result = await task.annotations.select(annotations, 613, 811);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYGON);
expect(result.state.points.length).toBe(94);
result = await task.annotations.select(annotations, 600, 900);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.CUBOID);
expect(result.state.points.length).toBe(16);
});
test('select object in a job', async () => {
@ -733,6 +744,9 @@ describe('Feature: select object', () => {
result = await job.annotations.select(annotations, 1490, 237);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYGON);
expect(result.state.points.length).toBe(94);
result = await job.annotations.select(annotations, 600, 900);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.CUBOID);
expect(result.state.points.length).toBe(16);
});
test('trying to select from not object states', async () => {

@ -289,7 +289,7 @@ describe('Feature: delete object', () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotationsBefore = await task.annotations.get(0);
const { length } = annotationsBefore;
await annotationsBefore[0].delete();
await annotationsBefore[0].delete(0);
const annotationsAfter = await task.annotations.get(0);
expect(annotationsAfter).toHaveLength(length - 1);
});
@ -298,7 +298,7 @@ describe('Feature: delete object', () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotationsBefore = await task.annotations.get(0);
const { length } = annotationsBefore;
await annotationsBefore[0].delete();
await annotationsBefore[0].delete(0);
const annotationsAfter = await task.annotations.get(0);
expect(annotationsAfter).toHaveLength(length - 1);
});

@ -18,10 +18,10 @@ jest.mock('../../src/server-proxy', () => {
// Initialize api
window.cvat = require('../../src/api');
const {
AnnotationFormat,
AnnotationFormats,
Loader,
Dumper,
} = require('../../src/annotation-format');
} = require('../../src/annotation-formats');
// Test cases
describe('Feature: get info about cvat', () => {
@ -58,24 +58,18 @@ describe('Feature: get share storage info', () => {
describe('Feature: get annotation formats', () => {
test('get annotation formats from a server', async () => {
const result = await window.cvat.server.formats();
expect(Array.isArray(result)).toBeTruthy();
for (const format of result) {
expect(format).toBeInstanceOf(AnnotationFormat);
}
expect(result).toBeInstanceOf(AnnotationFormats);
});
});
describe('Feature: get annotation loaders', () => {
test('get annotation formats from a server', async () => {
const result = await window.cvat.server.formats();
expect(Array.isArray(result)).toBeTruthy();
for (const format of result) {
expect(format).toBeInstanceOf(AnnotationFormat);
const { loaders } = format;
expect(Array.isArray(loaders)).toBeTruthy();
for (const loader of loaders) {
expect(loader).toBeInstanceOf(Loader);
}
expect(result).toBeInstanceOf(AnnotationFormats);
const { loaders } = result;
expect(Array.isArray(loaders)).toBeTruthy();
for (const loader of loaders) {
expect(loader).toBeInstanceOf(Loader);
}
});
});
@ -83,14 +77,11 @@ describe('Feature: get annotation loaders', () => {
describe('Feature: get annotation dumpers', () => {
test('get annotation formats from a server', async () => {
const result = await window.cvat.server.formats();
expect(Array.isArray(result)).toBeTruthy();
for (const format of result) {
expect(format).toBeInstanceOf(AnnotationFormat);
const { dumpers } = format;
expect(Array.isArray(dumpers)).toBeTruthy();
for (const dumper of dumpers) {
expect(dumper).toBeInstanceOf(Dumper);
}
expect(result).toBeInstanceOf(AnnotationFormats);
const { dumpers } = result;
expect(Array.isArray(dumpers)).toBeTruthy();
for (const dumper of dumpers) {
expect(dumper).toBeInstanceOf(Dumper);
}
});
});

@ -6,84 +6,47 @@ const aboutDummyData = {
"version": "0.5.dev20190516142240"
}
const formatsDummyData = [{
"id": 1,
"dumpers": [
const formatsDummyData = {
"exporters": [
{
"display_name": "CVAT XML 1.1 for videos",
"format": "XML",
"name": "CVAT for video 1.1",
"ext": "XML",
"version": "1.1",
"handler": "dump_as_cvat_interpolation"
},
{
"display_name": "CVAT XML 1.1 for images",
"format": "XML",
"name": "CVAT for images 1.1",
"ext": "XML",
"version": "1.1",
"handler": "dump_as_cvat_annotation"
}
],
"loaders": [
{
"display_name": "CVAT XML 1.1",
"format": "XML",
"version": "1.1",
"handler": "load"
}
],
"name": "CVAT",
"created_date": "2019-08-08T12:18:56.571488+03:00",
"updated_date": "2019-08-08T12:18:56.571533+03:00",
"handler_file": "cvat/apps/annotation/cvat.py",
"owner": null
},
{
"id": 2,
"dumpers": [
},
{
"display_name": "PASCAL VOC ZIP 1.0",
"format": "ZIP",
"name": "PASCAL VOC 1.0",
"ext": "ZIP",
"version": "1.0",
"handler": "dump"
}
],
"loaders": [
},
{
"display_name": "PASCAL VOC ZIP 1.0",
"format": "ZIP",
"name": "YOLO 1.0",
"ext": "ZIP",
"version": "1.0",
"handler": "load"
}
},
],
"name": "PASCAL VOC",
"created_date": "2019-08-08T12:18:56.625025+03:00",
"updated_date": "2019-08-08T12:18:56.625071+03:00",
"handler_file": "cvat/apps/annotation/pascal_voc.py",
"owner": null
},
{
"id": 3,
"dumpers": [
"importers": [
{
"name": "CVAT 1.1",
"ext": "XML, ZIP",
"version": "1.1",
},
{
"display_name": "YOLO ZIP 1.0",
"format": "ZIP",
"name": "PASCAL VOC 1.0",
"ext": "ZIP",
"version": "1.0",
"handler": "dump"
}
],
"loaders": [
},
{
"display_name": "YOLO ZIP 1.0",
"format": "ZIP",
"name": "MYFORMAT 1.0",
"ext": "TXT",
"version": "1.0",
"handler": "load"
}
],
"name": "YOLO",
"created_date": "2019-08-08T12:18:56.667534+03:00",
"updated_date": "2019-08-08T12:18:56.667578+03:00",
"handler_file": "cvat/apps/annotation/yolo.py",
"owner": null
}];
};
const usersDummyData = {
"count": 2,
@ -2514,6 +2477,35 @@ const taskAnnotationsDummyData = {
"label_id": 2,
"group": 0,
"attributes": []
}, {
"type": "cuboid",
"occluded": false,
"z_order":12,
"points": [
37.037109375,
834.1583663313359,
37.037109375,
1005.6748046875,
500.1052119006872,
850.3421313142153,
500.1052119006872,
1021.9585696703798,
600.6842465753452,
763.1514501284273,
600.6842465753452,
934.6678884845915,
137.82724152601259,
747.0278858154179,
137.82724152601259,
918.4444406426646,
],
"id": 137,
"frame": 0,
"label_id": 1,
"group": 0,
"attributes": [
]
}],
"tracks":[]
}
@ -2522,78 +2514,126 @@ const taskAnnotationsDummyData = {
const jobAnnotationsDummyData = JSON.parse(JSON.stringify(taskAnnotationsDummyData));
const frameMetaDummyData = {
1: [{
"width": 1920,
"height": 1080
}, {
"width": 1600,
"height": 1143
}, {
"width": 1600,
"height": 859
}, {
"width": 3840,
"height": 2160
}, {
"width": 2560,
"height": 1920
}, {
"width": 1920,
"height": 1080
}, {
"width": 1920,
"height": 1080
}, {
"width": 700,
"height": 453
}, {
"width": 1920,
"height": 1200
}],
2: [{
"width": 1920,
"height": 1080
}],
3: [{
"width": 1888,
"height": 1408
}],
100: [{
"width": 1920,
"height": 1080
}, {
"width": 1600,
"height": 1143
}, {
"width": 1600,
"height": 859
}, {
"width": 3840,
"height": 2160
}, {
"width": 2560,
"height": 1920
}, {
"width": 1920,
"height": 1080
}, {
"width": 1920,
"height": 1080
}, {
"width": 700,
"height": 453
}, {
"width": 1920,
"height": 1200
}],
101: [{
"width": 1888,
"height": 1408
}],
102: [{
"width":1920,
"height":1080
}],
1: {
"chunk_size": 36,
"size": 9,
"image_quality": 95,
"start_frame": 0,
"stop_frame": 8,
"frame_filter": "",
"frames":[{
"width": 1920,
"height": 1080
}, {
"width": 1600,
"height": 1143
}, {
"width": 1600,
"height": 859
}, {
"width": 3840,
"height": 2160
}, {
"width": 2560,
"height": 1920
}, {
"width": 1920,
"height": 1080
}, {
"width": 1920,
"height": 1080
}, {
"width": 700,
"height": 453
}, {
"width": 1920,
"height": 1200
}],
},
2: {
"chunk_size": 36,
"size": 75,
"image_quality": 50,
"start_frame": 0,
"stop_frame": 74,
"frame_filter": "",
"frames": [{
"width": 1920,
"height": 1080
}],
},
3: {
"chunk_size": 36,
"size": 5002,
"image_quality": 50,
"start_frame": 0,
"stop_frame": 5001,
"frame_filter": "",
"frames": [{
"width": 1888,
"height": 1408
}],
},
100: {
"chunk_size": 36,
"size": 9,
"image_quality": 50,
"start_frame": 0,
"stop_frame": 8,
"frame_filter": "",
"frames": [{
"width": 1920,
"height": 1080
}, {
"width": 1600,
"height": 1143
}, {
"width": 1600,
"height": 859
}, {
"width": 3840,
"height": 2160
}, {
"width": 2560,
"height": 1920
}, {
"width": 1920,
"height": 1080
}, {
"width": 1920,
"height": 1080
}, {
"width": 700,
"height": 453
}, {
"width": 1920,
"height": 1200
}],
},
101: {
"chunk_size": 36,
"size": 5002,
"image_quality": 50,
"start_frame": 0,
"stop_frame": 5001,
"frame_filter": "",
"frames": [{
"width": 1888,
"height": 1408
}],
},
102: {
"chunk_size": 36,
"size": 1,
"image_quality": 50,
"start_frame": 0,
"stop_frame": 0,
"frame_filter": "",
"frames": [{
"width":1920,
"height":1080
}],
},
}
module.exports = {
@ -2606,3 +2646,4 @@ module.exports = {
frameMetaDummyData,
formatsDummyData,
}

@ -46,13 +46,33 @@ const webConfig = {
options: {
presets: [
['@babel/preset-env', {
targets: '> 2.5%', // https://github.com/browserslist/browserslist
targets: '> 2.5%',
}],
],
sourceType: 'unambiguous',
},
},
}],
}, {
test: /3rdparty\/.*\.worker\.js$/,
use: {
loader: 'worker-loader',
options: {
publicPath: '/static/engine/js/3rdparty/',
name: '[name].js',
},
},
}, {
test: /\.worker\.js$/,
exclude: /3rdparty/,
use: {
loader: 'worker-loader',
options: {
publicPath: '/static/engine/js/',
name: '[name].js',
},
},
},
],
},
};

@ -0,0 +1 @@
**/3rdparty/*.js

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

@ -0,0 +1 @@
dist

@ -0,0 +1,14 @@
# cvat-data module
```bash
npm run build # build with minification
npm run build -- --mode=development # build without minification
npm run server # run debug server
```
## Versioning
If you make changes in this package, please do following:
- After not important changes (typos, backward compatible bug fixes, refactoring) do: ``npm version patch``
- After changing API (backward compatible new features) do: ``npm version minor``
- After changing API (changes that break backward compatibility) do: ``npm version major``

File diff suppressed because it is too large Load Diff

@ -0,0 +1,34 @@
{
"name": "cvat-data",
"version": "1.0.0",
"description": "",
"main": "src/js/cvat-data.js",
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"babel-loader": "^8.0.6",
"copy-webpack-plugin": "^5.0.5",
"eslint": "^6.4.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-no-unsafe-innerhtml": "^1.0.16",
"eslint-plugin-no-unsanitized": "^3.0.2",
"eslint-plugin-security": "^1.4.0",
"nodemon": "^1.19.2",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.7",
"worker-loader": "^2.0.0"
},
"dependencies": {
"async-mutex": "^0.1.4",
"jszip": "3.1.5"
},
"scripts": {
"patch": "cd src/js && patch --dry-run --forward -p0 < 3rdparty_patch.diff >> /dev/null && patch -p0 < 3rdparty_patch.diff; true",
"build": "npm run patch; webpack --config ./webpack.config.js",
"server": "npm run patch; nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'"
},
"author": "Intel",
"license": "MIT"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,88 @@
## 3rdparty components
These files are from the [Broadway.js](https://github.com/mbebenita/Broadway) repository:
- Decoder.js
- mp4.js
- avc.wasm
### Why do we store them here?
Authors don't provide an npm package, so we need to store these components in our repository.
We use this dependency to decode video chunks from a server and split them to frames on client side.
We need to run this package in node environent (for example for debug, or for running unit tests).
But there aren't any ways to do that (even with syntetic environment, provided for example by the package ``browser-env``).
For example there are issues with canvas using (webpack doesn't work with binary canvas package for node-js) and others.
So, we have solved to write patch file for this library.
It modifies source code a little to support our scenario of using.
### How to build awc.wasm and Decoder.js
1. Clone Emscripten SDK, install and activate the latest fastcomp SDK:
```sh
git clone https://github.com/emscripten-core/emsdk.git && cd emsdk
```
```sh
./emsdk install latest-fastcomp
```
```sh
./emsdk activate latest-fastcomp
```
1. Clone Broadway.js
```sh
git clone https://github.com/mbebenita/Broadway.git && cd Broadway/Decoder
```
1. Edit `make.py`:
- Remove or comment the following options:
`'-s', 'NO_BROWSER=1',`\
`'-s', 'PRECISE_I64_MATH=0',`
- Remove `"HEAP8", "HEAP16", "HEAP32"` from the `EXPORTED_FUNCTIONS` list.
- Increase total memory to make possible decode 4k videos
(or try to enable `ALLOW_MEMORY_GROWTH`, but this option has not been tested):\
`'-s', 'TOTAL_MEMORY=' + str(100*1024*1024),`
- Add the following options:\
`'-s', "ENVIRONMENT='worker'",`\
`'-s', 'WASM=1',`
1. Activate emsdk environment and build Broadway.js:
```sh
. /tmp/emsdk/emsdk_env.sh
```
```sh
python2 make.py
```
1. Copy the following files to cvat-data 3rdparty source folder:
```sh
cd ..
```
```sh
cp Player/avc.wasm Player/Decoder.js Player/mp4.js <CVAT_FOLDER>/cvat-data/src/
```
```sh
js/3rdparty
```
### How work with a patch file
```bash
# from cvat-data/src/js
cp -r 3rdparty 3rdparty_edited
# change 3rdparty edited as we need
diff -u 3rdparty 3rdparty_edited/ > 3rdparty_patch.diff
patch -p0 < 3rdparty_patch.diff # apply patch from cvat-data/src/js
```
Also these files have been added to ignore for git in all future revisions:
```bash
# from cvat-data dir
git update-index --skip-worktree src/js/3rdparty/*.js
```
This behaviour can be reset with:
```bash
# from cvat-data dir
git update-index --no-skip-worktree src/js/3rdparty/*.js
```
[Stackoverflow issue](https://stackoverflow.com/questions/4348590/how-can-i-make-git-ignore-future-revisions-to-a-file)

Binary file not shown.

@ -0,0 +1,977 @@
module.exports = (function(){
'use strict';
function assert(condition, message) {
if (!condition) {
error(message);
}
};
/**
* Represents a 2-dimensional size value.
*/
var Size = (function size() {
function constructor(w, h) {
this.w = w;
this.h = h;
}
constructor.prototype = {
toString: function () {
return "(" + this.w + ", " + this.h + ")";
},
getHalfSize: function() {
return new Size(this.w >>> 1, this.h >>> 1);
},
length: function() {
return this.w * this.h;
}
};
return constructor;
})();
var Bytestream = (function BytestreamClosure() {
function constructor(arrayBuffer, start, length) {
this.bytes = new Uint8Array(arrayBuffer);
this.start = start || 0;
this.pos = this.start;
this.end = (start + length) || this.bytes.length;
}
constructor.prototype = {
get length() {
return this.end - this.start;
},
get position() {
return this.pos;
},
get remaining() {
return this.end - this.pos;
},
readU8Array: function (length) {
if (this.pos > this.end - length)
return null;
var res = this.bytes.subarray(this.pos, this.pos + length);
this.pos += length;
return res;
},
readU32Array: function (rows, cols, names) {
cols = cols || 1;
if (this.pos > this.end - (rows * cols) * 4)
return null;
if (cols == 1) {
var array = new Uint32Array(rows);
for (var i = 0; i < rows; i++) {
array[i] = this.readU32();
}
return array;
} else {
var array = new Array(rows);
for (var i = 0; i < rows; i++) {
var row = null;
if (names) {
row = {};
for (var j = 0; j < cols; j++) {
row[names[j]] = this.readU32();
}
} else {
row = new Uint32Array(cols);
for (var j = 0; j < cols; j++) {
row[j] = this.readU32();
}
}
array[i] = row;
}
return array;
}
},
read8: function () {
return this.readU8() << 24 >> 24;
},
readU8: function () {
if (this.pos >= this.end)
return null;
return this.bytes[this.pos++];
},
read16: function () {
return this.readU16() << 16 >> 16;
},
readU16: function () {
if (this.pos >= this.end - 1)
return null;
var res = this.bytes[this.pos + 0] << 8 | this.bytes[this.pos + 1];
this.pos += 2;
return res;
},
read24: function () {
return this.readU24() << 8 >> 8;
},
readU24: function () {
var pos = this.pos;
var bytes = this.bytes;
if (pos > this.end - 3)
return null;
var res = bytes[pos + 0] << 16 | bytes[pos + 1] << 8 | bytes[pos + 2];
this.pos += 3;
return res;
},
peek32: function (advance) {
var pos = this.pos;
var bytes = this.bytes;
if (pos > this.end - 4)
return null;
var res = bytes[pos + 0] << 24 | bytes[pos + 1] << 16 | bytes[pos + 2] << 8 | bytes[pos + 3];
if (advance) {
this.pos += 4;
}
return res;
},
read32: function () {
return this.peek32(true);
},
readU32: function () {
return this.peek32(true) >>> 0;
},
read4CC: function () {
var pos = this.pos;
if (pos > this.end - 4)
return null;
var res = "";
for (var i = 0; i < 4; i++) {
res += String.fromCharCode(this.bytes[pos + i]);
}
this.pos += 4;
return res;
},
readFP16: function () {
return this.read32() / 65536;
},
readFP8: function () {
return this.read16() / 256;
},
readISO639: function () {
var bits = this.readU16();
var res = "";
for (var i = 0; i < 3; i++) {
var c = (bits >>> (2 - i) * 5) & 0x1f;
res += String.fromCharCode(c + 0x60);
}
return res;
},
readUTF8: function (length) {
var res = "";
for (var i = 0; i < length; i++) {
res += String.fromCharCode(this.readU8());
}
return res;
},
readPString: function (max) {
var len = this.readU8();
assert (len <= max);
var res = this.readUTF8(len);
this.reserved(max - len - 1, 0);
return res;
},
skip: function (length) {
this.seek(this.pos + length);
},
reserved: function (length, value) {
for (var i = 0; i < length; i++) {
assert (this.readU8() == value);
}
},
seek: function (index) {
if (index < 0 || index > this.end) {
error("Index out of bounds (bounds: [0, " + this.end + "], index: " + index + ").");
}
this.pos = index;
},
subStream: function (start, length) {
return new Bytestream(this.bytes.buffer, start, length);
}
};
return constructor;
})();
var PARANOID = true; // Heavy-weight assertions.
/**
* Reads an mp4 file and constructs a object graph that corresponds to the box/atom
* structure of the file. Mp4 files are based on the ISO Base Media format, which in
* turn is based on the Apple Quicktime format. The Quicktime spec is available at:
* http://developer.apple.com/library/mac/#documentation/QuickTime/QTFF. An mp4 spec
* also exists, but I cannot find it freely available.
*
* Mp4 files contain a tree of boxes (or atoms in Quicktime). The general structure
* is as follows (in a pseudo regex syntax):
*
* Box / Atom Structure:
*
* [size type [version flags] field* box*]
* <32> <4C> <--8--> <24-> <-?-> <?>
* <------------- box size ------------>
*
* The box size indicates the entire size of the box and its children, we can use it
* to skip over boxes that are of no interest. Each box has a type indicated by a
* four character code (4C), this describes how the box should be parsed and is also
* used as an object key name in the resulting box tree. For example, the expression:
* "moov.trak[0].mdia.minf" can be used to access individual boxes in the tree based
* on their 4C name. If two or more boxes with the same 4C name exist in a box, then
* an array is built with that name.
*
*/
var MP4Reader = (function reader() {
var BOX_HEADER_SIZE = 8;
var FULL_BOX_HEADER_SIZE = BOX_HEADER_SIZE + 4;
function constructor(stream) {
this.stream = stream;
this.tracks = {};
}
constructor.prototype = {
readBoxes: function (stream, parent) {
while (stream.peek32()) {
var child = this.readBox(stream);
if (child.type in parent) {
var old = parent[child.type];
if (!(old instanceof Array)) {
parent[child.type] = [old];
}
parent[child.type].push(child);
} else {
parent[child.type] = child;
}
}
},
readBox: function readBox(stream) {
var box = { offset: stream.position };
function readHeader() {
box.size = stream.readU32();
box.type = stream.read4CC();
}
function readFullHeader() {
box.version = stream.readU8();
box.flags = stream.readU24();
}
function remainingBytes() {
return box.size - (stream.position - box.offset);
}
function skipRemainingBytes () {
stream.skip(remainingBytes());
}
var readRemainingBoxes = function () {
var subStream = stream.subStream(stream.position, remainingBytes());
this.readBoxes(subStream, box);
stream.skip(subStream.length);
}.bind(this);
readHeader();
switch (box.type) {
case 'ftyp':
box.name = "File Type Box";
box.majorBrand = stream.read4CC();
box.minorVersion = stream.readU32();
box.compatibleBrands = new Array((box.size - 16) / 4);
for (var i = 0; i < box.compatibleBrands.length; i++) {
box.compatibleBrands[i] = stream.read4CC();
}
break;
case 'moov':
box.name = "Movie Box";
readRemainingBoxes();
break;
case 'mvhd':
box.name = "Movie Header Box";
readFullHeader();
assert (box.version == 0);
box.creationTime = stream.readU32();
box.modificationTime = stream.readU32();
box.timeScale = stream.readU32();
box.duration = stream.readU32();
box.rate = stream.readFP16();
box.volume = stream.readFP8();
stream.skip(10);
box.matrix = stream.readU32Array(9);
stream.skip(6 * 4);
box.nextTrackId = stream.readU32();
break;
case 'trak':
box.name = "Track Box";
readRemainingBoxes();
this.tracks[box.tkhd.trackId] = new Track(this, box);
break;
case 'tkhd':
box.name = "Track Header Box";
readFullHeader();
assert (box.version == 0);
box.creationTime = stream.readU32();
box.modificationTime = stream.readU32();
box.trackId = stream.readU32();
stream.skip(4);
box.duration = stream.readU32();
stream.skip(8);
box.layer = stream.readU16();
box.alternateGroup = stream.readU16();
box.volume = stream.readFP8();
stream.skip(2);
box.matrix = stream.readU32Array(9);
box.width = stream.readFP16();
box.height = stream.readFP16();
break;
case 'mdia':
box.name = "Media Box";
readRemainingBoxes();
break;
case 'mdhd':
box.name = "Media Header Box";
readFullHeader();
assert (box.version == 0);
box.creationTime = stream.readU32();
box.modificationTime = stream.readU32();
box.timeScale = stream.readU32();
box.duration = stream.readU32();
box.language = stream.readISO639();
stream.skip(2);
break;
case 'hdlr':
box.name = "Handler Reference Box";
readFullHeader();
stream.skip(4);
box.handlerType = stream.read4CC();
stream.skip(4 * 3);
var bytesLeft = box.size - 32;
if (bytesLeft > 0) {
box.name = stream.readUTF8(bytesLeft);
}
break;
case 'minf':
box.name = "Media Information Box";
readRemainingBoxes();
break;
case 'stbl':
box.name = "Sample Table Box";
readRemainingBoxes();
break;
case 'stsd':
box.name = "Sample Description Box";
readFullHeader();
box.sd = [];
var entries = stream.readU32();
readRemainingBoxes();
break;
case 'avc1':
stream.reserved(6, 0);
box.dataReferenceIndex = stream.readU16();
assert (stream.readU16() == 0); // Version
assert (stream.readU16() == 0); // Revision Level
stream.readU32(); // Vendor
stream.readU32(); // Temporal Quality
stream.readU32(); // Spatial Quality
box.width = stream.readU16();
box.height = stream.readU16();
box.horizontalResolution = stream.readFP16();
box.verticalResolution = stream.readFP16();
assert (stream.readU32() == 0); // Reserved
box.frameCount = stream.readU16();
box.compressorName = stream.readPString(32);
box.depth = stream.readU16();
assert (stream.readU16() == 0xFFFF); // Color Table Id
readRemainingBoxes();
break;
case 'mp4a':
stream.reserved(6, 0);
box.dataReferenceIndex = stream.readU16();
box.version = stream.readU16();
stream.skip(2);
stream.skip(4);
box.channelCount = stream.readU16();
box.sampleSize = stream.readU16();
box.compressionId = stream.readU16();
box.packetSize = stream.readU16();
box.sampleRate = stream.readU32() >>> 16;
// TODO: Parse other version levels.
assert (box.version == 0);
readRemainingBoxes();
break;
case 'esds':
box.name = "Elementary Stream Descriptor";
readFullHeader();
// TODO: Do we really need to parse this?
skipRemainingBytes();
break;
case 'avcC':
box.name = "AVC Configuration Box";
box.configurationVersion = stream.readU8();
box.avcProfileIndication = stream.readU8();
box.profileCompatibility = stream.readU8();
box.avcLevelIndication = stream.readU8();
box.lengthSizeMinusOne = stream.readU8() & 3;
assert (box.lengthSizeMinusOne == 3, "TODO");
var count = stream.readU8() & 31;
box.sps = [];
for (var i = 0; i < count; i++) {
box.sps.push(stream.readU8Array(stream.readU16()));
}
var count = stream.readU8() & 31;
box.pps = [];
for (var i = 0; i < count; i++) {
box.pps.push(stream.readU8Array(stream.readU16()));
}
skipRemainingBytes();
break;
case 'btrt':
box.name = "Bit Rate Box";
box.bufferSizeDb = stream.readU32();
box.maxBitrate = stream.readU32();
box.avgBitrate = stream.readU32();
break;
case 'stts':
box.name = "Decoding Time to Sample Box";
readFullHeader();
box.table = stream.readU32Array(stream.readU32(), 2, ["count", "delta"]);
break;
case 'stss':
box.name = "Sync Sample Box";
readFullHeader();
box.samples = stream.readU32Array(stream.readU32());
break;
case 'stsc':
box.name = "Sample to Chunk Box";
readFullHeader();
box.table = stream.readU32Array(stream.readU32(), 3,
["firstChunk", "samplesPerChunk", "sampleDescriptionId"]);
break;
case 'stsz':
box.name = "Sample Size Box";
readFullHeader();
box.sampleSize = stream.readU32();
var count = stream.readU32();
if (box.sampleSize == 0) {
box.table = stream.readU32Array(count);
}
break;
case 'stco':
box.name = "Chunk Offset Box";
readFullHeader();
box.table = stream.readU32Array(stream.readU32());
break;
case 'smhd':
box.name = "Sound Media Header Box";
readFullHeader();
box.balance = stream.readFP8();
stream.reserved(2, 0);
break;
case 'mdat':
box.name = "Media Data Box";
assert (box.size >= 8, "Cannot parse large media data yet.");
box.data = stream.readU8Array(remainingBytes());
break;
default:
skipRemainingBytes();
break;
};
return box;
},
read: function () {
var start = (new Date).getTime();
this.file = {};
this.readBoxes(this.stream, this.file);
console.info("Parsed stream in " + ((new Date).getTime() - start) + " ms");
},
traceSamples: function () {
var video = this.tracks[1];
var audio = this.tracks[2];
console.info("Video Samples: " + video.getSampleCount());
console.info("Audio Samples: " + audio.getSampleCount());
var vi = 0;
var ai = 0;
for (var i = 0; i < 100; i++) {
var vo = video.sampleToOffset(vi);
var ao = audio.sampleToOffset(ai);
var vs = video.sampleToSize(vi, 1);
var as = audio.sampleToSize(ai, 1);
if (vo < ao) {
console.info("V Sample " + vi + " Offset : " + vo + ", Size : " + vs);
vi ++;
} else {
console.info("A Sample " + ai + " Offset : " + ao + ", Size : " + as);
ai ++;
}
}
}
};
return constructor;
})();
var Track = (function track () {
function constructor(file, trak) {
this.file = file;
this.trak = trak;
}
constructor.prototype = {
getSampleSizeTable: function () {
return this.trak.mdia.minf.stbl.stsz.table;
},
getSampleCount: function () {
return this.getSampleSizeTable().length;
},
/**
* Computes the size of a range of samples, returns zero if length is zero.
*/
sampleToSize: function (start, length) {
var table = this.getSampleSizeTable();
var size = 0;
for (var i = start; i < start + length; i++) {
size += table[i];
}
return size;
},
/**
* Computes the chunk that contains the specified sample, as well as the offset of
* the sample in the computed chunk.
*/
sampleToChunk: function (sample) {
/* Samples are grouped in chunks which may contain a variable number of samples.
* The sample-to-chunk table in the stsc box describes how samples are arranged
* in chunks. Each table row corresponds to a set of consecutive chunks with the
* same number of samples and description ids. For example, the following table:
*
* +-------------+-------------------+----------------------+
* | firstChunk | samplesPerChunk | sampleDescriptionId |
* +-------------+-------------------+----------------------+
* | 1 | 3 | 23 |
* | 3 | 1 | 23 |
* | 5 | 1 | 24 |
* +-------------+-------------------+----------------------+
*
* describes 5 chunks with a total of (2 * 3) + (2 * 1) + (1 * 1) = 9 samples,
* each chunk containing samples 3, 3, 1, 1, 1 in chunk order, or
* chunks 1, 1, 1, 2, 2, 2, 3, 4, 5 in sample order.
*
* This function determines the chunk that contains a specified sample by iterating
* over every entry in the table. It also returns the position of the sample in the
* chunk which can be used to compute the sample's exact position in the file.
*
* TODO: Determine if we should memoize this function.
*/
var table = this.trak.mdia.minf.stbl.stsc.table;
if (table.length === 1) {
var row = table[0];
assert (row.firstChunk === 1);
return {
index: Math.floor(sample / row.samplesPerChunk),
offset: sample % row.samplesPerChunk
};
}
var totalChunkCount = 0;
for (var i = 0; i < table.length; i++) {
var row = table[i];
if (i > 0) {
var previousRow = table[i - 1];
var previousChunkCount = row.firstChunk - previousRow.firstChunk;
var previousSampleCount = previousRow.samplesPerChunk * previousChunkCount;
if (sample >= previousSampleCount) {
sample -= previousSampleCount;
if (i == table.length - 1) {
return {
index: totalChunkCount + previousChunkCount + Math.floor(sample / row.samplesPerChunk),
offset: sample % row.samplesPerChunk
};
}
} else {
return {
index: totalChunkCount + Math.floor(sample / previousRow.samplesPerChunk),
offset: sample % previousRow.samplesPerChunk
};
}
totalChunkCount += previousChunkCount;
}
}
assert(false);
},
chunkToOffset: function (chunk) {
var table = this.trak.mdia.minf.stbl.stco.table;
return table[chunk];
},
sampleToOffset: function (sample) {
var res = this.sampleToChunk(sample);
var offset = this.chunkToOffset(res.index);
return offset + this.sampleToSize(sample - res.offset, res.offset);
},
/**
* Computes the sample at the specified time.
*/
timeToSample: function (time) {
/* In the time-to-sample table samples are grouped by their duration. The count field
* indicates the number of consecutive samples that have the same duration. For example,
* the following table:
*
* +-------+-------+
* | count | delta |
* +-------+-------+
* | 4 | 3 |
* | 2 | 1 |
* | 3 | 2 |
* +-------+-------+
*
* describes 9 samples with a total time of (4 * 3) + (2 * 1) + (3 * 2) = 20.
*
* This function determines the sample at the specified time by iterating over every
* entry in the table.
*
* TODO: Determine if we should memoize this function.
*/
var table = this.trak.mdia.minf.stbl.stts.table;
var sample = 0;
for (var i = 0; i < table.length; i++) {
var delta = table[i].count * table[i].delta;
if (time >= delta) {
time -= delta;
sample += table[i].count;
} else {
return sample + Math.floor(time / table[i].delta);
}
}
},
/**
* Gets the total time of the track.
*/
getTotalTime: function () {
if (PARANOID) {
var table = this.trak.mdia.minf.stbl.stts.table;
var duration = 0;
for (var i = 0; i < table.length; i++) {
duration += table[i].count * table[i].delta;
}
assert (this.trak.mdia.mdhd.duration == duration);
}
return this.trak.mdia.mdhd.duration;
},
getTotalTimeInSeconds: function () {
return this.timeToSeconds(this.getTotalTime());
},
getTimeScale: function () {
return this.trak.mdia.mdhd.timeScale;
},
/**
* Converts time units to real time (seconds).
*/
timeToSeconds: function (time) {
return time / this.getTimeScale();
},
/**
* Converts real time (seconds) to time units.
*/
secondsToTime: function (seconds) {
return seconds * this.getTimeScale();
},
foo: function () {
/*
for (var i = 0; i < this.getSampleCount(); i++) {
var res = this.sampleToChunk(i);
console.info("Sample " + i + " -> " + res.index + " % " + res.offset +
" @ " + this.chunkToOffset(res.index) +
" @@ " + this.sampleToOffset(i));
}
console.info("Total Time: " + this.timeToSeconds(this.getTotalTime()));
var total = this.getTotalTimeInSeconds();
for (var i = 50; i < total; i += 0.1) {
// console.info("Time: " + i.toFixed(2) + " " + this.secondsToTime(i));
console.info("Time: " + i.toFixed(2) + " " + this.timeToSample(this.secondsToTime(i)));
}
*/
},
/**
* AVC samples contain one or more NAL units each of which have a length prefix.
* This function returns an array of NAL units without their length prefixes.
*/
getSampleNALUnits: function (sample) {
var bytes = this.file.stream.bytes;
var offset = this.sampleToOffset(sample);
var end = offset + this.sampleToSize(sample, 1);
var nalUnits = [];
while(end - offset > 0) {
var length = (new Bytestream(bytes.buffer, offset)).readU32();
nalUnits.push(bytes.subarray(offset + 4, offset + length + 4));
offset = offset + length + 4;
}
return nalUnits;
}
};
return constructor;
})();
// Only add setZeroTimeout to the window object, and hide everything
// else in a closure. (http://dbaron.org/log/20100309-faster-timeouts)
(function() {
var timeouts = [];
var messageName = "zero-timeout-message";
// Like setTimeout, but only takes a function argument. There's
// no time argument (always zero) and no arguments (you have to
// use a closure).
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, "*");
}
function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}
window.addEventListener("message", handleMessage, true);
// Add the one thing we want added to the window object.
window.setZeroTimeout = setZeroTimeout;
})();
var MP4Player = (function reader() {
var defaultConfig = {
filter: "original",
filterHorLuma: "optimized",
filterVerLumaEdge: "optimized",
getBoundaryStrengthsA: "optimized"
};
function constructor(stream, useWorkers, webgl, render) {
this.stream = stream;
this.useWorkers = useWorkers;
this.webgl = webgl;
this.render = render;
this.statistics = {
videoStartTime: 0,
videoPictureCounter: 0,
windowStartTime: 0,
windowPictureCounter: 0,
fps: 0,
fpsMin: 1000,
fpsMax: -1000,
webGLTextureUploadTime: 0
};
this.onStatisticsUpdated = function () {};
this.avc = new Player({
useWorker: useWorkers,
reuseMemory: true,
webgl: webgl,
size: {
width: 640,
height: 368
}
});
this.webgl = this.avc.webgl;
var self = this;
this.avc.onPictureDecoded = function(){
updateStatistics.call(self);
};
this.canvas = this.avc.canvas;
}
function updateStatistics() {
var s = this.statistics;
s.videoPictureCounter += 1;
s.windowPictureCounter += 1;
var now = Date.now();
if (!s.videoStartTime) {
s.videoStartTime = now;
}
var videoElapsedTime = now - s.videoStartTime;
s.elapsed = videoElapsedTime / 1000;
if (videoElapsedTime < 1000) {
return;
}
if (!s.windowStartTime) {
s.windowStartTime = now;
return;
} else if ((now - s.windowStartTime) > 1000) {
var windowElapsedTime = now - s.windowStartTime;
var fps = (s.windowPictureCounter / windowElapsedTime) * 1000;
s.windowStartTime = now;
s.windowPictureCounter = 0;
if (fps < s.fpsMin) s.fpsMin = fps;
if (fps > s.fpsMax) s.fpsMax = fps;
s.fps = fps;
}
var fps = (s.videoPictureCounter / videoElapsedTime) * 1000;
s.fpsSinceStart = fps;
this.onStatisticsUpdated(this.statistics);
return;
}
constructor.prototype = {
readAll: function(callback) {
console.info("MP4Player::readAll()");
this.stream.readAll(null, function (buffer) {
this.reader = new MP4Reader(new Bytestream(buffer));
this.reader.read();
var video = this.reader.tracks[1];
this.size = new Size(video.trak.tkhd.width, video.trak.tkhd.height);
console.info("MP4Player::readAll(), length: " + this.reader.stream.length);
if (callback) callback();
}.bind(this));
},
play: function() {
var reader = this.reader;
if (!reader) {
this.readAll(this.play.bind(this));
return;
};
var video = reader.tracks[1];
var audio = reader.tracks[2];
var avc = reader.tracks[1].trak.mdia.minf.stbl.stsd.avc1.avcC;
var sps = avc.sps[0];
var pps = avc.pps[0];
/* Decode Sequence & Picture Parameter Sets */
this.avc.decode(sps);
this.avc.decode(pps);
/* Decode Pictures */
var pic = 0;
setTimeout(function foo() {
var avc = this.avc;
video.getSampleNALUnits(pic).forEach(function (nal) {
avc.decode(nal);
});
pic ++;
if (pic < 3000) {
setTimeout(foo.bind(this), 1);
};
}.bind(this), 1);
}
};
return constructor;
})();
var Broadway = (function broadway() {
function constructor(div) {
var src = div.attributes.src ? div.attributes.src.value : undefined;
var width = div.attributes.width ? div.attributes.width.value : 640;
var height = div.attributes.height ? div.attributes.height.value : 480;
var controls = document.createElement('div');
controls.setAttribute('style', "z-index: 100; position: absolute; bottom: 0px; background-color: rgba(0,0,0,0.8); height: 30px; width: 100%; text-align: left;");
this.info = document.createElement('div');
this.info.setAttribute('style', "font-size: 14px; font-weight: bold; padding: 6px; color: lime;");
controls.appendChild(this.info);
div.appendChild(controls);
var useWorkers = div.attributes.workers ? div.attributes.workers.value == "true" : false;
var render = div.attributes.render ? div.attributes.render.value == "true" : false;
var webgl = "auto";
if (div.attributes.webgl){
if (div.attributes.webgl.value == "true"){
webgl = true;
};
if (div.attributes.webgl.value == "false"){
webgl = false;
};
};
var infoStrPre = "Click canvas to load and play - ";
var infoStr = "";
if (useWorkers){
infoStr += "worker thread ";
}else{
infoStr += "main thread ";
};
this.player = new MP4Player(new Stream(src), useWorkers, webgl, render);
this.canvas = this.player.canvas;
this.canvas.onclick = function () {
this.play();
}.bind(this);
div.appendChild(this.canvas);
infoStr += " - webgl: " + this.player.webgl;
this.info.innerHTML = infoStrPre + infoStr;
this.score = null;
this.player.onStatisticsUpdated = function (statistics) {
if (statistics.videoPictureCounter % 10 != 0) {
return;
}
var info = "";
if (statistics.fps) {
info += " fps: " + statistics.fps.toFixed(2);
}
if (statistics.fpsSinceStart) {
info += " avg: " + statistics.fpsSinceStart.toFixed(2);
}
var scoreCutoff = 1200;
if (statistics.videoPictureCounter < scoreCutoff) {
this.score = scoreCutoff - statistics.videoPictureCounter;
} else if (statistics.videoPictureCounter == scoreCutoff) {
this.score = statistics.fpsSinceStart.toFixed(2);
}
// info += " score: " + this.score;
this.info.innerHTML = infoStr + info;
}.bind(this);
}
constructor.prototype = {
play: function () {
this.player.play();
}
};
return constructor;
})();
return {
Size,
Track,
MP4Reader,
MP4Player,
Bytestream,
Broadway,
}
})();

File diff suppressed because one or more lines are too long

@ -0,0 +1,350 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:true
*/
const { Mutex } = require('async-mutex');
// eslint-disable-next-line max-classes-per-file
const { MP4Reader, Bytestream } = require('./3rdparty/mp4');
const ZipDecoder = require('./unzip_imgs.worker');
const H264Decoder = require('./3rdparty/Decoder.worker');
const BlockType = Object.freeze({
MP4VIDEO: 'mp4video',
ARCHIVE: 'archive',
});
class FrameProvider {
constructor(blockType, blockSize, cachedBlockCount,
decodedBlocksCacheSize = 5, maxWorkerThreadCount = 2) {
this._frames = {};
this._cachedBlockCount = Math.max(1, cachedBlockCount); // number of stored blocks
this._decodedBlocksCacheSize = decodedBlocksCacheSize;
this._blocksRanges = [];
this._blocks = {};
this._blockSize = blockSize;
this._running = false;
this._blockType = blockType;
this._currFrame = -1;
this._requestedBlockDecode = null;
this._width = null;
this._height = null;
this._decodingBlocks = {};
this._decodeThreadCount = 0;
this._timerId = setTimeout(this._worker.bind(this), 100);
this._mutex = new Mutex();
this._promisedFrames = {};
this._maxWorkerThreadCount = maxWorkerThreadCount;
}
async _worker() {
if (this._requestedBlockDecode !== null
&& this._decodeThreadCount < this._maxWorkerThreadCount) {
await this.startDecode();
}
this._timerId = setTimeout(this._worker.bind(this), 100);
}
isChunkCached(start, end) {
return (`${start}:${end}` in this._blocksRanges);
}
/* This method removes extra data from a cache when memory overflow */
async _cleanup() {
if (this._blocksRanges.length > this._cachedBlockCount) {
const shifted = this._blocksRanges.shift(); // get the oldest block
const [start, end] = shifted.split(':').map((el) => +el);
delete this._blocks[start / this._blockSize];
for (let i = start; i <= end; i++) {
delete this._frames[i];
}
}
// delete frames whose are not in areas of current frame
const distance = Math.floor(this._decodedBlocksCacheSize / 2);
for (let i = 0; i < this._blocksRanges.length; i++) {
const [start, end] = this._blocksRanges[i].split(':').map((el) => +el);
if (end < this._currFrame - distance * this._blockSize
|| start > this._currFrame + distance * this._blockSize) {
for (let j = start; j <= end; j++) {
delete this._frames[j];
}
}
}
}
async requestDecodeBlock(block, start, end, resolveCallback, rejectCallback) {
const release = await this._mutex.acquire();
try {
if (this._requestedBlockDecode !== null) {
if (start === this._requestedBlockDecode.start
&& end === this._requestedBlockDecode.end) {
this._requestedBlockDecode.resolveCallback = resolveCallback;
this._requestedBlockDecode.rejectCallback = rejectCallback;
} else if (this._requestedBlockDecode.rejectCallback) {
this._requestedBlockDecode.rejectCallback();
}
}
if (!(`${start}:${end}` in this._decodingBlocks)) {
this._requestedBlockDecode = {
block: block || this._blocks[Math.floor(start / this._blockSize)],
start,
end,
resolveCallback,
rejectCallback,
};
} else {
this._decodingBlocks[`${start}:${end}`].rejectCallback = rejectCallback;
this._decodingBlocks[`${start}:${end}`].resolveCallback = resolveCallback;
}
} finally {
release();
}
}
isRequestExist() {
return this._requestedBlockDecode !== null;
}
setRenderSize(width, height) {
this._width = width;
this._height = height;
}
/* Method returns frame from collection. Else method returns 0 */
async frame(frameNumber) {
this._currFrame = frameNumber;
return new Promise((resolve, reject) => {
if (frameNumber in this._frames) {
if (this._frames[frameNumber] !== null) {
resolve(this._frames[frameNumber]);
} else {
this._promisedFrames[frameNumber] = {
resolve,
reject,
};
}
} else {
resolve(null);
}
});
}
isNextChunkExists(frameNumber) {
const nextChunkNum = Math.floor(frameNumber / this._blockSize) + 1;
if (this._blocks[nextChunkNum] === 'loading') {
return true;
}
return nextChunkNum in this._blocks;
}
/*
Method start asynchronic decode a block of data
@param block - is a data from a server as is (ts file or archive)
@param start {number} - is the first frame of a block
@param end {number} - is the last frame of a block + 1
@param callback - callback)
*/
setReadyToLoading(chunkNumber) {
this._blocks[chunkNumber] = 'loading';
}
static cropImage(imageBuffer, imageWidth, imageHeight, xOffset, yOffset, width, height) {
if (xOffset === 0 && width === imageWidth
&& yOffset === 0 && height === imageHeight) {
return new ImageData(new Uint8ClampedArray(imageBuffer), width, height);
}
const source = new Uint32Array(imageBuffer);
const bufferSize = width * height * 4;
const buffer = new ArrayBuffer(bufferSize);
const rgbaInt32 = new Uint32Array(buffer);
const rgbaInt8Clamped = new Uint8ClampedArray(buffer);
if (imageWidth === width) {
return new ImageData(
new Uint8ClampedArray(imageBuffer, yOffset * 4, bufferSize),
width,
height,
);
}
let writeIdx = 0;
for (let row = yOffset; row < height; row++) {
const start = row * imageWidth + xOffset;
rgbaInt32.set(source.subarray(start, start + width), writeIdx);
writeIdx += width;
}
return new ImageData(rgbaInt8Clamped, width, height);
}
async startDecode() {
const release = await this._mutex.acquire();
try {
const height = this._height;
const width = this._width;
const { start, end, block } = this._requestedBlockDecode;
this._blocksRanges.push(`${start}:${end}`);
this._decodingBlocks[`${start}:${end}`] = this._requestedBlockDecode;
this._requestedBlockDecode = null;
this._blocks[Math.floor((start + 1) / this._blockSize)] = block;
for (let i = start; i <= end; i++) {
this._frames[i] = null;
}
this._cleanup();
if (this._blockType === BlockType.MP4VIDEO) {
const worker = new H264Decoder();
let index = start;
worker.onmessage = (e) => {
if (e.data.consoleLog) { // ignore initialization message
return;
}
const scaleFactor = Math.ceil(this._height / e.data.height);
this._frames[index] = FrameProvider.cropImage(
e.data.buf, e.data.width, e.data.height, 0, 0,
Math.floor(width / scaleFactor), Math.floor(height / scaleFactor),
);
if (this._decodingBlocks[`${start}:${end}`].resolveCallback) {
this._decodingBlocks[`${start}:${end}`].resolveCallback(index);
}
if (index in this._promisedFrames) {
this._promisedFrames[index].resolve(this._frames[index]);
delete this._promisedFrames[index];
}
if (index === end) {
this._decodeThreadCount--;
delete this._decodingBlocks[`${start}:${end}`];
worker.terminate();
}
index++;
};
worker.onerror = (e) => {
worker.terminate();
this._decodeThreadCount--;
for (let i = index; i <= end; i++) {
if (i in this._promisedFrames) {
this._promisedFrames[i].reject();
delete this._promisedFrames[i];
}
}
if (this._decodingBlocks[`${start}:${end}`].rejectCallback) {
this._decodingBlocks[`${start}:${end}`].rejectCallback(Error(e));
}
delete this._decodingBlocks[`${start}:${end}`];
};
worker.postMessage({
type: 'Broadway.js - Worker init',
options: {
rgb: true,
reuseMemory: false,
},
});
const reader = new MP4Reader(new Bytestream(block));
reader.read();
const video = reader.tracks[1];
const avc = reader.tracks[1].trak.mdia.minf.stbl.stsd.avc1.avcC;
const sps = avc.sps[0];
const pps = avc.pps[0];
/* Decode Sequence & Picture Parameter Sets */
worker.postMessage({ buf: sps, offset: 0, length: sps.length });
worker.postMessage({ buf: pps, offset: 0, length: pps.length });
/* Decode Pictures */
for (let sample = 0; sample < video.getSampleCount(); sample++) {
video.getSampleNALUnits(sample).forEach((nal) => {
worker.postMessage({ buf: nal, offset: 0, length: nal.length });
});
}
this._decodeThreadCount++;
} else {
const worker = new ZipDecoder();
let index = start;
worker.onerror = (e) => {
for (let i = start; i <= end; i++) {
if (i in this._promisedFrames) {
this._promisedFrames[i].reject();
delete this._promisedFrames[i];
}
}
if (this._decodingBlocks[`${start}:${end}`].rejectCallback) {
this._decodingBlocks[`${start}:${end}`].rejectCallback(Error(e));
}
this._decodeThreadCount--;
worker.terminate();
};
worker.onmessage = (event) => {
this._frames[event.data.index] = event.data.data;
if (this._decodingBlocks[`${start}:${end}`].resolveCallback) {
this._decodingBlocks[`${start}:${end}`].resolveCallback(event.data.index);
}
if (event.data.index in this._promisedFrames) {
this._promisedFrames[event.data.index].resolve(
this._frames[event.data.index],
);
delete this._promisedFrames[event.data.index];
}
if (index === end) {
worker.terminate();
delete this._decodingBlocks[`${start}:${end}`];
this._decodeThreadCount--;
}
index++;
};
worker.postMessage({ block, start, end });
this._decodeThreadCount++;
}
} finally {
release();
}
}
get decodeThreadCount() {
return this._decodeThreadCount;
}
get decodedBlocksCacheSize() {
return this._decodedBlocksCacheSize;
}
/*
Method returns a list of cached ranges
Is an array of strings like "start:end"
*/
get cachedFrames() {
return [...this._blocksRanges].sort(
(a, b) => a.split(':')[0] - b.split(':')[0],
);
}
}
module.exports = {
FrameProvider,
BlockType,
};

@ -0,0 +1,35 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:true
*/
const JSZip = require('jszip');
onmessage = (e) => {
const zip = new JSZip();
if (e.data) {
const { start, end, block } = e.data;
zip.loadAsync(block).then((_zip) => {
let index = start;
_zip.forEach((relativePath) => {
const fileIndex = index++;
if (fileIndex <= end) {
_zip.file(relativePath).async('blob').then((fileData) => {
createImageBitmap(fileData).then((img) => {
postMessage({
fileName: relativePath,
index: fileIndex,
data: img,
});
});
});
}
});
});
}
};

@ -0,0 +1,64 @@
/* global
require:true,
__dirname:true,
*/
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const cvatData = {
target: 'web',
mode: 'production',
entry: './src/js/cvat-data.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cvat-data.min.js',
library: 'cvatData',
libraryTarget: 'window',
},
module: {
rules: [
{
test: /.js?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: '> 2.5%', // https://github.com/browserslist/browserslist
}],
],
sourceType: 'unambiguous',
},
},
}, {
test: /\.worker\.js$/,
exclude: /3rdparty/,
use: {
loader: 'worker-loader',
options: {
publicPath: '/',
name: '[name].js',
},
},
}, {
test: /3rdparty\/.*\.worker\.js$/,
use: {
loader: 'worker-loader',
options: {
publicPath: '/3rdparty/',
name: '3rdparty/[name].js',
},
},
},
],
},
plugins: [
new CopyPlugin([
'./src/js/3rdparty/avc.wasm',
]),
],
};
module.exports = cvatData;

@ -0,0 +1,37 @@
# cvat-ui module
## Description
This is a client UI for Computer Vision Annotation Tool based on React, Redux and Antd
## Versioning
If you make changes in this package, please do following:
- After not important changes (typos, bug fixes, refactoring) do: ``npm version patch``
- After adding new features do: ``npm version minor``
- After significant UI redesign do: ``npm version major``
Important: If you have changed versions for ``cvat-core``, ``cvat-canvas``, ``cvat-data``,
you also need to do ``npm install`` to update ``package-lock.json``
## Commands
- Installing dependencies:
```bash
cd ../cvat-core && npm install && cd - && npm install
```
- Running development UI server with autorebuild on change
```bash
npm start
```
- Building the module from sources in the ```dist``` directory:
```bash
npm run build
npm run build -- --mode=development # without a minification
```
Important: You also have to run CVAT REST API server (please read ``CONTRIBUTING.md``)
to correct working since UI gets all necessary data (tasks, users, annotations) from there

@ -3,3 +3,4 @@
// SPDX-License-Identifier: MIT
declare module '*.svg';
declare module 'cvat-core/src/api';

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "0.5.2",
"version": "1.2.0",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
@ -23,6 +23,7 @@
"@typescript-eslint/parser": "^2.19.2",
"babel-loader": "^8.0.6",
"babel-plugin-import": "^1.12.2",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.2.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-typescript": "^7.0.0",
@ -32,8 +33,6 @@
"eslint-plugin-react": "^7.17.0",
"eslint-plugin-react-hooks": "^1.7.0",
"html-webpack-plugin": "^3.2.0",
"less": "^3.10.3",
"less-loader": "^5.0.0",
"node-sass": "^4.13.0",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
@ -42,11 +41,14 @@
"style-loader": "^1.0.0",
"tsconfig-paths-webpack-plugin": "^3.2.0",
"typescript": "^3.7.3",
"webpack": "^4.41.2",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.8",
"webpack-dev-server": "^3.8.0"
"webpack-dev-server": "^3.8.0",
"worker-loader": "^2.0.0"
},
"dependencies": {
"cvat-core": "file:../cvat-core",
"cvat-canvas": "file:../cvat-canvas",
"@types/react": "^16.9.2",
"@types/react-dom": "^16.9.0",
"@types/react-redux": "^7.1.2",
@ -57,6 +59,7 @@
"antd": "^3.25.2",
"copy-to-clipboard": "^3.2.0",
"dotenv-webpack": "^1.7.0",
"error-stack-parser": "^2.0.6",
"moment": "^2.24.0",
"prop-types": "^15.7.2",
"react": "^16.9.0",

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core';
import getCore from 'cvat-core-wrapper';
const core = getCore();

@ -18,12 +18,22 @@ import {
Task,
FrameSpeed,
Rotation,
ContextMenuType,
Workspace,
} from 'reducers/interfaces';
import getCore from 'cvat-core';
import { RectDrawingMethod } from 'cvat-canvas';
import getCore from 'cvat-core-wrapper';
import logger, { LogType } from 'cvat-logger';
import { RectDrawingMethod } from 'cvat-canvas-wrapper';
import { getCVATStore } from 'cvat-store';
interface AnnotationsParameters {
filters: string[];
frame: number;
showAllInterpolationTracks: boolean;
jobInstance: any;
}
const cvat = getCore();
let store: null | Store<CombinedState> = null;
@ -34,27 +44,46 @@ function getStore(): Store<CombinedState> {
return store;
}
function receiveAnnotationsParameters():
{ filters: string[]; frame: number; showAllInterpolationTracks: boolean } {
function receiveAnnotationsParameters(): AnnotationsParameters {
if (store === null) {
store = getCVATStore();
}
const state: CombinedState = getStore().getState();
const { filters } = state.annotation.annotations;
const frame = state.annotation.player.frame.number;
const { showAllInterpolationTracks } = state.settings.workspace;
const {
annotation: {
annotations: {
filters,
},
player: {
frame: {
number: frame,
},
},
job: {
instance: jobInstance,
},
},
settings: {
workspace: {
showAllInterpolationTracks,
},
},
} = state;
return {
filters,
frame,
jobInstance,
showAllInterpolationTracks,
};
}
function computeZRange(states: any[]): number[] {
let minZ = states.length ? states[0].zOrder : 0;
let maxZ = states.length ? states[0].zOrder : 0;
states.forEach((state: any): void => {
export function computeZRange(states: any[]): number[] {
const filteredStates = states.filter((state: any): any => state.objectType !== ObjectType.TAG);
let minZ = filteredStates.length ? filteredStates[0].zOrder : 0;
let maxZ = filteredStates.length ? filteredStates[0].zOrder : 0;
filteredStates.forEach((state: any): void => {
minZ = Math.min(minZ, state.zOrder);
maxZ = Math.max(maxZ, state.zOrder);
});
@ -62,6 +91,25 @@ function computeZRange(states: any[]): number[] {
return [minZ, maxZ];
}
async function jobInfoGenerator(job: any): Promise<Record<string, number>> {
const { total } = await job.annotations.statistics();
return {
'frame count': job.stopFrame - job.startFrame + 1,
'track count': total.rectangle.shape + total.rectangle.track
+ total.polygon.shape + total.polygon.track
+ total.polyline.shape + total.polyline.track
+ total.points.shape + total.points.track
+ total.cuboid.shape + total.cuboid.track,
'object count': total.total,
'box count': total.rectangle.shape + total.rectangle.track,
'polygon count': total.polygon.shape + total.polygon.track,
'polyline count': total.polyline.shape + total.polyline.track,
'points count': total.points.shape + total.points.track,
'cuboids count': total.cuboid.shape + total.cuboid.track,
'tag count': total.tags,
};
}
export enum AnnotationActionTypes {
GET_JOB = 'GET_JOB',
GET_JOB_SUCCESS = 'GET_JOB_SUCCESS',
@ -84,10 +132,10 @@ export enum AnnotationActionTypes {
COPY_SHAPE = 'COPY_SHAPE',
PASTE_SHAPE = 'PASTE_SHAPE',
EDIT_SHAPE = 'EDIT_SHAPE',
DRAW_SHAPE = 'DRAW_SHAPE',
REPEAT_DRAW_SHAPE = 'REPEAT_DRAW_SHAPE',
SHAPE_DRAWN = 'SHAPE_DRAWN',
RESET_CANVAS = 'RESET_CANVAS',
REMEMBER_CREATED_OBJECT = 'REMEMBER_CREATED_OBJECT',
UPDATE_ANNOTATIONS_SUCCESS = 'UPDATE_ANNOTATIONS_SUCCESS',
UPDATE_ANNOTATIONS_FAILED = 'UPDATE_ANNOTATIONS_FAILED',
CREATE_ANNOTATIONS_SUCCESS = 'CREATE_ANNOTATIONS_SUCCESS',
@ -138,11 +186,44 @@ export enum AnnotationActionTypes {
SWITCH_Z_LAYER = 'SWITCH_Z_LAYER',
ADD_Z_LAYER = 'ADD_Z_LAYER',
SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED',
CHANGE_WORKSPACE = 'CHANGE_WORKSPACE',
SAVE_LOGS_SUCCESS = 'SAVE_LOGS_SUCCESS',
SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED',
}
export function saveLogsAsync():
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>) => {
try {
await logger.save();
dispatch({
type: AnnotationActionTypes.SAVE_LOGS_SUCCESS,
payload: {},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.SAVE_LOGS_FAILED,
payload: {
error,
},
});
}
};
}
export function changeWorkspace(workspace: Workspace): AnyAction {
return {
type: AnnotationActionTypes.CHANGE_WORKSPACE,
payload: {
workspace,
},
};
}
export function addZLayer(): AnyAction {
return {
type: AnnotationActionTypes.ADD_Z_LAYER,
payload: {},
};
}
@ -155,12 +236,16 @@ export function switchZLayer(cur: number): AnyAction {
};
}
export function fetchAnnotationsAsync(sessionInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function fetchAnnotationsAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations
const {
filters,
frame,
showAllInterpolationTracks,
jobInstance,
} = receiveAnnotationsParameters();
const states = await jobInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
@ -204,79 +289,21 @@ export function changeAnnotationsFilters(filters: string[]): AnyAction {
};
}
export function undoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
// TODO: use affected IDs as an optimization
await sessionInstance.actions.undo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
dispatch({
type: AnnotationActionTypes.UNDO_ACTION_SUCCESS,
payload: {
history,
states,
minZ,
maxZ,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.UNDO_ACTION_FAILED,
payload: {
error,
},
});
}
};
}
export function redoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
// TODO: use affected IDs as an optimization
await sessionInstance.actions.redo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
dispatch({
type: AnnotationActionTypes.REDO_ACTION_SUCCESS,
payload: {
history,
states,
minZ,
maxZ,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.REDO_ACTION_FAILED,
payload: {
error,
},
});
}
};
}
export function updateCanvasContextMenu(visible: boolean, left: number, top: number): AnyAction {
export function updateCanvasContextMenu(
visible: boolean,
left: number,
top: number,
pointID: number | null = null,
type?: ContextMenuType,
): AnyAction {
return {
type: AnnotationActionTypes.UPDATE_CANVAS_CONTEXT_MENU,
payload: {
visible,
left,
top,
type,
pointID,
},
};
}
@ -331,6 +358,16 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
const frame = state.annotation.player.frame.number;
await job.annotations.upload(file, loader);
await job.logger.log(
LogType.uploadAnnotations, {
...(await jobInfoGenerator(job)),
},
);
await job.annotations.clear(true);
await job.actions.clear();
const history = await job.actions.get();
// One more update to escape some problems
// in canvas when shape with the same
// clientID has different type (polygon, rectangle) for example
@ -339,12 +376,10 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
payload: {
job,
states: [],
history,
},
});
await job.annotations.clear(true);
await job.actions.clear();
const history = await job.actions.get();
const states = await job.annotations.get(frame, showAllInterpolationTracks, filters);
setTimeout(() => {
@ -457,6 +492,9 @@ export function propagateObjectAsync(
frame: from,
};
await sessionInstance.logger.log(
LogType.propagateObject, { count: to - from + 1 },
);
const states = [];
for (let frame = from; frame <= to; frame++) {
copy.frame = frame;
@ -507,7 +545,10 @@ export function removeObjectAsync(sessionInstance: any, objectState: any, force:
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const removed = await objectState.delete(force);
await sessionInstance.logger.log(LogType.deleteObject, { count: 1 });
const { frame } = receiveAnnotationsParameters();
const removed = await objectState.delete(frame, force);
const history = await sessionInstance.actions.get();
if (removed) {
@ -542,6 +583,9 @@ export function editShape(enabled: boolean): AnyAction {
}
export function copyShape(objectState: any): AnyAction {
const job = getStore().getState().annotation.job.instance;
job.logger.log(LogType.copyObject, { count: 1 });
return {
type: AnnotationActionTypes.COPY_SHAPE,
payload: {
@ -559,11 +603,15 @@ export function selectObjects(selectedStatesID: number[]): AnyAction {
};
}
export function activateObject(activatedStateID: number | null): AnyAction {
export function activateObject(
activatedStateID: number | null,
activatedAttributeID: number | null,
): AnyAction {
return {
type: AnnotationActionTypes.ACTIVATE_OBJECT,
payload: {
activatedStateID,
activatedAttributeID,
},
};
}
@ -610,7 +658,7 @@ export function switchPlay(playing: boolean): AnyAction {
};
}
export function changeFrameAsync(toFrame: number):
export function changeFrameAsync(toFrame: number, fillBuffer?: boolean, frameStep?: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const state: CombinedState = getStore().getState();
@ -628,7 +676,13 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
payload: {
number: state.annotation.player.frame.number,
data: state.annotation.player.frame.data,
filename: state.annotation.player.frame.filename,
delay: state.annotation.player.frame.delay,
changeTime: state.annotation.player.frame.changeTime,
states: state.annotation.annotations.states,
minZ: state.annotation.annotations.zLayer.min,
maxZ: state.annotation.annotations.zLayer.max,
curZ: state.annotation.annotations.zLayer.cur,
},
});
@ -641,7 +695,13 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
payload: {},
});
const data = await job.frames.get(toFrame);
await job.logger.log(
LogType.changeFrame, {
from: frame,
to: toFrame,
},
);
const data = await job.frames.get(toFrame, fillBuffer, frameStep);
const states = await job.annotations.get(toFrame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
const currentTime = new Date().getTime();
@ -661,23 +721,71 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}
const delay = Math.max(0, Math.round(1000 / frameSpeed)
- currentTime + (state.annotation.player.frame.changeTime as number));
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: {
number: toFrame,
data,
filename: data.filename,
states,
minZ,
maxZ,
curZ: maxZ,
changeTime: currentTime + delay,
delay,
},
});
} catch (error) {
if (error !== 'not needed') {
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_FAILED,
payload: {
number: toFrame,
error,
},
});
}
}
};
}
export function undoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const state = getStore().getState();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
// TODO: use affected IDs as an optimization
const [undo] = state.annotation.annotations.history.undo.slice(-1);
const undoLog = await sessionInstance.logger.log(LogType.undoAction, {
name: undo[0],
frame: undo[1],
count: 1,
}, true);
dispatch(changeFrameAsync(undo[1]));
await sessionInstance.actions.undo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
await undoLog.close();
dispatch({
type: AnnotationActionTypes.UNDO_ACTION_SUCCESS,
payload: {
history,
states,
minZ,
maxZ,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_FAILED,
type: AnnotationActionTypes.UNDO_ACTION_FAILED,
payload: {
number: toFrame,
error,
},
});
@ -685,17 +793,77 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
};
}
export function redoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const state = getStore().getState();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
// TODO: use affected IDs as an optimization
const [redo] = state.annotation.annotations.history.redo.slice(-1);
const redoLog = await sessionInstance.logger.log(LogType.redoAction, {
name: redo[0],
frame: redo[1],
count: 1,
}, true);
dispatch(changeFrameAsync(redo[1]));
await sessionInstance.actions.redo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
await redoLog.close();
dispatch({
type: AnnotationActionTypes.REDO_ACTION_SUCCESS,
payload: {
history,
states,
minZ,
maxZ,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.REDO_ACTION_FAILED,
payload: {
error,
},
});
}
};
}
export function rotateCurrentFrame(rotation: Rotation): AnyAction {
const state: CombinedState = getStore().getState();
const { number: frameNumber } = state.annotation.player.frame;
const { startFrame } = state.annotation.job.instance;
const { frameAngles } = state.annotation.player;
const { rotateAll } = state.settings.player;
const {
annotation: {
player: {
frame: {
number: frameNumber,
},
frameAngles,
},
job: {
instance: job,
instance: {
startFrame,
},
},
},
settings: {
player: {
rotateAll,
},
},
} = state;
const frameAngle = (frameAngles[frameNumber - startFrame]
+ (rotation === Rotation.CLOCKWISE90 ? 90 : 270)) % 360;
job.logger.log(LogType.rotateImage, { angle: frameAngle });
return {
type: AnnotationActionTypes.ROTATE_FRAME,
payload: {
@ -745,11 +913,6 @@ export function getJobAsync(
initialFilters: string[],
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch({
type: AnnotationActionTypes.GET_JOB,
payload: {},
});
try {
const state: CombinedState = getStore().getState();
const filters = initialFilters;
@ -762,6 +925,20 @@ export function getJobAsync(
});
}
dispatch({
type: AnnotationActionTypes.GET_JOB,
payload: {
requestedId: jid,
},
});
const loadJobEvent = await logger.log(
LogType.loadJob, {
task_id: tid,
job_id: jid,
}, true,
);
// Check state if the task is already there
let task = state.tasks.current
.filter((_task: Task) => _task.instance.id === tid)
@ -781,17 +958,23 @@ export function getJobAsync(
const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame);
const frameData = await job.frames.get(frameNumber);
// call first getting of frame data before rendering interface
// to load and decode first chunk
await frameData.data();
const states = await job.annotations
.get(frameNumber, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
const colors = [...cvat.enums.colors];
loadJobEvent.close(await jobInfoGenerator(job));
dispatch({
type: AnnotationActionTypes.GET_JOB_SUCCESS,
payload: {
job,
states,
frameNumber,
frameFilename: frameData.filename,
frameData,
colors,
filters,
@ -799,6 +982,7 @@ export function getJobAsync(
maxZ,
},
});
dispatch(changeFrameAsync(frameNumber, false));
} catch (error) {
dispatch({
type: AnnotationActionTypes.GET_JOB_FAILED,
@ -813,12 +997,18 @@ export function getJobAsync(
export function saveAnnotationsAsync(sessionInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const { filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters();
dispatch({
type: AnnotationActionTypes.SAVE_ANNOTATIONS,
payload: {},
});
try {
const saveJobEvent = await sessionInstance.logger.log(
LogType.saveJob, {}, true,
);
await sessionInstance.annotations.save((status: string) => {
dispatch({
type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS,
@ -828,9 +1018,20 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
});
});
const states = await sessionInstance
.annotations.get(frame, showAllInterpolationTracks, filters);
await saveJobEvent.close();
await sessionInstance.logger.log(
LogType.sendTaskInfo,
await jobInfoGenerator(sessionInstance),
);
dispatch(saveLogsAsync());
dispatch({
type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS,
payload: {},
payload: {
states,
},
});
} catch (error) {
dispatch({
@ -843,24 +1044,29 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
};
}
export function drawShape(
shapeType: ShapeType,
labelID: number,
// used to reproduce the latest drawing (in case of tags just creating) by using N
export function rememberObject(
objectType: ObjectType,
labelID: number,
shapeType?: ShapeType,
points?: number,
rectDrawingMethod?: RectDrawingMethod,
): AnyAction {
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (shapeType === ShapeType.POLYGON) {
let activeControl = ActiveControl.CURSOR;
if (shapeType === ShapeType.RECTANGLE) {
activeControl = ActiveControl.DRAW_RECTANGLE;
} else if (shapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (shapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
} else if (shapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} else if (shapeType === ShapeType.CUBOID) {
activeControl = ActiveControl.DRAW_CUBOID;
}
return {
type: AnnotationActionTypes.DRAW_SHAPE,
type: AnnotationActionTypes.REMEMBER_CREATED_OBJECT,
payload: {
shapeType,
labelID,
@ -906,19 +1112,26 @@ export function splitTrack(enabled: boolean): AnyAction {
};
}
export function updateAnnotationsAsync(sessionInstance: any, frame: number, statesToUpdate: any[]):
export function updateAnnotationsAsync(statesToUpdate: any[]):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
jobInstance,
filters,
frame,
showAllInterpolationTracks,
} = receiveAnnotationsParameters();
try {
if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) {
// deactivate object to visualize changes immediately (UX)
dispatch(activateObject(null));
dispatch(activateObject(null, null));
}
const promises = statesToUpdate
.map((objectState: any): Promise<any> => objectState.save());
const states = await Promise.all(promises);
const history = await sessionInstance.actions.get();
const history = await jobInstance.actions.get();
const [minZ, maxZ] = computeZRange(states);
dispatch({
@ -931,8 +1144,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
},
});
} catch (error) {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations
const states = await jobInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
dispatch({
type: AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED,
@ -1110,8 +1322,6 @@ export function changeLabelColorAsync(
}
export function changeGroupColorAsync(
sessionInstance: any,
frameNumber: number,
group: number,
color: string,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
@ -1121,9 +1331,9 @@ export function changeGroupColorAsync(
.filter((_state: any): boolean => _state.group.id === group);
if (groupStates.length) {
groupStates[0].group.color = color;
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, groupStates));
dispatch(updateAnnotationsAsync(groupStates));
} else {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, []));
dispatch(updateAnnotationsAsync([]));
}
};
}
@ -1153,17 +1363,35 @@ export function searchAnnotationsAsync(
export function pasteShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const initialState = getStore().getState().annotation.drawing.activeInitialState;
const { instance: canvasInstance } = getStore().getState().annotation.canvas;
const {
canvas: {
instance: canvasInstance,
},
job: {
instance: jobInstance,
},
player: {
frame: {
number: frameNumber,
},
},
drawing: {
activeInitialState: initialState,
},
} = getStore().getState().annotation;
if (initialState) {
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (initialState.shapeType === ShapeType.POINTS) {
let activeControl = ActiveControl.CURSOR;
if (initialState.shapeType === ShapeType.RECTANGLE) {
activeControl = ActiveControl.DRAW_RECTANGLE;
} else if (initialState.shapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} else if (initialState.shapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (initialState.shapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
} else if (initialState.shapeType === ShapeType.CUBOID) {
activeControl = ActiveControl.DRAW_CUBOID;
}
dispatch({
@ -1174,10 +1402,20 @@ export function pasteShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction>
});
canvasInstance.cancel();
canvasInstance.draw({
enabled: true,
initialState,
});
if (initialState.objectType === ObjectType.TAG) {
const objectState = new cvat.classes.ObjectState({
objectType: ObjectType.TAG,
label: initialState.label,
attributes: initialState.attributes,
frame: frameNumber,
});
dispatch(createAnnotationsAsync(jobInstance, frameNumber, [objectState]));
} else {
canvasInstance.draw({
enabled: true,
initialState,
});
}
}
};
}
@ -1185,20 +1423,38 @@ export function pasteShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction>
export function repeatDrawShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
activeShapeType,
activeNumOfPoints,
activeRectDrawingMethod,
} = getStore().getState().annotation.drawing;
const { instance: canvasInstance } = getStore().getState().annotation.canvas;
canvas: {
instance: canvasInstance,
},
job: {
labels,
instance: jobInstance,
},
player: {
frame: {
number: frameNumber,
},
},
drawing: {
activeObjectType,
activeLabelID,
activeShapeType,
activeNumOfPoints,
activeRectDrawingMethod,
},
} = getStore().getState().annotation;
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (activeShapeType === ShapeType.POLYGON) {
let activeControl = ActiveControl.CURSOR;
if (activeShapeType === ShapeType.RECTANGLE) {
activeControl = ActiveControl.DRAW_RECTANGLE;
} else if (activeShapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} else if (activeShapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (activeShapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
} else if (activeShapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} else if (activeShapeType === ShapeType.CUBOID) {
activeControl = ActiveControl.DRAW_CUBOID;
}
dispatch({
@ -1209,12 +1465,21 @@ export function repeatDrawShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAc
});
canvasInstance.cancel();
canvasInstance.draw({
enabled: true,
rectDrawingMethod: activeRectDrawingMethod,
numberOfPoints: activeNumOfPoints,
shapeType: activeShapeType,
crosshair: activeShapeType === ShapeType.RECTANGLE,
});
if (activeObjectType === ObjectType.TAG) {
const objectState = new cvat.classes.ObjectState({
objectType: ObjectType.TAG,
label: labels.filter((label: any) => label.id === activeLabelID)[0],
frame: frameNumber,
});
dispatch(createAnnotationsAsync(jobInstance, frameNumber, [objectState]));
} else {
canvasInstance.draw({
enabled: true,
rectDrawingMethod: activeRectDrawingMethod,
numberOfPoints: activeNumOfPoints,
shapeType: activeShapeType,
crosshair: activeShapeType === ShapeType.RECTANGLE,
});
}
};
}

@ -3,7 +3,8 @@
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core';
import { UserConfirmation } from 'components/register-page/register-form';
import getCore from 'cvat-core-wrapper';
const cvat = getCore();
@ -44,13 +45,14 @@ export const registerAsync = (
email: string,
password1: string,
password2: string,
confirmations: UserConfirmation[],
): ThunkAction => async (
dispatch,
) => {
dispatch(authActions.register());
try {
await cvat.server.register(username, firstName, lastName, email, password1, password2);
await cvat.server.register(username, firstName, lastName, email, password1, password2, confirmations);
const users = await cvat.users.get({ self: true });
dispatch(authActions.registerSuccess(users[0]));

@ -0,0 +1,88 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import {
ActionUnion,
createAction,
ThunkAction,
ThunkDispatch,
} from 'utils/redux';
import getCore from 'cvat-core-wrapper';
import { LogType } from 'cvat-logger';
import { computeZRange } from './annotation-actions';
const cvat = getCore();
export enum BoundariesActionTypes {
RESET_AFTER_ERROR = 'RESET_AFTER_ERROR',
THROW_RESET_ERROR = 'THROW_RESET_ERROR',
}
export const boundariesActions = {
resetAfterError: (
job: any,
states: any[],
frameNumber: number,
frameData: any | null,
minZ: number,
maxZ: number,
colors: string[],
) => createAction(BoundariesActionTypes.RESET_AFTER_ERROR, {
job,
states,
frameNumber,
frameData,
minZ,
maxZ,
colors,
}),
throwResetError: () => createAction(BoundariesActionTypes.THROW_RESET_ERROR),
};
export function resetAfterErrorAsync(): ThunkAction {
return async (dispatch: ThunkDispatch, getState): Promise<void> => {
try {
const state = getState();
const job = state.annotation.job.instance;
if (job) {
const currentFrame = state.annotation.player.frame.number;
const { showAllInterpolationTracks } = state.settings.workspace;
const frameNumber = Math.max(Math.min(job.stopFrame, currentFrame), job.startFrame);
const states = await job.annotations
.get(frameNumber, showAllInterpolationTracks, []);
const frameData = await job.frames.get(frameNumber);
const [minZ, maxZ] = computeZRange(states);
const colors = [...cvat.enums.colors];
await job.logger.log(LogType.restoreJob);
dispatch(boundariesActions.resetAfterError(
job,
states,
frameNumber,
frameData,
minZ,
maxZ,
colors,
));
} else {
dispatch(boundariesActions.resetAfterError(
null,
[],
0,
null,
0,
0,
[],
));
}
} catch (error) {
dispatch(boundariesActions.throwResetError());
}
};
}
export type boundariesActions = ActionUnion<typeof boundariesActions>;

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core';
import getCore from 'cvat-core-wrapper';
const cvat = getCore();
@ -15,10 +15,9 @@ export enum FormatsActionTypes {
const formatsActions = {
getFormats: () => createAction(FormatsActionTypes.GET_FORMATS),
getFormatsSuccess: (annotationFormats: any[], datasetFormats: any[]) => (
getFormatsSuccess: (annotationFormats: any) => (
createAction(FormatsActionTypes.GET_FORMATS_SUCCESS, {
annotationFormats,
datasetFormats,
})
),
getFormatsFailed: (error: any) => (
@ -32,14 +31,12 @@ export function getFormatsAsync(): ThunkAction {
return async (dispatch): Promise<void> => {
dispatch(formatsActions.getFormats());
let annotationFormats = null;
let datasetFormats = null;
try {
annotationFormats = await cvat.server.formats();
datasetFormats = await cvat.server.datasetFormats();
dispatch(
formatsActions.getFormatsSuccess(annotationFormats, datasetFormats),
formatsActions.getFormatsSuccess(annotationFormats),
);
} catch (error) {
dispatch(formatsActions.getFormatsFailed(error));

@ -10,7 +10,7 @@ import {
ActiveInference,
CombinedState,
} from 'reducers/interfaces';
import getCore from 'cvat-core';
import getCore from 'cvat-core-wrapper';
export enum PreinstalledModels {
RCNN = 'RCNN Object Detector',

@ -33,6 +33,8 @@ export function checkPluginsAsync(): ThunkAction {
GIT_INTEGRATION: false,
TF_ANNOTATION: false,
TF_SEGMENTATION: false,
REID: false,
DEXTR_SEGMENTATION: false,
};
const promises: Promise<boolean>[] = [
@ -41,15 +43,13 @@ export function checkPluginsAsync(): ThunkAction {
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] = values;
[, plugins.AUTO_ANNOTATION] = values;
[,, plugins.GIT_INTEGRATION] = values;
[,,, plugins.TF_ANNOTATION] = values;
[,,,, plugins.TF_SEGMENTATION] = values;
[plugins.ANALYTICS, plugins.AUTO_ANNOTATION, plugins.GIT_INTEGRATION, plugins.TF_ANNOTATION,
plugins.TF_SEGMENTATION, plugins.DEXTR_SEGMENTATION, plugins.REID] = values;
dispatch(pluginActions.checkedAllPlugins(plugins));
};
}

@ -18,6 +18,8 @@ export enum SettingsActionTypes {
CHANGE_SELECTED_SHAPES_OPACITY = 'CHANGE_SELECTED_SHAPES_OPACITY',
CHANGE_SHAPES_COLOR_BY = 'CHANGE_SHAPES_COLOR_BY',
CHANGE_SHAPES_BLACK_BORDERS = 'CHANGE_SHAPES_BLACK_BORDERS',
CHANGE_SHAPES_SHOW_PROJECTIONS = 'CHANGE_SHAPES_SHOW_PROJECTIONS',
CHANGE_SHOW_UNLABELED_REGIONS = 'CHANGE_SHOW_UNLABELED_REGIONS',
CHANGE_FRAME_STEP = 'CHANGE_FRAME_STEP',
CHANGE_FRAME_SPEED = 'CHANGE_FRAME_SPEED',
SWITCH_RESET_ZOOM = 'SWITCH_RESET_ZOOM',
@ -27,7 +29,9 @@ export enum SettingsActionTypes {
SWITCH_AUTO_SAVE = 'SWITCH_AUTO_SAVE',
CHANGE_AUTO_SAVE_INTERVAL = 'CHANGE_AUTO_SAVE_INTERVAL',
CHANGE_AAM_ZOOM_MARGIN = 'CHANGE_AAM_ZOOM_MARGIN',
SWITCH_AUTOMATIC_BORDERING = 'SWITCH_AUTOMATIC_BORDERING',
SWITCH_SHOWNIG_INTERPOLATED_TRACKS = 'SWITCH_SHOWNIG_INTERPOLATED_TRACKS',
SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS = 'SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS',
}
export function changeShapesOpacity(opacity: number): AnyAction {
@ -66,6 +70,24 @@ export function changeShapesBlackBorders(blackBorders: boolean): AnyAction {
};
}
export function changeShowBitmap(showBitmap: boolean): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SHOW_UNLABELED_REGIONS,
payload: {
showBitmap,
},
};
}
export function changeShowProjections(showProjections: boolean): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SHAPES_SHOW_PROJECTIONS,
payload: {
showProjections,
},
};
}
export function switchRotateAll(rotateAll: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_ROTATE_ALL,
@ -200,3 +222,21 @@ export function switchShowingInterpolatedTracks(showAllInterpolationTracks: bool
},
};
}
export function switchShowingObjectsTextAlways(showObjectsTextAlways: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS,
payload: {
showObjectsTextAlways,
},
};
}
export function switchAutomaticBordering(automaticBordering: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_AUTOMATIC_BORDERING,
payload: {
automaticBordering,
},
};
}

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core';
import getCore from 'cvat-core-wrapper';
import { ShareFileInfo } from 'reducers/interfaces';

@ -9,7 +9,7 @@ import {
CombinedState,
} from 'reducers/interfaces';
import { getCVATStore } from 'cvat-store';
import getCore from 'cvat-core';
import getCore from 'cvat-core-wrapper';
import { getInferenceStatusAsync } from './models-actions';
const cvat = getCore();
@ -169,7 +169,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch(dumpAnnotation(task, dumper));
const url = await task.annotations.dump(task.name, dumper);
const url = await task.annotations.dump(dumper);
const downloadAnchor = (window.document.getElementById('downloadAnchor') as HTMLAnchorElement);
downloadAnchor.href = url;
downloadAnchor.click();
@ -280,7 +280,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
dispatch(exportDataset(task, exporter));
try {
const url = await task.annotations.exportDataset(exporter.tag);
const url = await task.annotations.exportDataset(exporter.name);
const downloadAnchor = (window.document.getElementById('downloadAnchor') as HTMLAnchorElement);
downloadAnchor.href = url;
downloadAnchor.click();
@ -389,6 +389,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
labels: data.labels,
z_order: data.advanced.zOrder,
image_quality: 70,
use_zip_chunks: data.advanced.useZipChunks,
};
if (data.advanced.bugTracker) {
@ -412,6 +413,9 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
if (data.advanced.imageQuality) {
description.image_quality = data.advanced.imageQuality;
}
if (data.advanced.dataChunkSize) {
description.data_chunk_size = data.advanced.dataChunkSize;
}
const taskInstance = new cvat.classes.Task(description);
taskInstance.clientFiles = data.files.local;

@ -0,0 +1,38 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core-wrapper';
import { UserAgreement } from 'reducers/interfaces'
const core = getCore();
export enum UserAgreementsActionTypes {
GET_USER_AGREEMENTS = 'GET_USER_AGREEMENTS',
GET_USER_AGREEMENTS_SUCCESS = 'GET_USER_AGREEMENTS_SUCCESS',
GET_USER_AGREEMENTS_FAILED = 'GET_USER_AGREEMENTS_FAILED',
}
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 }),
};
export type UserAgreementsActions = ActionUnion<typeof userAgreementsActions>;
export const getUserAgreementsAsync = (): ThunkAction => async (dispatch): Promise<void> => {
dispatch(userAgreementsActions.getUserAgreements());
try {
const userAgreements = await core.server.userAgreements();
dispatch(
userAgreementsActions.getUserAgreementsSuccess(userAgreements),
);
} catch (error) {
dispatch(userAgreementsActions.getUserAgreementsFailed(error));
}
};

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core';
import getCore from 'cvat-core-wrapper';
const core = getCore();

@ -0,0 +1 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 13L2.34921 12.2407L2 12.5401V13H3ZM3 33H2V34H3V33ZM30 33V34H30.3699L30.6508 33.7593L30 33ZM37 27L37.6508 27.7593L38 27.4599V27H37ZM37 7H38V6H37V7ZM10 7V6H9.63008L9.34921 6.24074L10 7ZM2 13V33H4V13H2ZM3 34H30V32H3V34ZM30.6508 33.7593L37.6508 27.7593L36.3492 26.2407L29.3492 32.2407L30.6508 33.7593ZM38 27V7H36V27H38ZM36.3492 6.24074L29.3492 12.2407L30.6508 13.7593L37.6508 7.75926L36.3492 6.24074ZM30 12H3V14H30V12ZM31 33V13H29V33H31ZM3.65079 13.7593L10.6508 7.75926L9.34921 6.24074L2.34921 12.2407L3.65079 13.7593ZM10 8H37V6H10V8Z" fill="black"/></svg>

After

Width:  |  Height:  |  Size: 660 B

@ -4,13 +4,8 @@
import './styles.scss';
import React from 'react';
import {
Menu,
Modal,
} from 'antd';
import { ClickParam } from 'antd/lib/menu/index';
import Menu, { ClickParam } from 'antd/lib/menu';
import Modal from 'antd/lib/modal';
import DumpSubmenu from './dump-submenu';
import LoadSubmenu from './load-submenu';
@ -23,7 +18,6 @@ interface Props {
loaders: string[];
dumpers: string[];
exporters: string[];
loadActivity: string | null;
dumpActivities: string[] | null;
exportActivities: string[] | null;
@ -58,7 +52,6 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
dumpers,
loaders,
exporters,
onClickMenu,
dumpActivities,
exportActivities,
@ -138,7 +131,7 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
}
{
ExportSubmenu({
exporters,
exporters: dumpers,
exportActivities,
menuKey: Actions.EXPORT_TASK_DATASET,
})

@ -3,17 +3,13 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import {
Menu,
Icon,
} from 'antd';
import Menu from 'antd/lib/menu';
import Icon from 'antd/lib/icon';
import Text from 'antd/lib/typography/Text';
function isDefaultFormat(dumperName: string, taskMode: string): boolean {
return (dumperName === 'CVAT XML 1.1 for videos' && taskMode === 'interpolation')
|| (dumperName === 'CVAT XML 1.1 for images' && taskMode === 'annotation');
return (dumperName === 'CVAT for video 1.1' && taskMode === 'interpolation')
|| (dumperName === 'CVAT for images 1.1' && taskMode === 'annotation');
}
interface Props {

@ -3,12 +3,8 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import {
Menu,
Icon,
} from 'antd';
import Menu from 'antd/lib/menu';
import Icon from 'antd/lib/icon';
import Text from 'antd/lib/typography/Text';
interface Props {

@ -3,14 +3,10 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import {
Menu,
Icon,
Upload,
Button,
} from 'antd';
import Menu from 'antd/lib/menu';
import Icon from 'antd/lib/icon';
import Upload from 'antd/lib/upload';
import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text';
interface Props {

@ -3,32 +3,48 @@
// SPDX-License-Identifier: MIT
import './styles.scss';
import React from 'react';
import {
Layout,
Spin,
Result,
} from 'antd';
import React, { useEffect } from 'react';
import Layout from 'antd/lib/layout';
import Spin from 'antd/lib/spin';
import Result from 'antd/lib/result';
import { Workspace } from 'reducers/interfaces';
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
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';
interface Props {
job: any | null | undefined;
fetching: boolean;
getJob(): void;
saveLogs(): void;
workspace: Workspace;
}
export default function AnnotationPageComponent(props: Props): JSX.Element {
const {
job,
fetching,
getJob,
saveLogs,
workspace,
} = props;
useEffect(() => {
saveLogs();
const root = window.document.getElementById('root');
if (root) {
root.style.minHeight = '768px';
}
return () => {
saveLogs();
if (root) {
root.style.minHeight = '';
}
};
}, []);
if (job === null) {
if (!fetching) {
@ -51,8 +67,18 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
return (
<Layout className='cvat-annotation-page'>
<AnnotationTopBarContainer />
<StandardWorkspaceComponent />
<Layout.Header className='cvat-annotation-header'>
<AnnotationTopBarContainer />
</Layout.Header>
{ workspace === Workspace.STANDARD ? (
<Layout.Content>
<StandardWorkspaceComponent />
</Layout.Content>
) : (
<Layout.Content>
<AttributeAnnotationWorkspace />
</Layout.Content>
)}
<StatisticsModalContainer />
</Layout>
);

@ -0,0 +1,189 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState } from 'react';
import { connect } from 'react-redux';
import Select, { SelectValue, LabeledValue } from 'antd/lib/select';
import Title from 'antd/lib/typography/Title';
import Text from 'antd/lib/typography/Text';
import Paragraph from 'antd/lib/typography/Paragraph';
import Tooltip from 'antd/lib/tooltip';
import Modal from 'antd/lib/modal';
import Icon from 'antd/lib/icon';
import {
changeAnnotationsFilters as changeAnnotationsFiltersAction,
fetchAnnotationsAsync,
} from 'actions/annotation-actions';
import { CombinedState } from 'reducers/interfaces';
interface StateToProps {
annotationsFilters: string[];
annotationsFiltersHistory: string[];
searchForwardShortcut: string;
searchBackwardShortcut: string;
}
interface DispatchToProps {
changeAnnotationsFilters(value: SelectValue): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
annotations: {
filters: annotationsFilters,
filtersHistory: annotationsFiltersHistory,
},
},
shortcuts: {
normalizedKeyMap,
},
} = state;
return {
annotationsFilters,
annotationsFiltersHistory,
searchForwardShortcut: normalizedKeyMap.SEARCH_FORWARD,
searchBackwardShortcut: normalizedKeyMap.SEARCH_BACKWARD,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
changeAnnotationsFilters(value: SelectValue) {
if (typeof (value) === 'string') {
dispatch(changeAnnotationsFiltersAction([value]));
dispatch(fetchAnnotationsAsync());
} else if (Array.isArray(value)
&& value.every((element: string | number | LabeledValue): boolean => (
typeof (element) === 'string'
))
) {
dispatch(changeAnnotationsFiltersAction(value as string[]));
dispatch(fetchAnnotationsAsync());
}
},
};
}
function filtersHelpModalContent(
searchForwardShortcut: string,
searchBackwardShortcut: string,
): JSX.Element {
return (
<>
<Paragraph>
<Title level={3}>General</Title>
</Paragraph>
<Paragraph>
You can use filters to display only subset of objects on a frame
or to search objects that satisfy the filters using hotkeys
<Text strong>
{` ${searchForwardShortcut} `}
</Text>
and
<Text strong>
{` ${searchBackwardShortcut} `}
</Text>
</Paragraph>
<Paragraph>
<Text strong>Supported properties: </Text>
width, height, label, serverID, clientID, type, shape, occluded
<br />
<Text strong>Supported operators: </Text>
==, !=, &gt;, &gt;=, &lt;, &lt;=, (), &amp; and |
<br />
<Text strong>
If you have double quotes in your query string,
please escape them using back slash: \&quot; (see the latest example)
</Text>
<br />
All properties and values are case-sensitive.
CVAT uses json queries to perform search.
</Paragraph>
<Paragraph>
<Title level={3}>Examples</Title>
<ul>
<li>label==&quot;car&quot; | label==[&quot;road sign&quot;]</li>
<li>shape == &quot;polygon&quot;</li>
<li>width &gt;= height</li>
<li>attr[&quot;Attribute 1&quot;] == attr[&quot;Attribute 2&quot;]</li>
<li>clientID == 50</li>
<li>
(label==&quot;car&quot; &amp; attr[&quot;parked&quot;]==true)
| (label==&quot;pedestrian&quot; &amp; width &gt; 150)
</li>
<li>
(( label==[&quot;car \&quot;mazda\&quot;&quot;])
&amp; (attr[&quot;sunglasses&quot;]==true
| (width &gt; 150 | height &gt; 150 &amp; (clientID == serverID)))))
</li>
</ul>
</Paragraph>
</>
);
}
function AnnotationsFiltersInput(props: StateToProps & DispatchToProps): JSX.Element {
const {
annotationsFilters,
annotationsFiltersHistory,
searchForwardShortcut,
searchBackwardShortcut,
changeAnnotationsFilters,
} = props;
const [underCursor, setUnderCursor] = useState(false);
return (
<Select
className='cvat-annotations-filters-input'
allowClear
value={annotationsFilters}
mode='tags'
style={{ width: '100%' }}
placeholder={
underCursor ? (
<>
<Tooltip title='Click to open help'>
<Icon
type='filter'
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
Modal.info({
width: 700,
title: 'How to use filters?',
content: filtersHelpModalContent(
searchForwardShortcut,
searchBackwardShortcut,
),
});
}}
/>
</Tooltip>
</>
) : (
<>
<Icon style={{ transform: 'scale(0.9)' }} type='filter' />
<span style={{ marginLeft: 5 }}>Annotations filters</span>
</>
)
}
onChange={changeAnnotationsFilters}
onMouseEnter={() => setUnderCursor(true)}
onMouseLeave={() => setUnderCursor(false)}
>
{annotationsFiltersHistory.map((element: string): JSX.Element => (
<Select.Option key={element} value={element}>{element}</Select.Option>
))}
</Select>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(AnnotationsFiltersInput);

@ -0,0 +1,320 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
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 { LogType } from 'cvat-logger';
import {
activateObject as activateObjectAction,
updateAnnotationsAsync,
} from 'actions/annotation-actions';
import { CombinedState } from 'reducers/interfaces';
import AnnotationsFiltersInput from 'components/annotation-page/annotations-filters-input';
import ObjectSwitcher from './object-switcher';
import AttributeSwitcher from './attribute-switcher';
import ObjectBasicsEditor from './object-basics-edtior';
import AttributeEditor from './attribute-editor';
interface StateToProps {
activatedStateID: number | null;
activatedAttributeID: number | null;
states: any[];
labels: any[];
jobInstance: any;
keyMap: Record<string, ExtendedKeyMapOptions>;
normalizedKeyMap: Record<string, string>;
}
interface DispatchToProps {
activateObject(clientID: number | null, attrID: number | null): void;
updateAnnotations(statesToUpdate: any[]): void;
}
interface LabelAttrMap {
[index: number]: any;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
annotations: {
activatedStateID,
activatedAttributeID,
states,
},
job: {
instance: jobInstance,
labels,
},
},
shortcuts: {
keyMap,
normalizedKeyMap,
},
} = state;
return {
jobInstance,
labels,
activatedStateID,
activatedAttributeID,
states,
keyMap,
normalizedKeyMap,
};
}
function mapDispatchToProps(dispatch: ThunkDispatch<CombinedState, {}, Action>): DispatchToProps {
return {
activateObject(clientID: number, attrID: number): void {
dispatch(activateObjectAction(clientID, attrID));
},
updateAnnotations(states): void {
dispatch(updateAnnotationsAsync(states));
},
};
}
function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Element {
const {
labels,
states,
activatedStateID,
activatedAttributeID,
jobInstance,
updateAnnotations,
activateObject,
keyMap,
normalizedKeyMap,
} = props;
const [labelAttrMap, setLabelAttrMap] = useState(
labels.reduce((acc, label): LabelAttrMap => {
acc[label.id] = label.attributes.length ? label.attributes[0] : null;
return acc;
}, {}),
);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [activeObjectState] = activatedStateID === null
? [null] : states.filter((objectState: any): boolean => (
objectState.clientID === activatedStateID
));
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);
}
} 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);
let nextIndex = index + step;
if (nextIndex > states.length - 1) {
nextIndex = 0;
} else if (nextIndex < 0) {
nextIndex = states.length - 1;
}
if (nextIndex !== index) {
const attribute = labelAttrMap[states[nextIndex].label.id];
activateObject(states[nextIndex].clientID, attribute ? attribute.id : null);
}
}
};
const nextAttribute = (step: number): void => {
if (activeObjectState) {
const { label } = activeObjectState;
const { attributes } = label;
if (attributes.length) {
const index = attributes.indexOf(activeAttribute);
let nextIndex = index + step;
if (nextIndex > attributes.length - 1) {
nextIndex = 0;
} else if (nextIndex < 0) {
nextIndex = attributes.length - 1;
}
if (index !== nextIndex) {
const updatedLabelAttrMap = { ...labelAttrMap };
updatedLabelAttrMap[label.id] = attributes[nextIndex];
setLabelAttrMap(updatedLabelAttrMap);
}
}
}
};
useEffect(() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}, []);
const siderProps: SiderProps = {
className: 'attribute-annotation-sidebar',
theme: 'light',
width: 300,
collapsedWidth: 0,
reverseArrow: true,
collapsible: true,
trigger: null,
collapsed: sidebarCollapsed,
};
const subKeyMap = {
NEXT_ATTRIBUTE: keyMap.NEXT_ATTRIBUTE,
PREVIOUS_ATTRIBUTE: keyMap.PREVIOUS_ATTRIBUTE,
NEXT_OBJECT: keyMap.NEXT_OBJECT,
PREVIOUS_OBJECT: keyMap.PREVIOUS_OBJECT,
};
const handlers = {
NEXT_ATTRIBUTE: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
nextAttribute(1);
},
PREVIOUS_ATTRIBUTE: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
nextAttribute(-1);
},
NEXT_OBJECT: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
nextObject(1);
},
PREVIOUS_OBJECT: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
nextObject(-1);
},
};
if (activeObjectState) {
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={() => setSidebarCollapsed(!sidebarCollapsed)}
>
{sidebarCollapsed ? <Icon type='menu-fold' title='Show' />
: <Icon type='menu-unfold' title='Hide' />}
</span>
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} allowChanges />
<Row className='cvat-objects-sidebar-filter-input'>
<Col>
<AnnotationsFiltersInput />
</Col>
</Row>
<ObjectSwitcher
currentLabel={activeObjectState.label.name}
clientID={activeObjectState.clientID}
occluded={activeObjectState.occluded}
objectsCount={states.length}
currentIndex={states.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
.filter((_label): boolean => _label.name === labelName);
activeObjectState.label = newLabel;
updateAnnotations([activeObjectState]);
}}
setOccluded={(event: CheckboxChangeEvent): void => {
activeObjectState.occluded = event.target.checked;
updateAnnotations([activeObjectState]);
}}
/>
{
activeAttribute
? (
<>
<AttributeSwitcher
currentAttribute={activeAttribute.name}
currentIndex={activeObjectState.label.attributes
.indexOf(activeAttribute)}
attributesCount={activeObjectState.label.attributes.length}
normalizedKeyMap={normalizedKeyMap}
nextAttribute={nextAttribute}
/>
<AttributeEditor
attribute={activeAttribute}
currentValue={activeObjectState.attributes[activeAttribute.id]}
onChange={(value: string) => {
const { attributes } = activeObjectState;
jobInstance.logger.log(
LogType.changeAttribute, {
id: activeAttribute.id,
object_id: activeObjectState.clientID,
value,
},
);
attributes[activeAttribute.id] = value;
activeObjectState.attributes = attributes;
updateAnnotations([activeObjectState]);
}}
/>
</>
) : (
<div className='attribute-annotations-sidebar-not-found-wrapper'>
<Text strong>No attributes found</Text>
</div>
)
}
</Layout.Sider>
);
}
return (
<Layout.Sider {...siderProps}>
<div className='attribute-annotations-sidebar-not-found-wrapper'>
<Text strong>No objects found</Text>
</div>
</Layout.Sider>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(AttributeAnnotationSidebar);

@ -0,0 +1,284 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import Text from 'antd/lib/typography/Text';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
import Select, { SelectValue } from 'antd/lib/select';
import Radio, { RadioChangeEvent } from 'antd/lib/radio';
import Input from 'antd/lib/input';
import consts from 'consts';
interface InputElementParameters {
attrID: number;
inputType: string;
values: string[];
currentValue: string;
onChange(value: string): void;
}
function renderInputElement(parameters: InputElementParameters): JSX.Element {
const {
inputType,
attrID,
values,
currentValue,
onChange,
} = parameters;
const renderCheckbox = (): JSX.Element => (
<>
<Text strong>Checkbox: </Text>
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Checkbox
onChange={(event: CheckboxChangeEvent): void => (
onChange(event.target.checked ? 'true' : 'false')
)}
checked={currentValue === 'true'}
/>
</div>
</>
);
const renderSelect = (): JSX.Element => (
<>
<Text strong>Values: </Text>
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Select
value={currentValue}
style={{ width: '80%' }}
onChange={(value: SelectValue) => (
onChange(value as string)
)}
>
{values.map((value: string): JSX.Element => (
<Select.Option key={value} value={value}>
{value === consts.UNDEFINED_ATTRIBUTE_VALUE
? consts.NO_BREAK_SPACE : value}
</Select.Option>
))}
</Select>
</div>
</>
);
const renderRadio = (): JSX.Element => (
<>
<Text strong>Values: </Text>
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Radio.Group
value={currentValue}
onChange={(event: RadioChangeEvent) => (
onChange(event.target.value)
)}
>
{values.map((value: string): JSX.Element => (
<Radio style={{ display: 'block' }} key={value} value={value}>
{value === consts.UNDEFINED_ATTRIBUTE_VALUE
? consts.NO_BREAK_SPACE : value}
</Radio>
))}
</Radio.Group>
</div>
</>
);
const handleKeydown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
if (['ArrowDown', 'ArrowUp', 'ArrowLeft',
'ArrowRight', 'Tab', 'Shift', 'Control']
.includes(event.key)
) {
event.preventDefault();
const copyEvent = new KeyboardEvent('keydown', event);
window.document.dispatchEvent(copyEvent);
}
};
const renderText = (): JSX.Element => (
<>
{inputType === 'number' ? <Text strong>Number: </Text> : <Text strong>Text: </Text>}
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Input
autoFocus
key={attrID}
defaultValue={currentValue}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
if (inputType === 'number') {
if (value !== '') {
const numberValue = +value;
if (!Number.isNaN(numberValue)) {
onChange(`${numberValue}`);
}
}
} else {
onChange(value);
}
}}
onKeyDown={handleKeydown}
/>
</div>
</>
);
let element = null;
if (inputType === 'checkbox') {
element = renderCheckbox();
} else if (inputType === 'select') {
element = renderSelect();
} else if (inputType === 'radio') {
element = renderRadio();
} else {
element = renderText();
}
return (
<div className='attribute-annotation-sidebar-attr-editor'>
{element}
</div>
);
}
interface ListParameters {
inputType: string;
values: string[];
onChange(value: string): void;
}
function renderList(parameters: ListParameters): JSX.Element | null {
const { inputType, values, onChange } = parameters;
if (inputType === 'checkbox') {
const sortedValues = ['true', 'false'];
if (values[0].toLowerCase() !== 'true') {
sortedValues.reverse();
}
const keyMap: KeyMap = {};
const handlers: {
[key: string]: (keyEvent?: KeyboardEvent) => void;
} = {};
sortedValues.forEach((value: string, index: number): void => {
const key = `SET_${index}_VALUE`;
keyMap[key] = {
name: `Set value "${value}"`,
description: `Change current value for the attribute to "${value}"`,
sequence: `${index}`,
action: 'keydown',
};
handlers[key] = (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
onChange(value);
};
});
return (
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers} allowChanges />
<div>
<Text strong>0:</Text>
<Text>{` ${sortedValues[0]}`}</Text>
</div>
<div>
<Text strong>1:</Text>
<Text>{` ${sortedValues[1]}`}</Text>
</div>
</div>
);
}
if (inputType === 'radio' || inputType === 'select') {
const keyMap: KeyMap = {};
const handlers: {
[key: string]: (keyEvent?: KeyboardEvent) => void;
} = {};
const filteredValues = values
.filter((value: string): boolean => value !== consts.UNDEFINED_ATTRIBUTE_VALUE);
filteredValues.slice(0, 10).forEach((value: string, index: number): void => {
const key = `SET_${index}_VALUE`;
keyMap[key] = {
name: `Set value "${value}"`,
description: `Change current value for the attribute to "${value}"`,
sequence: `${index}`,
action: 'keydown',
};
handlers[key] = (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
onChange(value);
};
});
return (
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers} allowChanges />
{filteredValues.map((value: string, index: number): JSX.Element => (
<div key={value}>
<Text strong>{`${index}:`}</Text>
<Text>{` ${value}`}</Text>
</div>
))}
</div>
);
}
if (inputType === 'number') {
return (
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
<div>
<Text strong>From:</Text>
<Text>{` ${values[0]}`}</Text>
</div>
<div>
<Text strong>To:</Text>
<Text>{` ${values[1]}`}</Text>
</div>
<div>
<Text strong>Step:</Text>
<Text>{` ${values[2]}`}</Text>
</div>
</div>
);
}
return null;
}
interface Props {
attribute: any;
currentValue: string;
onChange(value: string): void;
}
function AttributeEditor(props: Props): JSX.Element {
const { attribute, currentValue, onChange } = props;
const { inputType, values, id: attrID } = attribute;
return (
<div>
{renderList({ values, inputType, onChange })}
<hr />
{renderInputElement({
attrID,
inputType,
currentValue,
values,
onChange,
})}
</div>
);
}
export default React.memo(AttributeEditor);

@ -0,0 +1,49 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Icon from 'antd/lib/icon';
import Text from 'antd/lib/typography/Text';
import Tooltip from 'antd/lib/tooltip';
import Button from 'antd/lib/button';
interface Props {
currentAttribute: string;
currentIndex: number;
attributesCount: number;
normalizedKeyMap: Record<string, string>;
nextAttribute(step: number): void;
}
function AttributeSwitcher(props: Props): JSX.Element {
const {
currentAttribute,
currentIndex,
attributesCount,
nextAttribute,
normalizedKeyMap,
} = props;
const title = `${currentAttribute} [${currentIndex + 1}/${attributesCount}]`;
return (
<div className='attribute-annotation-sidebar-switcher'>
<Tooltip title={`Previous attribute ${normalizedKeyMap.PREVIOUS_ATTRIBUTE}`}>
<Button disabled={attributesCount <= 1} onClick={() => nextAttribute(-1)}>
<Icon type='left' />
</Button>
</Tooltip>
<Tooltip title={title}>
<Text className='cvat-text'>{currentAttribute}</Text>
<Text strong>{` [${currentIndex + 1}/${attributesCount}]`}</Text>
</Tooltip>
<Tooltip title={`Next attribute ${normalizedKeyMap.NEXT_ATTRIBUTE}`}>
<Button disabled={attributesCount <= 1} onClick={() => nextAttribute(1)}>
<Icon type='right' />
</Button>
</Tooltip>
</div>
);
}
export default React.memo(AttributeSwitcher);

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

Loading…
Cancel
Save