Release 0.6.0 (#1238)

* Release 0.5 (#705)

* Changed version number (0, 5, 'final', 0).
* Updated changelog file.
* fixed default attribute values for tracked shapes (#703)

* typo ?

Should not this be cvat_redis -> redis ?

* Fixed labels regex for non-latin characters (#708)

* Update README.md

* Update README.md

* Don't save shapes with keyframe==False

* Selecting non images leads to 400 error (#734)

* Fix HTTP 400 error if together with vision data the user submit non-vision data (e.g. text files)
* Ignore SVG images because Pillow doesn't work with them.

* Fix the problem with duplicated frames in case of "share" (#735)

* Fix the problem with duplicated frames in case of "share".
* Fix a case when the code works incorrectly

/a/b/c
/a/b/c0

Previously only /a/b/c will be in output but should be both.

* added method docs to Auto Annotation inference.py (#725)

* remove deprecated method call `from_ir` (#726)

* New command line tool for working with tasks (#732)

* Adding new command line tool for performing common task related
operations (create, list, delete, etc.)
* Replaced @exception decorator with try/except in main()
* Replaced optional --name with positional name and removed default
* Added license text to files
* Added django units to cover future API changes
* Refactored into submodules to better support tests

* Fix an issue with permissions (observer can change annotations) (#745)

* Fixed a problem with observer (check_object_permissions method was not called)
* Added a test case to cover issue #712.

* COCO Annotation IDs should begin with 1 (#748)

Currently the annotation ID begins with 0 which is interpreted by cocoapi as a false detection. The array dtm saves the matches via the ground truth annotation ID. The variable dtm is initialized as an array of zeros. 636becdc73/PythonAPI/pycocotools/cocoeval.py (L269)
636becdc73/PythonAPI/pycocotools/cocoeval.py (L295)
636becdc73/PythonAPI/pycocotools/cocoeval.py (L375)

* Slightly enhance command line interface feature (#746)

* Slightly enhance command line interface feature.
Added README.md, run tests using travis, run CLI tests from VS code.
* Removed formatted string due to a limitation on our python version inside the container.
* Add information about command line interface to the main page.

* Projects (server only, REST API) (#754)

* Initial version of projects
* Added tests for Projects REST API.
* Added information about projects into CHANGELOG

* Updating string format for case missed in PR #746. (#757)

* add robust JSON handeling for auto annotation runner (#758)

* Basic user information (#761)

* Fix https://github.com/opencv/cvat/issues/750
* Updated CHANGELOG
* Added more tests for /api/v1/users* REST API.

* Disable fix_segments_intersections for now (#751)

* Disable fix_segments_intersections for now

When the bounding boxes had intersections and were exported with the COCO JSON format they were often cut off. I commented out the line with the function fix_segments_intersections and replaced it with lines of that function. This helped with the bounding boxes and keeps the masks as they are created with CVAT. It is probably inconvenient for the user to get something fixed in the export without an active agreement of the user. Secondly letting a function automatically fix segments could result in a bad fix.

* Use fix_segments_intersections only with z-order

The fix_segments_intersections will only be used when the z-order flag is set. This is useful for bounding boxes or masks which don't need to be fixed. This fix was created according to Andrey Zhavoronkov's (@azhavoro) advice.

* Added information about a fixed issue. (#765)

* Add more information into questions section (#766)

* User interface with react and antd (#755)

* Login page, router
* Registration
* Tasks view

* add in serializing check in auto annotation model runner (#770)

* allow security segmentation models to be used in auto annotation (#759)

* Integration with Zenodo (#779)

* Updated CHANGELOG with information about Zenodo
* Updated version of the project.

* Fixed a case when a task's owner can be undefined. (#782)

* Added `restart` tag to docker-compose for `cvat_ui` (#789)

* User interface with React and antd (#785)

* Dump & refactoring
* Upload annotations, cvat-core from sources
* Added download icon
* Added icon

* Update documentation to point to OpenVino component documentation (#752)

* Change the version of OpenVINO compatibility (#797)

* Change the version of OpenVINO compatibility

* added mask RCNN script (#780)

* added in yolo auto annotation sciprt (#794)

* Annotation formats documentation (#719)

* added handling of truncated and difficult attributes for pascal voc
loader/dumper
added descriptions of supported annotation formats
* added YOLO example
* made match_frame as Annotations method
changed 'image/source_id' field TF feature from int64 to string
(according to TF OD API dataset utlis)
* updated README
improved match_frame function
* added unit tests for dump/load

* added in semantic segmentation instructions to README (#804)

* fix off by one error in mask rcnn (#801)

* Fix Yolo: swap width, height; Change box coord order; parsing fix (#802)

* Auto segmentation using Mask_RCNN (#767)

* Update CHANGELOG.md

* Bump pillow from 5.1.0 to 6.2.0 in /cvat/requirements (#808)

Bumps [pillow](https://github.com/python-pillow/Pillow) from 5.1.0 to 6.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/5.1.0...6.2.0)

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

* Bump pillow from 5.3.0 to 6.2.0 in /utils/cli (#807)

Bumps [pillow](https://github.com/python-pillow/Pillow) from 5.3.0 to 6.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/5.3.0...6.2.0)

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

* Bump eslint-utils from 1.4.0 to 1.4.3 in /cvat-canvas (#809)

Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.4.0 to 1.4.3.
- [Release notes](https://github.com/mysticatea/eslint-utils/releases)
- [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.4.0...v1.4.3)

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

* fix serialize bug when using AutoAnnotation runner (#810)

* User interface with React and antd (#811)

* Fixed links for analytics and help
* Delete task functionality
* Added navigation for create and open task
* Added icon for help
* Added easy plugin checker
* Header dependes on installed plugins
* Menu depends on installed plugins
* Shared actions menu component, base layout for task page
* Task page based (relations with redux, base layout)
* Added attribute form
* Finished label creator
* Added jobs table
* Added job assignee
* Save updated labels on server
* Added imports plugin, updated webpack
* Editable bug tracker
* Clean task update
* Change assignee

* Fix login problem (unathorized user cannot login). (#812)

* Fix upload anno for COCO (#788)

* COCO: load bbox as rectangle if segmentation field is empty
* added unit test for coco format (case: object segment field is empty)

* Add support for ip git repo urls (#827)

* Add support for ip v4 git repo urls
* Add tests for git urls

* React & Antd UI: Create task (#840)

* Separated component user selector
* Change job assignee
* Basic create task window
* Bug fixes and refactoring
* Create task connected with a server
* Loading status for a button
* Reset loading on error response
* UI improvements
* Github/feedback/share window

* added in new interp files for pixel link v0004 (#852)

* Add LabelMe format support (#844)

* Add labelme export
* Add LabelMe import
* Add labelme format to readme
* Updated CHANGELOG.md

* Adding dump and load support for MOT CSV format. (#830)

* Adding dump and load support for MOT CSV format.
* Updated test cases to use correct track annotations for MOT format.
* Removed behaviour of MOT loader which would duplicate the last track
shape prior to setting outside=True.

* Add dataset export facility (#813)

* Add datumaro django application
* Add cvat task datumaro bindings
* Add REST api for task export
* Add scheduler service
* Updated CHANGELOG.md

* Mit license for pixellink and changelog (#862)

* React & Antd UI: Model manager (#856)

* Supported git to create and sync
* Updated antd
* Updated icons
* Improved header
* Top bar for models & empty models list
* Removed one extra reducer and actions
* Removed one extra reducer and actions
* Crossplatform css
* Models reducers, some models actions, base for model list, imrovements
* Models list, ability to delete models
* Added ability to upload models
* Improved form, reinit models after create
* Removed some importants in css
* Model running dialog window, a lot of fixes

* Add a dataset export button for tasks (#834)

* Add dataset export button for tasks in dashboard
* Fix downloading, shrink list of export formats
* Add strict export format check
* Add strict export format check
* Change REST api paths
* Move formats declarations to server,

* Coco converter updates (#864)

* [Datumaro] Fix coco images export (#875)

* Update test
* Fix export
* Support several image paths in coco extractor

* [Datumaro] Disable lazy image caching by default (#876)

* Disable lazy image caching by default
* Deterministic cache test
* Add displacing image cache

* React & Antd UI: Export dataset, refactoring & fixes (#872)

* Automatic label matching (by the same name) in model running window
* Improved create task window
* Improved upload model window
* Fixed: error window showed twice
* Updated CONTRIBUTING.md
* Removed token before login, fixed dump submenu (adjustment), fixed case when empty models list displayed
* Export as dataset, better error showing system
* Removed extra requests, improved UI
* Fixed a name of a format
* Show inference progress
* Fixed model loading after a model was uploaded

* Fix redirect (#878)

* Add cvat cli to datumaro project export (#870)

* Configurable REST for UI, minor improvements (#880)

* [Datumaro] Pip installation (#881)

* Add version file
* Remove unnecessary dependencies
* Add lxml use motivation
* Add pip setup script
* Reduce opencv dependency
* Fix cli command
* Codacy

* page_size parameter for all REST API methods (#884)

* Added page_size parameter for all REST API methods which returns list of objects.

Also it is possible to specify page_size=all to return all elements.

* Updated changelog.md

* VOC converter: Use depth from CVAT XML if available (#885)

* Token auth for non-REST API apps (#889)

* Token authorization for non REST API apps (e.g. git, tf annotation, tf segmentation)

* set CORS_REPLACE_HTTPS_REFERER option to True (#895)

* Fix some spelling (#897)

* React  & Antd: Dashboard migration (#892)

* Removed old dashboard
* Getting all users
* Updated changelog
* Reimplemented login decorator
* Implicit host, scheme in docker-compose
* Fixed issue with pagination
* Implicit page size parameter for tasks
* Fixed linkedin icon, added links to tasks in notifications
* Configurable method for check plugin

* Bump django from 2.2.4 to 2.2.8 in /cvat/requirements (#902)

Bumps [django](https://github.com/django/django) from 2.2.4 to 2.2.8.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.4...2.2.8)

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

* Az/fix meta requests (#903)

* fixed processing of meta requests

* Fixed some issues with dump (#904)

* Changed method for downloading annotations

* Initial commit

* Initial commit

* Updated download method for dataset

* fixed eslint error

* Restore session id (#905)

* Restore session id when we use token authorization.

* UI eslint fixes (#908)

* Installed airbnb fullsettings
* Fixed actions menu
* Create model/task page
* File manager, header
* Labels editor
* Login, register
* Models page & model runner
* Tasks page
* Feedback and base app
* Tasks page
* Containers
* Reducers
* Fixed additional issues
* Small pagination fix

* implemented adas semantic segmentation

* Copy JOB info to clibpard

* Yolov3 interpretration script fix for 'Annotation failed' and changes to mapping.json (#896) (#912)

* [Datumaro] Add YOLO converter (#906)

* Add YOLO converter
* Added yolo extractor
* Added YOLO format test
* Add YOLO export in UI

* Added padding

* Remove deprecated html attributes (#924)

* Updated message

* Improved some hints

* Added 3rdparty library to clipboard

* Updated doc

* Added ability to copy labels without IDs

* Removed extra lines

* Updated contributing

* Updated contributing

* Task name displayed better

* Improved tasks routing

* Ability to show hidden task

* Destroy messages before getting new tasks

* Fixed eslint

* Names of selected files when creating a new task

* [Datumaro] Added tf detection api tfrecord import and export (#894)

* Added tf detection api tfrecord import and export
* Added export button in dashboard
* Add tf to requirements
* Extend test
* Add tf dependency
* Require images in tfrecord export
* Add video task case handling
* Maintain image order in CVAT export
* Fix Task image id-path conversions
* Update tfrecord tests
* Extend image utilities
* Update tfrecord format
* Fix image loading bug
* Add some logs
* Add 'copy' option to project import command
* Reduce default cache size

* Improve UX with creating new shape by shortkey (#941)

* Fixed command in CONTRIBUTING.md (#947)

* Fixed command in CONTRIBUTING.md

* Removed daemon, updated command

* [Datumaro] COCO 'merge instance polygons' option (#938)

* Add polygon merging option to coco converter
* Add test, refactor coco, add support for cli args
* Drop colormap application in datumaro format
* Add cli support in voc converter
* Add cli support in yolo converter
* Add converter cli options in project cli
* Add image data type conversion in image saving

* [Datumaro] Fix voc colormap (#945)

* Add polygon merging option to coco converter

* Add test, refactor coco, add support for cli args

* Drop colormap application in datumaro format

* Add cli support in voc converter

* Add cli support in yolo converter

* Add converter cli options in project cli

* Add image data type conversion in image saving

* Add image data type conversion in image saving

* Update mask support in voc

* Replace null with quotes in coco export

* Improve cli

* Enable Datumaro intellisense in vs cde

* Adjust fields in voc detection export

* Return pylint to config (#951)

* Update docker base images (#950)

Don't fix minor/patch version to get security updates and bug fixes.

* Fixed git plugin (#961)

* Add upload annotation function to cli (#958)

* add upload annotation function to cli

* Update core.py

Removing whitespace

* React, Antd, Redux: Left sidebar and top for annotation page (#963)

* Rebased from develop
* Improved getting icons method
* Added more icons
* Left menu
* Initial commit
* Setup SVGO, added some buttons to top
* Top bar progress
* Top bar for annotation page
* Updated styles

* added in label visualization to auto annotation runner (#931)

* Bump tensorflow from 1.13.1 to 1.15.0 in /utils/tfrecords (#967)

Bumps [tensorflow](https://github.com/tensorflow/tensorflow) from 1.13.1 to 1.15.0.
- [Release notes](https://github.com/tensorflow/tensorflow/releases)
- [Changelog](https://github.com/tensorflow/tensorflow/blob/master/RELEASE.md)
- [Commits](https://github.com/tensorflow/tensorflow/compare/v1.13.1...v1.15.0)

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

* Fixed number attribute (#972)

* CSS Enhancement (#971)

* Removed vendor/specific rules
* Sass for CVAT, less for Antd, added autoprefixer and css polyfills
* Removed extra line
* Changed update state

* [Datumaro] VOC labelmap support (#957)

* Add import result checks and options to skip
* Add label-specific attributes
* Overwrite option for export
* Add labelmap file support in voc
* Add labelmap tests
* Little refactoring

* Bump tensorflow from 1.12.3 to 1.15.0 in /cvat/requirements (#968)

* Bump tensorflow from 1.12.3 to 1.15.0 in /cvat/requirements

Bumps [tensorflow](https://github.com/tensorflow/tensorflow) from 1.12.3 to 1.15.0.
- [Release notes](https://github.com/tensorflow/tensorflow/releases)
- [Changelog](https://github.com/tensorflow/tensorflow/blob/master/RELEASE.md)
- [Commits](https://github.com/tensorflow/tensorflow/compare/v1.12.3...v1.15.0)

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

* Update pip because tensorflow 1.15 cannot not be found.

* Fix a typo (pip -> pip3)

* Replaced pip3 by python3 -m pip.

* Change-submit-button-style (#976)

* UI/UX improvement. Changed buttons type for create task / upload model

* Added documentation for swagger page (#936)

* Styles refactoring (#977)

* Add polygon point count checks (#975)

* User Guide update (#953)

* Swagger documentation (#978)

* Fix swagger problems (exceptions, /api/swagger.json, /api/docs/)

* [Datumaro] CVAT format import (#974)

* Add label-specific attributes
* Add CVAT format import
* Register CVAT format
* Add little more logs
* Little refactoring for tests
* Cvat format checks
* Add missing check
* Refactor datumaro format
* Little refactoring
* Regularize dataset importer logic
* Fix project import issue
* Refactor coco extractor
* Refactor tests
* Codacy

* Fix label for mask rcnn (#980)

* UI Enhancements  (#985)

* Single import of basic styles
* A little bit redesigned header
* Specified min resolution 1280x768
* Getting a job instance
* Improved handling when task doesn't exist

* Adding dump for VOC instance mask.  (#859)

* Add mask instance dumper
* Fix bug
* Merge mask instance into mask
* Merge the change into mask
* Create MaskColorizer
* Add dump method

* Updating the Model Manager section of the CVAT User Guide (#991)

* Added Code Climate, CodeBeat badges. (#995)

* [Datumaro] Fix TFrecord converter constructor (#993)

* Resolved performance bottleneck in merge function (#999)

* Fixed issue: Unknown shape type found (#998)

* Automatic bordering feature during drawing/editing (#997)

* Change Modal submit button okType (#1001)

* Fixed comparison of shapes (#1000)

* Add test code for cli upload function (#986)

* pass in model name and task id to run auto annotation script (#934)

* fix dockerfile for PDF (#939)

* Updating the Auto Annotation section of the CVAT User Guide (#996)

* Updating the Task synchronization with a repository section of the CVAT User Guide (#1006)

* Fix timezone bug (#1010)

* [Datumaro] Fix project loading (#1013)

* Fix occasional infinite loop in project loading

* Fix project import source options saving

* Fix project import .git dir placement

* Make code aware of grayscale images

* Added root folder for share functionality (#1005)

* Improved feature: common borders (#1016)

* Auto borders -> common borders, invisible when do not edit or draw, don't reset state

* Reset sticker after clicking outside

* Update AWS-Deployment-Guide.md (#1019)

Fixed documentation typo for file extension

* Correct link to #automatic-annotation in README (#1029)

* AWS deployment guide updated #1009 (#1031)

* Add info about auto segmentation to advanced topics of the installation guide (#1033)

* correct path to eula.cfg (#1037)

* Update README.md (#1040)

* Removed patool package with GPL license (it is not used) (#1045)

* Removed VIM package (it isn't necessary) (#1046)

* Trim possible attribute values like attribute values setup by a user (#1044)

* React UI: Player in annotation view & settings page (#1018)

* Active player controls
* Setup packages
* Playing
* Fold/unfold sidebar, minor issues
* Improved cvat-canvas integration
* Resolved some issues
* Added cvat-canvas to Dockerfile.ui
* Fit canvas method
* Added annotation reducer
* Added annotation actions
* Added containers
* Added components
* cvat-canvas removed from dockerignore
* Added settings page
* Minor improvements
* Container for canvas wrapper
* Configurable grid
* Rotation
* fitCanvas added to readme
* Aligned table

* Changed CharField(64) -> CharField(4096) for attribute value (#1048)

* [Datumaro] Add cvat format export (#1034)

* Add cvat format export

* Remove wrong items in test

* [Datumaro] Instance polygon-mask conversions in COCO format (#1008)

* Microoptimizations

* Mask conversion functions

* Add mask-polygon conversions

* Add mask-polygon conversions in coco

* Add mask-polygon conversions in coco

* Update requirements

* Option to disable crop

* Fix cli parameter passing

* Fix test

* Fixes in COCO

* [Datumaro] Dataset annotations filter (#1053)

* Fix deprecation message

* Update launcher interface

* Add dataset entity, anno filter, remove filter from project config, update transform

* Update project and source cli

* Fix help message

* Refactor tests

* Added ability to match many model labels to one task labels (#1051)

* Added ability to match many model labels to one task labels

* Fixed grammar

* React UI: Player updates (#1058)

* Move, zoom integration
* Moving, zooming, additional canvas handler
* Activating & changing for objects
* Improved colors
* Saving annotations on the server
* Fixed size
* Refactoring
* Added couple of notifications
* Basic shape drawing
* Cancel previous drawing
* Refactoring
* Minor draw improvings
* Merge, group, split
* Improved colors

* Fixed: Uncaught TypeError: Cannot read property 'nodeValue' of undefined (#1068)

* Add about CVAT (#1024)

* Fix typos in xml_format.md (#1069)

typo fixes

* Update CONTRIBUTING.md (#1072)

* align serializer max length of attribute value with the model (#1074)

* Cleanup Dockerfiles for CVAT (#1060)

* Replaced wget by curl

* Moved CI stuff into Dockerfile.ci

* Use docker-compose to run commnands inside docker (need environment variables)

* Added patool again (to support different archive formats)

* Roll back tensorflow version: 1.15 -> 1.13.1

Fixed https://github.com/opencv/cvat/issues/982
Fixed https://github.com/opencv/cvat/issues/1017

* datumaro install tensorflow 2.x now. It breaks automatic annotation
using TF.

* Follow redirects in curl (auto_segmentation)

* Update method call (#1085)

* React UI: Sidebar with objects and optimizations for annotation view (#1089)

* Basic layout for objects panel

* Objects header

* A little name refactoring

* Side panel base layout

* Firefox specific exceptions

* Some minor fixes

* React & canvas optimizations

* Icons refactoring

* Little style refactoring

* Some style fixes

* Improved side panel with objects

* Actual attribute values

* Actual icons

* Hidden > visible

* hidden -> __internal

* Fixed hidden in ui

* Fixed some issues in canvas

* Fixed list height

* Color picker for labels

* A bit fixed design

* Actual header icons

* Changing attributes and switchable buttons

* Removed react memo (will reoptimize better)

* Sorting methods, removed cache from cvat-core (a lot of bugs related with it)

* Label switchers

* Fixed bug with update timestamp for shapes

* Annotation state refactoring

* Removed old resetCache calls

* Optimized top & left panels. Number of renders significantly decreased

* Optimized some extra renders

* Accelerated performance

* Fixed two minor issues

* Canvas improvements

* Minor fixes

* Removed extra code

* resolving import error caused by pip 20.0 (#1094)

* [Datumaro] CLI updates + better documentation (#1057)

* Optimize mask conversions (#1097)

* Update base.py (#1099)

Modification necessary for using CVAT from remote machines when accessing with FQDNs
See https://github.com/opencv/cvat/issues/1011#issue-542817055
and https://github.com/opencv/cvat/pull/1098

"I believe the reason for this is that sometimes if the port number is :80 and the URL is not in the LAN (:port), but instead it is a Fully Qualified Domain Name (:port), the port 80 is redundant (mydomain.com:80) and the errors arise."

* fixed dump of interpolation points object && statistics calculation (#1108)

* Add extreme clicking feature to draw box by 4 points (#1111)

* Add extreme clicking feature to draw box by 4 points

* Add documentation for extreme clicking

* React UI: Annotation view enhancements  (#1106)

* Keyframes navigation

* Synchronized objects on canvas and in side panel

* Fixed minor bug with collapse

* Fixed css property 'pointer-events'

* Drawn appearance block

* Removed extra force reflow

* Finished appearance block, fixed couple bugs

* Improved save() in cvat-core, changed approach to highlight shapes

* Fixed exception in edit function, fixed filling for polylines and points, fixed wrong image navigation, remove and copy

* Added lock

* Some fixes with points

* Minor appearance fixes

* Fixed insert for points

* Fixed unit tests

* Fixed control

* Fixed list size

* Added propagate

* Minor fix with attr saving

* Some div changed to buttons

* Locked some buttons for unimplemented functionalities

* Statistics modal, changing a job status

* Minor fix with shapes counting

* Couple of fixes to improve visibility

* Added fullscreen

* SVG Canvas -> HTML Canvas frame (#1113)

* SVG Frame -> HTML Canvas frame

* React UI: Added annotation menus, added shape context menu, added some confirmations before dangerous actions (#1123)

* Annotation menu, modified tasks menu

* Removed extra styles

* Context menu using side panel

* Mousewheel on draw

* Added more cursor icons

* Do not check .svg & .scss by eslint

* [Datumaro] Plugins and transforms (#1126)

* Fix model run command

* Rename annotation types, update class interfaces

* Fix random cvat format test fails

* Mask operations and dataset format fixes

* Update tests, extract format testing functions

* Add transform interface

* Implement plugin system

* Update tests with plugins

* Fix logging

* Add transfroms

* Update cvat integration

* Fix tensorflow installation (#1129)

* Make tf dependency optional

* Reduce opencv dependency

* Import tf eagerly as it is a plugin

* Do not install TF with Datumaro

* Add plugin system documentation (#1131)

* React UI: Improved mouse behaviour during draw/merge/edit/group/split (#1130)

* Moving image with mouse during drawing, paste, group, split, merge

* Babel plugin to dev deps

* Move mouse during editing

* Minor issues

* [Datumaro] fixes (#1137)

* Fix import command

* Fix project name for spawned projects

* Fix voc and coco converter parameters

* Fix voc colormap color interpretation

* Change order of image search for cvat extractor

* fix CVAT image search paths

* Bump django from 2.2.8 to 2.2.10 in /cvat/requirements (#1139)

Bumps [django](https://github.com/django/django) from 2.2.8 to 2.2.10.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.8...2.2.10)

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

* Add extreme clicking method in cvat-canvas and cvat-ui (#1127)

* Add extreme clicking method in cvat-canvas and cvat-ui

* Fix bugs and issues, update readme

* Fix error after rebasing develop

* updated CUDA to version 10 (#1138)

* updated CUDA to version 10

* updated tensorflow

* added comment about NVIDIA_REQUIRE_CUDA env varOF

* React UI: Undo/redo (#1135)

* Typed reducers (#1136)

* Added typed actions/reducers
* Added commands to check types / eslint issues
* Added redux dev tools

* Bump gitpython version (#1146)

* Fix postgres startup.

* React UI: Objects filtering & search (#1155)

* Initial filter function

* Updated method for filtering

* Updated documentation

* Added annotations filter file

* Updated some comments

* Added filter to UI

* Implemented search alorithm

* Removed extra code

* Fixed typos

* Added frame URL

* Object URL

* Removed extra encoding/decoding

* Fixed dump for cases when special URL characters in task name (#1162)

* Add offline subset remapping and bbox conversion (#1147)

* Avoid tf deprecation warning (#1148)

* [Datumaro] Pretty output folder names (#1149)

* Generate output dir name from operation parameters

* Fix failing command

* Update changelog (#1165)

* [Datumaro] Introduce image info (#1140)

* Employ transforms and item wrapper

* Add image class and tests

* Add image info support to formats

* Fix cli

* Fix merge and voc converte

* Update remote images extractor

* Codacy

* Remove item name, require path in Image

* Merge images of dataset items

* Update tests

* Add image dir converter

* Update Datumaro format

* Update COCO format with image info

* Update CVAT format with image info

* Update TFrecord format with image info

* Update VOC formar with image info

* Update YOLO format with image info

* Update dataset manager bindings with image info

* Add image name to id transform

* Fix coco export

* More types in actions and reducers (#1166)

* [Datumaro] Add masks to tfrecord format (#1156)

* Employ transforms and item wrapper

* Add image class and tests

* Add image info support to formats

* Fix cli

* Fix merge and voc converte

* Update remote images extractor

* Codacy

* Remove item name, require path in Image

* Merge images of dataset items

* Update tests

* Add image dir converter

* Update Datumaro format

* Update COCO format with image info

* Update CVAT format with image info

* Update TFrecord format with image info

* Update VOC formar with image info

* Update YOLO format with image info

* Update dataset manager bindings with image info

* Add image name to id transform

* Fix coco export

* Add masks support for tfrecord

* Refactor coco

* Fix comparison

* Remove dead code

* Extract common code for instances

* Replace YOLO format support in CVAT with Datumaro (#1151)

* Employ transforms and item wrapper

* Add image class and tests

* Add image info support to formats

* Fix cli

* Fix merge and voc converte

* Update remote images extractor

* Codacy

* Remove item name, require path in Image

* Merge images of dataset items

* Update tests

* Add image dir converter

* Update Datumaro format

* Update COCO format with image info

* Update CVAT format with image info

* Update TFrecord format with image info

* Update VOC formar with image info

* Update YOLO format with image info

* Update dataset manager bindings with image info

* Add image name to id transform

* Replace YOLO export and import in CVAT with Datumaro

* Add editorconfig (#1142)

* Add editorconfig

* Update indent value

* Cuboid annotation (#678)

* Cuboid feature

* migration files

* Refactored cuboidShape
Fixed a bug where coloring by label would not update cuboids properly
Fixed a bug where the select points would not scale properly on initialization

* Removed math.js dependency
Implemented custom line intersection function

* new cvat formatting with labelled points

* Added MIT License to js files that were missing it

* Added simple constraints to the cuboids

* reverted commit for settings for vscode to hide local path

* fixed locking for cuboids

* fixed cuboid View when locked

* fixed occlusion view for cuboids

* Allow cuboid points to be outside the frame dimensions.

Signed-off-by: Tritin Truong <truongtritin98@gmail.com>

* Added stricter constraints on cuboid edges.

* Slightly stricter restrictions for edge case

* Cleaned up unused imports

* removed dashed lines on cuboids

* Moved projection lines to settings tab

* Fixed Cuboid shape buffer \

* Fix migrations (two 022 migrations after merge with the develop branch).

* Fix compatibility issues with auto segmentation.

* Grab points and update control scheme

* Greatly improved control scheme, fixed shape merging
Fixed Cuboid upload

* Fixed slight visual bug when dragging faces

* Some optimizations

* Hiding the grab point on creation
Small refactoring

* Fixed some cases where cuboid breaks

* Fixed upload for videos

* Removed perspective effects

* Made left back edge editable

* left back edge resizable

* fix statistics bug

* added toggles for the back edges

* Constraints for the back edges

* Fix creation bug

* Tightened creation constraints

* Fixing the code style

* updated message for invalid cuboids

* Code style

* More style fixes

* Codacy fixes

* added shift control for edges

* More Codacy fixes

* More Codacy fixes

* Double arrows for cursor

* Fix Drag bug

* More Codacy fixes

* Fix double quotes

* Fix camel case

* More camelcase fixes

* Generic object sink fixes

* Various codacy fixes

* Codacy

* Double quotes

* Fix migrations

* Updated shape creation
Fix jittering

* Adjusted constraints

* Codacy fixes

* Codacy fixes again

* Drawing cuboids from the top and bottom

* Codacy

* Resetting perspective on cuboids

* Choosing orientation of cuboids.

* Codacy fix

* Merge cleanup

* revert vs-code settings

* Update settings.json

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

* Update yolo format description (#1173)

* Replace tfrecord format support in CVAT with Datumaro (#1157)

* Replace mask format support with Datumaro (#1163)

* Add box to mask transform

* Fix 'source' labelmap mode in voc converter

* Import groups

* Replace mask format support

* Update mask format documentation

* codacy

* Fix tests

* Fix dataset

* Fix segments grouping

* Merge instances in mask export

* Update Onepanel demo information and link (#1189)

* Added displayed versions of core, canvas, and ui in about (#1191)

* Added displayed versions of core, canvas, and ui in about

* Removed extra method

* React UI: ZOrder implementation (#1176)

* Drawn z-order switcher

* Z layer was added to state

* Added ZLayer API method cvat-canvas

* Added sorting by Z

* Displaying points in top

* Removed old code

* Improved sort function

* Drawn a couple of icons

* Send to foreground / send to background

* Updated unit tests

* Added unit tests for filter parser

* Removed extra code

* Updated README.md

* Replace VOC format support in CVAT with Datumaro (#1167)

* Add image meta reading to voc

* Replace voc support in cvat

* Bump format version

* Materialize lazy transforms in voc export

* Store voc instance id as group id

* Add flat format import

* Add documentation

* Fix format name in doc

* [Datumaro] Remote project export fixes (#1193)

* Export project with trask name

* Do not expose server paths

* Fix tfrecord mask reading in tf>1.14

* Setuptools compatibility

* Replace COCO implementation (#1195)

* Fixed lags (#1197)

* React UI: Changing color for a shape (#1194)

* Minimized size of an element in side panel

* To background / to foreground like in legacy UI

* Added color changer for a shape

* Adjusted color updating

* React-UI: settings (#1164)

* Image filters: brightness, contrast, saturation
* Auto saving
* Frame auto fit
* Player speed
* Leave confirmation for unsaved changes

* React UI: Changing color for a group (#1205)

* Added license headers (#1208)

* Added licenser

* Added license headers for cvat-canvas and cvat-ui

* Move project dir to .datumaro (#1207)

* Updated svg.js version (#1212)

* React UI: Batch of fixes (#1211)

* Disabled tracks for polyshapes in UI

* RectDrawingMethod enum pushed to cvat-canvas, fixed some code issues

* Optional arguments

* Draw a text for locked shapes, some fixes with not keyframe shapes

* Fixed zooming & batch grouping

* Reset zoom for tasks with images

* Fixed putting shapes out of canvas

* Fixed grid opacity, little refactoring of componentDidUpdate in canvas-wrapper component

* Fixed corner cases for drawing

* Fixed putting shapes out of canvas

* Improved drawing

* Removed extra event handler

* Auto-generate labelmap for voc from task (#1214)

* Add random split transform (#1213)

* React UI: Improved rotation feature (#1206)

Co-authored-by: Boris Sekachev <40690378+bsekachev@users.noreply.github.com>

* Az/cvat proxy (#1177)

* added nginx proxy

* removed unnecessary port configuration & build arg

* updated installation guide

* Add tags to cvat xml (#1200)

* Extend cvat format test

* Add tags to cvat for images

* Add tags to cvat format in dm

* Add import of tags from datumaro

* React UI: Pinned option was added (#1202)

* Fix remainder logic for subset splitting (#1222)

* Add tags support for VOC (#1201)

* Extend voc format test with tags

* Add import and export of voc labels

* Fix voc and yolo format version numbers

* React UI: batch of fixes (#1227)

* Fix: keyframes navigation

* Fix: handled removing of the latest keyframe

* Fix: activating a shape when another shape is being changed

* Fix: up points in the side bar on points click

* Fix: editable shape isn't transformed when change zoom

* Updated message

* React UI: Filters history (#1225)

* Added filters history

* Fixed unclosed dropdown

* Added saving filters to localStrorage

* 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

* 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

* Fixed date in CHANGELOG.md

* sort frame shapes by z_order (#1258)

Co-authored-by: vfdev <vfdev.5@gmail.com>
Co-authored-by: Boris Sekachev <40690378+bsekachev@users.noreply.github.com>
Co-authored-by: Ben Hoff <hoff.benjamin.k@gmail.com>
Co-authored-by: Boris Sekachev <boris.sekachev@yandex.ru>
Co-authored-by: Ben Hoff <bhoff@nciinc.com>
Co-authored-by: telenachos <54951461+telenachos@users.noreply.github.com>
Co-authored-by: Johannes222 <johannes.halaoui@alumni.fh-aachen.de>
Co-authored-by: RS Nikhil Krishna <rsnk96@users.noreply.github.com>
Co-authored-by: Andrey Zhavoronkov <41117609+azhavoro@users.noreply.github.com>
Co-authored-by: Reza Malek <malekabbasi@meam.ir>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: zhiltsov-max <zhiltsov.max35@gmail.com>
Co-authored-by: a-andre <a-andre@users.noreply.github.com>
Co-authored-by: Maksim Markelov <maks-markel@mail.ru>
Co-authored-by: himalayanZephyr <42401082+himalayanZephyr@users.noreply.github.com>
Co-authored-by: Seungwon Jeong <jsw1295@gmail.com>
Co-authored-by: Maya <49038720+Marishka17@users.noreply.github.com>
Co-authored-by: TOsmanov <54434686+TOsmanov@users.noreply.github.com>
Co-authored-by: vugia truong <vugiatruong88@gmail.com>
Co-authored-by: roho <mrtn.etchart@gmail.com>
Co-authored-by: Christian <christian.roemer@udo.edu>
Co-authored-by: provider161 <provider8@yandex.ru>
Co-authored-by: Radhika <43014570+radhika1601@users.noreply.github.com>
Co-authored-by: Tanvi Anand <tanviaanand@gmail.com>
Co-authored-by: Lisa <38404726+LiSa20120@users.noreply.github.com>
Co-authored-by: Josh Bradley <jgbrad1@umd.edu>
Co-authored-by: Priya4607 <59498234+Priya4607@users.noreply.github.com>
Co-authored-by: LukeAI <43993778+LukeAI@users.noreply.github.com>
Co-authored-by: Jijoong Kim <joong937@gmail.com>
Co-authored-by: Nikita Glazov <nglazov@gmail.com>
Co-authored-by: Tritin Truong <tritin_truong@yahoo.com>
Co-authored-by: timbowl <54648082+timbowl@users.noreply.github.com>
Co-authored-by: Rush Tehrani <r@onepanel.io>
Co-authored-by: Dmitry Kalinin <chchchoon.dk@gmail.com>
Co-authored-by: Tritin Truong <truongtritin98@gmail.com>
main
Nikita Manovich 6 years ago committed by GitHub
parent 42aad8b56b
commit 8bf647b360
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

@ -7,5 +7,4 @@
/db.sqlite3 /db.sqlite3
/keys /keys
**/node_modules **/node_modules
cvat-ui
cvat-canvas

@ -0,0 +1,17 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# Change these settings to your own preference
indent_style = space
indent_size = 4
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

2
.gitignore vendored

@ -12,7 +12,7 @@
/ssh/* /ssh/*
!/ssh/README.md !/ssh/README.md
node_modules node_modules
/Mask_RCNN/
# Ignore temporary files # Ignore temporary files
docker-compose.override.yml docker-compose.override.yml

@ -7,10 +7,11 @@ python:
services: services:
- docker - docker
before_script: before_script:
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml up --build -d - docker-compose -f docker-compose.yml -f docker-compose.ci.yml build
script: script:
- docker exec -it cvat /bin/bash -c 'python3 manage.py test cvat/apps/engine' - 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 exec -it cvat /bin/bash -c 'cd cvat-core && npm install && npm run test && npm run coveralls' - 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'

@ -71,6 +71,22 @@
"env": {}, "env": {},
"console": "internalConsole" "console": "internalConsole"
}, },
{
"name": "server: RQ - scheduler",
"type": "python",
"request": "launch",
"stopOnEntry": false,
"justMyCode": false,
"pythonPath": "${config:python.pythonPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"rqscheduler",
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {},
"console": "internalConsole"
},
{ {
"name": "server: RQ - low", "name": "server: RQ - low",
"type": "python", "type": "python",
@ -134,7 +150,8 @@
"test", "test",
"--settings", "--settings",
"cvat.settings.testing", "cvat.settings.testing",
"cvat/apps/engine", "cvat/apps",
"utils/cli"
], ],
"django": true, "django": true,
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
@ -176,6 +193,7 @@
"server: django", "server: django",
"server: RQ - default", "server: RQ - default",
"server: RQ - low", "server: RQ - low",
"server: RQ - scheduler",
"server: git", "server: git",
] ]
} }

@ -0,0 +1 @@
PYTHONPATH="datumaro/:$PYTHONPATH"

@ -3,7 +3,8 @@
"eslint.enable": true, "eslint.enable": true,
"eslint.validate": [ "eslint.validate": [
"javascript", "javascript",
"typescript" "typescript",
"typescriptreact",
], ],
"eslint.workingDirectories": [ "eslint.workingDirectories": [
{ {
@ -14,10 +15,23 @@
"directory": "./cvat-canvas", "directory": "./cvat-canvas",
"changeProcessCWD": true "changeProcessCWD": true
}, },
{
"directory": "./cvat-ui",
"changeProcessCWD": true
},
{ {
"directory": ".", "directory": ".",
"changeProcessCWD": true "changeProcessCWD": true
} }
], ],
"python.linting.pylintEnabled": true "python.linting.pylintEnabled": true,
"python.envFile": "${workspaceFolder}/.vscode/python.env",
"python.testing.unittestEnabled": true,
"python.testing.unittestArgs": [
"-v",
"-s",
"./datumaro",
],
"licenser.license": "Custom",
"licenser.customHeader": "Copyright (C) @YEAR@ Intel Corporation\n\nSPDX-License-Identifier: MIT"
} }

@ -4,6 +4,38 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.6.0] - 2020-03-15
### Added
- Server only support for projects. Extend REST API v1 (/api/v1/projects*)
- Ability to get basic information about users without admin permissions ([#750](https://github.com/opencv/cvat/issues/750))
- Changed REST API: removed PUT and added DELETE methods for /api/v1/users/ID
- Mask-RCNN Auto Annotation Script in OpenVINO format
- Yolo Auto Annotation Script
- Auto segmentation using Mask_RCNN component (Keras+Tensorflow Mask R-CNN Segmentation)
- REST API to export an annotation task (images + annotations)
- [Datumaro](https://github.com/opencv/cvat/tree/develop/datumaro) - a framework to build, analyze, debug and visualize datasets
- Text Detection Auto Annotation Script in OpenVINO format for version 4
- Added in OpenVINO Semantic Segmentation for roads
- Ability to visualize labels when using Auto Annotation runner
- MOT CSV format support ([#830](https://github.com/opencv/cvat/pull/830))
- LabelMe format support ([#844](https://github.com/opencv/cvat/pull/844))
- Segmentation MASK format import (as polygons) ([#1163](https://github.com/opencv/cvat/pull/1163))
- Git repositories can be specified with IPv4 address ([#827](https://github.com/opencv/cvat/pull/827))
### Changed
- page_size parameter for all REST API methods
- 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))
### Fixed
- Exception in Git plugin [#826](https://github.com/opencv/cvat/issues/826)
- Label ids in TFrecord format now start from 1 [#866](https://github.com/opencv/cvat/issues/866)
- Mask problem in COCO JSON style [#718](https://github.com/opencv/cvat/issues/718)
- Datasets (or tasks) can be joined and split to subsets with Datumaro [#791](https://github.com/opencv/cvat/issues/791)
- Output labels for VOC format can be specified with Datumaro [#942](https://github.com/opencv/cvat/issues/942)
- Annotations can be filtered before dumping with Datumaro [#994](https://github.com/opencv/cvat/issues/994)
## [0.5.2] - 2019-12-15 ## [0.5.2] - 2019-12-15
### Fixed ### Fixed
- Frozen version of scikit-image==0.15 in requirements.txt because next releases don't support Python 3.5 - Frozen version of scikit-image==0.15 in requirements.txt because next releases don't support Python 3.5
@ -29,6 +61,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ability to dump/load annotations in several formats from UI (CVAT, Pascal VOC, YOLO, MS COCO, png mask, TFRecord) - Ability to dump/load annotations in several formats from UI (CVAT, Pascal VOC, YOLO, MS COCO, png mask, TFRecord)
- Auth for REST API (api/v1/auth/): login, logout, register, ... - Auth for REST API (api/v1/auth/): login, logout, register, ...
- Preview for the new CVAT UI (dashboard only) is available: http://localhost:9080/ - Preview for the new CVAT UI (dashboard only) is available: http://localhost:9080/
- Added command line tool for performing common task operations (/utils/cli/)
### Changed ### Changed
- Outside and keyframe buttons in the side panel for all interpolation shapes (they were only for boxes before) - Outside and keyframe buttons in the side panel for all interpolation shapes (they were only for boxes before)

@ -15,22 +15,24 @@ Next steps should work on clear Ubuntu 18.04.
- Install necessary dependencies: - Install necessary dependencies:
```sh ```sh
$ sudo apt-get install -y curl redis-server python3-dev python3-pip python3-venv libldap2-dev libsasl2-dev $ sudo apt update && apt install -y nodejs npm curl redis-server python3-dev python3-pip python3-venv libldap2-dev libsasl2-dev
``` ```
- Install [Visual Studio Code](https://code.visualstudio.com/docs/setup/linux#_debian-and-ubuntu-based-distributions) for development - Install [Visual Studio Code](https://code.visualstudio.com/docs/setup/linux#_debian-and-ubuntu-based-distributions)
for development
- Install CVAT on your local host: - Install CVAT on your local host:
```sh ```sh
$ git clone https://github.com/opencv/cvat git clone https://github.com/opencv/cvat
$ cd cvat && mkdir logs keys cd cvat && mkdir logs keys
$ python3 -m venv .env python3 -m venv .env
$ . .env/bin/activate . .env/bin/activate
$ pip install -U pip wheel pip install -U pip wheel
$ pip install -r cvat/requirements/development.txt pip install -r cvat/requirements/development.txt
$ python manage.py migrate pip install -r datumaro/requirements.txt
$ python manage.py collectstatic python manage.py migrate
python manage.py collectstatic
``` ```
- Create a super user for CVAT: - Create a super user for CVAT:
@ -43,21 +45,102 @@ Password: ***
Password (again): *** Password (again): ***
``` ```
- Run Visual Studio Code from the virtual environment - 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
``` ```
$ code .
- Open new terminal (Ctrl + Shift + T), run Visual Studio Code from the virtual environment
```sh
cd .. && source .env/bin/activate && code
``` ```
- Inside Visual Studio Code install [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) and [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) extensions - Install followig vscode extensions:
- [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
- [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
- [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)
- Reload Visual Studio Code - Reload Visual Studio Code from virtual environment
- Select `server: debug` configuration and start debugging (F5) - Select `server: debug` configuration and start it (F5) to run REST server and its workers
You have done! Now it is possible to insert breakpoints and debug server and client of the tool. You have done! Now it is possible to insert breakpoints and debug server and client of the tool.
## JavaScript coding style ## How to setup additional components in development environment
### Automatic annotation
- Install OpenVINO on your host machine according to instructions from
[OpenVINO website](https://docs.openvinotoolkit.org/latest/index.html)
- Add some environment variables (copy code below to the end of ``.env/bin/activate`` file):
```sh
source /opt/intel/openvino/bin/setupvars.sh
export OPENVINO_TOOLKIT="yes"
export IE_PLUGINS_PATH="/opt/intel/openvino/deployment_tools/inference_engine/lib/intel64"
export OpenCV_DIR="/usr/local/lib/cmake/opencv4"
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:/opt/intel/openvino/inference_engine/lib/intel64"
```
Notice 1: be sure that these paths actually exist. Some of them can differ in different OpenVINO versions.
Notice 2: you need to deactivate, activate again and restart vs code
to changes in ``.env/bin/activate`` file are active.
### ReID algorithm
- Perform all steps in the automatic annotation section
- Download ReID model and save it somewhere:
```sh
curl https://download.01.org/openvinotoolkit/2018_R5/open_model_zoo/person-reidentification-retail-0079/FP32/person-reidentification-retail-0079.xml -o reid.xml
curl https://download.01.org/openvinotoolkit/2018_R5/open_model_zoo/person-reidentification-retail-0079/FP32/person-reidentification-retail-0079.bin -o reid.bin
```
- Add next line to ``.env/bin/activate``:
```sh
export REID_MODEL_DIR="/path/to/dir" # dir must contain .xml and .bin files
```
### Deep Extreme Cut
- Perform all steps in the automatic annotation section
- Download Deep Extreme Cut model, unpack it, and save somewhere:
```sh
curl https://download.01.org/openvinotoolkit/models_contrib/cvat/dextr_model_v1.zip -o dextr.zip
unzip dextr.zip
```
- Add next lines to ``.env/bin/activate``:
```sh
export WITH_DEXTR="yes"
export DEXTR_MODEL_DIR="/path/to/dir" # dir must contain .xml and .bin files
```
### Tensorflow RCNN
- Download RCNN model, unpack it, and save it somewhere:
```sh
curl http://download.tensorflow.org/models/object_detection/faster_rcnn_inception_resnet_v2_atrous_coco_2018_01_28.tar.gz -o model.tar.gz && \
tar -xzf model.tar.gz
```
- Add next lines to ``.env/bin/activate``:
```sh
export TF_ANNOTATION="yes"
export TF_ANNOTATION_MODEL_PATH="/path/to/the/model/graph" # truncate .pb extension
```
### Tensorflow Mask RCNN
- Download Mask RCNN model, and save it somewhere:
```sh
curl https://github.com/matterport/Mask_RCNN/releases/download/v2.0/mask_rcnn_coco.h5 -o mask_rcnn_coco.h5
```
- Add next lines to ``.env/bin/activate``:
```sh
export AUTO_SEGMENTATION="yes"
export AUTO_SEGMENTATION_PATH="/path/to/dir" # dir must contain mask_rcnn_coco.h5 file
```
## JavaScript/Typescript coding style
We use the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) for JavaScript code with a We use the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) for JavaScript code with a
litle exception - we prefere 4 spaces for indentation of nested blocks and statements. litle exception - we prefere 4 spaces for indentation of nested blocks and statements.

@ -22,9 +22,7 @@ ENV DJANGO_CONFIGURATION=${DJANGO_CONFIGURATION}
# Install necessary apt packages # Install necessary apt packages
RUN apt-get update && \ RUN apt-get update && \
apt-get install -yq \ apt-get install -yq \
python-software-properties \ software-properties-common && \
software-properties-common \
wget && \
add-apt-repository ppa:mc3man/xerus-media -y && \ add-apt-repository ppa:mc3man/xerus-media -y && \
add-apt-repository ppa:mc3man/gstffmpeg-keep -y && \ add-apt-repository ppa:mc3man/gstffmpeg-keep -y && \
apt-get update && \ apt-get update && \
@ -40,11 +38,19 @@ RUN apt-get update && \
python3-dev \ python3-dev \
python3-pip \ python3-pip \
tzdata \ tzdata \
unzip \
unrar \
p7zip-full \ p7zip-full \
vim && \ git \
pip3 install -U setuptools && \ ssh \
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 && \
python3 -m pip install --no-cache-dir -U pip==20.0.1 setuptools && \
ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \ ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \ dpkg-reconfigure -f noninteractive tzdata && \
add-apt-repository --remove ppa:mc3man/gstffmpeg-keep -y && \ add-apt-repository --remove ppa:mc3man/gstffmpeg-keep -y && \
@ -66,8 +72,8 @@ ENV REID_MODEL_DIR=${HOME}/reid
RUN if [ "$OPENVINO_TOOLKIT" = "yes" ]; then \ RUN if [ "$OPENVINO_TOOLKIT" = "yes" ]; then \
/tmp/components/openvino/install.sh && \ /tmp/components/openvino/install.sh && \
mkdir ${REID_MODEL_DIR} && \ mkdir ${REID_MODEL_DIR} && \
wget https://download.01.org/openvinotoolkit/2018_R5/open_model_zoo/person-reidentification-retail-0079/FP32/person-reidentification-retail-0079.xml -O reid/reid.xml && \ curl https://download.01.org/openvinotoolkit/2018_R5/open_model_zoo/person-reidentification-retail-0079/FP32/person-reidentification-retail-0079.xml -o reid/reid.xml && \
wget https://download.01.org/openvinotoolkit/2018_R5/open_model_zoo/person-reidentification-retail-0079/FP32/person-reidentification-retail-0079.bin -O reid/reid.bin; \ curl https://download.01.org/openvinotoolkit/2018_R5/open_model_zoo/person-reidentification-retail-0079/FP32/person-reidentification-retail-0079.bin -o reid/reid.bin; \
fi fi
# Tensorflow annotation support # Tensorflow annotation support
@ -78,48 +84,21 @@ RUN if [ "$TF_ANNOTATION" = "yes" ]; then \
bash -i /tmp/components/tf_annotation/install.sh; \ bash -i /tmp/components/tf_annotation/install.sh; \
fi fi
ARG WITH_TESTS # Auto segmentation support. by Mohammad
RUN if [ "$WITH_TESTS" = "yes" ]; then \ ARG AUTO_SEGMENTATION
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \ ENV AUTO_SEGMENTATION=${AUTO_SEGMENTATION}
echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list && \ ENV AUTO_SEGMENTATION_PATH=${HOME}/Mask_RCNN
wget -qO- https://deb.nodesource.com/setup_9.x | bash - && \ RUN if [ "$AUTO_SEGMENTATION" = "yes" ]; then \
apt-get update && \ bash -i /tmp/components/auto_segmentation/install.sh; \
DEBIAN_FRONTEND=noninteractive apt-get install -yq \
google-chrome-stable \
nodejs && \
rm -rf /var/lib/apt/lists/*; \
mkdir tests && cd tests && npm install \
eslint \
eslint-detailed-reporter \
karma \
karma-chrome-launcher \
karma-coveralls \
karma-coverage \
karma-junit-reporter \
karma-qunit \
qunit; \
echo "export PATH=~/tests/node_modules/.bin:${PATH}" >> ~/.bashrc; \
fi fi
# Install and initialize CVAT, copy all necessary files # Install and initialize CVAT, copy all necessary files
COPY cvat/requirements/ /tmp/requirements/ COPY cvat/requirements/ /tmp/requirements/
COPY supervisord.conf mod_wsgi.conf wait-for-it.sh manage.py ${HOME}/ COPY supervisord.conf mod_wsgi.conf wait-for-it.sh manage.py ${HOME}/
RUN pip3 install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt RUN python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt
# pycocotools package is impossible to install with its dependencies by one pip install command # pycocotools package is impossible to install with its dependencies by one pip install command
RUN pip3 install --no-cache-dir pycocotools==2.0.0 RUN python3 -m pip install --no-cache-dir pycocotools==2.0.0
# Install git application dependencies
RUN apt-get update && \
apt-get install -y ssh netcat-openbsd git curl zip && \
wget -qO /dev/stdout https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \
apt-get install -y git-lfs && \
git lfs install && \
rm -rf /var/lib/apt/lists/* && \
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
# CUDA support # CUDA support
ARG CUDA_SUPPORT ARG CUDA_SUPPORT
@ -134,14 +113,19 @@ ENV WITH_DEXTR=${WITH_DEXTR}
ENV DEXTR_MODEL_DIR=${HOME}/dextr ENV DEXTR_MODEL_DIR=${HOME}/dextr
RUN if [ "$WITH_DEXTR" = "yes" ]; then \ RUN if [ "$WITH_DEXTR" = "yes" ]; then \
mkdir ${DEXTR_MODEL_DIR} -p && \ mkdir ${DEXTR_MODEL_DIR} -p && \
wget https://download.01.org/openvinotoolkit/models_contrib/cvat/dextr_model_v1.zip -O ${DEXTR_MODEL_DIR}/dextr.zip && \ curl https://download.01.org/openvinotoolkit/models_contrib/cvat/dextr_model_v1.zip -o ${DEXTR_MODEL_DIR}/dextr.zip && \
unzip ${DEXTR_MODEL_DIR}/dextr.zip -d ${DEXTR_MODEL_DIR} && rm ${DEXTR_MODEL_DIR}/dextr.zip; \ 7z e ${DEXTR_MODEL_DIR}/dextr.zip -o${DEXTR_MODEL_DIR} && rm ${DEXTR_MODEL_DIR}/dextr.zip; \
fi fi
COPY ssh ${HOME}/.ssh COPY ssh ${HOME}/.ssh
COPY utils ${HOME}/utils
COPY cvat/ ${HOME}/cvat COPY cvat/ ${HOME}/cvat
COPY cvat-core/ ${HOME}/cvat-core COPY cvat-core/ ${HOME}/cvat-core
COPY tests ${HOME}/tests COPY tests ${HOME}/tests
COPY datumaro/ ${HOME}/datumaro
RUN python3 -m pip install --no-cache-dir -r ${HOME}/datumaro/requirements.txt
# Binary option is necessary to correctly apply the patch on Windows platform. # Binary option is necessary to correctly apply the patch on Windows platform.
# https://unix.stackexchange.com/questions/239364/how-to-fix-hunk-1-failed-at-1-different-line-endings-message # https://unix.stackexchange.com/questions/239364/how-to-fix-hunk-1-failed-at-1-different-line-endings-message
RUN patch --binary -p1 < ${HOME}/cvat/apps/engine/static/engine/js/3rdparty.patch RUN patch --binary -p1 < ${HOME}/cvat/apps/engine/static/engine/js/3rdparty.patch

@ -0,0 +1,32 @@
FROM cvat
ENV DJANGO_CONFIGURATION=testing
USER root
RUN curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list && \
curl https://deb.nodesource.com/setup_9.x | bash - && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq \
google-chrome-stable \
nodejs && \
rm -rf /var/lib/apt/lists/*;
RUN python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt
# RUN all commands below as 'django' user
USER ${USER}
RUN mkdir -p tests && cd tests && npm install \
eslint \
eslint-detailed-reporter \
karma \
karma-chrome-launcher \
karma-coveralls \
karma-coverage \
karma-junit-reporter \
karma-qunit \
qunit; \
echo "export PATH=~/tests/node_modules/.bin:${PATH}" >> ~/.bashrc;
ENTRYPOINT []

@ -0,0 +1,43 @@
FROM node:lts-alpine AS cvat-ui
ARG http_proxy
ARG https_proxy
ARG no_proxy
ARG socks_proxy
ENV TERM=xterm \
http_proxy=${http_proxy} \
https_proxy=${https_proxy} \
no_proxy=${no_proxy} \
socks_proxy=${socks_proxy}
ENV LANG='C.UTF-8' \
LC_ALL='C.UTF-8'
# Install 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/
# Install cvat-core dependencies
WORKDIR /tmp/cvat-core/
RUN npm install
# Install cvat-canvas dependencies
WORKDIR /tmp/cvat-canvas/
RUN npm install
# Install cvat-ui dependencies
WORKDIR /tmp/cvat-ui/
RUN npm install
# Build source code
COPY cvat-core/ /tmp/cvat-core/
COPY cvat-canvas/ /tmp/cvat-canvas/
COPY cvat-ui/ /tmp/cvat-ui/
RUN npm run build
FROM nginx:stable-alpine
# Replace default.conf configuration to remove unnecessary rules
COPY cvat-ui/react_nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=cvat-ui /tmp/cvat-ui/dist /usr/share/nginx/html/

@ -4,6 +4,8 @@
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/840351da141e4eaeac6476fd19ec0a33)](https://app.codacy.com/app/cvat/cvat?utm_source=github.com&utm_medium=referral&utm_content=opencv/cvat&utm_campaign=Badge_Grade_Dashboard) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/840351da141e4eaeac6476fd19ec0a33)](https://app.codacy.com/app/cvat/cvat?utm_source=github.com&utm_medium=referral&utm_content=opencv/cvat&utm_campaign=Badge_Grade_Dashboard)
[![Gitter chat](https://badges.gitter.im/opencv-cvat/gitter.png)](https://gitter.im/opencv-cvat) [![Gitter chat](https://badges.gitter.im/opencv-cvat/gitter.png)](https://gitter.im/opencv-cvat)
[![Coverage Status](https://coveralls.io/repos/github/opencv/cvat/badge.svg?branch=)](https://coveralls.io/github/opencv/cvat?branch=develop) [![Coverage Status](https://coveralls.io/repos/github/opencv/cvat/badge.svg?branch=)](https://coveralls.io/github/opencv/cvat?branch=develop)
[![codebeat badge](https://codebeat.co/badges/53cd0d16-fddc-46f8-903c-f43ed9abb6dd)](https://codebeat.co/projects/github-com-opencv-cvat-develop)
[![DOI](https://zenodo.org/badge/139156354.svg)](https://zenodo.org/badge/latestdoi/139156354)
CVAT is free, online, interactive video and image annotation tool for computer vision. It is being used by our team to annotate million of objects with different properties. Many UI and UX decisions are based on feedbacks from professional data annotation team. 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.
@ -14,6 +16,8 @@ CVAT is free, online, interactive video and image annotation tool for computer v
- [Installation guide](cvat/apps/documentation/installation.md) - [Installation guide](cvat/apps/documentation/installation.md)
- [User's guide](cvat/apps/documentation/user_guide.md) - [User's guide](cvat/apps/documentation/user_guide.md)
- [Django REST API documentation](#rest-api) - [Django REST API documentation](#rest-api)
- [Datumaro dataset framework](datumaro/README.md)
- [Command line interface](utils/cli/)
- [XML annotation format](cvat/apps/documentation/xml_format.md) - [XML annotation format](cvat/apps/documentation/xml_format.md)
- [AWS Deployment Guide](cvat/apps/documentation/AWS-Deployment-Guide.md) - [AWS Deployment Guide](cvat/apps/documentation/AWS-Deployment-Guide.md)
- [Questions](#questions) - [Questions](#questions)
@ -31,16 +35,20 @@ CVAT is free, online, interactive video and image annotation tool for computer v
## Supported annotation formats ## Supported annotation formats
Format selection is possible after clicking on the Upload annotation / Dump annotation button. Format selection is possible after clicking on the Upload annotation / Dump annotation button.
[Datumaro](datumaro/README.md) dataset framework allows additional dataset transformations
| Annotation format | Dumper | Loader | via its command line tool.
| ---------------------------------------------------------------------------------- | ------ | ------ |
| [CVAT XML v1.1 for images](cvat/apps/documentation/xml_format.md#annotation) | X | X | | Annotation format | Dumper | Loader |
| [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 XML v1.1 for images](cvat/apps/documentation/xml_format.md#annotation) | X | X |
| [YOLO](https://pjreddie.com/darknet/yolo/) | X | X | | [CVAT XML v1.1 for a video](cvat/apps/documentation/xml_format.md#interpolation) | X | X |
| [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X | | [Pascal VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| PNG mask | X | | | [YOLO](https://pjreddie.com/darknet/yolo/) | X | X |
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tf_records) | 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 |
## Links ## Links
- [Intel AI blog: New Computer Vision Tool Accelerates Annotation of Digital Images and Video](https://www.intel.ai/introducing-cvat) - [Intel AI blog: New Computer Vision Tool Accelerates Annotation of Digital Images and Video](https://www.intel.ai/introducing-cvat)
@ -49,12 +57,7 @@ Format selection is possible after clicking on the Upload annotation / Dump anno
## Online Demo ## Online Demo
[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). [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).
After you click the link above:
- Click on "GO TO WORKSPACE" and the CVAT environment will load up
- The environment is backed by a K80 GPU
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. 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.
@ -75,7 +78,11 @@ contributors and other users.
However, if you have a feature request or a bug report that can reproduced, However, if you have a feature request or a bug report that can reproduced,
feel free to open an issue (with steps to reproduce the bug if it's a bug feel free to open an issue (with steps to reproduce the bug if it's a bug
report). report) on [GitHub* issues](https://github.com/opencv/cvat/issues).
If you are not sure or just want to browse other users common questions, If you are not sure or just want to browse other users common questions,
[Gitter chat](https://gitter.im/opencv-cvat) is the way to go. [Gitter chat](https://gitter.im/opencv-cvat) is the way to go.
Other ways to ask questions and get our support:
* [\#cvat](https://stackoverflow.com/search?q=%23cvat) tag on StackOverflow*
* [Forum on Intel Developer Zone](https://software.intel.com/en-us/forums/computer-vision)

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

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

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

@ -15,4 +15,9 @@ services:
environment: environment:
NVIDIA_VISIBLE_DEVICES: all NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: compute,utility NVIDIA_DRIVER_CAPABILITIES: compute,utility
NVIDIA_REQUIRE_CUDA: "cuda>=9.0" # That environment variable is used by the Nvidia Container Runtime.
# The Nvidia Container Runtime parses this as:
# :space:: logical OR
# ,: Logical AND
# https://gitlab.com/nvidia/container-images/cuda/issues/31#note_149432780
NVIDIA_REQUIRE_CUDA: "cuda>=10.0 brand=tesla,driver>=384,driver<385 brand=tesla,driver>=410,driver<411"

@ -14,24 +14,25 @@ echo "$NVIDIA_GPGKEY_SUM cudasign.pub" | sha256sum -c --strict - && rm cudasign
echo "deb http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/x86_64 /" > /etc/apt/sources.list.d/cuda.list && \ echo "deb http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/x86_64 /" > /etc/apt/sources.list.d/cuda.list && \
echo "deb http://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu1604/x86_64 /" > /etc/apt/sources.list.d/nvidia-ml.list echo "deb http://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu1604/x86_64 /" > /etc/apt/sources.list.d/nvidia-ml.list
CUDA_VERSION=9.0.176 CUDA_VERSION=10.0.130
NCCL_VERSION=2.1.15 NCCL_VERSION=2.5.6
CUDNN_VERSION=7.6.2.24 CUDNN_VERSION=7.6.5.32
CUDA_PKG_VERSION="9-0=${CUDA_VERSION}-1" CUDA_PKG_VERSION="10-0=$CUDA_VERSION-1"
echo 'export PATH=/usr/local/nvidia/bin:/usr/local/cuda/bin:${PATH}' >> ${HOME}/.bashrc echo 'export PATH=/usr/local/nvidia/bin:/usr/local/cuda/bin:${PATH}' >> ${HOME}/.bashrc
echo 'export LD_LIBRARY_PATH=/usr/local/nvidia/lib:/usr/local/nvidia/lib64:${LD_LIBRARY_PATH}' >> ${HOME}/.bashrc echo 'export LD_LIBRARY_PATH=/usr/local/nvidia/lib:/usr/local/nvidia/lib64:${LD_LIBRARY_PATH}' >> ${HOME}/.bashrc
apt-get update && apt-get install -y --no-install-recommends --allow-unauthenticated \ apt-get update && apt-get install -y --no-install-recommends --allow-unauthenticated \
libprotobuf-dev \
libprotoc-dev \
protobuf-compiler \
cuda-cudart-$CUDA_PKG_VERSION \ cuda-cudart-$CUDA_PKG_VERSION \
cuda-compat-10-0 \
cuda-libraries-$CUDA_PKG_VERSION \ cuda-libraries-$CUDA_PKG_VERSION \
libnccl2=$NCCL_VERSION-1+cuda9.0 \ cuda-nvtx-$CUDA_PKG_VERSION \
libcudnn7=$CUDNN_VERSION-1+cuda9.0 && \ libnccl2=$NCCL_VERSION-1+cuda10.0 \
ln -s cuda-9.0 /usr/local/cuda && \ libcudnn7=$CUDNN_VERSION-1+cuda10.0 && \
rm -rf /var/lib/apt/lists/* \ ln -s cuda-10.0 /usr/local/cuda && \
apt-mark hold libnccl2 libcudnn7 && \
rm -rf /var/lib/apt/lists/* \
/etc/apt/sources.list.d/nvidia-ml.list /etc/apt/sources.list.d/cuda.list /etc/apt/sources.list.d/nvidia-ml.list /etc/apt/sources.list.d/cuda.list
pip3 uninstall -y tensorflow python3 -m pip uninstall -y tensorflow
pip3 install --no-cache-dir tensorflow-gpu==1.12.3 python3 -m pip install --no-cache-dir tensorflow-gpu==1.15.2

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

@ -7,7 +7,7 @@
set -e set -e
cd ${HOME} && \ cd ${HOME} && \
wget -O model.tar.gz http://download.tensorflow.org/models/object_detection/faster_rcnn_inception_resnet_v2_atrous_coco_2018_01_28.tar.gz && \ curl http://download.tensorflow.org/models/object_detection/faster_rcnn_inception_resnet_v2_atrous_coco_2018_01_28.tar.gz -o model.tar.gz && \
tar -xzf model.tar.gz && rm model.tar.gz && \ tar -xzf model.tar.gz && rm model.tar.gz && \
mv faster_rcnn_inception_resnet_v2_atrous_coco_2018_01_28 ${HOME}/rcnn && cd ${HOME} && \ mv faster_rcnn_inception_resnet_v2_atrous_coco_2018_01_28 ${HOME}/rcnn && cd ${HOME} && \
mv rcnn/frozen_inference_graph.pb rcnn/inference_graph.pb mv rcnn/frozen_inference_graph.pb rcnn/inference_graph.pb

@ -32,14 +32,15 @@ Canvas itself handles:
### API Methods ### API Methods
```ts ```ts
enum Rotation { enum RectDrawingMethod {
ANTICLOCKWISE90, CLASSIC = 'By 2 points',
CLOCKWISE90, EXTREME_POINTS = 'By 4 points'
} }
interface DrawData { interface DrawData {
enabled: boolean; enabled: boolean;
shapeType?: string; shapeType?: string;
rectDrawingMethod?: RectDrawingMethod;
numberOfPoints?: number; numberOfPoints?: number;
initialState?: any; initialState?: any;
crosshair?: boolean; crosshair?: boolean;
@ -70,9 +71,10 @@ Canvas itself handles:
interface Canvas { interface Canvas {
html(): HTMLDivElement; html(): HTMLDivElement;
setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void; setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID?: number): void; activate(clientID: number, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void; rotate(frameAngle: number): void;
focus(clientID: number, padding?: number): void; focus(clientID: number, padding?: number): void;
fit(): void; fit(): void;
grid(stepX: number, stepY: number): void; grid(stepX: number, stepY: number): void;
@ -83,6 +85,10 @@ Canvas itself handles:
merge(mergeData: MergeData): void; merge(mergeData: MergeData): void;
select(objectState: any): void; select(objectState: any): void;
fitCanvas(): void;
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
cancel(): void; cancel(): void;
} }
``` ```
@ -99,7 +105,7 @@ Canvas itself handles:
- Drawn texts have the class ```cvat_canvas_text``` - Drawn texts have the class ```cvat_canvas_text```
- Tags have the class ```cvat_canvas_tag``` - Tags have the class ```cvat_canvas_tag```
- Canvas image has ID ```cvat_canvas_image``` - Canvas image has ID ```cvat_canvas_image```
- Grid on the canvas has ID ```cvat_canvas_grid_pattern``` - Grid on the canvas has ID ```cvat_canvas_grid``` and ```cvat_canvas_grid_pattern```
- Crosshair during a draw has class ```cvat_canvas_crosshair``` - Crosshair during a draw has class ```cvat_canvas_crosshair```
### Events ### Events
@ -107,16 +113,21 @@ Canvas itself handles:
Standard JS events are used. Standard JS events are used.
```js ```js
- canvas.setup - canvas.setup
- canvas.activated => ObjectState - canvas.activated => {state: ObjectState}
- canvas.deactivated - canvas.clicked => {state: ObjectState}
- canvas.moved => {states: ObjectState[], x: number, y: number} - canvas.moved => {states: ObjectState[], x: number, y: number}
- canvas.find => {states: ObjectState[], x: number, y: number} - canvas.find => {states: ObjectState[], x: number, y: number}
- canvas.drawn => {state: DrawnData} - canvas.drawn => {state: DrawnData}
- canvas.editstart
- canvas.edited => {state: ObjectState, points: number[]} - canvas.edited => {state: ObjectState, points: number[]}
- canvas.splitted => {state: ObjectState} - canvas.splitted => {state: ObjectState}
- canvas.groupped => {states: ObjectState[]} - canvas.groupped => {states: ObjectState[]}
- canvas.merged => {states: ObjectState[]} - canvas.merged => {states: ObjectState[]}
- canvas.canceled - canvas.canceled
- canvas.dragstart
- canvas.dragstop
- canvas.zoomstart
- canvas.zoomstop
``` ```
### WEB ### WEB
@ -124,75 +135,39 @@ Standard JS events are used.
// Create an instance of a canvas // Create an instance of a canvas
const canvas = new window.canvas.Canvas(); const canvas = new window.canvas.Canvas();
// Put canvas to a html container console.log('Version', window.canvas.CanvasVersion);
htmlContainer.appendChild(canvas.html());
// Next you can use its API methods. For example:
canvas.rotate(window.Canvas.Rotation.CLOCKWISE90);
canvas.draw({
enabled: true,
shapeType: 'rectangle',
crosshair: true,
});
```
### TypeScript
- Add to ```tsconfig.json```:
```json
"compilerOptions": {
"paths": {
"cvat-canvas.node": ["3rdparty/cvat-canvas.node"]
}
}
```
- ```3rdparty``` directory contains both ```cvat-canvas.node.js``` and ```cvat-canvas.node.d.ts```.
- Add alias to ```webpack.config.js```:
```js
module.exports = {
resolve: {
alias: {
'cvat-canvas.node': path.resolve(__dirname, '3rdparty/cvat-canvas.node.js'),
}
}
}
```
Than you can use it in TypeScript:
```ts
import * as CANVAS from 'cvat-canvas.node';
// Create an instance of a canvas
const canvas = new CANVAS.Canvas();
// Put canvas to a html container // Put canvas to a html container
htmlContainer.appendChild(canvas.html()); htmlContainer.appendChild(canvas.html());
canvas.fitCanvas();
// Next you can use its API methods. For example: // Next you can use its API methods. For example:
canvas.rotate(CANVAS.Rotation.CLOCKWISE90); canvas.rotate(270);
canvas.draw({ canvas.draw({
enabled: true, enabled: true,
shapeType: 'rectangle', shapeType: 'rectangle',
crosshair: true, crosshair: true,
rectDrawingMethod: window.Canvas.RectDrawingMethod.CLASSIC,
}); });
``` ```
## States
![](images/states.svg)
## API Reaction ## API Reaction
| | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING | | | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING | DRAG | ZOOM |
|------------|------|----------|-----------|---------|---------|---------| |--------------|------|----------|-----------|---------|---------|---------|------|------|
| html() | + | + | + | + | + | + | | html() | + | + | + | + | + | + | + | + |
| setup() | + | + | + | + | + | - | | setup() | + | + | + | + | + | - | + | + |
| activate() | + | - | - | - | - | - | | activate() | + | - | - | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | | rotate() | + | + | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | | focus() | + | + | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + | | fit() | + | + | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + | | grid() | + | + | + | + | + | + | + | + |
| draw() | + | - | - | - | - | - | | draw() | + | - | - | - | - | - | - | - |
| split() | + | - | + | - | - | - | | split() | + | - | + | - | - | - | - | - |
| group | + | + | - | - | - | - | | group() | + | + | - | - | - | - | - | - |
| merge() | + | - | - | - | + | - | | merge() | + | - | - | - | + | - | - | - |
| cancel() | - | + | + | + | + | + | | fitCanvas() | + | + | + | + | + | + | + | + |
| dragCanvas() | + | - | - | - | - | - | + | - |
| zoomCanvas() | + | - | - | - | - | - | - | + |
| cancel() | - | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + |

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "0.1.0", "version": "0.5.2",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library", "description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts", "main": "src/canvas.ts",
"scripts": { "scripts": {
@ -31,7 +31,11 @@
"eslint-config-airbnb-typescript": "^4.0.1", "eslint-config-airbnb-typescript": "^4.0.1",
"eslint-config-typescript-recommended": "^1.4.17", "eslint-config-typescript-recommended": "^1.4.17",
"eslint-plugin-import": "^2.18.2", "eslint-plugin-import": "^2.18.2",
"node-sass": "^4.13.0",
"nodemon": "^1.19.1", "nodemon": "^1.19.1",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"sass-loader": "^8.0.0",
"style-loader": "^1.0.0", "style-loader": "^1.0.0",
"typescript": "^3.5.3", "typescript": "^3.5.3",
"webpack": "^4.36.1", "webpack": "^4.36.1",

@ -0,0 +1,13 @@
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/* eslint-disable */
module.exports = {
parser: false,
plugins: {
'postcss-preset-env': {
browsers: '> 2.5%', // https://github.com/browserslist/browserslist
},
},
};

@ -1,17 +1,41 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
.cvat_canvas_hidden { .cvat_canvas_hidden {
display: none; display: none;
} }
.cvat_canvas_shape { .cvat_canvas_shape {
fill-opacity: 0.05;
stroke-opacity: 1; stroke-opacity: 1;
} }
polyline.cvat_canvas_shape { polyline.cvat_canvas_shape {
fill-opacity: 0; fill-opacity: 0;
}
.cvat_shape_action_opacity {
fill-opacity: 0.5;
stroke-opacity: 1;
}
polyline.cvat_shape_action_opacity {
fill-opacity: 0;
}
.cvat_shape_drawing_opacity {
fill-opacity: 0.2;
stroke-opacity: 1; stroke-opacity: 1;
} }
polyline.cvat_shape_drawing_opacity {
fill-opacity: 0;
}
.cvat_shape_action_dasharray {
stroke-dasharray: 4 1 2 3;
}
.cvat_canvas_text { .cvat_canvas_text {
font-weight: bold; font-weight: bold;
font-size: 1.2em; font-size: 1.2em;
@ -27,47 +51,54 @@ polyline.cvat_canvas_shape {
stroke: red; stroke: red;
} }
.cvat_canvas_shape_activated {
fill-opacity: 0.3;
}
.cvat_canvas_shape_grouping { .cvat_canvas_shape_grouping {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: darkmagenta; fill: darkmagenta;
fill-opacity: 0.5;
} }
polyline.cvat_canvas_shape_grouping { polyline.cvat_canvas_shape_grouping {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: darkmagenta; stroke: darkmagenta;
stroke-opacity: 1;
} }
.cvat_canvas_shape_merging { .cvat_canvas_shape_merging {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: blue; fill: blue;
fill-opacity: 0.5; }
polyline.cvat_canvas_shape_merging {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: blue;
} }
polyline.cvat_canvas_shape_splitting { polyline.cvat_canvas_shape_splitting {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: dodgerblue; stroke: dodgerblue;
stroke-opacity: 1;
} }
.cvat_canvas_shape_splitting { .cvat_canvas_shape_splitting {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: dodgerblue; fill: dodgerblue;
fill-opacity: 0.5;
}
polyline.cvat_canvas_shape_merging {
stroke: blue;
stroke-opacity: 1;
} }
.cvat_canvas_shape_drawing { .cvat_canvas_shape_drawing {
fill-opacity: 0.1; @extend .cvat_shape_drawing_opacity;
stroke-opacity: 1;
fill: white; fill: white;
stroke: black; stroke: black;
} }
.cvat_canvas_zoom_selection {
@extend .cvat_shape_action_dasharray;
stroke: #096dd9;
fill-opacity: 0;
}
.cvat_canvas_shape_occluded { .cvat_canvas_shape_occluded {
stroke-dasharray: 5; stroke-dasharray: 5;
} }
@ -78,8 +109,9 @@ polyline.cvat_canvas_shape_merging {
} }
#cvat_canvas_wrapper { #cvat_canvas_wrapper {
width: 100%; width: calc(100% - 10px);
height: 93%; height: calc(100% - 10px);
margin: 5px;
border-radius: 5px; border-radius: 5px;
background-color: white; background-color: white;
overflow: hidden; overflow: hidden;
@ -102,6 +134,7 @@ polyline.cvat_canvas_shape_merging {
} }
#cvat_canvas_text_content { #cvat_canvas_text_content {
text-rendering: optimizeSpeed;
position: absolute; position: absolute;
z-index: 3; z-index: 3;
pointer-events: none; pointer-events: none;
@ -134,6 +167,7 @@ polyline.cvat_canvas_shape_merging {
} }
#cvat_canvas_content { #cvat_canvas_content {
filter: contrast(120%) saturate(150%);
position: absolute; position: absolute;
z-index: 2; z-index: 2;
outline: 10px solid black; outline: 10px solid black;
@ -145,4 +179,4 @@ polyline.cvat_canvas_shape_merging {
0% {stroke-dashoffset: 1; stroke: #09c;} 0% {stroke-dashoffset: 1; stroke: #09c;}
50% {stroke-dashoffset: 100; stroke: #f44;} 50% {stroke-dashoffset: 100; stroke: #f44;}
100% {stroke-dashoffset: 300; stroke: #09c;} 100% {stroke-dashoffset: 300; stroke: #09c;}
} }

@ -1,16 +1,16 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
import { import {
Rotation, Mode,
DrawData, DrawData,
MergeData, MergeData,
SplitData, SplitData,
GroupData, GroupData,
CanvasModel, CanvasModel,
CanvasModelImpl, CanvasModelImpl,
RectDrawingMethod,
} from './canvasModel'; } from './canvasModel';
import { import {
@ -27,15 +27,17 @@ import {
CanvasViewImpl, CanvasViewImpl,
} from './canvasView'; } from './canvasView';
import '../scss/canvas.scss';
import pjson from '../../package.json';
import '../css/canvas.css'; const CanvasVersion = pjson.version;
interface Canvas { interface Canvas {
html(): HTMLDivElement; html(): HTMLDivElement;
setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void; setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID?: number): void; activate(clientID: number | null, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void; rotate(rotationAngle: number): void;
focus(clientID: number, padding?: number): void; focus(clientID: number, padding?: number): void;
fit(): void; fit(): void;
grid(stepX: number, stepY: number): void; grid(stepX: number, stepY: number): void;
@ -46,6 +48,11 @@ interface Canvas {
merge(mergeData: MergeData): void; merge(mergeData: MergeData): void;
select(objectState: any): void; select(objectState: any): void;
fitCanvas(): void;
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
mode(): void;
cancel(): void; cancel(): void;
} }
@ -64,16 +71,35 @@ class CanvasImpl implements Canvas {
return this.view.html(); return this.view.html();
} }
public setZLayer(zLayer: number | null): void {
this.model.setZLayer(zLayer);
}
public setup(frameData: any, objectStates: any[]): void { public setup(frameData: any, objectStates: any[]): void {
this.model.setup(frameData, objectStates); this.model.setup(frameData, objectStates);
} }
public activate(clientID: number, attributeID: number = null): void { public fitCanvas(): void {
this.model.fitCanvas(
this.view.html().clientWidth,
this.view.html().clientHeight,
);
}
public dragCanvas(enable: boolean): void {
this.model.dragCanvas(enable);
}
public zoomCanvas(enable: boolean): void {
this.model.zoomCanvas(enable);
}
public activate(clientID: number | null, attributeID: number | null = null): void {
this.model.activate(clientID, attributeID); this.model.activate(clientID, attributeID);
} }
public rotate(rotation: Rotation, remember: boolean = false): void { public rotate(rotationAngle: number): void {
this.model.rotate(rotation, remember); this.model.rotate(rotationAngle);
} }
public focus(clientID: number, padding: number = 0): void { public focus(clientID: number, padding: number = 0): void {
@ -108,13 +134,18 @@ class CanvasImpl implements Canvas {
this.model.select(objectState); this.model.select(objectState);
} }
public mode(): Mode {
return this.model.mode;
}
public cancel(): void { public cancel(): void {
this.model.cancel(); this.model.cancel();
} }
} }
export { export {
CanvasImpl as Canvas, CanvasImpl as Canvas,
Rotation, CanvasVersion,
RectDrawingMethod,
Mode as CanvasMode,
}; };

@ -1,7 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
import { import {
CanvasModel, CanvasModel,
@ -18,6 +17,7 @@ import {
export interface CanvasController { export interface CanvasController {
readonly objects: any[]; readonly objects: any[];
readonly zLayer: number | null;
readonly focusData: FocusData; readonly focusData: FocusData;
readonly activeElement: ActiveElement; readonly activeElement: ActiveElement;
readonly drawData: DrawData; readonly drawData: DrawData;
@ -105,6 +105,10 @@ export class CanvasControllerImpl implements CanvasController {
this.model.geometry = geometry; this.model.geometry = geometry;
} }
public get zLayer(): number | null {
return this.model.zLayer;
}
public get objects(): any[] { public get objects(): any[] {
return this.model.objects; return this.model.objects;
} }

@ -1,14 +1,9 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
// Disable till full implementation
/* eslint class-methods-use-this: "off" */
import { MasterImpl } from './master'; import { MasterImpl } from './master';
export interface Size { export interface Size {
width: number; width: number;
height: number; height: number;
@ -36,13 +31,19 @@ export interface FocusData {
} }
export interface ActiveElement { export interface ActiveElement {
clientID: number; clientID: number | null;
attributeID: number; attributeID: number | null;
}
export enum RectDrawingMethod {
CLASSIC = 'By 2 points',
EXTREME_POINTS = 'By 4 points'
} }
export interface DrawData { export interface DrawData {
enabled: boolean; enabled: boolean;
shapeType?: string; shapeType?: string;
rectDrawingMethod?: RectDrawingMethod;
numberOfPoints?: number; numberOfPoints?: number;
initialState?: any; initialState?: any;
crosshair?: boolean; crosshair?: boolean;
@ -71,26 +72,28 @@ export enum FrameZoom {
MAX = 10, MAX = 10,
} }
export enum Rotation {
ANTICLOCKWISE90,
CLOCKWISE90,
}
export enum UpdateReasons { export enum UpdateReasons {
IMAGE = 'image', IMAGE_CHANGED = 'image_changed',
OBJECTS = 'objects', IMAGE_ZOOMED = 'image_zoomed',
ZOOM = 'zoom', IMAGE_FITTED = 'image_fitted',
FIT = 'fit', IMAGE_MOVED = 'image_moved',
MOVE = 'move', GRID_UPDATED = 'grid_updated',
GRID = 'grid', SET_Z_LAYER = 'set_z_layer',
FOCUS = 'focus',
ACTIVATE = 'activate', OBJECTS_UPDATED = 'objects_updated',
SHAPE_ACTIVATED = 'shape_activated',
SHAPE_FOCUSED = 'shape_focused',
FITTED_CANVAS = 'fitted_canvas',
DRAW = 'draw', DRAW = 'draw',
MERGE = 'merge', MERGE = 'merge',
SPLIT = 'split', SPLIT = 'split',
GROUP = 'group', GROUP = 'group',
SELECT = 'select', SELECT = 'select',
CANCEL = 'cancel', CANCEL = 'cancel',
DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'ZOOM_CANVAS',
} }
export enum Mode { export enum Mode {
@ -102,11 +105,14 @@ export enum Mode {
MERGE = 'merge', MERGE = 'merge',
SPLIT = 'split', SPLIT = 'split',
GROUP = 'group', GROUP = 'group',
DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'zoom_canvas',
} }
export interface CanvasModel { export interface CanvasModel {
readonly image: string; readonly image: HTMLImageElement | null;
readonly objects: any[]; readonly objects: any[];
readonly zLayer: number | null;
readonly gridSize: Size; readonly gridSize: Size;
readonly focusData: FocusData; readonly focusData: FocusData;
readonly activeElement: ActiveElement; readonly activeElement: ActiveElement;
@ -118,12 +124,13 @@ export interface CanvasModel {
geometry: Geometry; geometry: Geometry;
mode: Mode; mode: Mode;
setZLayer(zLayer: number | null): void;
zoom(x: number, y: number, direction: number): void; zoom(x: number, y: number, direction: number): void;
move(topOffset: number, leftOffset: number): void; move(topOffset: number, leftOffset: number): void;
setup(frameData: any, objectStates: any[]): void; setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID: number): void; activate(clientID: number | null, attributeID: number | null): void;
rotate(rotation: Rotation, remember: boolean): void; rotate(rotationAngle: number): void;
focus(clientID: number, padding: number): void; focus(clientID: number, padding: number): void;
fit(): void; fit(): void;
grid(stepX: number, stepY: number): void; grid(stepX: number, stepY: number): void;
@ -134,6 +141,10 @@ export interface CanvasModel {
merge(mergeData: MergeData): void; merge(mergeData: MergeData): void;
select(objectState: any): void; select(objectState: any): void;
fitCanvas(width: number, height: number): void;
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
cancel(): void; cancel(): void;
} }
@ -142,16 +153,17 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
activeElement: ActiveElement; activeElement: ActiveElement;
angle: number; angle: number;
canvasSize: Size; canvasSize: Size;
image: string; image: HTMLImageElement | null;
imageID: number | null;
imageOffset: number; imageOffset: number;
imageSize: Size; imageSize: Size;
focusData: FocusData; focusData: FocusData;
gridSize: Size; gridSize: Size;
left: number; left: number;
objects: any[]; objects: any[];
rememberAngle: boolean;
scale: number; scale: number;
top: number; top: number;
zLayer: number | null;
drawData: DrawData; drawData: DrawData;
mergeData: MergeData; mergeData: MergeData;
groupData: GroupData; groupData: GroupData;
@ -173,7 +185,8 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
height: 0, height: 0,
width: 0, width: 0,
}, },
image: '', image: null,
imageID: null,
imageOffset: 0, imageOffset: 0,
imageSize: { imageSize: {
height: 0, height: 0,
@ -189,13 +202,11 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}, },
left: 0, left: 0,
objects: [], objects: [],
rememberAngle: false,
scale: 1, scale: 1,
top: 0, top: 0,
zLayer: null,
drawData: { drawData: {
enabled: false, enabled: false,
shapeType: null,
numberOfPoints: null,
initialState: null, initialState: null,
}, },
mergeData: { mergeData: {
@ -208,10 +219,15 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
enabled: false, enabled: false,
}, },
selected: null, selected: null,
mode: null, mode: Mode.IDLE,
}; };
} }
public setZLayer(zLayer: number | null): void {
this.data.zLayer = zLayer;
this.notify(UpdateReasons.SET_Z_LAYER);
}
public zoom(x: number, y: number, direction: number): void { public zoom(x: number, y: number, direction: number): void {
const oldScale: number = this.data.scale; const oldScale: number = this.data.scale;
const newScale: number = direction > 0 ? oldScale * 6 / 5 : oldScale * 5 / 6; const newScale: number = direction > 0 ? oldScale * 6 / 5 : oldScale * 5 / 6;
@ -233,42 +249,89 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
* (oldScale / this.data.scale - 1)) * this.data.scale; * (oldScale / this.data.scale - 1)) * this.data.scale;
} }
this.notify(UpdateReasons.ZOOM); this.notify(UpdateReasons.IMAGE_ZOOMED);
} }
public move(topOffset: number, leftOffset: number): void { public move(topOffset: number, leftOffset: number): void {
this.data.top += topOffset; this.data.top += topOffset;
this.data.left += leftOffset; this.data.left += leftOffset;
this.notify(UpdateReasons.MOVE); this.notify(UpdateReasons.IMAGE_MOVED);
}
public fitCanvas(width: number, height: number): void {
this.data.canvasSize.height = height;
this.data.canvasSize.width = width;
this.data.imageOffset = Math.floor(Math.max(
this.data.canvasSize.height / FrameZoom.MIN,
this.data.canvasSize.width / FrameZoom.MIN,
));
this.notify(UpdateReasons.FITTED_CANVAS);
this.notify(UpdateReasons.OBJECTS_UPDATED);
}
public dragCanvas(enable: boolean): void {
if (enable && this.data.mode !== Mode.IDLE) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (!enable && this.data.mode !== Mode.DRAG_CANVAS) {
throw Error(`Canvas is not in the drag mode. Action: ${this.data.mode}`);
}
this.data.mode = enable ? Mode.DRAG_CANVAS : Mode.IDLE;
this.notify(UpdateReasons.DRAG_CANVAS);
}
public zoomCanvas(enable: boolean): void {
if (enable && this.data.mode !== Mode.IDLE) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (!enable && this.data.mode !== Mode.ZOOM_CANVAS) {
throw Error(`Canvas is not in the zoom mode. Action: ${this.data.mode}`);
}
this.data.mode = enable ? Mode.ZOOM_CANVAS : Mode.IDLE;
this.notify(UpdateReasons.ZOOM_CANVAS);
} }
public setup(frameData: any, objectStates: any[]): void { public setup(frameData: any, objectStates: any[]): void {
if (frameData.number === this.data.imageID) {
this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED);
return;
}
this.data.imageID = frameData.number;
frameData.data( frameData.data(
(): void => { (): void => {
this.data.image = ''; this.data.image = null;
this.notify(UpdateReasons.IMAGE); this.notify(UpdateReasons.IMAGE_CHANGED);
}, },
).then((data: string): void => { ).then((data: HTMLImageElement): void => {
if (frameData.number !== this.data.imageID) {
// already another image
return;
}
this.data.imageSize = { this.data.imageSize = {
height: (frameData.height as number), height: (frameData.height as number),
width: (frameData.width as number), width: (frameData.width as number),
}; };
if (!this.data.rememberAngle) {
this.data.angle = 0;
}
this.data.image = data; this.data.image = data;
this.notify(UpdateReasons.IMAGE); this.notify(UpdateReasons.IMAGE_CHANGED);
this.data.objects = objectStates; this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS); this.notify(UpdateReasons.OBJECTS_UPDATED);
}).catch((exception: any): void => { }).catch((exception: any): void => {
throw exception; throw exception;
}); });
} }
public activate(clientID: number, attributeID: number): void { public activate(clientID: number | null, attributeID: number | null): void {
if (this.data.mode !== Mode.IDLE) { if (this.data.mode !== Mode.IDLE && clientID !== null) {
// Exception or just return? // Exception or just return?
throw Error(`Canvas is busy. Action: ${this.data.mode}`); throw Error(`Canvas is busy. Action: ${this.data.mode}`);
} }
@ -278,19 +341,14 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
attributeID, attributeID,
}; };
this.notify(UpdateReasons.ACTIVATE); this.notify(UpdateReasons.SHAPE_ACTIVATED);
} }
public rotate(rotation: Rotation, remember: boolean = false): void { public rotate(rotationAngle: number): void {
if (rotation === Rotation.CLOCKWISE90) { if (this.data.angle !== rotationAngle) {
this.data.angle += 90; this.data.angle = (360 + Math.floor((rotationAngle) / 90) * 90) % 360;
} else { this.fit();
this.data.angle -= 90;
} }
this.data.angle %= 360;
this.data.rememberAngle = remember;
this.fit();
} }
public focus(clientID: number, padding: number): void { public focus(clientID: number, padding: number): void {
@ -299,7 +357,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
padding, padding,
}; };
this.notify(UpdateReasons.FOCUS); this.notify(UpdateReasons.SHAPE_FOCUSED);
} }
public fit(): void { public fit(): void {
@ -326,7 +384,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.top = (this.data.canvasSize.height / 2 - this.data.imageSize.height / 2); this.data.top = (this.data.canvasSize.height / 2 - this.data.imageSize.height / 2);
this.data.left = (this.data.canvasSize.width / 2 - this.data.imageSize.width / 2); this.data.left = (this.data.canvasSize.width / 2 - this.data.imageSize.width / 2);
this.notify(UpdateReasons.FIT); this.notify(UpdateReasons.IMAGE_FITTED);
} }
public grid(stepX: number, stepY: number): void { public grid(stepX: number, stepY: number): void {
@ -335,7 +393,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
width: stepX, width: stepX,
}; };
this.notify(UpdateReasons.GRID); this.notify(UpdateReasons.GRID_UPDATED);
} }
public draw(drawData: DrawData): void { public draw(drawData: DrawData): void {
@ -454,11 +512,20 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
)); ));
} }
public get image(): string { public get zLayer(): number | null {
return this.data.zLayer;
}
public get image(): HTMLImageElement | null {
return this.data.image; return this.data.image;
} }
public get objects(): any[] { public get objects(): any[] {
if (this.data.zLayer !== null) {
return this.data.objects
.filter((object: any): boolean => object.zOrder <= this.data.zLayer);
}
return this.data.objects; return this.data.objects;
} }

File diff suppressed because it is too large Load Diff

@ -1,10 +1,9 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
const BASE_STROKE_WIDTH = 2; const BASE_STROKE_WIDTH = 1.75;
const BASE_GRID_WIDTH = 1; const BASE_GRID_WIDTH = 2;
const BASE_POINT_SIZE = 5; const BASE_POINT_SIZE = 5;
const TEXT_MARGIN = 10; const TEXT_MARGIN = 10;
const AREA_THRESHOLD = 9; const AREA_THRESHOLD = 9;

@ -1,7 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
import * as SVG from 'svg.js'; import * as SVG from 'svg.js';
import consts from './consts'; import consts from './consts';
@ -11,11 +10,11 @@ import './svg.patch';
import { import {
DrawData, DrawData,
Geometry, Geometry,
RectDrawingMethod,
} from './canvasModel'; } from './canvasModel';
import { import {
translateToSVG, translateToSVG,
translateBetweenSVG,
displayShapeSize, displayShapeSize,
ShapeSizeElement, ShapeSizeElement,
pointsToString, pointsToString,
@ -26,15 +25,19 @@ import {
export interface DrawHandler { export interface DrawHandler {
draw(drawData: DrawData, geometry: Geometry): void; draw(drawData: DrawData, geometry: Geometry): void;
transform(geometry: Geometry): void;
cancel(): void; cancel(): void;
} }
export class DrawHandlerImpl implements DrawHandler { export class DrawHandlerImpl implements DrawHandler {
// callback is used to notify about creating new shape // callback is used to notify about creating new shape
private onDrawDone: (data: object) => void; private onDrawDone: (data: object, continueDraw?: boolean) => void;
private canvas: SVG.Container; private canvas: SVG.Container;
private text: SVG.Container; private text: SVG.Container;
private background: SVGSVGElement; private cursorPosition: {
x: number;
y: number;
};
private crosshair: { private crosshair: {
x: SVG.Line; x: SVG.Line;
y: SVG.Line; y: SVG.Line;
@ -45,17 +48,17 @@ export class DrawHandlerImpl implements DrawHandler {
// we should use any instead of SVG.Shape because svg plugins cannot change declared interface // we should use any instead of SVG.Shape because svg plugins cannot change declared interface
// so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist // so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist
private drawInstance: any; private drawInstance: any;
private initialized: boolean;
private pointsGroup: SVG.G | null;
private shapeSizeElement: ShapeSizeElement; private shapeSizeElement: ShapeSizeElement;
private getFinalRectCoordinates(bbox: BBox): number[] { private getFinalRectCoordinates(bbox: BBox): number[] {
const frameWidth = this.geometry.image.width; const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height; const frameHeight = this.geometry.image.height;
const { offset } = this.geometry;
let [xtl, ytl, xbr, ybr] = translateBetweenSVG( let [xtl, ytl, xbr, ybr] = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height]
this.canvas.node as any as SVGSVGElement, .map((coord: number): number => coord - offset);
this.background,
[bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height],
);
xtl = Math.min(Math.max(xtl, 0), frameWidth); xtl = Math.min(Math.max(xtl, 0), frameWidth);
xbr = Math.min(Math.max(xbr, 0), frameWidth); xbr = Math.min(Math.max(xbr, 0), frameWidth);
@ -69,12 +72,8 @@ export class DrawHandlerImpl implements DrawHandler {
points: number[]; points: number[];
box: Box; box: Box;
} { } {
const points = translateBetweenSVG( const { offset } = this.geometry;
this.canvas.node as any as SVGSVGElement, const points = targetPoints.map((coord: number): number => coord - offset);
this.background,
targetPoints,
);
const box = { const box = {
xtl: Number.MAX_SAFE_INTEGER, xtl: Number.MAX_SAFE_INTEGER,
ytl: Number.MAX_SAFE_INTEGER, ytl: Number.MAX_SAFE_INTEGER,
@ -101,12 +100,13 @@ export class DrawHandlerImpl implements DrawHandler {
} }
private addCrosshair(): void { private addCrosshair(): void {
const { x, y } = this.cursorPosition;
this.crosshair = { this.crosshair = {
x: this.canvas.line(0, 0, this.canvas.node.clientWidth, 0).attr({ x: this.canvas.line(0, y, this.canvas.node.clientWidth, y).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale), 'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER, zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'), }).addClass('cvat_canvas_crosshair'),
y: this.canvas.line(0, 0, 0, this.canvas.node.clientHeight).attr({ y: this.canvas.line(x, 0, x, this.canvas.node.clientHeight).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale), 'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER, zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'), }).addClass('cvat_canvas_crosshair'),
@ -120,22 +120,37 @@ export class DrawHandlerImpl implements DrawHandler {
} }
private release(): void { private release(): void {
if (!this.initialized) {
// prevents recursive calls
return;
}
this.initialized = false;
this.canvas.off('mousedown.draw'); this.canvas.off('mousedown.draw');
this.canvas.off('mouseup.draw');
this.canvas.off('mousemove.draw'); this.canvas.off('mousemove.draw');
this.canvas.off('click.draw'); this.canvas.off('click.draw');
if (this.drawInstance) { if (this.pointsGroup) {
// Draw plugin isn't activated when draw from initialState this.pointsGroup.remove();
// So, we don't need to use any draw events this.pointsGroup = null;
if (!this.drawData.initialState) { }
this.drawInstance.off('drawdone');
this.drawInstance.off('drawstop');
this.drawInstance.draw('stop');
}
this.drawInstance.remove(); // Draw plugin in some cases isn't activated
this.drawInstance = null; // For example when draw from initialState
// 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') {
// Check for unsaved drawn shapes
this.drawInstance.draw('done');
}
// Clear drawing
this.drawInstance.draw('stop');
} }
this.drawInstance.off();
this.drawInstance.remove();
this.drawInstance = null;
if (this.shapeSizeElement) { if (this.shapeSizeElement) {
this.shapeSizeElement.rm(); this.shapeSizeElement.rm();
@ -153,60 +168,64 @@ export class DrawHandlerImpl implements DrawHandler {
} }
} }
private closeDrawing(): void {
if (this.drawInstance) {
// Draw plugin isn't activated when draw from initialState
// So, we don't need to use any draw events
if (!this.drawData.initialState) {
const { drawInstance } = this;
this.drawInstance = null;
if (this.drawData.shapeType === 'rectangle') {
drawInstance.draw('cancel');
} else {
drawInstance.draw('done');
}
this.drawInstance = drawInstance;
this.release();
} else {
this.release();
this.onDrawDone(null);
}
// here is a cycle
// onDrawDone => controller => model => view => closeDrawing
// one call of closeDrawing is unuseful, but it's okey
}
}
private drawBox(): void { private drawBox(): void {
this.drawInstance = this.canvas.rect(); this.drawInstance = this.canvas.rect();
this.drawInstance.draw({ this.drawInstance.on('drawstop', (e: Event): void => {
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
z_order: Number.MAX_SAFE_INTEGER,
}).on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
}).on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox(); const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox); const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType } = this.drawData;
this.cancel();
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) { if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone({ this.onDrawDone({
shapeType: this.drawData.shapeType, shapeType,
points: [xtl, ytl, xbr, ybr], points: [xtl, ytl, xbr, ybr],
}); });
} else {
this.onDrawDone(null);
} }
}).on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
}).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
}); });
} }
private drawPolyshape(): void { private drawBoxBy4Points(): void {
this.drawInstance.attr({ let numberOfPoints = 0;
z_order: Number.MAX_SAFE_INTEGER, this.drawInstance = (this.canvas as any).polygon()
}); .addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': 0,
opacity: 0,
}).on('drawstart', (): void => {
// init numberOfPoints as one on drawstart
numberOfPoints = 1;
}).on('drawpoint', (e: CustomEvent): void => {
// increase numberOfPoints by one on drawpoint
numberOfPoints += 1;
// finish if numberOfPoints are exactly four
if (numberOfPoints === 4) {
const bbox = (e.target as SVGPolylineElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType } = this.drawData;
this.cancel();
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone({
shapeType,
points: [xtl, ytl, xbr, ybr],
});
}
}
}).on('undopoint', (): void => {
if (numberOfPoints > 0) {
numberOfPoints -= 1;
}
});
this.drawPolyshape();
}
private drawPolyshape(): void {
let size = this.drawData.numberOfPoints; let size = this.drawData.numberOfPoints;
const sizeDecrement = function sizeDecrement(): void { const sizeDecrement = function sizeDecrement(): void {
if (!--size) { if (!--size) {
@ -214,25 +233,20 @@ export class DrawHandlerImpl implements DrawHandler {
} }
}.bind(this); }.bind(this);
const sizeIncrement = function sizeIncrement(): void {
size++;
};
if (this.drawData.numberOfPoints) { if (this.drawData.numberOfPoints) {
this.drawInstance.on('drawstart', sizeDecrement); this.drawInstance.on('drawstart', sizeDecrement);
this.drawInstance.on('drawpoint', sizeDecrement); this.drawInstance.on('drawpoint', sizeDecrement);
this.drawInstance.on('undopoint', sizeIncrement); this.drawInstance.on('undopoint', (): number => size++);
} }
// Add ability to cancel the latest drawn point // Add ability to cancel the latest drawn point
const handleUndo = function handleUndo(e: MouseEvent): void { this.canvas.on('mousedown.draw', (e: MouseEvent): void => {
if (e.which === 3) { if (e.which === 3) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.drawInstance.draw('undo'); this.drawInstance.draw('undo');
} }
}.bind(this); });
this.canvas.on('mousedown.draw', handleUndo);
// Add ability to draw shapes by sliding // Add ability to draw shapes by sliding
// We need to remember last drawn point // We need to remember last drawn point
@ -245,7 +259,7 @@ export class DrawHandlerImpl implements DrawHandler {
y: null, y: null,
}; };
const handleSlide = function handleSlide(e: MouseEvent): void { this.canvas.on('mousemove.draw', (e: MouseEvent): void => {
// TODO: Use enumeration after typification cvat-core // TODO: Use enumeration after typification cvat-core
if (e.shiftKey && ['polygon', 'polyline'].includes(this.drawData.shapeType)) { if (e.shiftKey && ['polygon', 'polyline'].includes(this.drawData.shapeType)) {
if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) { if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) {
@ -260,14 +274,15 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance.draw('point', e); this.drawInstance.draw('point', e);
} }
} }
e.stopPropagation();
e.preventDefault();
} }
}.bind(this); });
this.canvas.on('mousemove.draw', handleSlide);
// We need scale just drawn points // We need scale just drawn points
const self = this;
this.drawInstance.on('drawstart drawpoint', (e: CustomEvent): void => { this.drawInstance.on('drawstart drawpoint', (e: CustomEvent): void => {
self.transform(self.geometry); this.transform(this.geometry);
lastDrawnPoint.x = e.detail.event.clientX; lastDrawnPoint.x = e.detail.event.clientX;
lastDrawnPoint.y = e.detail.event.clientY; lastDrawnPoint.y = e.detail.event.clientY;
}); });
@ -275,134 +290,129 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance.on('drawdone', (e: CustomEvent): void => { this.drawInstance.on('drawdone', (e: CustomEvent): void => {
const targetPoints = pointsToArray((e.target as SVGElement).getAttribute('points')); const targetPoints = pointsToArray((e.target as SVGElement).getAttribute('points'));
const { const { points, box } = this.getFinalPolyshapeCoordinates(targetPoints);
points, const { shapeType } = this.drawData;
box, this.cancel();
} = this.getFinalPolyshapeCoordinates(targetPoints);
if (this.drawData.shapeType === 'polygon' if (shapeType === 'polygon'
&& ((box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD) && ((box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD)
&& points.length >= 3 * 2) { && points.length >= 3 * 2) {
this.onDrawDone({ this.onDrawDone({
shapeType: this.drawData.shapeType, shapeType,
points, points,
}); });
} else if (this.drawData.shapeType === 'polyline' } else if (shapeType === 'polyline'
&& ((box.xbr - box.xtl) >= consts.SIZE_THRESHOLD && ((box.xbr - box.xtl) >= consts.SIZE_THRESHOLD
|| (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD) || (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD)
&& points.length >= 2 * 2) { && points.length >= 2 * 2) {
this.onDrawDone({ this.onDrawDone({
shapeType: this.drawData.shapeType, shapeType,
points, points,
}); });
} else if (this.drawData.shapeType === 'points' } else if (shapeType === 'points'
&& (e.target as any).getAttribute('points') !== '0,0') { && (e.target as any).getAttribute('points') !== '0,0') {
this.onDrawDone({ this.onDrawDone({
shapeType: this.drawData.shapeType, shapeType,
points, points,
}); });
} else {
this.onDrawDone(null);
} }
}); });
} }
private drawPolygon(): void { private drawPolygon(): void {
this.drawInstance = (this.canvas as any).polygon().draw({ this.drawInstance = (this.canvas as any).polygon()
snapToGrid: 0.1, .addClass('cvat_canvas_shape_drawing').attr({
}).addClass('cvat_canvas_shape_drawing').style({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, });
});
this.drawPolyshape(); this.drawPolyshape();
} }
private drawPolyline(): void { private drawPolyline(): void {
this.drawInstance = (this.canvas as any).polyline().draw({ this.drawInstance = (this.canvas as any).polyline()
snapToGrid: 0.1, .addClass('cvat_canvas_shape_drawing').attr({
}).addClass('cvat_canvas_shape_drawing').style({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'fill-opacity': 0,
'fill-opacity': 0, });
});
this.drawPolyshape(); this.drawPolyshape();
} }
private drawPoints(): void { private drawPoints(): void {
this.drawInstance = (this.canvas as any).polygon().draw({ this.drawInstance = (this.canvas as any).polygon()
snapToGrid: 0.1, .addClass('cvat_canvas_shape_drawing').attr({
}).addClass('cvat_canvas_shape_drawing').style({ 'stroke-width': 0,
'stroke-width': 0, opacity: 0,
opacity: 0, });
});
this.drawPolyshape(); this.drawPolyshape();
} }
private pastePolyshape(): void { private pastePolyshape(): void {
this.canvas.on('click.draw', (e: MouseEvent): void => { this.drawInstance.on('done', (e: CustomEvent): void => {
const targetPoints = (e.target as SVGElement) const targetPoints = this.drawInstance
.getAttribute('points') .attr('points')
.split(/[,\s]/g) .split(/[,\s]/g)
.map((coord): number => +coord); .map((coord: string): number => +coord);
const { points } = this.getFinalPolyshapeCoordinates(targetPoints); const { points } = this.getFinalPolyshapeCoordinates(targetPoints);
this.release(); this.release();
this.onDrawDone({ this.onDrawDone({
shapeType: this.drawData.shapeType, shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points, points,
occluded: this.drawData.initialState.occluded, occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes }, attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label, label: this.drawData.initialState.label,
color: this.drawData.initialState.color, color: this.drawData.initialState.color,
}); }, e.detail.originalEvent.ctrlKey);
}); });
} }
// Common settings for rectangle and polyshapes // Common settings for rectangle and polyshapes
private pasteShape(): void { private pasteShape(): void {
this.drawInstance.attr({ function moveShape(shape: SVG.Shape, x: number, y: number): void {
z_order: Number.MAX_SAFE_INTEGER, const bbox = shape.bbox();
}); shape.move(x - bbox.width / 2, y - bbox.height / 2);
}
this.canvas.on('mousemove.draw', (e: MouseEvent): void => { const { x: initialX, y: initialY } = this.cursorPosition;
const [x, y] = translateToSVG( moveShape(this.drawInstance, initialX, initialY);
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
const bbox = this.drawInstance.bbox(); this.canvas.on('mousemove.draw', (): void => {
this.drawInstance.move(x - bbox.width / 2, y - bbox.height / 2); const { x, y } = this.cursorPosition; // was computer in another callback
moveShape(this.drawInstance, x, y);
}); });
} }
private pasteBox(box: BBox): void { private pasteBox(box: BBox): void {
this.drawInstance = (this.canvas as any).rect(box.width, box.height) this.drawInstance = (this.canvas as any).rect(box.width, box.height)
.move(box.x, box.y) .move(box.x, box.y)
.addClass('cvat_canvas_shape_drawing').style({ .addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
}); });
this.pasteShape(); this.pasteShape();
this.canvas.on('click.draw', (e: MouseEvent): void => { this.drawInstance.on('done', (e: CustomEvent): void => {
const bbox = (e.target as SVGRectElement).getBBox(); const bbox = this.drawInstance.node.getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox); const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
this.release(); this.release();
this.onDrawDone({ this.onDrawDone({
shapeType: this.drawData.shapeType, shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points: [xtl, ytl, xbr, ybr], points: [xtl, ytl, xbr, ybr],
occluded: this.drawData.initialState.occluded, occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes }, attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label, label: this.drawData.initialState.label,
color: this.drawData.initialState.color, color: this.drawData.initialState.color,
}); }, e.detail.originalEvent.ctrlKey);
}); });
} }
private pastePolygon(points: string): void { private pastePolygon(points: string): void {
this.drawInstance = (this.canvas as any).polygon(points) this.drawInstance = (this.canvas as any).polygon(points)
.addClass('cvat_canvas_shape_drawing').style({ .addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
}); });
this.pasteShape(); this.pasteShape();
@ -411,31 +421,126 @@ export class DrawHandlerImpl implements DrawHandler {
private pastePolyline(points: string): void { private pastePolyline(points: string): void {
this.drawInstance = (this.canvas as any).polyline(points) this.drawInstance = (this.canvas as any).polyline(points)
.addClass('cvat_canvas_shape_drawing').style({ .addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
}); });
this.pasteShape(); this.pasteShape();
this.pastePolyshape(); this.pastePolyshape();
} }
private pastePoints(points: string): void { private pastePoints(initialPoints: string): void {
this.drawInstance = (this.canvas as any).polyline(points) function moveShape(
shape: SVG.PolyLine,
group: SVG.G,
x: number,
y: number,
scale: number,
): void {
const bbox = shape.bbox();
shape.move(x - bbox.width / 2, y - bbox.height / 2);
const points = shape.attr('points').split(' ');
const radius = consts.BASE_POINT_SIZE / scale;
group.children().forEach((child: SVG.Element, idx: number): void => {
const [px, py] = points[idx].split(',');
child.move(px - radius / 2, py - radius / 2);
});
}
const { x: initialX, y: initialY } = this.cursorPosition;
this.pointsGroup = this.canvas.group();
this.drawInstance = (this.canvas as any).polyline(initialPoints)
.addClass('cvat_canvas_shape_drawing').style({ .addClass('cvat_canvas_shape_drawing').style({
'stroke-width': 0, 'stroke-width': 0,
}); });
this.pasteShape();
let numOfPoints = initialPoints.split(' ').length;
while (numOfPoints) {
numOfPoints--;
const radius = consts.BASE_POINT_SIZE / this.geometry.scale;
const stroke = consts.POINTS_STROKE_WIDTH / this.geometry.scale;
this.pointsGroup.circle().fill('white').stroke('black').attr({
r: radius,
'stroke-width': stroke,
});
}
moveShape(
this.drawInstance, this.pointsGroup, initialX, initialY, this.geometry.scale,
);
this.canvas.on('mousemove.draw', (): void => {
const { x, y } = this.cursorPosition; // was computer in another callback
moveShape(
this.drawInstance, this.pointsGroup, x, y, this.geometry.scale,
);
});
this.pastePolyshape(); this.pastePolyshape();
} }
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 });
}
}
});
}
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);
}
}
}
});
}
private startDraw(): void { private startDraw(): void {
// TODO: Use enums after typification cvat-core // TODO: Use enums after typification cvat-core
if (this.drawData.initialState) { if (this.drawData.initialState) {
const { offset } = this.geometry;
if (this.drawData.shapeType === 'rectangle') { if (this.drawData.shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = translateBetweenSVG( const [xtl, ytl, xbr, ybr] = this.drawData.initialState.points
this.background, .map((coord: number): number => coord + offset);
this.canvas.node as any as SVGSVGElement,
this.drawData.initialState.points,
);
this.pasteBox({ this.pasteBox({
x: xtl, x: xtl,
@ -444,12 +549,8 @@ export class DrawHandlerImpl implements DrawHandler {
height: ybr - ytl, height: ybr - ytl,
}); });
} else { } else {
const points = translateBetweenSVG( const points = this.drawData.initialState.points
this.background, .map((coord: number): number => coord + offset);
this.canvas.node as any as SVGSVGElement,
this.drawData.initialState.points,
);
const stringifiedPoints = pointsToString(points); const stringifiedPoints = pointsToString(points);
if (this.drawData.shapeType === 'polygon') { if (this.drawData.shapeType === 'polygon') {
@ -460,50 +561,59 @@ export class DrawHandlerImpl implements DrawHandler {
this.pastePoints(stringifiedPoints); this.pastePoints(stringifiedPoints);
} }
} }
} else if (this.drawData.shapeType === 'rectangle') { this.setupPasteEvents();
this.drawBox(); } else {
// Draw instance was initialized after drawBox(); if (this.drawData.shapeType === 'rectangle') {
this.shapeSizeElement = displayShapeSize(this.canvas, this.text); if (this.drawData.rectDrawingMethod === RectDrawingMethod.EXTREME_POINTS) {
} else if (this.drawData.shapeType === 'polygon') { // draw box by extreme clicking
this.drawPolygon(); this.drawBoxBy4Points();
} else if (this.drawData.shapeType === 'polyline') { } else {
this.drawPolyline(); // default box drawing
} else if (this.drawData.shapeType === 'points') { this.drawBox();
this.drawPoints(); // Draw instance was initialized after drawBox();
this.shapeSizeElement = displayShapeSize(this.canvas, this.text);
}
} else if (this.drawData.shapeType === 'polygon') {
this.drawPolygon();
} else if (this.drawData.shapeType === 'polyline') {
this.drawPolyline();
} else if (this.drawData.shapeType === 'points') {
this.drawPoints();
}
this.setupDrawEvents();
} }
this.initialized = true;
} }
public constructor( public constructor(
onDrawDone: (data: object) => void, onDrawDone: (data: object, continueDraw?: boolean) => void,
canvas: SVG.Container, canvas: SVG.Container,
text: SVG.Container, text: SVG.Container,
background: SVGSVGElement,
) { ) {
this.onDrawDone = onDrawDone; this.onDrawDone = onDrawDone;
this.canvas = canvas; this.canvas = canvas;
this.text = text; this.text = text;
this.background = background; this.initialized = false;
this.drawData = null; this.drawData = null;
this.geometry = null; this.geometry = null;
this.crosshair = null; this.crosshair = null;
this.drawInstance = null; this.drawInstance = null;
this.pointsGroup = null;
this.cursorPosition = {
x: 0,
y: 0,
};
this.canvas.on('mousemove.crosshair', (e: MouseEvent): void => { this.canvas.on('mousemove.crosshair', (e: MouseEvent): void => {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
this.cursorPosition = { x, y };
if (this.crosshair) { if (this.crosshair) {
const [x, y] = translateToSVG( this.crosshair.x.attr({ y1: y, y2: y });
this.canvas.node as any as SVGSVGElement, this.crosshair.y.attr({ x1: x, x2: x });
[e.clientX, e.clientY],
);
this.crosshair.x.attr({
y1: y,
y2: y,
});
this.crosshair.y.attr({
x1: x,
x2: x,
});
} }
}); });
} }
@ -524,16 +634,25 @@ export class DrawHandlerImpl implements DrawHandler {
}); });
} }
if (this.pointsGroup) {
for (const point of this.pointsGroup.children()) {
point.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / geometry.scale,
r: consts.BASE_POINT_SIZE / geometry.scale,
});
}
}
if (this.drawInstance) { if (this.drawInstance) {
this.drawInstance.draw('transform'); this.drawInstance.draw('transform');
this.drawInstance.style({ this.drawInstance.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
}); });
const paintHandler = this.drawInstance.remember('_paintHandler'); const paintHandler = this.drawInstance.remember('_paintHandler');
for (const point of (paintHandler as any).set.members) { for (const point of (paintHandler as any).set.members) {
point.style( point.attr(
'stroke-width', 'stroke-width',
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`, `${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
); );
@ -553,7 +672,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.initDrawing(); this.initDrawing();
this.startDraw(); this.startDraw();
} else { } else {
this.closeDrawing(); this.cancel();
this.drawData = drawData; this.drawData = drawData;
} }
} }
@ -561,8 +680,5 @@ export class DrawHandlerImpl implements DrawHandler {
public cancel(): void { public cancel(): void {
this.release(); this.release();
this.onDrawDone(null); this.onDrawDone(null);
// here is a cycle
// onDrawDone => controller => model => view => closeDrawing
// one call of closeDrawing is unuseful, but it's okey
} }
} }

@ -1,7 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
import * as SVG from 'svg.js'; import * as SVG from 'svg.js';
import 'svg.select.js'; import 'svg.select.js';
@ -9,7 +8,6 @@ import 'svg.select.js';
import consts from './consts'; import consts from './consts';
import { import {
translateFromSVG, translateFromSVG,
translateBetweenSVG,
pointsToArray, pointsToArray,
} from './shared'; } from './shared';
import { import {
@ -27,7 +25,6 @@ export class EditHandlerImpl implements EditHandler {
private onEditDone: (state: any, points: number[]) => void; private onEditDone: (state: any, points: number[]) => void;
private geometry: Geometry; private geometry: Geometry;
private canvas: SVG.Container; private canvas: SVG.Container;
private background: SVGSVGElement;
private editData: EditData; private editData: EditData;
private editedShape: SVG.Shape; private editedShape: SVG.Shape;
private editLine: SVG.PolyLine; private editLine: SVG.PolyLine;
@ -40,6 +37,14 @@ export class EditHandlerImpl implements EditHandler {
this.editedShape.attr('points').split(' ')[this.editData.pointID].split(','), this.editedShape.attr('points').split(' ')[this.editData.pointID].split(','),
); );
// generate mouse event
const dummyEvent = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
clientX,
clientY,
});
// Add ability to edit shapes by sliding // Add ability to edit shapes by sliding
// We need to remember last drawn point // We need to remember last drawn point
// to implementation of slide drawing // to implementation of slide drawing
@ -51,10 +56,10 @@ export class EditHandlerImpl implements EditHandler {
y: null, y: null,
}; };
const handleSlide = function handleSlide(e: MouseEvent): void { this.canvas.on('mousemove.edit', (e: MouseEvent): void => {
if (e.shiftKey) { if (e.shiftKey && ['polygon', 'polyline'].includes(this.editData.state.shapeType)) {
if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) { if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) {
this.editLine.draw('point', e); (this.editLine as any).draw('point', e);
} else { } else {
const deltaTreshold = 15; const deltaTreshold = 15;
const delta = Math.sqrt( const delta = Math.sqrt(
@ -62,53 +67,67 @@ export class EditHandlerImpl implements EditHandler {
+ ((e.clientY - lastDrawnPoint.y) ** 2), + ((e.clientY - lastDrawnPoint.y) ** 2),
); );
if (delta > deltaTreshold) { if (delta > deltaTreshold) {
this.editLine.draw('point', e); (this.editLine as any).draw('point', e);
} }
} }
} }
}.bind(this); });
this.canvas.on('mousemove.draw', handleSlide);
this.editLine = (this.canvas as any).polyline().draw({ this.editLine = (this.canvas as any).polyline();
snapToGrid: 0.1, (this.editLine as any).addClass('cvat_canvas_shape_drawing').style({
}).addClass('cvat_canvas_shape_drawing').style({
'pointer-events': 'none', 'pointer-events': 'none',
'fill-opacity': 0, 'fill-opacity': 0,
}).on('drawstart drawpoint', (e: CustomEvent): void => { }).on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry); this.transform(this.geometry);
lastDrawnPoint.x = e.detail.event.clientX; lastDrawnPoint.x = e.detail.event.clientX;
lastDrawnPoint.y = e.detail.event.clientY; lastDrawnPoint.y = e.detail.event.clientY;
}); }).draw(dummyEvent, { snapToGrid: 0.1 });
if (this.editData.state.shapeType === 'points') { if (this.editData.state.shapeType === 'points') {
this.editLine.style('stroke-width', 0); this.editLine.attr('stroke-width', 0);
} else { (this.editLine as any).draw('undo');
// generate mouse event
const dummyEvent = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
clientX,
clientY,
});
(this.editLine as any).draw('point', dummyEvent);
} }
this.setupEditEvents();
} }
private stopEdit(e: MouseEvent): void { private setupEditEvents(): void {
function selectPolygon(shape: SVG.Polygon): void { let mouseX: number | null = null;
const points = translateBetweenSVG( let mouseY: number | null = null;
this.canvas.node as any as SVGSVGElement,
this.background, this.canvas.on('mousedown.edit', (e: MouseEvent): void => {
pointsToArray(shape.attr('points')), if (e.which === 1) {
); mouseX = e.clientX;
mouseY = e.clientY;
const { state } = this.editData; }
this.edit({ });
enabled: false,
}); this.canvas.on('mouseup.edit', (e: MouseEvent): void => {
this.onEditDone(state, points); 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);
}
}
});
}
private selectPolygon(shape: SVG.Polygon): void {
const { offset } = this.geometry;
const points = pointsToArray(shape.attr('points'))
.map((coord: number): number => coord - offset);
const { state } = this.editData;
this.edit({
enabled: false,
});
this.onEditDone(state, points);
}
private stopEdit(e: MouseEvent): void {
if (!this.editLine) { if (!this.editLine) {
return; return;
} }
@ -149,12 +168,12 @@ export class EditHandlerImpl implements EditHandler {
for (const points of [firstPart, secondPart]) { for (const points of [firstPart, secondPart]) {
this.clones.push(this.canvas.polygon(points.join(' ')) this.clones.push(this.canvas.polygon(points.join(' '))
.attr('fill', this.editedShape.attr('fill')) .attr('fill', this.editedShape.attr('fill'))
.style('fill-opacity', '0.5') .attr('fill-opacity', '0.5')
.addClass('cvat_canvas_shape')); .addClass('cvat_canvas_shape'));
} }
for (const clone of this.clones) { for (const clone of this.clones) {
clone.on('click', selectPolygon.bind(this, clone)); clone.on('click', (): void => this.selectPolygon(clone));
clone.on('mouseenter', (): void => { clone.on('mouseenter', (): void => {
clone.addClass('cvat_canvas_shape_splitting'); clone.addClass('cvat_canvas_shape_splitting');
}).on('mouseleave', (): void => { }).on('mouseleave', (): void => {
@ -162,6 +181,11 @@ 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'); (this.editLine as any).draw('stop');
this.editLine.remove(); this.editLine.remove();
this.editLine = null; this.editLine = null;
@ -170,6 +194,7 @@ export class EditHandlerImpl implements EditHandler {
} }
let points = null; let points = null;
const { offset } = this.geometry;
if (this.editData.state.shapeType === 'polyline') { if (this.editData.state.shapeType === 'polyline') {
if (start !== this.editData.pointID) { if (start !== this.editData.pointID) {
linePoints.reverse(); linePoints.reverse();
@ -181,11 +206,8 @@ export class EditHandlerImpl implements EditHandler {
points = oldPoints.concat(linePoints.slice(0, -1)); points = oldPoints.concat(linePoints.slice(0, -1));
} }
points = translateBetweenSVG( points = pointsToArray(points.join(' '))
this.canvas.node as any as SVGSVGElement, .map((coord: number): number => coord - offset);
this.background,
pointsToArray(points.join(' ')),
);
const { state } = this.editData; const { state } = this.editData;
this.edit({ this.edit({
@ -242,7 +264,9 @@ export class EditHandlerImpl implements EditHandler {
} }
private release(): void { private release(): void {
this.canvas.off('mousemove.draw'); this.canvas.off('mousedown.edit');
this.canvas.off('mouseup.edit');
this.canvas.off('mousemove.edit');
if (this.editedShape) { if (this.editedShape) {
this.setupPoints(false); this.setupPoints(false);
@ -284,11 +308,9 @@ export class EditHandlerImpl implements EditHandler {
public constructor( public constructor(
onEditDone: (state: any, points: number[]) => void, onEditDone: (state: any, points: number[]) => void,
canvas: SVG.Container, canvas: SVG.Container,
background: SVGSVGElement,
) { ) {
this.onEditDone = onEditDone; this.onEditDone = onEditDone;
this.canvas = canvas; this.canvas = canvas;
this.background = background;
this.editData = null; this.editData = null;
this.editedShape = null; this.editedShape = null;
this.editLine = null; this.editLine = null;
@ -318,10 +340,16 @@ export class EditHandlerImpl implements EditHandler {
public transform(geometry: Geometry): void { public transform(geometry: Geometry): void {
this.geometry = geometry; this.geometry = geometry;
if (this.editedShape) {
this.editedShape.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
});
}
if (this.editLine) { if (this.editLine) {
(this.editLine as any).draw('transform'); (this.editLine as any).draw('transform');
if (this.editData.state.shapeType !== 'points') { if (this.editData.state.shapeType !== 'points') {
this.editLine.style({ this.editLine.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
}); });
} }
@ -329,7 +357,7 @@ export class EditHandlerImpl implements EditHandler {
const paintHandler = this.editLine.remember('_paintHandler'); const paintHandler = this.editLine.remember('_paintHandler');
for (const point of (paintHandler as any).set.members) { for (const point of (paintHandler as any).set.members) {
point.style( point.attr(
'stroke-width', 'stroke-width',
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`, `${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
); );

@ -1,3 +1,7 @@
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js'; import * as SVG from 'svg.js';
import { GroupData } from './canvasModel'; import { GroupData } from './canvasModel';
@ -10,16 +14,17 @@ export interface GroupHandler {
group(groupData: GroupData): void; group(groupData: GroupData): void;
select(state: any): void; select(state: any): void;
cancel(): void; cancel(): void;
resetSelectedObjects(): void;
} }
export class GroupHandlerImpl implements GroupHandler { export class GroupHandlerImpl implements GroupHandler {
// callback is used to notify about grouping end // callback is used to notify about grouping end
private onGroupDone: (objects: any[]) => void; private onGroupDone: (objects?: any[]) => void;
private getStates: () => any[]; private getStates: () => any[];
private onFindObject: (event: MouseEvent) => void; private onFindObject: (event: MouseEvent) => void;
private onSelectStart: (event: MouseEvent) => void; private bindedOnSelectStart: (event: MouseEvent) => void;
private onSelectUpdate: (event: MouseEvent) => void; private bindedOnSelectUpdate: (event: MouseEvent) => void;
private onSelectStop: (event: MouseEvent) => void; private bindedOnSelectStop: (event: MouseEvent) => void;
private selectionRect: SVG.Rect; private selectionRect: SVG.Rect;
private startSelectionPoint: { private startSelectionPoint: {
x: number; x: number;
@ -27,7 +32,7 @@ export class GroupHandlerImpl implements GroupHandler {
}; };
private canvas: SVG.Container; private canvas: SVG.Container;
private initialized: boolean; private initialized: boolean;
private states: any[]; private statesToBeGroupped: any[];
private highlightedShapes: Record<number, SVG.Shape>; private highlightedShapes: Record<number, SVG.Shape>;
private getSelectionBox(event: MouseEvent): { private getSelectionBox(event: MouseEvent): {
@ -53,19 +58,72 @@ export class GroupHandlerImpl implements GroupHandler {
}; };
} }
private onSelectStart(event: MouseEvent): void {
if (!this.selectionRect) {
const point = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[event.clientX, event.clientY],
);
this.startSelectionPoint = {
x: point[0],
y: point[1],
};
this.selectionRect = this.canvas.rect().addClass('cvat_canvas_shape_grouping');
this.selectionRect.attr({ ...this.startSelectionPoint });
}
}
private onSelectUpdate(event: MouseEvent): void {
// called on mousemove
if (this.selectionRect) {
const box = this.getSelectionBox(event);
this.selectionRect.attr({
x: box.xtl,
y: box.ytl,
width: box.xbr - box.xtl,
height: box.ybr - box.ytl,
});
}
}
private onSelectStop(event: MouseEvent): void {
// called on mouseup, mouseleave
if (this.selectionRect) {
this.selectionRect.remove();
this.selectionRect = null;
const box = this.getSelectionBox(event);
const shapes = (this.canvas.select('.cvat_canvas_shape') as any).members;
for (const shape of shapes) {
// TODO: Doesn't work properly for groups
const bbox = shape.bbox();
const clientID = shape.attr('clientID');
if (bbox.x > box.xtl && bbox.y > box.ytl
&& bbox.x + bbox.width < box.xbr
&& bbox.y + bbox.height < box.ybr
&& !(clientID in this.highlightedShapes)) {
const objectState = this.getStates()
.filter((state: any): boolean => state.clientID === clientID)[0];
if (objectState) {
this.statesToBeGroupped.push(objectState);
this.highlightedShapes[clientID] = shape;
(shape as any).addClass('cvat_canvas_shape_grouping');
}
}
}
}
}
private release(): void { private release(): void {
this.canvas.node.removeEventListener('click', this.onFindObject); this.canvas.node.removeEventListener('click', this.onFindObject);
this.canvas.node.removeEventListener('mousedown', this.onSelectStart); this.canvas.node.removeEventListener('mousedown', this.bindedOnSelectStart);
this.canvas.node.removeEventListener('mousemove', this.onSelectUpdate); this.canvas.node.removeEventListener('mousemove', this.bindedOnSelectUpdate);
this.canvas.node.removeEventListener('mouseup', this.onSelectStop); this.canvas.node.removeEventListener('mouseup', this.bindedOnSelectStop);
this.canvas.node.removeEventListener('mouseleave', this.onSelectStop);
for (const state of this.states) { this.resetSelectedObjects();
const shape = this.highlightedShapes[state.clientID];
shape.removeClass('cvat_canvas_shape_grouping');
}
this.states = [];
this.highlightedShapes = {};
this.initialized = false; this.initialized = false;
this.selectionRect = null; this.selectionRect = null;
this.startSelectionPoint = { this.startSelectionPoint = {
@ -76,29 +134,28 @@ export class GroupHandlerImpl implements GroupHandler {
private initGrouping(): void { private initGrouping(): void {
this.canvas.node.addEventListener('click', this.onFindObject); this.canvas.node.addEventListener('click', this.onFindObject);
this.canvas.node.addEventListener('mousedown', this.onSelectStart); this.canvas.node.addEventListener('mousedown', this.bindedOnSelectStart);
this.canvas.node.addEventListener('mousemove', this.onSelectUpdate); this.canvas.node.addEventListener('mousemove', this.bindedOnSelectUpdate);
this.canvas.node.addEventListener('mouseup', this.onSelectStop); this.canvas.node.addEventListener('mouseup', this.bindedOnSelectStop);
this.canvas.node.addEventListener('mouseleave', this.onSelectStop);
this.initialized = true; this.initialized = true;
} }
private closeGrouping(): void { private closeGrouping(): void {
if (this.initialized) { if (this.initialized) {
const { states } = this; const { statesToBeGroupped } = this;
this.release(); this.release();
if (states.length) { if (statesToBeGroupped.length) {
this.onGroupDone(states); this.onGroupDone(statesToBeGroupped);
} else { } else {
this.onGroupDone(null); this.onGroupDone();
} }
} }
} }
public constructor( public constructor(
onGroupDone: (objects: any[]) => void, onGroupDone: (objects?: any[]) => void,
getStates: () => any[], getStates: () => any[],
onFindObject: (event: MouseEvent) => void, onFindObject: (event: MouseEvent) => void,
canvas: SVG.Container, canvas: SVG.Container,
@ -107,69 +164,18 @@ export class GroupHandlerImpl implements GroupHandler {
this.getStates = getStates; this.getStates = getStates;
this.onFindObject = onFindObject; this.onFindObject = onFindObject;
this.canvas = canvas; this.canvas = canvas;
this.states = []; this.statesToBeGroupped = [];
this.highlightedShapes = {}; this.highlightedShapes = {};
this.selectionRect = null; this.selectionRect = null;
this.initialized = false;
this.startSelectionPoint = { this.startSelectionPoint = {
x: null, x: null,
y: null, y: null,
}; };
this.onSelectStart = function (event: MouseEvent): void { this.bindedOnSelectStart = this.onSelectStart.bind(this);
if (!this.selectionRect) { this.bindedOnSelectUpdate = this.onSelectUpdate.bind(this);
const point = translateToSVG(this.canvas.node, [event.clientX, event.clientY]); this.bindedOnSelectStop = this.onSelectStop.bind(this);
this.startSelectionPoint = {
x: point[0],
y: point[1],
};
this.selectionRect = this.canvas.rect().addClass('cvat_canvas_shape_grouping');
this.selectionRect.attr({ ...this.startSelectionPoint });
}
}.bind(this);
this.onSelectUpdate = function (event: MouseEvent): void {
// called on mousemove
if (this.selectionRect) {
const box = this.getSelectionBox(event);
this.selectionRect.attr({
x: box.xtl,
y: box.ytl,
width: box.xbr - box.xtl,
height: box.ybr - box.ytl,
});
}
}.bind(this);
this.onSelectStop = function (event: MouseEvent): void {
// called on mouseup, mouseleave
if (this.selectionRect) {
this.selectionRect.remove();
this.selectionRect = null;
const box = this.getSelectionBox(event);
const shapes = (this.canvas.select('.cvat_canvas_shape') as any).members;
for (const shape of shapes) {
// TODO: Doesn't work properly for groups
const bbox = shape.bbox();
const clientID = shape.attr('clientID');
if (bbox.x > box.xtl && bbox.y > box.ytl
&& bbox.x + bbox.width < box.xbr
&& bbox.y + bbox.height < box.ybr
&& !(clientID in this.highlightedShapes)) {
const objectState = this.getStates()
.filter((state: any): boolean => state.clientID === clientID)[0];
if (objectState) {
this.states.push(objectState);
this.highlightedShapes[clientID] = shape;
(shape as any).addClass('cvat_canvas_shape_grouping');
}
}
}
}
}.bind(this);
} }
/* eslint-disable-next-line */ /* eslint-disable-next-line */
@ -182,11 +188,11 @@ export class GroupHandlerImpl implements GroupHandler {
} }
public select(objectState: any): void { public select(objectState: any): void {
const stateIndexes = this.states.map((state): number => state.clientID); const stateIndexes = this.statesToBeGroupped.map((state): number => state.clientID);
const includes = stateIndexes.indexOf(objectState.clientID); const includes = stateIndexes.indexOf(objectState.clientID);
if (includes !== -1) { if (includes !== -1) {
const shape = this.highlightedShapes[objectState.clientID]; const shape = this.highlightedShapes[objectState.clientID];
this.states.splice(includes, 1); this.statesToBeGroupped.splice(includes, 1);
if (shape) { if (shape) {
delete this.highlightedShapes[objectState.clientID]; delete this.highlightedShapes[objectState.clientID];
shape.removeClass('cvat_canvas_shape_grouping'); shape.removeClass('cvat_canvas_shape_grouping');
@ -194,15 +200,24 @@ export class GroupHandlerImpl implements GroupHandler {
} else { } else {
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first(); const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first();
if (shape) { if (shape) {
this.states.push(objectState); this.statesToBeGroupped.push(objectState);
this.highlightedShapes[objectState.clientID] = shape; this.highlightedShapes[objectState.clientID] = shape;
shape.addClass('cvat_canvas_shape_grouping'); shape.addClass('cvat_canvas_shape_grouping');
} }
} }
} }
public resetSelectedObjects(): void {
for (const state of this.statesToBeGroupped) {
const shape = this.highlightedShapes[state.clientID];
shape.removeClass('cvat_canvas_shape_grouping');
}
this.statesToBeGroupped = [];
this.highlightedShapes = {};
}
public cancel(): void { public cancel(): void {
this.release(); this.release();
this.onGroupDone(null); this.onGroupDone();
} }
} }

@ -1,7 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
export interface Master { export interface Master {
subscribe(listener: Listener): void; subscribe(listener: Listener): void;

@ -1,3 +1,7 @@
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js'; import * as SVG from 'svg.js';
import { MergeData } from './canvasModel'; import { MergeData } from './canvasModel';
@ -5,6 +9,7 @@ export interface MergeHandler {
merge(mergeData: MergeData): void; merge(mergeData: MergeData): void;
select(state: any): void; select(state: any): void;
cancel(): void; cancel(): void;
repeatSelection(): void;
} }
@ -14,7 +19,7 @@ export class MergeHandlerImpl implements MergeHandler {
private onFindObject: (event: MouseEvent) => void; private onFindObject: (event: MouseEvent) => void;
private canvas: SVG.Container; private canvas: SVG.Container;
private initialized: boolean; private initialized: boolean;
private states: any[]; // are being merged private statesToBeMerged: any[]; // are being merged
private highlightedShapes: Record<number, SVG.Shape>; private highlightedShapes: Record<number, SVG.Shape>;
private constraints: { private constraints: {
labelID: number; labelID: number;
@ -22,7 +27,7 @@ export class MergeHandlerImpl implements MergeHandler {
}; };
private addConstraints(): void { private addConstraints(): void {
const shape = this.states[0]; const shape = this.statesToBeMerged[0];
this.constraints = { this.constraints = {
labelID: shape.label.id, labelID: shape.label.id,
shapeType: shape.shapeType, shapeType: shape.shapeType,
@ -41,11 +46,11 @@ export class MergeHandlerImpl implements MergeHandler {
private release(): void { private release(): void {
this.removeConstraints(); this.removeConstraints();
this.canvas.node.removeEventListener('click', this.onFindObject); this.canvas.node.removeEventListener('click', this.onFindObject);
for (const state of this.states) { for (const state of this.statesToBeMerged) {
const shape = this.highlightedShapes[state.clientID]; const shape = this.highlightedShapes[state.clientID];
shape.removeClass('cvat_canvas_shape_merging'); shape.removeClass('cvat_canvas_shape_merging');
} }
this.states = []; this.statesToBeMerged = [];
this.highlightedShapes = {}; this.highlightedShapes = {};
this.initialized = false; this.initialized = false;
} }
@ -57,11 +62,11 @@ export class MergeHandlerImpl implements MergeHandler {
private closeMerging(): void { private closeMerging(): void {
if (this.initialized) { if (this.initialized) {
const { states } = this; const { statesToBeMerged } = this;
this.release(); this.release();
if (states.length > 1) { if (statesToBeMerged.length > 1) {
this.onMergeDone(states); this.onMergeDone(statesToBeMerged);
} else { } else {
this.onMergeDone(null); this.onMergeDone(null);
// here is a cycle // here is a cycle
@ -79,7 +84,7 @@ export class MergeHandlerImpl implements MergeHandler {
this.onMergeDone = onMergeDone; this.onMergeDone = onMergeDone;
this.onFindObject = onFindObject; this.onFindObject = onFindObject;
this.canvas = canvas; this.canvas = canvas;
this.states = []; this.statesToBeMerged = [];
this.highlightedShapes = {}; this.highlightedShapes = {};
this.constraints = null; this.constraints = null;
this.initialized = false; this.initialized = false;
@ -94,35 +99,45 @@ export class MergeHandlerImpl implements MergeHandler {
} }
public select(objectState: any): void { public select(objectState: any): void {
const stateIndexes = this.states.map((state): number => state.clientID); const stateIndexes = this.statesToBeMerged.map((state): number => state.clientID);
const stateFrames = this.states.map((state): number => state.frame); const stateFrames = this.statesToBeMerged.map((state): number => state.frame);
const includes = stateIndexes.indexOf(objectState.clientID); const includes = stateIndexes.indexOf(objectState.clientID);
if (includes !== -1) { if (includes !== -1) {
const shape = this.highlightedShapes[objectState.clientID]; const shape = this.highlightedShapes[objectState.clientID];
this.states.splice(includes, 1); this.statesToBeMerged.splice(includes, 1);
if (shape) { if (shape) {
delete this.highlightedShapes[objectState.clientID]; delete this.highlightedShapes[objectState.clientID];
shape.removeClass('cvat_canvas_shape_merging'); shape.removeClass('cvat_canvas_shape_merging');
} }
if (!this.states.length) { if (!this.statesToBeMerged.length) {
this.removeConstraints(); this.removeConstraints();
} }
} else { } else {
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first(); const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first();
if (shape && this.checkConstraints(objectState) if (shape && this.checkConstraints(objectState)
&& !stateFrames.includes(objectState.frame)) { && !stateFrames.includes(objectState.frame)) {
this.states.push(objectState); this.statesToBeMerged.push(objectState);
this.highlightedShapes[objectState.clientID] = shape; this.highlightedShapes[objectState.clientID] = shape;
shape.addClass('cvat_canvas_shape_merging'); shape.addClass('cvat_canvas_shape_merging');
if (this.states.length === 1) { if (this.statesToBeMerged.length === 1) {
this.addConstraints(); this.addConstraints();
} }
} }
} }
} }
public repeatSelection(): void {
for (const objectState of this.statesToBeMerged) {
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first();
if (shape) {
this.highlightedShapes[objectState.clientID] = shape;
shape.addClass('cvat_canvas_shape_merging');
}
}
}
public cancel(): void { public cancel(): void {
this.release(); this.release();
this.onMergeDone(null); this.onMergeDone(null);

@ -1,7 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
import * as SVG from 'svg.js'; import * as SVG from 'svg.js';
import consts from './consts'; import consts from './consts';
@ -26,11 +25,11 @@ export interface BBox {
y: number; y: number;
} }
// Translate point array from the client coordinate system // Translate point array from the canvas coordinate system
// to a coordinate system of a canvas // to the coordinate system of a client
export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] { export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] {
const output = []; const output = [];
const transformationMatrix = svg.getScreenCTM(); const transformationMatrix = svg.getScreenCTM() as DOMMatrix;
let pt = svg.createSVGPoint(); let pt = svg.createSVGPoint();
for (let i = 0; i < points.length - 1; i += 2) { for (let i = 0; i < points.length - 1; i += 2) {
pt.x = points[i]; pt.x = points[i];
@ -42,11 +41,11 @@ export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[]
return output; return output;
} }
// Translate point array from a coordinate system of a canvas // Translate point array from the coordinate system of a client
// to the client coordinate system // to the canvas coordinate system
export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] { export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
const output = []; const output = [];
const transformationMatrix = svg.getScreenCTM().inverse(); const transformationMatrix = (svg.getScreenCTM() as DOMMatrix).inverse();
let pt = svg.createSVGPoint(); let pt = svg.createSVGPoint();
for (let i = 0; i < points.length; i += 2) { for (let i = 0; i < points.length; i += 2) {
pt.x = points[i]; pt.x = points[i];
@ -58,23 +57,13 @@ export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
return output; return output;
} }
// Translate point array from the first canvas coordinate system
// to another
export function translateBetweenSVG(
from: SVGSVGElement,
to: SVGSVGElement,
points: number[],
): number[] {
return translateToSVG(to, translateFromSVG(from, points));
}
export function pointsToString(points: number[]): string { export function pointsToString(points: number[]): string {
return points.reduce((acc, val, idx): string => { return points.reduce((acc, val, idx): string => {
if (idx % 2) { if (idx % 2) {
return `${acc},${val}`; return `${acc},${val}`;
} }
return `${acc} ${val}`; return `${acc} ${val}`.trim();
}, ''); }, '');
} }

@ -1,3 +1,7 @@
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js'; import * as SVG from 'svg.js';
import { SplitData } from './canvasModel'; import { SplitData } from './canvasModel';
@ -27,13 +31,13 @@ export class SplitHandlerImpl implements SplitHandler {
private release(): void { private release(): void {
if (this.initialized) { if (this.initialized) {
this.resetShape(); this.resetShape();
this.canvas.node.removeEventListener('mousemove', this.onFindObject); this.canvas.node.removeEventListener('mousemove', this.findObject);
this.initialized = false; this.initialized = false;
} }
} }
private initSplitting(): void { private initSplitting(): void {
this.canvas.node.addEventListener('mousemove', this.onFindObject); this.canvas.node.addEventListener('mousemove', this.findObject);
this.initialized = true; this.initialized = true;
this.splitDone = false; this.splitDone = false;
} }
@ -47,6 +51,11 @@ export class SplitHandlerImpl implements SplitHandler {
this.release(); this.release();
} }
private findObject = (e: MouseEvent): void => {
this.resetShape();
this.onFindObject(e);
};
public constructor( public constructor(
onSplitDone: (object: any) => void, onSplitDone: (object: any) => void,
onFindObject: (event: MouseEvent) => void, onFindObject: (event: MouseEvent) => void,
@ -83,8 +92,6 @@ export class SplitHandlerImpl implements SplitHandler {
once: true, once: true,
}); });
} }
} else {
this.resetShape();
} }
} }

@ -1,7 +1,9 @@
import * as SVG from 'svg.js'; // Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/* eslint-disable */ /* eslint-disable */
import * as SVG from 'svg.js';
import 'svg.draggable.js'; import 'svg.draggable.js';
import 'svg.resize.js'; import 'svg.resize.js';
import 'svg.select.js'; import 'svg.select.js';
@ -14,7 +16,9 @@ SVG.Element.prototype.draw = function constructor(...args: any): any {
if (!handler) { if (!handler) {
originalDraw.call(this, ...args); originalDraw.call(this, ...args);
handler = this.remember('_paintHandler'); handler = this.remember('_paintHandler');
handler.set = new SVG.Set(); if (!handler.set) {
handler.set = new SVG.Set();
}
} else { } else {
originalDraw.call(this, ...args); originalDraw.call(this, ...args);
} }
@ -27,7 +31,7 @@ for (const key of Object.keys(originalDraw)) {
// Create undo for polygones and polylines // Create undo for polygones and polylines
function undo(): void { function undo(): void {
if (this.set.length()) { if (this.set && this.set.length()) {
this.set.members.splice(-1, 1)[0].remove(); this.set.members.splice(-1, 1)[0].remove();
this.el.array().value.splice(-2, 1); this.el.array().value.splice(-2, 1);
this.el.plot(this.el.array()); this.el.plot(this.el.array());

@ -0,0 +1,145 @@
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
import {
translateToSVG,
} from './shared';
import {
Geometry,
} from './canvasModel';
export interface ZoomHandler {
zoom(): void;
cancel(): void;
transform(geometry: Geometry): void;
}
export class ZoomHandlerImpl implements ZoomHandler {
private onZoomRegion: (x: number, y: number, width: number, height: number) => void;
private bindedOnSelectStart: (event: MouseEvent) => void;
private bindedOnSelectUpdate: (event: MouseEvent) => void;
private bindedOnSelectStop: (event: MouseEvent) => void;
private geometry: Geometry;
private canvas: SVG.Container;
private selectionRect: SVG.Rect | null;
private startSelectionPoint: {
x: number;
y: number;
};
private onSelectStart(event: MouseEvent): void {
if (!this.selectionRect && event.which === 1) {
const point = translateToSVG(
(this.canvas.node as any as SVGSVGElement),
[event.clientX, event.clientY],
);
this.startSelectionPoint = {
x: point[0],
y: point[1],
};
this.selectionRect = this.canvas.rect().addClass('cvat_canvas_zoom_selection');
this.selectionRect.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
...this.startSelectionPoint,
});
}
}
private getSelectionBox(event: MouseEvent): {
x: number;
y: number;
width: number;
height: number;
} {
const point = translateToSVG(
(this.canvas.node as any as SVGSVGElement),
[event.clientX, event.clientY],
);
const stopSelectionPoint = {
x: point[0],
y: point[1],
};
const xtl = Math.min(this.startSelectionPoint.x, stopSelectionPoint.x);
const ytl = Math.min(this.startSelectionPoint.y, stopSelectionPoint.y);
const xbr = Math.max(this.startSelectionPoint.x, stopSelectionPoint.x);
const ybr = Math.max(this.startSelectionPoint.y, stopSelectionPoint.y);
return {
x: xtl,
y: ytl,
width: xbr - xtl,
height: ybr - ytl,
};
}
private onSelectUpdate(event: MouseEvent): void {
if (this.selectionRect) {
this.selectionRect.attr({
...this.getSelectionBox(event),
});
}
}
private onSelectStop(event: MouseEvent): void {
if (this.selectionRect) {
const box = this.getSelectionBox(event);
this.selectionRect.remove();
this.selectionRect = null;
this.startSelectionPoint = {
x: 0,
y: 0,
};
const threshold = 5;
if (box.width > threshold && box.height > threshold) {
this.onZoomRegion(box.x, box.y, box.width, box.height);
}
}
}
public constructor(
onZoomRegion: (x: number, y: number, width: number, height: number) => void,
canvas: SVG.Container,
geometry: Geometry,
) {
this.onZoomRegion = onZoomRegion;
this.canvas = canvas;
this.geometry = geometry;
this.selectionRect = null;
this.startSelectionPoint = {
x: 0,
y: 0,
};
this.bindedOnSelectStart = this.onSelectStart.bind(this);
this.bindedOnSelectUpdate = this.onSelectUpdate.bind(this);
this.bindedOnSelectStop = this.onSelectStop.bind(this);
}
public zoom(): void {
this.canvas.node.addEventListener('mousedown', this.bindedOnSelectStart);
this.canvas.node.addEventListener('mousemove', this.bindedOnSelectUpdate);
this.canvas.node.addEventListener('mouseup', this.bindedOnSelectStop);
}
public cancel(): void {
this.canvas.node.removeEventListener('mousedown', this.bindedOnSelectStart);
this.canvas.node.removeEventListener('mousemove', this.bindedOnSelectUpdate);
this.canvas.node.removeEventListener('mouseup ', this.bindedOnSelectStop);
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.selectionRect) {
this.selectionRect.style({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
});
}
}
}

@ -7,6 +7,8 @@
"noImplicitAny": true, "noImplicitAny": true,
"preserveConstEnums": true, "preserveConstEnums": true,
"declaration": true, "declaration": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"moduleResolution": "node", "moduleResolution": "node",
"declarationDir": "dist/declaration", "declarationDir": "dist/declaration",
"paths": { "paths": {

@ -1,3 +1,8 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* eslint-disable */ /* eslint-disable */
const path = require('path'); const path = require('path');
const DtsBundleWebpack = require('dts-bundle-webpack') const DtsBundleWebpack = require('dts-bundle-webpack')
@ -70,15 +75,23 @@ const webConfig = {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
presets: [ presets: [
['@babel/preset-env'], ['@babel/preset-env', {
targets: '> 2.5%', // https://github.com/browserslist/browserslist
}],
['@babel/typescript'], ['@babel/typescript'],
], ],
sourceType: 'unambiguous', sourceType: 'unambiguous',
}, },
}, },
}, { }, {
test: /\.css$/, test: /\.scss$/,
use: ['style-loader', 'css-loader'] exclude: /node_modules/,
use: ['style-loader', {
loader: 'css-loader',
options: {
importLoaders: 2,
},
}, 'postcss-loader', 'sass-loader']
}], }],
}, },
plugins: [ plugins: [

@ -0,0 +1,5 @@
/dist
/docs
/node_modules
/reports

@ -50,5 +50,6 @@
"func-names": [0], "func-names": [0],
"valid-typeof": [0], "valid-typeof": [0],
"no-console": [0], // this rule deprecates console.log, console.warn etc. because "it is not good in production code" "no-console": [0], // this rule deprecates console.log, console.warn etc. because "it is not good in production code"
"max-classes-per-file": [0],
}, },
}; };

@ -1,6 +1,6 @@
{ {
"name": "cvat-core.js", "name": "cvat-core.js",
"version": "0.1.0", "version": "0.5.2",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration", "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js", "main": "babel.config.js",
"scripts": { "scripts": {
@ -18,7 +18,6 @@
"airbnb": "0.0.2", "airbnb": "0.0.2",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"core-js": "^3.0.1",
"coveralls": "^3.0.5", "coveralls": "^3.0.5",
"eslint": "6.1.0", "eslint": "6.1.0",
"eslint-config-airbnb-base": "14.0.0", "eslint-config-airbnb-base": "14.0.0",
@ -39,6 +38,7 @@
"form-data": "^2.5.0", "form-data": "^2.5.0",
"jest-config": "^24.8.0", "jest-config": "^24.8.0",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"jsonpath": "^1.0.2",
"platform": "^1.3.5", "platform": "^1.3.5",
"store": "^2.0.12" "store": "^2.0.12"
} }

@ -138,8 +138,8 @@
handler_file: initialData.handler_file, handler_file: initialData.handler_file,
}; };
data.dumpers = initialData.dumpers.map(el => new Dumper(el)); data.dumpers = initialData.dumpers.map((el) => new Dumper(el));
data.loaders = initialData.loaders.map(el => new Loader(el)); data.loaders = initialData.loaders.map((el) => new Loader(el));
// Now all fields are readonly // Now all fields are readonly
Object.defineProperties(this, { Object.defineProperties(this, {

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 Intel Corporation * Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -22,6 +22,7 @@
Tag, Tag,
objectStateFactory, objectStateFactory,
} = require('./annotations-objects'); } = require('./annotations-objects');
const AnnotationsFilter = require('./annotations-filter');
const { checkObjectType } = require('./common'); const { checkObjectType } = require('./common');
const Statistics = require('./statistics'); const Statistics = require('./statistics');
const { Label } = require('./labels'); const { Label } = require('./labels');
@ -32,29 +33,13 @@
} = require('./exceptions'); } = require('./exceptions');
const { const {
HistoryActions,
ObjectShape, ObjectShape,
ObjectType, ObjectType,
colors,
} = require('./enums'); } = require('./enums');
const ObjectState = require('./object-state'); const ObjectState = require('./object-state');
const colors = [
'#0066FF', '#AF593E', '#01A368', '#FF861F', '#ED0A3F', '#FF3F34', '#76D7EA',
'#8359A3', '#FBE870', '#C5E17A', '#03BB85', '#FFDF00', '#8B8680', '#0A6B0D',
'#8FD8D8', '#A36F40', '#F653A6', '#CA3435', '#FFCBA4', '#FF99CC', '#FA9D5A',
'#FFAE42', '#A78B00', '#788193', '#514E49', '#1164B4', '#F4FA9F', '#FED8B1',
'#C32148', '#01796F', '#E90067', '#FF91A4', '#404E5A', '#6CDAE7', '#FFC1CC',
'#006A93', '#867200', '#E2B631', '#6EEB6E', '#FFC800', '#CC99BA', '#FF007C',
'#BC6CAC', '#DCCCD7', '#EBE1C2', '#A6AAAE', '#B99685', '#0086A7', '#5E4330',
'#C8A2C8', '#708EB3', '#BC8777', '#B2592D', '#497E48', '#6A2963', '#E6335F',
'#00755E', '#B5A895', '#0048ba', '#EED9C4', '#C88A65', '#FF6E4A', '#87421F',
'#B2BEB5', '#926F5B', '#00B9FB', '#6456B7', '#DB5079', '#C62D42', '#FA9C44',
'#DA8A67', '#FD7C6E', '#93CCEA', '#FCF686', '#503E32', '#FF5470', '#9DE093',
'#FF7A00', '#4F69C6', '#A50B5E', '#F0E68C', '#FDFF00', '#F091A9', '#FFFF66',
'#6F9940', '#FC74FD', '#652DC1', '#D6AEDD', '#EE34D2', '#BB3385', '#6B3FA0',
'#33CC99', '#FFDB00', '#87FF2A', '#6EEB6E', '#FFC800', '#CC99BA', '#7A89B8',
'#006A93', '#867200', '#E2B631', '#D9D6CF',
];
function shapeFactory(shapeData, clientID, injection) { function shapeFactory(shapeData, clientID, injection) {
const { type } = shapeData; const { type } = shapeData;
const color = colors[clientID % colors.length]; const color = colors[clientID % colors.length];
@ -126,31 +111,42 @@
return labelAccumulator; return labelAccumulator;
}, {}); }, {});
this.annotationsFilter = new AnnotationsFilter();
this.history = data.history;
this.shapes = {}; // key is a frame this.shapes = {}; // key is a frame
this.tags = {}; // key is a frame this.tags = {}; // key is a frame
this.tracks = []; this.tracks = [];
this.objects = {}; // key is a client id this.objects = {}; // key is a client id
this.count = 0; this.count = 0;
this.flush = false; this.flush = false;
this.collectionZ = {}; // key is a frame, {max, min} are values
this.groups = { this.groups = {
max: 0, max: 0,
}; // it is an object to we can pass it as an argument by a reference }; // it is an object to we can pass it as an argument by a reference
this.injection = { this.injection = {
labels: this.labels, labels: this.labels,
collectionZ: this.collectionZ,
groups: this.groups, groups: this.groups,
frameMeta: this.frameMeta, frameMeta: this.frameMeta,
history: this.history,
groupColors: {},
}; };
} }
import(data) { import(data) {
const result = {
tags: [],
shapes: [],
tracks: [],
};
for (const tag of data.tags) { for (const tag of data.tags) {
const clientID = ++this.count; const clientID = ++this.count;
const tagModel = new Tag(tag, clientID, this.injection); const color = colors[clientID % colors.length];
const tagModel = new Tag(tag, clientID, color, this.injection);
this.tags[tagModel.frame] = this.tags[tagModel.frame] || []; this.tags[tagModel.frame] = this.tags[tagModel.frame] || [];
this.tags[tagModel.frame].push(tagModel); this.tags[tagModel.frame].push(tagModel);
this.objects[clientID] = tagModel; this.objects[clientID] = tagModel;
result.tags.push(tagModel);
} }
for (const shape of data.shapes) { for (const shape of data.shapes) {
@ -159,6 +155,8 @@
this.shapes[shapeModel.frame] = this.shapes[shapeModel.frame] || []; this.shapes[shapeModel.frame] = this.shapes[shapeModel.frame] || [];
this.shapes[shapeModel.frame].push(shapeModel); this.shapes[shapeModel.frame].push(shapeModel);
this.objects[clientID] = shapeModel; this.objects[clientID] = shapeModel;
result.shapes.push(shapeModel);
} }
for (const track of data.tracks) { for (const track of data.tracks) {
@ -169,51 +167,74 @@
if (trackModel) { if (trackModel) {
this.tracks.push(trackModel); this.tracks.push(trackModel);
this.objects[clientID] = trackModel; this.objects[clientID] = trackModel;
result.tracks.push(trackModel);
} }
} }
return this; return result;
} }
export() { export() {
const data = { const data = {
tracks: this.tracks.filter(track => !track.removed) tracks: this.tracks.filter((track) => !track.removed)
.map(track => track.toJSON()), .map((track) => track.toJSON()),
shapes: Object.values(this.shapes) shapes: Object.values(this.shapes)
.reduce((accumulator, value) => { .reduce((accumulator, value) => {
accumulator.push(...value); accumulator.push(...value);
return accumulator; return accumulator;
}, []).filter(shape => !shape.removed) }, []).filter((shape) => !shape.removed)
.map(shape => shape.toJSON()), .map((shape) => shape.toJSON()),
tags: Object.values(this.tags).reduce((accumulator, value) => { tags: Object.values(this.tags).reduce((accumulator, value) => {
accumulator.push(...value); accumulator.push(...value);
return accumulator; return accumulator;
}, []).filter(tag => !tag.removed) }, []).filter((tag) => !tag.removed)
.map(tag => tag.toJSON()), .map((tag) => tag.toJSON()),
}; };
return data; return data;
} }
get(frame) { get(frame, allTracks, filters) {
const { tracks } = this; const { tracks } = this;
const shapes = this.shapes[frame] || []; const shapes = this.shapes[frame] || [];
const tags = this.tags[frame] || []; const tags = this.tags[frame] || [];
const objects = tracks.concat(shapes).concat(tags).filter(object => !object.removed); const objects = [].concat(tracks, shapes, tags);
// filtering here const visible = {
models: [],
data: [],
};
const objectStates = [];
for (const object of objects) { for (const object of objects) {
if (object.removed) {
continue;
}
const stateData = object.get(frame); const stateData = object.get(frame);
if (stateData.outside && !stateData.keyframe) { if (!allTracks && stateData.outside && !stateData.keyframe) {
continue; continue;
} }
const objectState = objectStateFactory.call(object, frame, stateData); visible.models.push(object);
objectStates.push(objectState); visible.data.push(stateData);
}
const [, query] = this.annotationsFilter.toJSONQuery(filters);
let filtered = [];
if (filters.length) {
filtered = this.annotationsFilter.filter(visible.data, query);
} }
const objectStates = [];
visible.data.forEach((stateData, idx) => {
if (!filters.length || filtered.includes(stateData.clientID)) {
const model = visible.models[idx];
const objectState = objectStateFactory.call(model, frame, stateData);
objectStates.push(objectState);
}
});
return objectStates; return objectStates;
} }
@ -370,7 +391,7 @@
const clientID = ++this.count; const clientID = ++this.count;
const track = { const track = {
frame: Math.min.apply(null, Object.keys(keyframes).map(frame => +frame)), frame: Math.min.apply(null, Object.keys(keyframes).map((frame) => +frame)),
shapes: Object.values(keyframes), shapes: Object.values(keyframes),
group: 0, group: 0,
label_id: label.id, label_id: label.id,
@ -394,10 +415,19 @@
// Remove other shapes // Remove other shapes
for (const object of objectsForMerge) { for (const object of objectsForMerge) {
object.removed = true; object.removed = true;
if (typeof (object.resetCache) === 'function') {
object.resetCache();
}
} }
this.history.do(HistoryActions.MERGED_OBJECTS, () => {
trackModel.removed = true;
for (const object of objectsForMerge) {
object.removed = false;
}
}, () => {
trackModel.removed = false;
for (const object of objectsForMerge) {
object.removed = true;
}
}, [...objectsForMerge.map((object) => object.clientID), trackModel.clientID]);
} }
split(objectState, frame) { split(objectState, frame) {
@ -416,7 +446,7 @@
} }
const keyframes = Object.keys(object.shapes).sort((a, b) => +a - +b); const keyframes = Object.keys(object.shapes).sort((a, b) => +a - +b);
if (frame <= +keyframes[0] || frame > keyframes[keyframes.length - 1]) { if (frame <= +keyframes[0]) {
return; return;
} }
@ -431,7 +461,7 @@
points: [...objectState.points], points: [...objectState.points],
occluded: objectState.occluded, occluded: objectState.occluded,
outside: objectState.outside, outside: objectState.outside,
zOrder: 0, zOrder: objectState.zOrder,
attributes: Object.keys(objectState.attributes) attributes: Object.keys(objectState.attributes)
.reduce((accumulator, attrID) => { .reduce((accumulator, attrID) => {
if (!labelAttributes[attrID].mutable) { if (!labelAttributes[attrID].mutable) {
@ -483,7 +513,16 @@
// Remove source object // Remove source object
object.removed = true; object.removed = true;
object.resetCache();
this.history.do(HistoryActions.SPLITTED_TRACK, () => {
object.removed = false;
prevTrack.removed = true;
nextTrack.removed = true;
}, () => {
object.removed = true;
prevTrack.removed = false;
nextTrack.removed = false;
}, [object.clientID, prevTrack.clientID, nextTrack.clientID]);
} }
group(objectStates, reset) { group(objectStates, reset) {
@ -501,12 +540,21 @@
}); });
const groupIdx = reset ? 0 : ++this.groups.max; const groupIdx = reset ? 0 : ++this.groups.max;
const undoGroups = objectsForGroup.map((object) => object.group);
for (const object of objectsForGroup) { for (const object of objectsForGroup) {
object.group = groupIdx; object.group = groupIdx;
if (typeof (object.resetCache) === 'function') {
object.resetCache();
}
} }
const redoGroups = objectsForGroup.map((object) => object.group);
this.history.do(HistoryActions.GROUPED_OBJECTS, () => {
objectsForGroup.forEach((object, idx) => {
object.group = undoGroups[idx];
});
}, () => {
objectsForGroup.forEach((object, idx) => {
object.group = redoGroups[idx];
});
}, objectsForGroup.map((object) => object.clientID));
return groupIdx; return groupIdx;
} }
@ -553,6 +601,10 @@
} }
for (const object of Object.values(this.objects)) { for (const object of Object.values(this.objects)) {
if (object.removed) {
continue;
}
let objectType = null; let objectType = null;
if (object instanceof Shape) { if (object instanceof Shape) {
objectType = 'shape'; objectType = 'shape';
@ -577,7 +629,7 @@
if (objectType === 'track') { if (objectType === 'track') {
const keyframes = Object.keys(object.shapes) const keyframes = Object.keys(object.shapes)
.sort((a, b) => +a - +b).map(el => +el); .sort((a, b) => +a - +b).map((el) => +el);
let prevKeyframe = keyframes[0]; let prevKeyframe = keyframes[0];
let visible = false; let visible = false;
@ -673,6 +725,7 @@
} else { } else {
checkObjectType('state occluded', state.occluded, 'boolean', null); checkObjectType('state occluded', state.occluded, 'boolean', null);
checkObjectType('state points', state.points, null, Array); checkObjectType('state points', state.points, null, Array);
checkObjectType('state zOrder', state.zOrder, 'integer', null);
for (const coord of state.points) { for (const coord of state.points) {
checkObjectType('point coordinate', coord, 'number', null); checkObjectType('point coordinate', coord, 'number', null);
@ -694,24 +747,24 @@
occluded: state.occluded || false, occluded: state.occluded || false,
points: [...state.points], points: [...state.points],
type: state.shapeType, type: state.shapeType,
z_order: 0, z_order: state.zOrder,
}); });
} else if (state.objectType === 'track') { } else if (state.objectType === 'track') {
constructed.tracks.push({ constructed.tracks.push({
attributes: attributes attributes: attributes
.filter(attr => !labelAttributes[attr.spec_id].mutable), .filter((attr) => !labelAttributes[attr.spec_id].mutable),
frame: state.frame, frame: state.frame,
group: 0, group: 0,
label_id: state.label.id, label_id: state.label.id,
shapes: [{ shapes: [{
attributes: attributes attributes: attributes
.filter(attr => labelAttributes[attr.spec_id].mutable), .filter((attr) => labelAttributes[attr.spec_id].mutable),
frame: state.frame, frame: state.frame,
occluded: state.occluded || false, occluded: state.occluded || false,
outside: false, outside: false,
points: [...state.points], points: [...state.points],
type: state.shapeType, type: state.shapeType,
z_order: 0, z_order: state.zOrder,
}], }],
}); });
} else { } else {
@ -724,7 +777,20 @@
} }
// Add constructed objects to a collection // Add constructed objects to a collection
this.import(constructed); const imported = this.import(constructed);
const importedArray = imported.tags
.concat(imported.tracks)
.concat(imported.shapes);
this.history.do(HistoryActions.CREATED_OBJECTS, () => {
importedArray.forEach((object) => {
object.removed = true;
});
}, () => {
importedArray.forEach((object) => {
object.removed = false;
});
}, importedArray.map((object) => object.clientID));
} }
select(objectStates, x, y) { select(objectStates, x, y) {
@ -736,7 +802,7 @@
let minimumState = null; let minimumState = null;
for (const state of objectStates) { for (const state of objectStates) {
checkObjectType('object state', state, null, ObjectState); checkObjectType('object state', state, null, ObjectState);
if (state.outside) continue; if (state.outside || state.hidden) continue;
const object = this.objects[state.clientID]; const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') { if (typeof (object) === 'undefined') {
@ -757,6 +823,109 @@
distance: minimumDistance, distance: minimumDistance,
}; };
} }
search(filters, frameFrom, frameTo) {
const [groups, query] = this.annotationsFilter.toJSONQuery(filters);
const sign = Math.sign(frameTo - frameFrom);
const flattenedQuery = groups.flat(Number.MAX_SAFE_INTEGER);
const containsDifficultProperties = flattenedQuery
.some((fragment) => fragment
.match(/^width/) || fragment.match(/^height/));
const deepSearch = (deepSearchFrom, deepSearchTo) => {
// deepSearchFrom is expected to be a frame that doesn't satisfy a filter
// deepSearchTo is expected to be a frame that satifies a filter
let [prev, next] = [deepSearchFrom, deepSearchTo];
// half division method instead of linear search
while (!(Math.abs(prev - next) === 1)) {
const middle = next + Math.floor((prev - next) / 2);
const shapesData = this.tracks.map((track) => track.get(middle));
const filtered = this.annotationsFilter.filter(shapesData, query);
if (filtered.length) {
next = middle;
} else {
prev = middle;
}
}
return next;
};
const keyframesMemory = {};
const predicate = sign > 0
? (frame) => frame <= frameTo
: (frame) => frame >= frameTo;
const update = sign > 0
? (frame) => frame + 1
: (frame) => frame - 1;
for (let frame = frameFrom; predicate(frame); frame = update(frame)) {
// First prepare all data for the frame
// Consider all shapes, tags, and not outside tracks that have keyframe here
// In particular consider first and last frame as keyframes for all frames
const statesData = [].concat(
(frame in this.shapes ? this.shapes[frame] : [])
.map((shape) => shape.get(frame)),
(frame in this.tags ? this.tags[frame] : [])
.map((tag) => tag.get(frame)),
);
const tracks = Object.values(this.tracks)
.filter((track) => (
frame in track.shapes
|| frame === frameFrom
|| frame === frameTo
));
statesData.push(
...tracks.map((track) => track.get(frame))
.filter((state) => !state.outside),
);
// Nothing to filtering, go to the next iteration
if (!statesData.length) {
continue;
}
// Filtering
const filtered = this.annotationsFilter.filter(statesData, query);
// Now we are checking whether we need deep search or not
// Deep search is needed in some difficult cases
// For example when filter contains fields which
// can be changed between keyframes (like: height and width of a shape)
// It's expected, that a track doesn't satisfy a filter on the previous keyframe
// At the same time it sutisfies the filter on the next keyframe
let withDeepSearch = false;
if (containsDifficultProperties) {
for (const track of tracks) {
const trackIsSatisfy = filtered.includes(track.clientID);
if (!trackIsSatisfy) {
keyframesMemory[track.clientID] = [
filtered.includes(track.clientID),
frame,
];
} else if (keyframesMemory[track.clientID]
&& keyframesMemory[track.clientID][0] === false) {
withDeepSearch = true;
}
}
}
if (withDeepSearch) {
const reducer = sign > 0 ? Math.min : Math.max;
const deepSearchFrom = reducer(
...Object.values(keyframesMemory).map((value) => value[1]),
);
return deepSearch(deepSearchFrom, frame);
}
if (filtered.length) {
return frame;
}
}
return null;
}
} }
module.exports = Collection; module.exports = Collection;

@ -0,0 +1,240 @@
/*
* Copyright (C) 2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
const jsonpath = require('jsonpath');
const { AttributeType } = require('./enums');
const { ArgumentError } = require('./exceptions');
class AnnotationsFilter {
constructor() {
// eslint-disable-next-line security/detect-unsafe-regex
this.operatorRegex = /(==|!=|<=|>=|>|<|~=)(?=(?:[^"]*(["])[^"]*\2)*[^"]*$)/g;
}
// Method splits expression by operators that are outside of any brackets
_splitWithOperator(container, expression) {
const operators = ['|', '&'];
const splitted = [];
let nestedCounter = 0;
let isQuotes = false;
let start = -1;
for (let i = 0; i < expression.length; i++) {
if (expression[i] === '"') {
// all quotes inside other quotes must
// be escaped by a user and changed to ` above
isQuotes = !isQuotes;
}
// We don't split with operator inside brackets
// It will be done later in recursive call
if (!isQuotes && expression[i] === '(') {
nestedCounter++;
}
if (!isQuotes && expression[i] === ')') {
nestedCounter--;
}
if (operators.includes(expression[i])) {
if (!nestedCounter) {
const subexpression = expression
.substr(start + 1, i - start - 1).trim();
splitted.push(subexpression);
splitted.push(expression[i]);
start = i;
}
}
}
const subexpression = expression
.substr(start + 1).trim();
splitted.push(subexpression);
splitted.forEach((internalExpression) => {
if (internalExpression === '|' || internalExpression === '&') {
container.push(internalExpression);
} else {
this._groupByBrackets(
container,
internalExpression,
);
}
});
}
// Method groups bracket containings to nested arrays of container
_groupByBrackets(container, expression) {
if (!(expression.startsWith('(') && expression.endsWith(')'))) {
container.push(expression);
}
let nestedCounter = 0;
let startBracket = null;
let endBracket = null;
let isQuotes = false;
for (let i = 0; i < expression.length; i++) {
if (expression[i] === '"') {
// all quotes inside other quotes must
// be escaped by a user and changed to ` above
isQuotes = !isQuotes;
}
if (!isQuotes && expression[i] === '(') {
nestedCounter++;
if (startBracket === null) {
startBracket = i;
}
}
if (!isQuotes && expression[i] === ')') {
nestedCounter--;
if (!nestedCounter) {
endBracket = i;
const subcontainer = [];
const subexpression = expression
.substr(startBracket + 1, endBracket - 1 - startBracket);
this._splitWithOperator(
subcontainer,
subexpression,
);
container.push(subcontainer);
startBracket = null;
endBracket = null;
}
}
}
if (startBracket !== null) {
throw Error('Extra opening bracket found');
}
if (endBracket !== null) {
throw Error('Extra closing bracket found');
}
}
_parse(expression) {
const groups = [];
this._splitWithOperator(groups, expression);
}
_join(groups) {
let expression = '';
for (const group of groups) {
if (Array.isArray(group)) {
expression += `(${this._join(group)})`;
} else if (typeof (group) === 'string') {
// it can be operator or expression
if (group === '|' || group === '&') {
expression += group;
} else {
let [field, operator, , value] = group.split(this.operatorRegex);
field = `@.${field.trim()}`;
operator = operator.trim();
value = value.trim();
if (value === 'width' || value === 'height' || value.startsWith('attr')) {
value = `@.${value}`;
}
expression += [field, operator, value].join('');
}
}
}
return expression;
}
_convertObjects(statesData) {
const objects = statesData.map((state) => {
const labelAttributes = state.label.attributes
.reduce((acc, attr) => {
acc[attr.id] = attr;
return acc;
}, {});
let xtl = Number.MAX_SAFE_INTEGER;
let xbr = Number.MIN_SAFE_INTEGER;
let ytl = Number.MAX_SAFE_INTEGER;
let ybr = Number.MIN_SAFE_INTEGER;
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];
let value = state.attributes[key].replace(/\\"/g, '`');
if (attr.inputType === AttributeType.NUMBER) {
value = +value;
} else if (attr.inputType === AttributeType.CHECKBOX) {
value = value === 'true';
}
acc[attr.name] = value;
return acc;
}, attributes);
return {
width,
height,
attr: attributes,
label: state.label.name.replace(/\\"/g, '`'),
serverID: state.serverID,
clientID: state.clientID,
type: state.objectType,
shape: state.objectShape,
occluded: state.occluded,
};
});
return {
objects,
};
}
toJSONQuery(filters) {
try {
if (!Array.isArray(filters) || filters.some((value) => typeof (value) !== 'string')) {
throw Error('Argument must be an array of strings');
}
if (!filters.length) {
return [[], '$.objects[*].clientID'];
}
const groups = [];
const expression = filters.map((filter) => `(${filter})`).join('|').replace(/\\"/g, '`');
this._splitWithOperator(groups, expression);
return [groups, `$.objects[?(${this._join(groups)})].clientID`];
} catch (error) {
throw new ArgumentError(`Wrong filter expression. ${error.toString()}`);
}
}
filter(statesData, query) {
try {
const objects = this._convertObjects(statesData);
return jsonpath.query(objects, query);
} catch (error) {
throw new ArgumentError(`Could not apply the filter. ${error.toString()}`);
}
}
}
module.exports = AnnotationsFilter;

@ -0,0 +1,71 @@
/*
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
const MAX_HISTORY_LENGTH = 128;
class AnnotationHistory {
constructor() {
this.clear();
}
get() {
return {
undo: this._undo.map((undo) => undo.action),
redo: this._redo.map((redo) => redo.action),
};
}
do(action, undo, redo, clientIDs) {
const actionItem = {
clientIDs,
action,
undo,
redo,
};
this._undo = this._undo.slice(-MAX_HISTORY_LENGTH + 1);
this._undo.push(actionItem);
this._redo = [];
}
undo(count) {
const affectedObjects = [];
for (let i = 0; i < count; i++) {
const action = this._undo.pop();
if (action) {
action.undo();
this._redo.push(action);
affectedObjects.push(...action.clientIDs);
} else {
break;
}
}
return affectedObjects;
}
redo(count) {
const affectedObjects = [];
for (let i = 0; i < count; i++) {
const action = this._redo.pop();
if (action) {
action.redo();
this._undo.push(action);
affectedObjects.push(...action.clientIDs);
} else {
break;
}
}
return affectedObjects;
}
clear() {
this._undo = [];
this._redo = [];
}
}
module.exports = AnnotationHistory;

File diff suppressed because it is too large Load Diff

@ -102,12 +102,18 @@
}, },
}; };
const keys = ['id', 'label_id', 'group', 'frame',
'occluded', 'z_order', 'points', 'type', 'shapes',
'attributes', 'value', 'spec_id', 'outside'];
// Find created and updated objects // Find created and updated objects
for (const type of Object.keys(exported)) { for (const type of Object.keys(exported)) {
for (const object of exported[type]) { for (const object of exported[type]) {
if (object.id in this.initialObjects[type]) { if (object.id in this.initialObjects[type]) {
const exportedHash = JSON.stringify(object); const exportedHash = JSON.stringify(object, keys);
const initialHash = JSON.stringify(this.initialObjects[type][object.id]); const initialHash = JSON.stringify(
this.initialObjects[type][object.id], keys,
);
if (exportedHash !== initialHash) { if (exportedHash !== initialHash) {
splitted.updated[type].push(object); splitted.updated[type].push(object);
} }
@ -161,10 +167,6 @@
for (let i = 0; i < indexes[type].length; i++) { for (let i = 0; i < indexes[type].length; i++) {
const clientID = indexes[type][i]; const clientID = indexes[type][i];
this.collection.objects[clientID].serverID = saved[type][i].id; this.collection.objects[clientID].serverID = saved[type][i].id;
if (type === 'tracks') {
// We have to reset cache because of old value of serverID was saved there
this.collection.objects[clientID].resetCache();
}
} }
} }
} }
@ -194,80 +196,67 @@
}; };
} }
try { const exported = this.collection.export();
const exported = this.collection.export(); const { flush } = this.collection;
const { flush } = this.collection; if (flush) {
if (flush) { onUpdate('Created objects are being saved on the server');
onUpdate('New objects are being saved..'); const indexes = this._receiveIndexes(exported);
const indexes = this._receiveIndexes(exported); const savedData = await this._put({ ...exported, version: this.version });
const savedData = await this._put({ ...exported, version: this.version }); this.version = savedData.version;
this.version = savedData.version; this.collection.flush = false;
this.collection.flush = false;
this._updateCreatedObjects(savedData, indexes);
onUpdate('Saved objects are being updated in the client');
this._updateCreatedObjects(savedData, indexes); this._resetState();
for (const type of Object.keys(this.initialObjects)) {
onUpdate('Initial state is being updated'); for (const object of savedData[type]) {
this.initialObjects[type][object.id] = object;
this._resetState();
for (const type of Object.keys(this.initialObjects)) {
for (const object of savedData[type]) {
this.initialObjects[type][object.id] = object;
}
} }
} else { }
const { } else {
created, const {
updated, created,
deleted, updated,
} = this._split(exported); deleted,
} = this._split(exported);
onUpdate('New objects are being saved..');
const indexes = this._receiveIndexes(created); onUpdate('Created objects are being saved on the server');
const createdData = await this._create({ ...created, version: this.version }); const indexes = this._receiveIndexes(created);
this.version = createdData.version; const createdData = await this._create({ ...created, version: this.version });
this.version = createdData.version;
onUpdate('Saved objects are being updated in the client');
this._updateCreatedObjects(createdData, indexes); this._updateCreatedObjects(createdData, indexes);
onUpdate('Initial state is being updated'); for (const type of Object.keys(this.initialObjects)) {
for (const type of Object.keys(this.initialObjects)) { for (const object of createdData[type]) {
for (const object of createdData[type]) { this.initialObjects[type][object.id] = object;
this.initialObjects[type][object.id] = object;
}
} }
}
onUpdate('Changed objects are being saved..'); onUpdate('Updated objects are being saved on the server');
this._receiveIndexes(updated); this._receiveIndexes(updated);
const updatedData = await this._update({ ...updated, version: this.version }); const updatedData = await this._update({ ...updated, version: this.version });
this.version = updatedData.version; this.version = updatedData.version;
onUpdate('Initial state is being updated'); for (const type of Object.keys(this.initialObjects)) {
for (const type of Object.keys(this.initialObjects)) { for (const object of updatedData[type]) {
for (const object of updatedData[type]) { this.initialObjects[type][object.id] = object;
this.initialObjects[type][object.id] = object;
}
} }
}
onUpdate('Changed objects are being saved..'); onUpdate('Deleted objects are being deleted from the server');
this._receiveIndexes(deleted); this._receiveIndexes(deleted);
const deletedData = await this._delete({ ...deleted, version: this.version }); const deletedData = await this._delete({ ...deleted, version: this.version });
this._version = deletedData.version; this._version = deletedData.version;
onUpdate('Initial state is being updated'); for (const type of Object.keys(this.initialObjects)) {
for (const type of Object.keys(this.initialObjects)) { for (const object of deletedData[type]) {
for (const object of deletedData[type]) { delete this.initialObjects[type][object.id];
delete this.initialObjects[type][object.id];
}
} }
} }
this.hash = this._getHash();
onUpdate('Saving is done');
} catch (error) {
onUpdate(`Can not save annotations: ${error.message}`);
throw error;
} }
this.hash = this._getHash();
} }
hasUnsavedChanges() { hasUnsavedChanges() {

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 Intel Corporation * Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -11,6 +11,7 @@
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
const Collection = require('./annotations-collection'); const Collection = require('./annotations-collection');
const AnnotationsSaver = require('./annotations-saver'); const AnnotationsSaver = require('./annotations-saver');
const AnnotationsHistory = require('./annotations-history');
const { checkObjectType } = require('./common'); const { checkObjectType } = require('./common');
const { Task } = require('./session'); const { Task } = require('./session');
const { const {
@ -56,28 +57,36 @@
frameMeta[i] = await session.frames.get(i); frameMeta[i] = await session.frames.get(i);
} }
const history = new AnnotationsHistory();
const collection = new Collection({ const collection = new Collection({
labels: session.labels || session.task.labels, labels: session.labels || session.task.labels,
history,
startFrame, startFrame,
stopFrame, stopFrame,
frameMeta, frameMeta,
}).import(rawAnnotations); });
collection.import(rawAnnotations);
const saver = new AnnotationsSaver(rawAnnotations.version, collection, session); const saver = new AnnotationsSaver(rawAnnotations.version, collection, session);
cache.set(session, { cache.set(session, {
collection, collection,
saver, saver,
history,
}); });
} }
} }
async function getAnnotations(session, frame, filter) { async function getAnnotations(session, frame, allTracks, filters) {
await getAnnotationsFromServer(session);
const sessionType = session instanceof Task ? 'task' : 'job'; const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType); const cache = getCache(sessionType);
return cache.get(session).collection.get(frame, filter);
if (cache.has(session)) {
return cache.get(session).collection.get(frame, allTracks, filters);
}
await getAnnotationsFromServer(session);
return cache.get(session).collection.get(frame, allTracks, filters);
} }
async function saveAnnotations(session, onUpdate) { async function saveAnnotations(session, onUpdate) {
@ -91,6 +100,19 @@
// If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it // If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it
} }
function searchAnnotations(session, filters, frameFrom, frameTo) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.search(filters, frameFrom, frameTo);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function mergeAnnotations(session, objectStates) { function mergeAnnotations(session, objectStates) {
const sessionType = session instanceof Task ? 'task' : 'job'; const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType); const cache = getCache(sessionType);
@ -225,12 +247,84 @@
return result; return result;
} }
async function exportDataset(session, format) {
if (!(format instanceof String || typeof format === 'string')) {
throw new ArgumentError(
'Format must be a string',
);
}
if (!(session instanceof Task)) {
throw new ArgumentError(
'A dataset can only be created from a task',
);
}
let result = null;
result = await serverProxy.tasks
.exportDataset(session.id, format);
return result;
}
function undoActions(session, count) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.undo(count);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function redoActions(session, count) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.redo(count);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function clearActions(session) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.clear();
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function getActions(session) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.get();
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
module.exports = { module.exports = {
getAnnotations, getAnnotations,
putAnnotations, putAnnotations,
saveAnnotations, saveAnnotations,
hasUnsavedChanges, hasUnsavedChanges,
mergeAnnotations, mergeAnnotations,
searchAnnotations,
splitAnnotations, splitAnnotations,
groupAnnotations, groupAnnotations,
clearAnnotations, clearAnnotations,
@ -238,5 +332,10 @@
selectObject, selectObject,
uploadAnnotations, uploadAnnotations,
dumpAnnotations, dumpAnnotations,
exportDataset,
undoActions,
redoActions,
clearActions,
getActions,
}; };
})(); })();

@ -9,7 +9,6 @@
require:false require:false
*/ */
(() => { (() => {
const PluginRegistry = require('./plugins'); const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
@ -31,6 +30,26 @@
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { Task } = require('./session'); const { Task } = require('./session');
function attachUsers(task, users) {
if (task.assignee !== null) {
[task.assignee] = users.filter((user) => user.id === task.assignee);
}
for (const segment of task.segments) {
for (const job of segment.jobs) {
if (job.assignee !== null) {
[job.assignee] = users.filter((user) => user.id === job.assignee);
}
}
}
if (task.owner !== null) {
[task.owner] = users.filter((user) => user.id === task.owner);
}
return task;
}
function implementAPI(cvat) { function implementAPI(cvat) {
cvat.plugins.list.implementation = PluginRegistry.list; cvat.plugins.list.implementation = PluginRegistry.list;
cvat.plugins.register.implementation = PluginRegistry.register.bind(cvat); cvat.plugins.register.implementation = PluginRegistry.register.bind(cvat);
@ -47,7 +66,12 @@
cvat.server.formats.implementation = async () => { cvat.server.formats.implementation = async () => {
const result = await serverProxy.server.formats(); const result = await serverProxy.server.formats();
return result.map(el => new AnnotationFormat(el)); return result.map((el) => new AnnotationFormat(el));
};
cvat.server.datasetFormats.implementation = async () => {
const result = await serverProxy.server.datasetFormats();
return result;
}; };
cvat.server.register.implementation = async (username, firstName, lastName, cvat.server.register.implementation = async (username, firstName, lastName,
@ -69,6 +93,11 @@
return result; return result;
}; };
cvat.server.request.implementation = async (url, data) => {
const result = await serverProxy.server.request(url, data);
return result;
};
cvat.users.get.implementation = async (filter) => { cvat.users.get.implementation = async (filter) => {
checkFilter(filter, { checkFilter(filter, {
self: isBoolean, self: isBoolean,
@ -82,7 +111,7 @@
users = await serverProxy.users.getUsers(); users = await serverProxy.users.getUsers();
} }
users = users.map(user => new User(user)); users = users.map((user) => new User(user));
return users; return users;
}; };
@ -116,8 +145,12 @@
// If task was found by its id, then create task instance and get Job instance from it // If task was found by its id, then create task instance and get Job instance from it
if (tasks !== null && tasks.length) { if (tasks !== null && tasks.length) {
const task = new Task(tasks[0]); const users = (await serverProxy.users.getUsers())
return filter.jobID ? task.jobs.filter(job => job.id === filter.jobID) : task.jobs; .map((userData) => new User(userData));
const task = new Task(attachUsers(tasks[0], users));
return filter.jobID ? task.jobs
.filter((job) => job.id === filter.jobID) : task.jobs;
} }
return []; return [];
@ -158,8 +191,14 @@
} }
} }
const users = (await serverProxy.users.getUsers())
.map((userData) => new User(userData));
const tasksData = await serverProxy.tasks.getTasks(searchParams.toString()); const tasksData = await serverProxy.tasks.getTasks(searchParams.toString());
const tasks = tasksData.map(task => new Task(task)); const tasks = tasksData
.map((task) => attachUsers(task, users))
.map((task) => new Task(task));
tasks.count = tasksData.count; tasks.count = tasksData.count;
return tasks; return tasks;

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 Intel Corporation * Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -27,8 +27,9 @@ function build() {
AttributeType, AttributeType,
ObjectType, ObjectType,
ObjectShape, ObjectShape,
VisibleState,
LogType, LogType,
HistoryActions,
colors,
} = require('./enums'); } = require('./enums');
const { const {
@ -115,6 +116,20 @@ function build() {
.apiWrapper(cvat.server.formats); .apiWrapper(cvat.server.formats);
return result; return result;
}, },
/**
* Method returns available dataset export formats
* @method exportFormats
* @async
* @memberof module:API.cvat.server
* @returns {module:String[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async datasetFormats() {
const result = await PluginRegistry
.apiWrapper(cvat.server.datasetFormats);
return result;
},
/** /**
* Method allows to register on a server * Method allows to register on a server
* @method register * @method register
@ -177,6 +192,22 @@ function build() {
.apiWrapper(cvat.server.authorized); .apiWrapper(cvat.server.authorized);
return result; return result;
}, },
/**
* Method allows to do requests via cvat-core with authorization headers
* @method request
* @async
* @memberof module:API.cvat.server
* @param {string} url
* @param {Object} data request parameters: method, headers, data, etc.
* @returns {Object | undefined} response data if exist
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async request(url, data) {
const result = await PluginRegistry
.apiWrapper(cvat.server.request, url, data);
return result;
},
}, },
/** /**
* Namespace is used for getting tasks * Namespace is used for getting tasks
@ -467,8 +498,9 @@ function build() {
AttributeType, AttributeType,
ObjectType, ObjectType,
ObjectShape, ObjectShape,
VisibleState,
LogType, LogType,
HistoryActions,
colors,
}, },
/** /**
* Namespace is used for access to exceptions * Namespace is used for access to exceptions

@ -56,7 +56,7 @@
if (typeof (value) !== type) { if (typeof (value) !== type) {
// specific case for integers which aren't native type in JS // specific case for integers which aren't native type in JS
if (type === 'integer' && Number.isInteger(value)) { if (type === 'integer' && Number.isInteger(value)) {
return; return true;
} }
throw new ArgumentError( throw new ArgumentError(
@ -77,6 +77,8 @@
); );
} }
} }
return true;
} }
module.exports = { module.exports = {

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 Intel Corporation * Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -102,22 +102,6 @@
POINTS: 'points', POINTS: 'points',
}); });
/**
* Object visibility states
* @enum {string}
* @name ObjectShape
* @memberof module:API.cvat.enums
* @property {string} ALL 'all'
* @property {string} SHAPE 'shape'
* @property {string} NONE 'none'
* @readonly
*/
const VisibleState = Object.freeze({
ALL: 'all',
SHAPE: 'shape',
NONE: 'none',
});
/** /**
* Event types * Event types
* @enum {number} * @enum {number}
@ -182,6 +166,61 @@
rotateImage: 26, rotateImage: 26,
}; };
/**
* Types of actions with annotations
* @enum {string}
* @name HistoryActions
* @memberof module:API.cvat.enums
* @property {string} CHANGED_LABEL Changed label
* @property {string} CHANGED_ATTRIBUTES Changed attributes
* @property {string} CHANGED_POINTS Changed points
* @property {string} CHANGED_OUTSIDE Changed outside
* @property {string} CHANGED_OCCLUDED Changed occluded
* @property {string} CHANGED_ZORDER Changed z-order
* @property {string} CHANGED_LOCK Changed lock
* @property {string} CHANGED_COLOR Changed color
* @property {string} CHANGED_HIDDEN Changed hidden
* @property {string} MERGED_OBJECTS Merged objects
* @property {string} SPLITTED_TRACK Splitted track
* @property {string} GROUPED_OBJECTS Grouped objects
* @property {string} CREATED_OBJECTS Created objects
* @property {string} REMOVED_OBJECT Removed object
* @readonly
*/
const HistoryActions = Object.freeze({
CHANGED_LABEL: 'Changed label',
CHANGED_ATTRIBUTES: 'Changed attributes',
CHANGED_POINTS: 'Changed points',
CHANGED_OUTSIDE: 'Changed outside',
CHANGED_OCCLUDED: 'Changed occluded',
CHANGED_ZORDER: 'Changed z-order',
CHANGED_KEYFRAME: 'Changed keyframe',
CHANGED_LOCK: 'Changed lock',
CHANGED_PINNED: 'Changed pinned',
CHANGED_COLOR: 'Changed color',
CHANGED_HIDDEN: 'Changed hidden',
MERGED_OBJECTS: 'Merged objects',
SPLITTED_TRACK: 'Splitted track',
GROUPED_OBJECTS: 'Grouped objects',
CREATED_OBJECTS: 'Created objects',
REMOVED_OBJECT: 'Removed object',
});
/**
* Array of hex colors
* @type {module:API.cvat.classes.Loader[]} values
* @name colors
* @memberof module:API.cvat.enums
* @type {string[]}
* @readonly
*/
const colors = [
'#FF355E', '#E936A7', '#FD5B78', '#FF007C', '#FF00CC', '#66FF66',
'#50BFE6', '#CCFF00', '#FFFF66', '#FF9966', '#FF6037', '#FFCC33',
'#AAF0D1', '#FF3855', '#FFF700', '#A7F432', '#FF5470', '#FAFA37',
'#FF7A00', '#FF9933', '#AFE313', '#00CC99', '#FF5050', '#733380',
];
module.exports = { module.exports = {
ShareFileType, ShareFileType,
TaskStatus, TaskStatus,
@ -189,7 +228,8 @@
AttributeType, AttributeType,
ObjectType, ObjectType,
ObjectShape, ObjectShape,
VisibleState,
LogType, LogType,
HistoryActions,
colors,
}; };
})(); })();

@ -52,6 +52,13 @@
value: tid, value: tid,
writable: false, writable: false,
}, },
/**
* @name number
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
number: { number: {
value: number, value: number,
writable: false, writable: false,
@ -93,9 +100,14 @@
} else if (isBrowser) { } else if (isBrowser) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
frameCache[this.tid][this.number] = reader.result; const image = new Image(frame.width, frame.height);
resolve(frameCache[this.tid][this.number]); image.onload = () => {
frameCache[this.tid][this.number] = image;
resolve(frameCache[this.tid][this.number]);
};
image.src = reader.result;
}; };
reader.readAsDataURL(frame); reader.readAsDataURL(frame);
} }
} }
@ -105,10 +117,31 @@
}); });
}; };
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);
if (isNode) {
resolve(global.Buffer.from(result, 'binary').toString('base64'));
} else if (isBrowser) {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.readAsDataURL(result);
}
} catch (error) {
reject(error);
}
});
}
async function getFrame(taskID, mode, frame) { async function getFrame(taskID, mode, frame) {
if (!(taskID in frameDataCache)) { if (!(taskID in frameDataCache)) {
frameDataCache[taskID] = {}; frameDataCache[taskID] = {
frameDataCache[taskID].meta = await serverProxy.frames.getMeta(taskID); meta: await serverProxy.frames.getMeta(taskID),
};
frameCache[taskID] = {}; frameCache[taskID] = {};
} }
@ -140,5 +173,6 @@
module.exports = { module.exports = {
FrameData, FrameData,
getFrame, getFrame,
getPreview,
}; };
})(); })();

@ -8,7 +8,10 @@
*/ */
(() => { (() => {
const { AttributeType } = require('./enums'); const {
AttributeType,
colors,
} = require('./enums');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
/** /**
@ -136,6 +139,7 @@
const data = { const data = {
id: undefined, id: undefined,
name: undefined, name: undefined,
color: undefined,
}; };
for (const key in data) { for (const key in data) {
@ -146,6 +150,9 @@
} }
} }
if (typeof (data.id) !== 'undefined') {
data.color = colors[data.id % colors.length];
}
data.attributes = []; data.attributes = [];
if (Object.prototype.hasOwnProperty.call(initialData, 'attributes') if (Object.prototype.hasOwnProperty.call(initialData, 'attributes')
@ -176,6 +183,23 @@
name: { name: {
get: () => data.name, get: () => data.name,
}, },
/**
* @name color
* @type {string}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
color: {
get: () => data.color,
set: (color) => {
if (colors.includes(color)) {
data.color = color;
} else {
throw new ArgumentError('Trying to set unknown color');
}
},
},
/** /**
* @name attributes * @name attributes
* @type {module:API.cvat.classes.Attribute[]} * @type {module:API.cvat.classes.Attribute[]}
@ -192,7 +216,7 @@
toJSON() { toJSON() {
const object = { const object = {
name: this.name, name: this.name,
attributes: [...this.attributes.map(el => el.toJSON())], attributes: [...this.attributes.map((el) => el.toJSON())],
}; };
if (typeof (this.id) !== 'undefined') { if (typeof (this.id) !== 'undefined') {

@ -19,13 +19,10 @@
/** /**
* @param {Object} serialized - is an dictionary which contains * @param {Object} serialized - is an dictionary which contains
* initial information about an ObjectState; * initial information about an ObjectState;
* Necessary fields: objectType, shapeType * </br> Necessary fields: objectType, shapeType, frame, updated, group
* (don't have setters) * </br> Optional fields: keyframes, clientID, serverID
* Necessary fields for objects which haven't been added to collection yet: frame * </br> Optional fields which can be set later: points, zOrder, outside,
* (doesn't have setters) * occluded, hidden, attributes, lock, label, color, keyframe
* Optional fields: points, group, zOrder, outside, occluded,
* attributes, lock, label, mode, color, keyframe, clientID, serverID
* These fields can be set later via setters
*/ */
constructor(serialized) { constructor(serialized) {
const data = { const data = {
@ -37,11 +34,14 @@
occluded: null, occluded: null,
keyframe: null, keyframe: null,
group: null,
zOrder: null, zOrder: null,
lock: null, lock: null,
color: null, color: null,
visibility: null, hidden: null,
pinned: null,
keyframes: serialized.keyframes,
group: serialized.group,
updated: serialized.updated,
clientID: serialized.clientID, clientID: serialized.clientID,
serverID: serialized.serverID, serverID: serialized.serverID,
@ -63,11 +63,13 @@
this.occluded = false; this.occluded = false;
this.keyframe = false; this.keyframe = false;
this.group = false;
this.zOrder = false; this.zOrder = false;
this.pinned = false;
this.lock = false; this.lock = false;
this.color = false; this.color = false;
this.visibility = false; this.hidden = false;
return reset;
}, },
writable: false, writable: false,
}); });
@ -153,17 +155,17 @@
data.color = color; data.color = color;
}, },
}, },
visibility: { hidden: {
/** /**
* @name visibility * @name hidden
* @type {module:API.cvat.enums.VisibleState} * @type {boolean}
* @memberof module:API.cvat.classes.ObjectState * @memberof module:API.cvat.classes.ObjectState
* @instance * @instance
*/ */
get: () => data.visibility, get: () => data.hidden,
set: (visibility) => { set: (hidden) => {
data.updateFlags.visibility = true; data.updateFlags.hidden = true;
data.visibility = visibility; data.hidden = hidden;
}, },
}, },
points: { points: {
@ -190,21 +192,19 @@
}, },
group: { group: {
/** /**
* Object with short group info { color, id }
* @name group * @name group
* @type {integer} * @type {object}
* @memberof module:API.cvat.classes.ObjectState * @memberof module:API.cvat.classes.ObjectState
* @instance * @instance
* @readonly
*/ */
get: () => data.group, get: () => data.group,
set: (group) => {
data.updateFlags.group = true;
data.group = group;
},
}, },
zOrder: { zOrder: {
/** /**
* @name zOrder * @name zOrder
* @type {integer} * @type {integer | null}
* @memberof module:API.cvat.classes.ObjectState * @memberof module:API.cvat.classes.ObjectState
* @instance * @instance
*/ */
@ -240,6 +240,23 @@
data.keyframe = keyframe; data.keyframe = keyframe;
}, },
}, },
keyframes: {
/**
* Object of keyframes { first, prev, next, last }
* @name keyframes
* @type {object | null}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => {
if (typeof (data.keyframes) === 'object') {
return { ...data.keyframes };
}
return null;
},
},
occluded: { occluded: {
/** /**
* @name occluded * @name occluded
@ -266,6 +283,36 @@
data.lock = lock; data.lock = lock;
}, },
}, },
pinned: {
/**
* @name pinned
* @type {boolean | null}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => {
if (typeof (data.pinned) === 'boolean') {
return data.pinned;
}
return null;
},
set: (pinned) => {
data.updateFlags.pinned = true;
data.pinned = pinned;
},
},
updated: {
/**
* Timestamp of the latest updated of the object
* @name updated
* @type {number}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @readonly
*/
get: () => data.updated,
},
attributes: { attributes: {
/** /**
* Object is id:value pairs where "id" is an integer * Object is id:value pairs where "id" is an integer
@ -295,20 +342,33 @@
})); }));
this.label = serialized.label; this.label = serialized.label;
this.group = serialized.group;
this.zOrder = serialized.zOrder;
this.outside = serialized.outside;
this.keyframe = serialized.keyframe;
this.occluded = serialized.occluded;
this.color = serialized.color;
this.lock = serialized.lock; this.lock = serialized.lock;
this.visibility = serialized.visibility;
// It can be undefined in a constructor and it can be defined later if (typeof (serialized.zOrder) === 'number') {
if (typeof (serialized.points) !== 'undefined') { this.zOrder = serialized.zOrder;
}
if (typeof (serialized.occluded) === 'boolean') {
this.occluded = serialized.occluded;
}
if (typeof (serialized.outside) === 'boolean') {
this.outside = serialized.outside;
}
if (typeof (serialized.keyframe) === 'boolean') {
this.keyframe = serialized.keyframe;
}
if (typeof (serialized.pinned) === 'boolean') {
this.pinned = serialized.pinned;
}
if (typeof (serialized.hidden) === 'boolean') {
this.hidden = serialized.hidden;
}
if (typeof (serialized.color) === 'string') {
this.color = serialized.color;
}
if (Array.isArray(serialized.points)) {
this.points = serialized.points; this.points = serialized.points;
} }
if (typeof (serialized.attributes) !== 'undefined') { if (typeof (serialized.attributes) === 'object') {
this.attributes = serialized.attributes; this.attributes = serialized.attributes;
} }
@ -348,42 +408,12 @@
.apiWrapper.call(this, ObjectState.prototype.delete, force); .apiWrapper.call(this, ObjectState.prototype.delete, force);
return result; return result;
} }
/**
* Set the highest ZOrder within a frame
* @method up
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
*/
async up() {
const result = await PluginRegistry
.apiWrapper.call(this, ObjectState.prototype.up);
return result;
}
/**
* Set the lowest ZOrder within a frame
* @method down
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
*/
async down() {
const result = await PluginRegistry
.apiWrapper.call(this, ObjectState.prototype.down);
return result;
}
} }
// Updates element in collection which contains it // Updates element in collection which contains it
ObjectState.prototype.save.implementation = async function () { ObjectState.prototype.save.implementation = async function () {
if (this.hidden && this.hidden.save) { if (this.__internal && this.__internal.save) {
return this.hidden.save(); return this.__internal.save();
} }
return this; return this;
@ -391,29 +421,12 @@
// Delete element from a collection which contains it // Delete element from a collection which contains it
ObjectState.prototype.delete.implementation = async function (force) { ObjectState.prototype.delete.implementation = async function (force) {
if (this.hidden && this.hidden.delete) { if (this.__internal && this.__internal.delete) {
return this.hidden.delete(force); return this.__internal.delete(force);
} }
return false; return false;
}; };
ObjectState.prototype.up.implementation = async function () {
if (this.hidden && this.hidden.up) {
return this.hidden.up();
}
return false;
};
ObjectState.prototype.down.implementation = async function () {
if (this.hidden && this.hidden.down) {
return this.hidden.down();
}
return false;
};
module.exports = ObjectState; module.exports = ObjectState;
})(); })();

@ -17,7 +17,7 @@
const pluginList = await PluginRegistry.list(); const pluginList = await PluginRegistry.list();
for (const plugin of pluginList) { for (const plugin of pluginList) {
const pluginDecorators = plugin.functions const pluginDecorators = plugin.functions
.filter(obj => obj.callback === wrappedFunc)[0]; .filter((obj) => obj.callback === wrappedFunc)[0];
if (pluginDecorators && pluginDecorators.enter) { if (pluginDecorators && pluginDecorators.enter) {
try { try {
await pluginDecorators.enter.call(this, plugin, ...args); await pluginDecorators.enter.call(this, plugin, ...args);
@ -35,7 +35,7 @@
for (const plugin of pluginList) { for (const plugin of pluginList) {
const pluginDecorators = plugin.functions const pluginDecorators = plugin.functions
.filter(obj => obj.callback === wrappedFunc)[0]; .filter((obj) => obj.callback === wrappedFunc)[0];
if (pluginDecorators && pluginDecorators.leave) { if (pluginDecorators && pluginDecorators.leave) {
try { try {
result = await pluginDecorators.leave.call(this, plugin, result, ...args); result = await pluginDecorators.leave.call(this, plugin, result, ...args);

@ -15,16 +15,14 @@
const store = require('store'); const store = require('store');
const config = require('./config'); const config = require('./config');
function generateError(errorData, baseMessage) { function generateError(errorData) {
if (errorData.response) { if (errorData.response) {
const message = `${baseMessage}. ` const message = `${errorData.message}. ${JSON.stringify(errorData.response.data) || ''}.`;
+ `${errorData.message}. ${JSON.stringify(errorData.response.data) || ''}.`;
return new ServerError(message, errorData.response.status); return new ServerError(message, errorData.response.status);
} }
// Server is unavailable (no any response) // Server is unavailable (no any response)
const message = `${baseMessage}. ` const message = `${errorData.message}.`; // usually is "Error Network"
+ `${errorData.message}.`; // usually is "Error Network"
return new ServerError(message, 0); return new ServerError(message, 0);
} }
@ -49,7 +47,7 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get "about" information from the server'); throw generateError(errorData);
} }
return response.data; return response.data;
@ -65,7 +63,7 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get "share" information from the server'); throw generateError(errorData);
} }
return response.data; return response.data;
@ -82,7 +80,7 @@
}, },
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not send an exception to the server'); throw generateError(errorData);
} }
} }
@ -95,12 +93,28 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get annotation formats from the server'); throw generateError(errorData);
} }
return response.data; return response.data;
} }
async function datasetFormats() {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/server/dataset/formats`, {
proxy: config.proxy,
});
response = JSON.parse(response.data);
} catch (errorData) {
throw generateError(errorData);
}
return response;
}
async function register(username, firstName, lastName, email, password1, password2) { async function register(username, firstName, lastName, email, password1, password2) {
let response = null; let response = null;
try { try {
@ -119,7 +133,7 @@
}, },
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, `Could not register '${username}' user on the server`); throw generateError(errorData);
} }
return response.data; return response.data;
@ -131,6 +145,7 @@
`${encodeURIComponent('password')}=${encodeURIComponent(password)}`, `${encodeURIComponent('password')}=${encodeURIComponent(password)}`,
]).join('&').replace(/%20/g, '+'); ]).join('&').replace(/%20/g, '+');
Axios.defaults.headers.common.Authorization = '';
let authenticationResponse = null; let authenticationResponse = null;
try { try {
authenticationResponse = await Axios.post( authenticationResponse = await Axios.post(
@ -140,7 +155,7 @@
}, },
); );
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not login on a server'); throw generateError(errorData);
} }
if (authenticationResponse.headers['set-cookie']) { if (authenticationResponse.headers['set-cookie']) {
@ -161,7 +176,7 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not logout from the server'); throw generateError(errorData);
} }
store.remove('token'); store.remove('token');
@ -182,16 +197,27 @@
return true; return true;
} }
async function serverRequest(url, data) {
try {
return (await Axios({
url,
...data,
})).data;
} catch (errorData) {
throw generateError(errorData);
}
}
async function getTasks(filter = '') { async function getTasks(filter = '') {
const { backendAPI } = config; const { backendAPI } = config;
let response = null; let response = null;
try { try {
response = await Axios.get(`${backendAPI}/tasks?${filter}`, { response = await Axios.get(`${backendAPI}/tasks?page_size=10&${filter}`, {
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get tasks from a server'); throw generateError(errorData);
} }
response.data.results.count = response.data.count; response.data.results.count = response.data.count;
@ -209,7 +235,7 @@
}, },
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not save the task on the server'); throw generateError(errorData);
} }
} }
@ -219,10 +245,36 @@
try { try {
await Axios.delete(`${backendAPI}/tasks/${id}`); await Axios.delete(`${backendAPI}/tasks/${id}`);
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not delete the task from the server'); throw generateError(errorData);
} }
} }
async function exportDataset(id, format) {
const { backendAPI } = config;
let url = `${backendAPI}/tasks/${id}/dataset?format=${format}`;
return new Promise((resolve, reject) => {
async function request() {
try {
const response = await Axios
.get(`${url}`, {
proxy: config.proxy,
});
if (response.status === 202) {
setTimeout(request, 3000);
} else {
url = `${url}&action=download`;
resolve(url);
}
} catch (errorData) {
reject(generateError(errorData));
}
}
setTimeout(request);
});
}
async function createTask(taskData, files, onUpdate) { async function createTask(taskData, files, onUpdate) {
const { backendAPI } = config; const { backendAPI } = config;
@ -254,7 +306,7 @@
} }
} catch (errorData) { } catch (errorData) {
reject( reject(
generateError(errorData, 'Could not put task to the server'), generateError(errorData),
); );
} }
} }
@ -283,7 +335,7 @@
}, },
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not put task to the server'); throw generateError(errorData);
} }
onUpdate('The data is being uploaded to the server..'); onUpdate('The data is being uploaded to the server..');
@ -298,7 +350,7 @@
// ignore // ignore
} }
throw generateError(errorData, 'Could not put data to the server'); throw generateError(errorData);
} }
try { try {
@ -321,7 +373,7 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get jobs from a server'); throw generateError(errorData);
} }
return response.data; return response.data;
@ -338,20 +390,26 @@
}, },
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not save the job on the server'); throw generateError(errorData);
} }
} }
async function getUsers() { async function getUsers(id = null) {
const { backendAPI } = config; const { backendAPI } = config;
let response = null; let response = null;
try { try {
response = await Axios.get(`${backendAPI}/users`, { if (id === null) {
proxy: config.proxy, response = await Axios.get(`${backendAPI}/users?page_size=all`, {
}); proxy: config.proxy,
});
} else {
response = await Axios.get(`${backendAPI}/users/${id}`, {
proxy: config.proxy,
});
}
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get users from the server'); throw generateError(errorData);
} }
return response.data.results; return response.data.results;
@ -366,7 +424,28 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData, 'Could not get user data from the server'); throw generateError(errorData);
}
return response.data;
}
async function getPreview(tid) {
const { backendAPI } = config;
let response = null;
try {
// TODO: change 0 frame to preview
response = await Axios.get(`${backendAPI}/tasks/${tid}/frames/0`, {
proxy: config.proxy,
responseType: 'blob',
});
} catch (errorData) {
const code = errorData.response ? errorData.response.status : errorData.code;
throw new ServerError(
`Could not get preview frame for the task ${tid} from the server`,
code,
);
} }
return response.data; return response.data;
@ -382,10 +461,7 @@
responseType: 'blob', responseType: 'blob',
}); });
} catch (errorData) { } catch (errorData) {
throw generateError( throw generateError(errorData);
errorData,
`Could not get frame ${frame} for the task ${tid} from the server`,
);
} }
return response.data; return response.data;
@ -400,10 +476,7 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError( throw generateError(errorData);
errorData,
`Could not get frame meta info for the task ${tid} from the server`,
);
} }
return response.data; return response.data;
@ -419,10 +492,7 @@
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
throw generateError( throw generateError(errorData);
errorData,
`Could not get annotations for the ${session} ${id} from the server`,
);
} }
return response.data; return response.data;
@ -450,10 +520,7 @@
}, },
}); });
} catch (errorData) { } catch (errorData) {
throw generateError( throw generateError(errorData);
errorData,
`Could not ${action} annotations for the ${session} ${id} on the server`,
);
} }
return response.data; return response.data;
@ -480,10 +547,7 @@
resolve(); resolve();
} }
} catch (errorData) { } catch (errorData) {
reject(generateError( reject(generateError(errorData));
errorData,
`Could not upload annotations for the ${session} ${id}`,
));
} }
} }
@ -495,27 +559,25 @@
async function dumpAnnotations(id, name, format) { async function dumpAnnotations(id, name, format) {
const { backendAPI } = config; const { backendAPI } = config;
const filename = name.replace(/\//g, '_'); const filename = name.replace(/\//g, '_');
let url = `${backendAPI}/tasks/${id}/annotations/${filename}?format=${format}`; const baseURL = `${backendAPI}/tasks/${id}/annotations/${encodeURIComponent(filename)}`;
let query = `format=${encodeURIComponent(format)}`;
let url = `${baseURL}?${query}`;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
async function request() { async function request() {
try { Axios.get(`${url}`, {
const response = await Axios proxy: config.proxy,
.get(`${url}`, { }).then((response) => {
proxy: config.proxy,
});
if (response.status === 202) { if (response.status === 202) {
setTimeout(request, 3000); setTimeout(request, 3000);
} else { } else {
url = `${url}&action=download`; query = `${query}&action=download`;
url = `${baseURL}?${query}`;
resolve(url); resolve(url);
} }
} catch (errorData) { }).catch((errorData) => {
reject(generateError( reject(generateError(errorData));
errorData, });
`Could not dump annotations for the task ${id} from the server`,
));
}
} }
setTimeout(request); setTimeout(request);
@ -528,11 +590,13 @@
about, about,
share, share,
formats, formats,
datasetFormats,
exception, exception,
login, login,
logout, logout,
authorized, authorized,
register, register,
request: serverRequest,
}), }),
writable: false, writable: false,
}, },
@ -543,6 +607,7 @@
saveTask, saveTask,
createTask, createTask,
deleteTask, deleteTask,
exportDataset,
}), }),
writable: false, writable: false,
}, },
@ -567,6 +632,7 @@
value: Object.freeze({ value: Object.freeze({
getData, getData,
getMeta, getMeta,
getPreview,
}), }),
writable: false, writable: false,
}, },

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 Intel Corporation * Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -10,10 +10,11 @@
(() => { (() => {
const PluginRegistry = require('./plugins'); const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
const { getFrame } = require('./frames'); const { getFrame, getPreview } = require('./frames');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { TaskStatus } = require('./enums'); const { TaskStatus } = require('./enums');
const { Label } = require('./labels'); const { Label } = require('./labels');
const User = require('./user');
function buildDublicatedAPI(prototype) { function buildDublicatedAPI(prototype) {
Object.defineProperties(prototype, { Object.defineProperties(prototype, {
@ -25,9 +26,9 @@
return result; return result;
}, },
async save() { async save(onUpdate) {
const result = await PluginRegistry const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.save); .apiWrapper.call(this, prototype.annotations.save, onUpdate);
return result; return result;
}, },
@ -55,16 +56,17 @@
return result; return result;
}, },
async get(frame, filter = {}) { async get(frame, allTracks = false, filters = []) {
const result = await PluginRegistry const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.get, frame, filter); .apiWrapper.call(this, prototype.annotations.get,
frame, allTracks, filters);
return result; return result;
}, },
async search(filter, frameFrom, frameTo) { async search(filters, frameFrom, frameTo) {
const result = await PluginRegistry const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.search, .apiWrapper.call(this, prototype.annotations.search,
filter, frameFrom, frameTo); filters, frameFrom, frameTo);
return result; return result;
}, },
@ -75,12 +77,6 @@
return result; return result;
}, },
async hasUnsavedChanges() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.hasUnsavedChanges);
return result;
},
async merge(objectStates) { async merge(objectStates) {
const result = await PluginRegistry const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.merge, objectStates); .apiWrapper.call(this, prototype.annotations.merge, objectStates);
@ -99,6 +95,18 @@
objectStates, reset); objectStates, reset);
return result; return result;
}, },
async exportDataset(format) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.exportDataset, format);
return result;
},
hasUnsavedChanges() {
const result = prototype.annotations
.hasUnsavedChanges.implementation.call(this);
return result;
},
}, },
writable: true, writable: true,
}), }),
@ -109,6 +117,11 @@
.apiWrapper.call(this, prototype.frames.get, frame); .apiWrapper.call(this, prototype.frames.get, frame);
return result; return result;
}, },
async preview() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.frames.preview);
return result;
},
}, },
writable: true, writable: true,
}), }),
@ -129,12 +142,12 @@
}), }),
actions: Object.freeze({ actions: Object.freeze({
value: { value: {
async undo(count) { async undo(count = 1) {
const result = await PluginRegistry const result = await PluginRegistry
.apiWrapper.call(this, prototype.actions.undo, count); .apiWrapper.call(this, prototype.actions.undo, count);
return result; return result;
}, },
async redo(count) { async redo(count = 1) {
const result = await PluginRegistry const result = await PluginRegistry
.apiWrapper.call(this, prototype.actions.redo, count); .apiWrapper.call(this, prototype.actions.redo, count);
return result; return result;
@ -144,6 +157,11 @@
.apiWrapper.call(this, prototype.actions.clear); .apiWrapper.call(this, prototype.actions.clear);
return result; return result;
}, },
async get() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.actions.get);
return result;
},
}, },
writable: true, writable: true,
}), }),
@ -182,7 +200,7 @@
* You need upload annotations from a server again after successful executing * You need upload annotations from a server again after successful executing
* @method upload * @method upload
* @memberof Session.annotations * @memberof Session.annotations
* @param {File} annotations - a text file with annotations * @param {File} annotations - a file with annotations
* @param {module:API.cvat.classes.Loader} loader - a loader * @param {module:API.cvat.classes.Loader} loader - a loader
* which will be used to upload * which will be used to upload
* @instance * @instance
@ -256,24 +274,34 @@
* @instance * @instance
* @async * @async
*/ */
/**
* @typedef {Object} ObjectFilter
* @property {string} [label] a name of a label
* @property {module:API.cvat.enums.ObjectType} [type]
* @property {module:API.cvat.enums.ObjectShape} [shape]
* @property {boolean} [occluded] a value of occluded property
* @property {boolean} [lock] a value of lock property
* @property {number} [width] a width of a shape
* @property {number} [height] a height of a shape
* @property {Object[]} [attributes] dictionary with "name: value" pairs
* @global
*/
/** /**
* Get annotations for a specific frame * Get annotations for a specific frame
* </br> Filter supports following operators:
* ==, !=, >, >=, <, <=, ~= and (), |, & for grouping.
* </br> Filter supports properties:
* width, height, label, serverID, clientID, type, shape, occluded
* </br> All prop values are case-sensitive. CVAT uses json queries for search.
* </br> Examples:
* <ul>
* <li> label=="car" | label==["road sign"] </li>
* <li> width >= height </li>
* <li> attr["Attribute 1"] == attr["Attribute 2"] </li>
* <li> type=="track" & shape="rectangle" </li>
* <li> clientID == 50 </li>
* <li> (label=="car" & attr["parked"]==true)
* | (label=="pedestrian" & width > 150) </li>
* <li> (( label==["car \"mazda\""]) &
* (attr["sunglass ( help ) es"]==true |
* (width > 150 | height > 150 & (clientID == serverID))))) </li>
* </ul>
* <b> If you have double quotes in your query string,
* please escape them using back slash: \" </b>
* @method get * @method get
* @param {integer} frame get objects from the frame * @param {integer} frame get objects from the frame
* @param {ObjectFilter[]} [filter = []] * @param {boolean} allTracks show all tracks
* get only objects are satisfied to specific filter * even if they are outside and not keyframe
* @param {string[]} [filters = []]
* get only objects that satisfied to specific filters
* @returns {module:API.cvat.classes.ObjectState[]} * @returns {module:API.cvat.classes.ObjectState[]}
* @memberof Session.annotations * @memberof Session.annotations
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
@ -282,13 +310,14 @@
* @async * @async
*/ */
/** /**
* Find frame which contains at least one object satisfied to a filter * Find a frame in the range [from, to]
* that contains at least one object satisfied to a filter
* @method search * @method search
* @memberof Session.annotations * @memberof Session.annotations
* @param {ObjectFilter} [filter = []] filter * @param {ObjectFilter} [filter = []] filter
* @param {integer} from lower bound of a search * @param {integer} from lower bound of a search
* @param {integer} to upper bound of a search * @param {integer} to upper bound of a search
* @returns {integer} the nearest frame which contains filtered objects * @returns {integer|null} a frame that contains objects according to the filter
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
* @instance * @instance
@ -352,13 +381,26 @@
* @async * @async
*/ */
/** /**
* Indicate if there are any changes in * Method indicates if there are any changes in
* annotations which haven't been saved on a server * annotations which haven't been saved on a server
* </br><b> This function cannot be wrapped with a plugin </b>
* @method hasUnsavedChanges * @method hasUnsavedChanges
* @memberof Session.annotations * @memberof Session.annotations
* @returns {boolean} * @returns {boolean}
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @instance * @instance
*/
/**
* Export as a dataset.
* Method builds a dataset in the specified format.
* @method exportDataset
* @memberof Session.annotations
* @param {module:String} format - a format
* @returns {string} An URL to the dataset file
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
* @async * @async
*/ */
@ -380,6 +422,17 @@
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
/**
* Get the first frame of a task for preview
* @method preview
* @memberof Session.frames
* @returns {string} - jpeg encoded image
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/** /**
* Namespace is used for an interaction with logs * Namespace is used for an interaction with logs
@ -418,28 +471,48 @@
*/ */
/** /**
* Is a dictionary of pairs "id:action" where "id" is an identifier of an object * @typedef {Object} HistoryActions
* which has been affected by undo/redo and "action" is what exactly has been * @property {string[]} [undo] - array of possible actions to undo
* done with the object. Action can be: "created", "deleted", "updated". * @property {string[]} [redo] - array of possible actions to redo
* Size of an output array equal the param "count".
* @typedef {Object} HistoryAction
* @global * @global
*/ */
/** /**
* Undo actions * Make undo
* @method undo * @method undo
* @memberof Session.actions * @memberof Session.actions
* @returns {HistoryAction} * @param {number} [count=1] number of actions to undo
* @returns {number[]} Array of affected objects
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance * @instance
* @async * @async
*/ */
/** /**
* Redo actions * Make redo
* @method redo * @method redo
* @memberof Session.actions * @memberof Session.actions
* @returns {HistoryAction} * @param {number} [count=1] number of actions to redo
* @returns {number[]} Array of affected objects
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
* @async
*/
/**
* Remove all actions from history
* @method clear
* @memberof Session.actions
* @throws {module:API.cvat.exceptions.PluginError}
* @instance
* @async
*/
/**
* Get actions
* @method get
* @memberof Session.actions
* @returns {HistoryActions}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance * @instance
* @async * @async
*/ */
@ -520,19 +593,19 @@
get: () => data.id, get: () => data.id,
}, },
/** /**
* Identifier of a user who is responsible for the job * Instance of a user who is responsible for the job
* @name assignee * @name assignee
* @type {integer} * @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @instance * @instance
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
assignee: { assignee: {
get: () => data.assignee, get: () => data.assignee,
set: () => (assignee) => { set: (assignee) => {
if (!Number.isInteger(assignee) || assignee < 0) { if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError( throw new ArgumentError(
'Value must be a non negative integer', 'Value must be a user instance',
); );
} }
data.assignee = assignee; data.assignee = assignee;
@ -610,6 +683,7 @@
split: Object.getPrototypeOf(this).annotations.split.bind(this), split: Object.getPrototypeOf(this).annotations.split.bind(this),
group: Object.getPrototypeOf(this).annotations.group.bind(this), group: Object.getPrototypeOf(this).annotations.group.bind(this),
clear: Object.getPrototypeOf(this).annotations.clear.bind(this), clear: Object.getPrototypeOf(this).annotations.clear.bind(this),
search: Object.getPrototypeOf(this).annotations.search.bind(this),
upload: Object.getPrototypeOf(this).annotations.upload.bind(this), upload: Object.getPrototypeOf(this).annotations.upload.bind(this),
select: Object.getPrototypeOf(this).annotations.select.bind(this), select: Object.getPrototypeOf(this).annotations.select.bind(this),
statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this), statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this),
@ -617,8 +691,16 @@
.annotations.hasUnsavedChanges.bind(this), .annotations.hasUnsavedChanges.bind(this),
}; };
this.actions = {
undo: Object.getPrototypeOf(this).actions.undo.bind(this),
redo: Object.getPrototypeOf(this).actions.redo.bind(this),
clear: Object.getPrototypeOf(this).actions.clear.bind(this),
get: Object.getPrototypeOf(this).actions.get.bind(this),
};
this.frames = { this.frames = {
get: Object.getPrototypeOf(this).frames.get.bind(this), get: Object.getPrototypeOf(this).frames.get.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
}; };
} }
@ -780,9 +862,9 @@
get: () => data.mode, get: () => data.mode,
}, },
/** /**
* Identificator of a user who has created the task * Instance of a user who has created the task
* @name owner * @name owner
* @type {integer} * @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Task * @memberof module:API.cvat.classes.Task
* @readonly * @readonly
* @instance * @instance
@ -791,19 +873,19 @@
get: () => data.owner, get: () => data.owner,
}, },
/** /**
* Identificator of a user who is responsible for the task * Instance of a user who is responsible for the task
* @name assignee * @name assignee
* @type {integer} * @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Task * @memberof module:API.cvat.classes.Task
* @instance * @instance
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
assignee: { assignee: {
get: () => data.assignee, get: () => data.assignee,
set: () => (assignee) => { set: (assignee) => {
if (!Number.isInteger(assignee) || assignee < 0) { if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError( throw new ArgumentError(
'Value must be a non negative integer', 'Value must be a user instance',
); );
} }
data.assignee = assignee; data.assignee = assignee;
@ -940,11 +1022,7 @@
} }
} }
if (typeof (data.id) === 'undefined') { data.labels = [...labels];
data.labels = [...labels];
} else {
data.labels = data.labels.concat([...labels]);
}
}, },
}, },
/** /**
@ -1113,15 +1191,26 @@
split: Object.getPrototypeOf(this).annotations.split.bind(this), split: Object.getPrototypeOf(this).annotations.split.bind(this),
group: Object.getPrototypeOf(this).annotations.group.bind(this), group: Object.getPrototypeOf(this).annotations.group.bind(this),
clear: Object.getPrototypeOf(this).annotations.clear.bind(this), clear: Object.getPrototypeOf(this).annotations.clear.bind(this),
search: Object.getPrototypeOf(this).annotations.search.bind(this),
upload: Object.getPrototypeOf(this).annotations.upload.bind(this), upload: Object.getPrototypeOf(this).annotations.upload.bind(this),
select: Object.getPrototypeOf(this).annotations.select.bind(this), select: Object.getPrototypeOf(this).annotations.select.bind(this),
statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this), statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this),
hasUnsavedChanges: Object.getPrototypeOf(this) hasUnsavedChanges: Object.getPrototypeOf(this)
.annotations.hasUnsavedChanges.bind(this), .annotations.hasUnsavedChanges.bind(this),
exportDataset: Object.getPrototypeOf(this)
.annotations.exportDataset.bind(this),
};
this.actions = {
undo: Object.getPrototypeOf(this).actions.undo.bind(this),
redo: Object.getPrototypeOf(this).actions.redo.bind(this),
clear: Object.getPrototypeOf(this).actions.clear.bind(this),
get: Object.getPrototypeOf(this).actions.get.bind(this),
}; };
this.frames = { this.frames = {
get: Object.getPrototypeOf(this).frames.get.bind(this), get: Object.getPrototypeOf(this).frames.get.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
}; };
} }
@ -1172,6 +1261,7 @@
putAnnotations, putAnnotations,
saveAnnotations, saveAnnotations,
hasUnsavedChanges, hasUnsavedChanges,
searchAnnotations,
mergeAnnotations, mergeAnnotations,
splitAnnotations, splitAnnotations,
groupAnnotations, groupAnnotations,
@ -1180,6 +1270,11 @@
annotationsStatistics, annotationsStatistics,
uploadAnnotations, uploadAnnotations,
dumpAnnotations, dumpAnnotations,
exportDataset,
undoActions,
redoActions,
clearActions,
getActions,
} = require('./annotations'); } = require('./annotations');
buildDublicatedAPI(Job.prototype); buildDublicatedAPI(Job.prototype);
@ -1190,6 +1285,7 @@
if (this.id) { if (this.id) {
const jobData = { const jobData = {
status: this.status, status: this.status,
assignee: this.assignee ? this.assignee.id : null,
}; };
await serverProxy.jobs.saveJob(this.id, jobData); await serverProxy.jobs.saveJob(this.id, jobData);
@ -1218,18 +1314,64 @@
return frameData; return frameData;
}; };
Job.prototype.frames.preview.implementation = async function () {
const frameData = await getPreview(this.task.id);
return frameData;
};
// TODO: Check filter for annotations // TODO: Check filter for annotations
Job.prototype.annotations.get.implementation = async function (frame, filter) { Job.prototype.annotations.get.implementation = async function (frame, allTracks, filters) {
if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) {
throw new ArgumentError(
'The filters argument must be an array of strings',
);
}
if (!Number.isInteger(frame)) {
throw new ArgumentError(
'The frame argument must be an integer',
);
}
if (frame < this.startFrame || frame > this.stopFrame) { if (frame < this.startFrame || frame > this.stopFrame) {
throw new ArgumentError( throw new ArgumentError(
`Frame ${frame} does not exist in the job`, `Frame ${frame} does not exist in the job`,
); );
} }
const annotationsData = await getAnnotations(this, frame, filter); const annotationsData = await getAnnotations(this, frame, allTracks, filters);
return annotationsData; return annotationsData;
}; };
Job.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) {
if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) {
throw new ArgumentError(
'The filters argument must be an array of strings',
);
}
if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) {
throw new ArgumentError(
'The start and end frames both must be an integer',
);
}
if (frameFrom < this.startFrame || frameFrom > this.stopFrame) {
throw new ArgumentError(
'The start frame is out of the job',
);
}
if (frameTo < this.startFrame || frameTo > this.stopFrame) {
throw new ArgumentError(
'The stop frame is out of the job',
);
}
const result = searchAnnotations(this, filters, frameFrom, frameTo);
return result;
};
Job.prototype.annotations.save.implementation = async function (onUpdate) { Job.prototype.annotations.save.implementation = async function (onUpdate) {
const result = await saveAnnotations(this, onUpdate); const result = await saveAnnotations(this, onUpdate);
return result; return result;
@ -1285,15 +1427,41 @@
return result; return result;
}; };
Job.prototype.annotations.exportDataset.implementation = async function (format) {
const result = await exportDataset(this.task, format);
return result;
};
Job.prototype.actions.undo.implementation = function (count) {
const result = undoActions(this, count);
return result;
};
Job.prototype.actions.redo.implementation = function (count) {
const result = redoActions(this, count);
return result;
};
Job.prototype.actions.clear.implementation = function () {
const result = clearActions(this);
return result;
};
Job.prototype.actions.get.implementation = function () {
const result = getActions(this);
return result;
};
Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) { Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) {
// TODO: Add ability to change an owner and an assignee // TODO: Add ability to change an owner and an assignee
if (typeof (this.id) !== 'undefined') { if (typeof (this.id) !== 'undefined') {
// If the task has been already created, we update it // If the task has been already created, we update it
const taskData = { const taskData = {
assignee: this.assignee ? this.assignee.id : null,
name: this.name, name: this.name,
bug_tracker: this.bugTracker, bug_tracker: this.bugTracker,
z_order: this.zOrder, z_order: this.zOrder,
labels: [...this.labels.map(el => el.toJSON())], labels: [...this.labels.map((el) => el.toJSON())],
}; };
await serverProxy.tasks.saveTask(this.id, taskData); await serverProxy.tasks.saveTask(this.id, taskData);
@ -1302,7 +1470,7 @@
const taskData = { const taskData = {
name: this.name, name: this.name,
labels: this.labels.map(el => el.toJSON()), labels: this.labels.map((el) => el.toJSON()),
image_quality: this.imageQuality, image_quality: this.imageQuality,
z_order: Boolean(this.zOrder), z_order: Boolean(this.zOrder),
}; };
@ -1358,8 +1526,19 @@
return result; return result;
}; };
Task.prototype.frames.preview.implementation = async function () {
const frameData = await getPreview(this.id);
return frameData;
};
// TODO: Check filter for annotations // TODO: Check filter for annotations
Task.prototype.annotations.get.implementation = async function (frame, filter) { Task.prototype.annotations.get.implementation = async function (frame, allTracks, filters) {
if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) {
throw new ArgumentError(
'The filters argument must be an array of strings',
);
}
if (!Number.isInteger(frame) || frame < 0) { if (!Number.isInteger(frame) || frame < 0) {
throw new ArgumentError( throw new ArgumentError(
`Frame must be a positive integer. Got: "${frame}"`, `Frame must be a positive integer. Got: "${frame}"`,
@ -1372,7 +1551,36 @@
); );
} }
const result = await getAnnotations(this, frame, filter); const result = await getAnnotations(this, frame, allTracks, filters);
return result;
};
Task.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) {
if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) {
throw new ArgumentError(
'The filters argument must be an array of strings',
);
}
if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) {
throw new ArgumentError(
'The start and end frames both must be an integer',
);
}
if (frameFrom < 0 || frameFrom >= this.size) {
throw new ArgumentError(
'The start frame is out of the task',
);
}
if (frameTo < 0 || frameTo >= this.size) {
throw new ArgumentError(
'The stop frame is out of the task',
);
}
const result = searchAnnotations(this, filters, frameFrom, frameTo);
return result; return result;
}; };
@ -1430,4 +1638,29 @@
const result = await dumpAnnotations(this, name, dumper); const result = await dumpAnnotations(this, name, dumper);
return result; return result;
}; };
Task.prototype.annotations.exportDataset.implementation = async function (format) {
const result = await exportDataset(this, format);
return result;
};
Task.prototype.actions.undo.implementation = function (count) {
const result = undoActions(this, count);
return result;
};
Task.prototype.actions.redo.implementation = function (count) {
const result = redoActions(this, count);
return result;
};
Task.prototype.actions.clear.implementation = function () {
const result = clearActions(this);
return result;
};
Task.prototype.actions.get.implementation = function () {
const result = getActions(this);
return result;
};
})(); })();

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018 Intel Corporation * Copyright (C) 2018-2020 Intel Corporation
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -85,6 +85,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
await task.annotations.put([state]); await task.annotations.put([state]);
@ -104,6 +105,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 100], points: [0, 0, 100, 100],
occluded: false, occluded: false,
label: job.task.labels[0], label: job.task.labels[0],
zOrder: 0,
}); });
await job.annotations.put([state]); await job.annotations.put([state]);
@ -123,6 +125,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
await task.annotations.put([state]); await task.annotations.put([state]);
@ -142,6 +145,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 100], points: [0, 0, 100, 100],
occluded: false, occluded: false,
label: job.task.labels[0], label: job.task.labels[0],
zOrder: 0,
}); });
await job.annotations.put([state]); await job.annotations.put([state]);
@ -158,6 +162,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
expect(task.annotations.put([state])) expect(task.annotations.put([state]))
@ -175,12 +180,45 @@ describe('Feature: put annotations', () => {
attributes: { 'bad key': 55 }, attributes: { 'bad key': 55 },
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
expect(task.annotations.put([state])) expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError); .rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('put shape with bad zOrder to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
await task.annotations.clear(true);
const state = new window.cvat.classes.ObjectState({
frame: 1,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
attributes: { 'bad key': 55 },
occluded: true,
label: task.labels[0],
zOrder: 'bad value',
});
expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
const state1 = new window.cvat.classes.ObjectState({
frame: 1,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
attributes: { 'bad key': 55 },
occluded: true,
label: task.labels[0],
zOrder: NaN,
});
expect(task.annotations.put([state1]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('put shape without points and with invalud points to a task', async () => { test('put shape without points and with invalud points to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0]; const task = (await window.cvat.tasks.get({ id: 101 }))[0];
await task.annotations.clear(true); await task.annotations.clear(true);
@ -191,6 +229,7 @@ describe('Feature: put annotations', () => {
occluded: true, occluded: true,
points: [], points: [],
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
await expect(task.annotations.put([state])) await expect(task.annotations.put([state]))
@ -214,6 +253,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
expect(task.annotations.put([state])) expect(task.annotations.put([state]))
@ -229,6 +269,7 @@ describe('Feature: put annotations', () => {
shapeType: window.cvat.enums.ObjectShape.POLYGON, shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
zOrder: 0,
}); });
await expect(task.annotations.put([state])) await expect(task.annotations.put([state]))
@ -253,6 +294,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
expect(task.annotations.put([state])) expect(task.annotations.put([state]))
@ -266,7 +308,7 @@ describe('Feature: check unsaved changes', () => {
expect(await task.annotations.hasUnsavedChanges()).toBe(false); expect(await task.annotations.hasUnsavedChanges()).toBe(false);
const annotations = await task.annotations.get(0); const annotations = await task.annotations.get(0);
annotations[0].keyframe = true; annotations[0].keyframe = false;
await annotations[0].save(); await annotations[0].save();
expect(await task.annotations.hasUnsavedChanges()).toBe(true); expect(await task.annotations.hasUnsavedChanges()).toBe(true);
@ -296,13 +338,14 @@ describe('Feature: save annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
expect(await task.annotations.hasUnsavedChanges()).toBe(false); expect(task.annotations.hasUnsavedChanges()).toBe(false);
await task.annotations.put([state]); await task.annotations.put([state]);
expect(await task.annotations.hasUnsavedChanges()).toBe(true); expect(task.annotations.hasUnsavedChanges()).toBe(true);
await task.annotations.save(); await task.annotations.save();
expect(await task.annotations.hasUnsavedChanges()).toBe(false); expect(task.annotations.hasUnsavedChanges()).toBe(false);
annotations = await task.annotations.get(0); annotations = await task.annotations.get(0);
expect(annotations).toHaveLength(length + 1); expect(annotations).toHaveLength(length + 1);
}); });
@ -311,23 +354,23 @@ describe('Feature: save annotations', () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0]; const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0); const annotations = await task.annotations.get(0);
expect(await task.annotations.hasUnsavedChanges()).toBe(false); expect(task.annotations.hasUnsavedChanges()).toBe(false);
annotations[0].occluded = true; annotations[0].occluded = true;
await annotations[0].save(); await annotations[0].save();
expect(await task.annotations.hasUnsavedChanges()).toBe(true); expect(task.annotations.hasUnsavedChanges()).toBe(true);
await task.annotations.save(); await task.annotations.save();
expect(await task.annotations.hasUnsavedChanges()).toBe(false); expect(task.annotations.hasUnsavedChanges()).toBe(false);
}); });
test('delete & save annotations for a task', async () => { test('delete & save annotations for a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0]; const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0); const annotations = await task.annotations.get(0);
expect(await task.annotations.hasUnsavedChanges()).toBe(false); expect(task.annotations.hasUnsavedChanges()).toBe(false);
await annotations[0].delete(); await annotations[0].delete();
expect(await task.annotations.hasUnsavedChanges()).toBe(true); expect(task.annotations.hasUnsavedChanges()).toBe(true);
await task.annotations.save(); await task.annotations.save();
expect(await task.annotations.hasUnsavedChanges()).toBe(false); expect(task.annotations.hasUnsavedChanges()).toBe(false);
}); });
test('create & save annotations for a job', async () => { test('create & save annotations for a job', async () => {
@ -341,13 +384,14 @@ describe('Feature: save annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: job.task.labels[0], label: job.task.labels[0],
zOrder: 0,
}); });
expect(await job.annotations.hasUnsavedChanges()).toBe(false); expect(job.annotations.hasUnsavedChanges()).toBe(false);
await job.annotations.put([state]); await job.annotations.put([state]);
expect(await job.annotations.hasUnsavedChanges()).toBe(true); expect(job.annotations.hasUnsavedChanges()).toBe(true);
await job.annotations.save(); await job.annotations.save();
expect(await job.annotations.hasUnsavedChanges()).toBe(false); expect(job.annotations.hasUnsavedChanges()).toBe(false);
annotations = await job.annotations.get(0); annotations = await job.annotations.get(0);
expect(annotations).toHaveLength(length + 1); expect(annotations).toHaveLength(length + 1);
}); });
@ -356,23 +400,23 @@ describe('Feature: save annotations', () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const annotations = await job.annotations.get(0); const annotations = await job.annotations.get(0);
expect(await job.annotations.hasUnsavedChanges()).toBe(false); expect(job.annotations.hasUnsavedChanges()).toBe(false);
annotations[0].points = [0, 100, 200, 300]; annotations[0].points = [0, 100, 200, 300];
await annotations[0].save(); await annotations[0].save();
expect(await job.annotations.hasUnsavedChanges()).toBe(true); expect(job.annotations.hasUnsavedChanges()).toBe(true);
await job.annotations.save(); await job.annotations.save();
expect(await job.annotations.hasUnsavedChanges()).toBe(false); expect(job.annotations.hasUnsavedChanges()).toBe(false);
}); });
test('delete & save annotations for a job', async () => { test('delete & save annotations for a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const annotations = await job.annotations.get(0); const annotations = await job.annotations.get(0);
expect(await job.annotations.hasUnsavedChanges()).toBe(false); expect(job.annotations.hasUnsavedChanges()).toBe(false);
await annotations[0].delete(); await annotations[0].delete();
expect(await job.annotations.hasUnsavedChanges()).toBe(true); expect(job.annotations.hasUnsavedChanges()).toBe(true);
await job.annotations.save(); await job.annotations.save();
expect(await job.annotations.hasUnsavedChanges()).toBe(false); expect(job.annotations.hasUnsavedChanges()).toBe(false);
}); });
test('delete & save annotations for a job when there are a track and a shape with the same id', async () => { test('delete & save annotations for a job when there are a track and a shape with the same id', async () => {
@ -548,7 +592,7 @@ describe('Feature: group annotations', () => {
expect(typeof (groupID)).toBe('number'); expect(typeof (groupID)).toBe('number');
annotations = await task.annotations.get(0); annotations = await task.annotations.get(0);
for (const state of annotations) { for (const state of annotations) {
expect(state.group).toBe(groupID); expect(state.group.id).toBe(groupID);
} }
}); });
@ -559,7 +603,7 @@ describe('Feature: group annotations', () => {
expect(typeof (groupID)).toBe('number'); expect(typeof (groupID)).toBe('number');
annotations = await job.annotations.get(0); annotations = await job.annotations.get(0);
for (const state of annotations) { for (const state of annotations) {
expect(state.group).toBe(groupID); expect(state.group.id).toBe(groupID);
} }
}); });
@ -574,6 +618,7 @@ describe('Feature: group annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
expect(task.annotations.group([state])) expect(task.annotations.group([state]))
@ -613,11 +658,11 @@ describe('Feature: clear annotations', () => {
expect(annotations.length).not.toBe(0); expect(annotations.length).not.toBe(0);
annotations[0].occluded = true; annotations[0].occluded = true;
await annotations[0].save(); await annotations[0].save();
expect(await task.annotations.hasUnsavedChanges()).toBe(true); expect(task.annotations.hasUnsavedChanges()).toBe(true);
await task.annotations.clear(true); await task.annotations.clear(true);
annotations = await task.annotations.get(0); annotations = await task.annotations.get(0);
expect(annotations.length).not.toBe(0); expect(annotations.length).not.toBe(0);
expect(await task.annotations.hasUnsavedChanges()).toBe(false); expect(task.annotations.hasUnsavedChanges()).toBe(false);
}); });
test('clear annotations with reload in a job', async () => { test('clear annotations with reload in a job', async () => {
@ -626,11 +671,11 @@ describe('Feature: clear annotations', () => {
expect(annotations.length).not.toBe(0); expect(annotations.length).not.toBe(0);
annotations[0].occluded = true; annotations[0].occluded = true;
await annotations[0].save(); await annotations[0].save();
expect(await job.annotations.hasUnsavedChanges()).toBe(true); expect(job.annotations.hasUnsavedChanges()).toBe(true);
await job.annotations.clear(true); await job.annotations.clear(true);
annotations = await job.annotations.get(0); annotations = await job.annotations.get(0);
expect(annotations.length).not.toBe(0); expect(annotations.length).not.toBe(0);
expect(await job.annotations.hasUnsavedChanges()).toBe(false); expect(job.annotations.hasUnsavedChanges()).toBe(false);
}); });
test('clear annotations with bad reload parameter', async () => { test('clear annotations with bad reload parameter', async () => {

@ -69,3 +69,17 @@ describe('Feature: get frame data', () => {
expect(typeof (frameData)).toBe('string'); expect(typeof (frameData)).toBe('string');
}); });
}); });
describe('Feature: get frame preview', () => {
test('get frame preview for a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const frame = await task.frames.preview();
expect(typeof (frame)).toBe('string');
});
test('get frame preview for a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const frame = await job.frames.preview();
expect(typeof (frame)).toBe('string');
});
});

@ -303,45 +303,3 @@ describe('Feature: delete object', () => {
expect(annotationsAfter).toHaveLength(length - 1); expect(annotationsAfter).toHaveLength(length - 1);
}); });
}); });
describe('Feature: change z order of an object', () => {
test('up z order for a shape', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
const state = annotations[0];
const { zOrder } = state;
await state.up();
expect(state.zOrder).toBeGreaterThan(zOrder);
});
test('up z order for a track', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0);
const state = annotations[0];
const { zOrder } = state;
await state.up();
expect(state.zOrder).toBeGreaterThan(zOrder);
});
test('down z order for a shape', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
const state = annotations[0];
const { zOrder } = state;
await state.down();
expect(state.zOrder).toBeLessThan(zOrder);
});
test('down z order for a track', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0);
const state = annotations[0];
const { zOrder } = state;
await state.down();
expect(state.zOrder).toBeLessThan(zOrder);
});
});

@ -129,7 +129,7 @@ describe('Feature: save a task', () => {
}], }],
}); });
result[0].labels = [newLabel]; result[0].labels = [...result[0].labels, newLabel];
result[0].save(); result[0].save();
result = await window.cvat.tasks.get({ result = await window.cvat.tasks.get({

@ -0,0 +1,124 @@
/*
* Copyright (C) 2018-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock');
return mock;
});
const AnnotationsFilter = require('../../src/annotations-filter');
// Initialize api
window.cvat = require('../../src/api');
// Test cases
describe('Feature: toJSONQuery', () => {
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter.toJSONQuery([]);
expect(Array.isArray(groups)).toBeTruthy();
expect(typeof (query)).toBe('string');
});
test('convert empty fitlers to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [, query] = annotationsFilter.toJSONQuery([]);
expect(query).toBe('$.objects[*].clientID');
});
test('convert wrong fitlers (empty string) to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
expect(() => {
annotationsFilter.toJSONQuery(['']);
}).toThrow(window.cvat.exceptions.ArgumentError);
});
test('convert wrong fitlers (wrong number argument) to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
expect(() => {
annotationsFilter.toJSONQuery(1);
}).toThrow(window.cvat.exceptions.ArgumentError);
});
test('convert wrong fitlers (wrong array argument) to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
expect(() => {
annotationsFilter.toJSONQuery(['clientID ==6', 1]);
}).toThrow(window.cvat.exceptions.ArgumentError);
});
test('convert wrong filters (wrong expression) to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
expect(() => {
annotationsFilter.toJSONQuery(['clientID=5']);
}).toThrow(window.cvat.exceptions.ArgumentError);
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter
.toJSONQuery(['clientID==5 & shape=="rectangle" & label==["car"]']);
expect(groups).toEqual([
['clientID==5', '&', 'shape=="rectangle"', '&', 'label==["car"]'],
]);
expect(query).toBe('$.objects[?((@.clientID==5&@.shape=="rectangle"&@.label==["car"]))].clientID');
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter
.toJSONQuery(['label=="car" | width >= height & type=="track"']);
expect(groups).toEqual([
['label=="car"', '|', 'width >= height', '&', 'type=="track"'],
]);
expect(query).toBe('$.objects[?((@.label=="car"|@.width>=@.height&@.type=="track"))].clientID');
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter
.toJSONQuery(['label=="person" & attr["Attribute 1"] ==attr["Attribute 2"]']);
expect(groups).toEqual([
['label=="person"', '&', 'attr["Attribute 1"] ==attr["Attribute 2"]'],
]);
expect(query).toBe('$.objects[?((@.label=="person"&@.attr["Attribute 1"]==@.attr["Attribute 2"]))].clientID');
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter
.toJSONQuery(['label=="car" & attr["parked"]==true', 'label=="pedestrian" & width > 150']);
expect(groups).toEqual([
['label=="car"', '&', 'attr["parked"]==true'],
'|',
['label=="pedestrian"', '&', 'width > 150'],
]);
expect(query).toBe('$.objects[?((@.label=="car"&@.attr["parked"]==true)|(@.label=="pedestrian"&@.width>150))].clientID');
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter
.toJSONQuery(['(( label==["car \\"mazda\\""]) & (attr["sunglass ( help ) es"]==true | (width > 150 | height > 150 & (clientID == serverID))))) ']);
expect(groups).toEqual([[[
['label==["car `mazda`"]'],
'&',
['attr["sunglass ( help ) es"]==true', '|',
['width > 150', '|', 'height > 150', '&',
[
'clientID == serverID',
],
],
],
]]]);
expect(query).toBe('$.objects[?((((@.label==["car `mazda`"])&(@.attr["sunglass ( help ) es"]==true|(@.width>150|@.height>150&(@.clientID==serverID))))))].clientID');
});
});

@ -192,6 +192,10 @@ class ServerProxy {
return JSON.parse(JSON.stringify(usersDummyData)).results[0]; return JSON.parse(JSON.stringify(usersDummyData)).results[0];
} }
async function getPreview() {
return 'DUMMY_IMAGE';
}
async function getData() { async function getData() {
return 'DUMMY_IMAGE'; return 'DUMMY_IMAGE';
} }
@ -282,6 +286,7 @@ class ServerProxy {
value: Object.freeze({ value: Object.freeze({
getData, getData,
getMeta, getMeta,
getPreview,
}), }),
writable: false, writable: false,
}, },

@ -7,13 +7,12 @@ const path = require('path');
const nodeConfig = { const nodeConfig = {
target: 'node', target: 'node',
mode: 'production', mode: 'development',
devtool: 'source-map', devtool: 'source-map',
entry: './src/api.js', entry: './src/api.js',
output: { output: {
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
filename: 'cvat-core.node.js', filename: 'cvat-core.node.js',
library: 'cvat',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
}, },
module: { module: {
@ -22,9 +21,6 @@ const nodeConfig = {
exclude: /node_modules/, exclude: /node_modules/,
}], }],
}, },
externals: {
canvas: 'commonjs canvas',
},
stats: { stats: {
warnings: false, warnings: false,
}, },
@ -50,16 +46,7 @@ const webConfig = {
options: { options: {
presets: [ presets: [
['@babel/preset-env', { ['@babel/preset-env', {
targets: { targets: '> 2.5%', // https://github.com/browserslist/browserslist
chrome: 58,
},
useBuiltIns: 'usage',
corejs: 3,
loose: false,
spec: false,
debug: false,
include: [],
exclude: [],
}], }],
], ],
sourceType: 'unambiguous', sourceType: 'unambiguous',

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

@ -1,9 +1,4 @@
REACT_APP_VERSION=${npm_package_version} // Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
REACT_APP_API_PROTOCOL=http
REACT_APP_API_HOST=localhost
REACT_APP_API_PORT=7000
REACT_APP_API_HOST_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT_APP_API_PORT}
REACT_APP_API_FULL_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT_APP_API_PORT}/api/v1
SKIP_PREFLIGHT_CHECK=true

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

@ -0,0 +1,49 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
module.exports = {
'env': {
'node': true,
'browser': true,
'es6': true,
},
'parserOptions': {
'parser': '@typescript-eslint/parser',
'ecmaVersion': 6,
'project': './tsconfig.json',
},
'plugins': [
'@typescript-eslint',
'import',
],
'ignorePatterns': ['*.svg', '*.scss'],
'extends': [
'plugin:@typescript-eslint/recommended',
'airbnb-typescript',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
],
'rules': {
'@typescript-eslint/indent': ['warn', 4],
'react/jsx-indent': ['warn', 4],
'react/jsx-indent-props': ['warn', 4],
'react/jsx-props-no-spreading': 0,
'jsx-quotes': ['error', 'prefer-single'],
'arrow-parens': ['error', 'always'],
'@typescript-eslint/no-explicit-any': [0],
'@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }],
'no-restricted-syntax': [0, {'selector': 'ForOfStatement'}],
'no-plusplus': [0],
'lines-between-class-members': 0,
'react/no-did-update-set-state': 0, // https://github.com/airbnb/javascript/issues/1875
},
'settings': {
'import/resolver': {
'typescript': {
'directory': './tsconfig.json'
}
},
},
};

20
cvat-ui/.gitignore vendored

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

@ -0,0 +1,5 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
declare module '*.svg';

File diff suppressed because it is too large Load Diff

@ -1,56 +1,74 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "0.1.0", "version": "0.5.2",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
"build": "webpack --config ./webpack.config.js",
"start": "REACT_APP_API_URL=http://localhost:7000 webpack-dev-server --config ./webpack.config.js --mode=development",
"type-check": "tsc --noEmit",
"type-check:watch": "npm run type-check -- --watch",
"lint": "eslint './src/**/*.{ts,tsx}'",
"lint:fix": "eslint './src/**/*.{ts,tsx}' --fix"
},
"author": "Intel",
"license": "MIT", "license": "MIT",
"private": true, "devDependencies": {
"@babel/core": "^7.6.0",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/preset-env": "^7.6.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.6.0",
"@typescript-eslint/eslint-plugin": "^2.19.2",
"@typescript-eslint/parser": "^2.19.2",
"babel-loader": "^8.0.6",
"babel-plugin-import": "^1.12.2",
"css-loader": "^3.2.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-typescript": "^7.0.0",
"eslint-import-resolver-typescript": "^2.0.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"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",
"react-svg-loader": "^3.0.3",
"sass-loader": "^8.0.0",
"style-loader": "^1.0.0",
"tsconfig-paths-webpack-plugin": "^3.2.0",
"typescript": "^3.7.3",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.8",
"webpack-dev-server": "^3.8.0"
},
"dependencies": { "dependencies": {
"@types/jest": "24.0.13", "@types/react": "^16.9.2",
"@types/node": "^12.0.3", "@types/react-dom": "^16.9.0",
"@types/react": "16.8.19", "@types/react-redux": "^7.1.2",
"@types/react-dom": "16.8.4", "@types/react-router": "^5.0.5",
"@types/react-redux": "^7.1.1", "@types/react-router-dom": "^5.1.0",
"@types/react-router-dom": "^4.3.4", "@types/react-share": "^3.0.1",
"@types/redux-logger": "^3.0.7", "@types/redux-logger": "^3.0.7",
"antd": "^3.19.1", "antd": "^3.25.2",
"babel-plugin-import": "^1.11.2", "copy-to-clipboard": "^3.2.0",
"customize-cra": "^0.2.12", "dotenv-webpack": "^1.7.0",
"less": "^3.9.0", "moment": "^2.24.0",
"less-loader": "^5.0.0", "prop-types": "^15.7.2",
"node-sass": "^4.12.0", "react": "^16.9.0",
"query-string": "^6.8.1", "react-dom": "^16.9.0",
"react": "^16.8.6", "react-hotkeys": "^2.0.0",
"react-app-rewired": "^2.1.3", "react-redux": "^7.1.1",
"react-dom": "^16.8.6", "react-router": "^5.1.0",
"react-redux": "^7.1.0", "react-router-dom": "^5.1.0",
"react-router-dom": "^5.0.1", "react-share": "^3.0.1",
"react-scripts": "3.0.1",
"react-scripts-ts": "^3.1.0",
"redux": "^4.0.4", "redux": "^4.0.4",
"redux-devtools-extension": "^2.13.8",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0"
"source-map-explorer": "^1.8.0",
"typescript": "3.4.5"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
} }
} }

@ -0,0 +1,13 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/* eslint-disable */
module.exports = {
parser: false,
plugins: {
'postcss-preset-env': {
browsers: '> 2.5%', // https://github.com/browserslist/browserslist
},
},
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

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

Before

Width:  |  Height:  |  Size: 1.1 KiB

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

Before

Width:  |  Height:  |  Size: 3.3 KiB

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

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

@ -0,0 +1,35 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core';
const core = getCore();
export enum AboutActionTypes {
GET_ABOUT = 'GET_ABOUT',
GET_ABOUT_SUCCESS = 'GET_ABOUT_SUCCESS',
GET_ABOUT_FAILED = 'GET_ABOUT_FAILED',
}
const aboutActions = {
getAbout: () => createAction(AboutActionTypes.GET_ABOUT),
getAboutSuccess: (server: any) => createAction(AboutActionTypes.GET_ABOUT_SUCCESS, { server }),
getAboutFailed: (error: any) => createAction(AboutActionTypes.GET_ABOUT_FAILED, { error }),
};
export type AboutActions = ActionUnion<typeof aboutActions>;
export const getAboutAsync = (): ThunkAction => async (dispatch): Promise<void> => {
dispatch(aboutActions.getAbout());
try {
const about = await core.server.about();
dispatch(
aboutActions.getAboutSuccess(about),
);
} catch (error) {
dispatch(aboutActions.getAboutFailed(error));
}
};

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,99 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core';
const cvat = getCore();
export enum AuthActionTypes {
AUTHORIZED_SUCCESS = 'AUTHORIZED_SUCCESS',
AUTHORIZED_FAILED = 'AUTHORIZED_FAILED',
LOGIN = 'LOGIN',
LOGIN_SUCCESS = 'LOGIN_SUCCESS',
LOGIN_FAILED = 'LOGIN_FAILED',
REGISTER = 'REGISTER',
REGISTER_SUCCESS = 'REGISTER_SUCCESS',
REGISTER_FAILED = 'REGISTER_FAILED',
LOGOUT = 'LOGOUT',
LOGOUT_SUCCESS = 'LOGOUT_SUCCESS',
LOGOUT_FAILED = 'LOGOUT_FAILED',
}
const authActions = {
authorizeSuccess: (user: any) => createAction(AuthActionTypes.AUTHORIZED_SUCCESS, { user }),
authorizeFailed: (error: any) => createAction(AuthActionTypes.AUTHORIZED_FAILED, { error }),
login: () => createAction(AuthActionTypes.LOGIN),
loginSuccess: (user: any) => createAction(AuthActionTypes.LOGIN_SUCCESS, { user }),
loginFailed: (error: any) => createAction(AuthActionTypes.LOGIN_FAILED, { error }),
register: () => createAction(AuthActionTypes.REGISTER),
registerSuccess: (user: any) => createAction(AuthActionTypes.REGISTER_SUCCESS, { user }),
registerFailed: (error: any) => createAction(AuthActionTypes.REGISTER_FAILED, { error }),
logout: () => createAction(AuthActionTypes.LOGOUT),
logoutSuccess: () => createAction(AuthActionTypes.LOGOUT_SUCCESS),
logoutFailed: (error: any) => createAction(AuthActionTypes.LOGOUT_FAILED, { error }),
};
export type AuthActions = ActionUnion<typeof authActions>;
export const registerAsync = (
username: string,
firstName: string,
lastName: string,
email: string,
password1: string,
password2: string,
): ThunkAction => async (
dispatch,
) => {
dispatch(authActions.register());
try {
await cvat.server.register(username, firstName, lastName, email, password1, password2);
const users = await cvat.users.get({ self: true });
dispatch(authActions.registerSuccess(users[0]));
} catch (error) {
dispatch(authActions.registerFailed(error));
}
};
export const loginAsync = (username: string, password: string): ThunkAction => async (dispatch) => {
dispatch(authActions.login());
try {
await cvat.server.login(username, password);
const users = await cvat.users.get({ self: true });
dispatch(authActions.loginSuccess(users[0]));
} catch (error) {
dispatch(authActions.loginFailed(error));
}
};
export const logoutAsync = (): ThunkAction => async (dispatch) => {
dispatch(authActions.logout());
try {
await cvat.server.logout();
dispatch(authActions.logoutSuccess());
} catch (error) {
dispatch(authActions.logoutFailed(error));
}
};
export const authorizedAsync = (): ThunkAction => async (dispatch) => {
try {
const result = await cvat.server.authorized();
if (result) {
const userInstance = (await cvat.users.get({ self: true }))[0];
dispatch(authActions.authorizeSuccess(userInstance));
} else {
dispatch(authActions.authorizeSuccess(null));
}
} catch (error) {
dispatch(authActions.authorizeFailed(error));
}
};

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

@ -0,0 +1,48 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core';
const cvat = getCore();
export enum FormatsActionTypes {
GET_FORMATS = 'GET_FORMATS',
GET_FORMATS_SUCCESS = 'GET_FORMATS_SUCCESS',
GET_FORMATS_FAILED = 'GET_FORMATS_FAILED',
}
const formatsActions = {
getFormats: () => createAction(FormatsActionTypes.GET_FORMATS),
getFormatsSuccess: (annotationFormats: any[], datasetFormats: any[]) => (
createAction(FormatsActionTypes.GET_FORMATS_SUCCESS, {
annotationFormats,
datasetFormats,
})
),
getFormatsFailed: (error: any) => (
createAction(FormatsActionTypes.GET_FORMATS_FAILED, { error })
),
};
export type FormatsActions = ActionUnion<typeof formatsActions>;
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),
);
} catch (error) {
dispatch(formatsActions.getFormatsFailed(error));
}
};
}

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

@ -0,0 +1,28 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { AnyAction } from 'redux';
export enum NotificationsActionType {
RESET_ERRORS = 'RESET_ERRORS',
RESET_MESSAGES = 'RESET_MESSAGES',
}
export function resetErrors(): AnyAction {
const action = {
type: NotificationsActionType.RESET_ERRORS,
payload: {},
};
return action;
}
export function resetMessages(): AnyAction {
const action = {
type: NotificationsActionType.RESET_MESSAGES,
payload: {},
};
return action;
}

@ -0,0 +1,55 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { SupportedPlugins } from 'reducers/interfaces';
import PluginChecker from 'utils/plugin-checker';
export enum PluginsActionTypes {
CHECK_PLUGINS = 'CHECK_PLUGINS',
CHECKED_ALL_PLUGINS = 'CHECKED_ALL_PLUGINS'
}
type PluginObjects = Record<SupportedPlugins, boolean>;
const pluginActions = {
checkPlugins: () => createAction(PluginsActionTypes.CHECK_PLUGINS),
checkedAllPlugins: (list: PluginObjects) => (
createAction(PluginsActionTypes.CHECKED_ALL_PLUGINS, {
list,
})
),
};
export type PluginActions = ActionUnion<typeof pluginActions>;
export function checkPluginsAsync(): ThunkAction {
return async (dispatch): Promise<void> => {
dispatch(pluginActions.checkPlugins());
const plugins: PluginObjects = {
ANALYTICS: false,
AUTO_ANNOTATION: false,
GIT_INTEGRATION: false,
TF_ANNOTATION: false,
TF_SEGMENTATION: false,
};
const promises: Promise<boolean>[] = [
PluginChecker.check(SupportedPlugins.ANALYTICS),
PluginChecker.check(SupportedPlugins.AUTO_ANNOTATION),
PluginChecker.check(SupportedPlugins.GIT_INTEGRATION),
PluginChecker.check(SupportedPlugins.TF_ANNOTATION),
PluginChecker.check(SupportedPlugins.TF_SEGMENTATION),
];
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;
dispatch(pluginActions.checkedAllPlugins(plugins));
};
}

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

@ -0,0 +1,202 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { AnyAction } from 'redux';
import {
GridColor,
ColorBy,
} from 'reducers/interfaces';
export enum SettingsActionTypes {
SWITCH_ROTATE_ALL = 'SWITCH_ROTATE_ALL',
SWITCH_GRID = 'SWITCH_GRID',
CHANGE_GRID_SIZE = 'CHANGE_GRID_SIZE',
CHANGE_GRID_COLOR = 'CHANGE_GRID_COLOR',
CHANGE_GRID_OPACITY = 'CHANGE_GRID_OPACITY',
CHANGE_SHAPES_OPACITY = 'CHANGE_SHAPES_OPACITY',
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_FRAME_STEP = 'CHANGE_FRAME_STEP',
CHANGE_FRAME_SPEED = 'CHANGE_FRAME_SPEED',
SWITCH_RESET_ZOOM = 'SWITCH_RESET_ZOOM',
CHANGE_BRIGHTNESS_LEVEL = 'CHANGE_BRIGHTNESS_LEVEL',
CHANGE_CONTRAST_LEVEL = 'CHANGE_CONTRAST_LEVEL',
CHANGE_SATURATION_LEVEL = 'CHANGE_SATURATION_LEVEL',
SWITCH_AUTO_SAVE = 'SWITCH_AUTO_SAVE',
CHANGE_AUTO_SAVE_INTERVAL = 'CHANGE_AUTO_SAVE_INTERVAL',
CHANGE_AAM_ZOOM_MARGIN = 'CHANGE_AAM_ZOOM_MARGIN',
SWITCH_SHOWNIG_INTERPOLATED_TRACKS = 'SWITCH_SHOWNIG_INTERPOLATED_TRACKS',
}
export function changeShapesOpacity(opacity: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SHAPES_OPACITY,
payload: {
opacity,
},
};
}
export function changeSelectedShapesOpacity(selectedOpacity: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SELECTED_SHAPES_OPACITY,
payload: {
selectedOpacity,
},
};
}
export function changeShapesColorBy(colorBy: ColorBy): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SHAPES_COLOR_BY,
payload: {
colorBy,
},
};
}
export function changeShapesBlackBorders(blackBorders: boolean): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SHAPES_BLACK_BORDERS,
payload: {
blackBorders,
},
};
}
export function switchRotateAll(rotateAll: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_ROTATE_ALL,
payload: {
rotateAll,
},
};
}
export function switchGrid(grid: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_GRID,
payload: {
grid,
},
};
}
export function changeGridSize(gridSize: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_GRID_SIZE,
payload: {
gridSize,
},
};
}
export function changeGridColor(gridColor: GridColor): AnyAction {
return {
type: SettingsActionTypes.CHANGE_GRID_COLOR,
payload: {
gridColor,
},
};
}
export function changeGridOpacity(gridOpacity: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_GRID_OPACITY,
payload: {
gridOpacity,
},
};
}
export function changeFrameStep(frameStep: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_FRAME_STEP,
payload: {
frameStep,
},
};
}
export function changeFrameSpeed(frameSpeed: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_FRAME_SPEED,
payload: {
frameSpeed,
},
};
}
export function switchResetZoom(resetZoom: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_RESET_ZOOM,
payload: {
resetZoom,
},
};
}
export function changeBrightnessLevel(level: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_BRIGHTNESS_LEVEL,
payload: {
level,
},
};
}
export function changeContrastLevel(level: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_CONTRAST_LEVEL,
payload: {
level,
},
};
}
export function changeSaturationLevel(level: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SATURATION_LEVEL,
payload: {
level,
},
};
}
export function switchAutoSave(autoSave: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_AUTO_SAVE,
payload: {
autoSave,
},
};
}
export function changeAutoSaveInterval(autoSaveInterval: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_AUTO_SAVE_INTERVAL,
payload: {
autoSaveInterval,
},
};
}
export function changeAAMZoomMargin(aamZoomMargin: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_AAM_ZOOM_MARGIN,
payload: {
aamZoomMargin,
},
};
}
export function switchShowingInterpolatedTracks(showAllInterpolationTracks: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_SHOWNIG_INTERPOLATED_TRACKS,
payload: {
showAllInterpolationTracks,
},
};
}

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

Loading…
Cancel
Save