Release 1.0.0 (#1335)
* Added button to cancel started automatic annotation (#1198) * [WIP] Cuboid feature user guide (#1218) * Initial cuboid description * Added Gifs * Added gifs to descriptions * Formatting fixes * Codacy Fixes * Az/fix annotation dump upload (#1229) * fixed upload annotation in case of frame step != 1 * fixed upload annotation in case of attribute value is empty * Setup tag forward to the state * React UI: Added shortcuts (#1230) * [Datumaro] Label remapping transform (#1233) * Add label remapping transform * Apply transforms before project saving * Refactor voc converter * [Datumaro] Optimize mask operations (#1232) * Optimize mask to rle * Optimize mask operations * Fix dm format cmdline * Use RLE masks in datumaro format * Added tag support in new UI (without canvas drawing) * merge fix * Fixed copying/pasting actions * Deleted unused objects * [Datumaro] Dataset format auto detection (#1242) * Add dataset format detection * Add auto format detection for import * Split VOC extractor * Some debian package manager tweaks (#1235) * Some debian package manager tweaks By default, Ubuntu or Debian based "apt" or "apt-get" system installs recommended but not suggested packages . By passing "--no-install-recommends" option, the user lets apt-get know not to consider recommended packages as a dependency to install. This results in smaller downloads and installation of packages . Refer to blog at [Ubuntu Blog](https://ubuntu.com/blog/we-reduced-our-docker-images-by-60-with-no-install-recommends) . * doc: fix description of attribute CVAT XML format (#1168) * Fixed security issues in Datumaro (#1244) * Fixed security issues reported by bandit. * Fixed voc_format extractor * Sorted requirements, added a comment, removed nosec for exec. * Fix copying and creating tags * fixed git sync app (#1247) * fixed git sync app * removed shell=True for subprocess call * Fixed tags color changing and hiding * Fixed filters with tags * wip * PR fixed * Styles fixed * PR fixed * Update develop from release-0.6.0 branch (#1266) * temp * React UI: Attribute annotation mode (#1255) * Done main work * Fixed mount/unmount for canvas wrapper * Refactoring, added filters * Added missed file * Removed unnecessary useEffect * Removed extra code * Max 9 attributes, inputNumber -> Input in aam * Added blur * Renamed component * Fixed condition when validate number attribute * Some minor fixes * Fixed hotkeys config * Fixed canvas zoom * Improved behaviour of number & text * Fixed attributes switching order * Fix tags * Fixed interval * Installation issues for development environment (#1280) * Installation issues * Added ffmpeg * Bump acorn from 6.3.0 to 6.4.1 in /cvat-ui (#1270) * Bump acorn from 6.3.0 to 6.4.1 in /cvat-ui Bumps [acorn](https://github.com/acornjs/acorn) from 6.3.0 to 6.4.1. - [Release notes](https://github.com/acornjs/acorn/releases) - [Commits](https://github.com/acornjs/acorn/compare/6.3.0...6.4.1) Signed-off-by: dependabot[bot] <support@github.com> * Updated CHANGELOG.md Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Boris Sekachev <boris.sekachev@yandex.ru> * Bump acorn from 6.2.1 to 6.4.1 in /cvat-canvas (#1281) Bumps [acorn](https://github.com/acornjs/acorn) from 6.2.1 to 6.4.1. - [Release notes](https://github.com/acornjs/acorn/releases) - [Commits](https://github.com/acornjs/acorn/compare/6.2.1...6.4.1) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Use source label map for voc export (#1276) * Use source label map for voc export * Add line to changelog * [Datumaro] Fix frame matching in video annotations import (#1274) * Add extra frame matching way for videos * Add line to changelog * [Datumaro] Allow empty COCO dataset export (#1272) * Allow empty dataset export in coco * Add line to changelog Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * [Datumaro] Fix occluded and z_order attributes export (#1271) * Fix occluded and z_order attributes export * Add line to changelog Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * Fix LabelMe format (#1260) * Fix labelme filenames * Change module path * Add tests for LabelMe * Update test * Fix test * Add line in changelog * Added point deletion context menu * React UI: Added logging (#1288) * OpenVino 2020 (#1269) * added support for OpenVINO 2020 * fixed dextr and tf_annotation Co-authored-by: Andrey Zhavoronkov <andrey.zhavoronkov@intel.com> * fixed point context menu for rectangles * Add recursive importers (#1290) * [Datumaro] MOT format (#1289) * Add mot format base * Add mot format * Extract common code * [Datumaro] LabelMe format (#1293) * Little refactoring * Add LabelMe format * [Datumaro] Update LabelMe format (#1296) * Little refactoring * Add LabelMe format * Add usernames * Update tests * Add extractor test * Add information about v0.6.1 release. * React UI: Better exception handling (#1297) * Fixed context menu on ubuntu * Fixed deleting of the latest point * fixes * Fix attributes with spaces in names (#1305) * fixed PR * [Datumaro] Fix image merging (#1301) * Always merge images for own dataset * Fix codacy * Validation for frame input value * Fixed UI fail when write characters in auto save interval input * Fixed input numbers in player settings * Fixed ui failing in propagate confirmation * Fixed latest input numbers, removed extra code, fixed typings * Fix navigation * Added undopoint in editing * Fixed: Could not receive frame (after merge on the latest frame) * Removed extra action dispatching * Which -> button property * Fixed: Inconsistent labels between UI and CLI/API * Fixed resize on right mouse button * Fixed create object URL after first save, fixed URL itself * Undo/redo returns frame where was a change (as it was done in previous version) * Fixed unit tests * [Datumaro] Extract common extractor functionality (#1319) * Extract common extractor functionality * Simplify coco extractor * Fix tfrecord * Fix AWS deployment (#1316) * Don't use antd less (big memory consumtion during the build process) * Fix AWS deployment guide * fix a problem with proxy and long domain names * remove sass loader for antd * Removed less and less-loader. * Simplified webpack config. * Data streaming using chunks (#1007) Huge feature (200+ commits from different developers). It completely changes layout of data (please expect very long DB migration process if you have a lot of tasks). The primary idea is to send data as zip chunks (e.g. 36 images in one chunk) or encoded video chunks and decode them on the client side. It helps to solve the problem with latency when you try to view a separate frame in the UI quickly (play mode). Another important feature of the patch is to provide access to the original images. Thus for annotations the client uses compressed chunks but if you want to export a dataset Datumaro will use original chunks (but video will be decoded with original quality and encoded with maximum/optimal quality in any case). * Shortcuts keymaps moved to state * Titles for objects in side menu * Fixed bug in menu * Titles in attribute annotations mode * Controls panel titles * Titles for object list header * Minor fixes * Added tooltips in top bar * Added settings tooltip * Optimized patch * Typos * Fix a problem with known hosts inside git app (cannot clone a repo from github.com) (#1330) * Fixed zOrder range computing in case when there are tags * Small preview and progress (#1331) * Reduce preview size (untested) * Fix tests * Improve media readers (untested) * fixed migration * fixed frame provider * fixed preview save * fixed stop frame * handle duration == None * codacy * added missed import * unified iteration over frames for media readers and fixed corner case when user specify stop_frame = 0 Co-authored-by: Nikita Manovich <nikita.manovich@intel.com> * Move annotation formats to dataset manager (#1256) * Move formats to dataset manager * Unify datataset export and anno export implementations * Add track_id to TrackedShape, export tracked shapes * Replace MOT format * Replace LabelMe format * Add new formats to dm * Add dm tests * Extend TrackedShape * Enable dm test in CI * Fix tests * Add import * Fix tests * Fix mot track ids * Fix mot format * Update attribute logic in labelme tests * Use common code in yolo * Put datumaro in path in settings * Expect labels file in MOT next to annotations file * Add MOT format description * Add import * Add labelme format description * Linter fix * Linter fix2 * Compare attributes ordered * Update docs * Update tests * Az/fix migration (#1333) * fixed migration because readers interface changed * fixed tests * Fixed escape in draw * Insert multiple shapes * Added dialog window with some help info about filters * Typos in doc * Add missing information into changelog. * Add information for next release + update version. * Updated changelog * Updated changelog * Special behaviour for the attribute value __undefined__ * Fixed license year * No break space const * Updated changelog * Fixed year in license headers * Implementation of bitmap in client * Updated changelog * Z-layer support * Fixed settings after reopen a job * Do not show invisible objects on bitmap * Fix point interpolation (#1344) * Extend formats tests with different track types * Add unordered list comparison * Skip empty list comparison * fix * fix * Reproduce problem * Fix point interpolation for single point * undo rest api refactor * Added button to reset color settings * Updated changelog * Added option to display shape text always * Updated changelog * Hidden/outside fix * Fixed screen scaling * fixed dump error after moving format files (#1342) * fixed dump error after moving format files * updated changelog * Az/fix dextr (#1348) * Fixed dextr_segmentation app * updated changelog Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * Disabled option by default * Fixed typos in interface * Increase preview size till 256, 256. Previous preview size was not optimal and led to a blurred image due to too small size. * Add a line into CHANGELOG.md about the change. * Refactor frame provider (#1355) * Refactor frame provider * fix * Add CODEOWNERS file (#1360) * Add CODEOWNERS file * Removed the outdated file. See https://github.com/opencv/cvat/graphs/contributors * Change codeowners * Add pull request and issue templates (#1359) * Add initial version of pull request template * Fix links * Fix codacy issues * Slightly improve titles of sections * Add a note about strikethough for the checklist. * Fix progress of a pull request (each checkbox is an issue) * Add the license header, checkboxes about the license. * Updated the license * Update the license to met https://github.com/licensee/licensee/blob/master/vendor/choosealicense.com/_licenses/mit.txt restrictions. * Fix the pull request template name * Make explaination text as comments (it will be visible when you edit the PR message) * Add initial version of the issue template. * Batch of fixes (#1370) * Some margins were change to paddings * Removed extra selected * Fix: added outside shapes when merge polyshapes * Fixed double scroll bars * Updated canvas table * Fixed setup methodf * Disabled change frame during drag, resize and editing * Fixed: hidden points are visible * Fixed: Merge is allowed for points, but clicks on points conflict with frame dragging logic * Fixed: do not filter removed objects * Updated CHANGELOG.md * Couple of headers updated * Added missed fields in exception logs (#1372) * Simplified codeowners file (#1376) * Fixed points visibility when go between frames * React UI: Added message when share is empty or not mounted (#1373) * Added message when share is empty or not mounted * Updated changelog * Update CHANGELOG.md Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * Save full image paths in coco (#1381) * Add chunk iterator cache to frame provider (#1367) * Add chunk iterator cache * fix * Update item "Creating an annotation task" in User Guide (#1363) * Az/fix remote files (#1392) * fixed task creation from remote files * Update CHANGELOG.md * [Datumaro] Fix COCO keypoint export bug (#1388) * Instructions on using HTTPS (#1357) * Fix label comparison in voc format (#1382) * Update item "Interface of the annotation tool" in User Guide (#1386) * React UI: Batch of fixes (#1383) * Fixed: cannot read property 'set' of undefined * Fixed UI failing: save during drag/resize * Fixed multiple saving (shortcut sticking) * Undo/redo fixed * Allowed one interpolated point * Fixed API reaction when repository synchronization is failed * Updated changelog * Update item "Basic navigation" in User Guide and bug fix in user_guide.md (#1395) * Fix git app paths (#1400) * changed paths for the git repos * Update CHANGELOG.md * updated licence header * React UI: Displaying public ssh keys in UI (#1375) * Updated changelog * Typos * Batch of fixes (#1403) * Fixed bug when job cannot be opened * Fixed bug when deactivated shape is still highlighted * Fixed Error: 'AttributeError: 'tuple' object has no attribute 'read' * Fixed: wrong semi-automatic segmentation near edges of an image * Updated changelog Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * React UI: Semi-automatic segmentation (#1398) * implemented checked * Implemented plugin * Added dialog windows * Updated changelo * Added cancel request * React UI: Automatic bordering for polygons and polylines during drawing/editing (#1394) * Fixed: cannot read property 'set' of undefined * Fixed UI failing: save during drag/resize * Fixed multiple saving (shortcut sticking) * Undo/redo fixed * Allowed one interpolated point * Fixed API reaction when repository synchronization is failed * Updated changelog * Auto bordering feature * Some fixes, added shortcuts * Fixed draw when start with one of supporting point * React UI: batch of fixes (#1404) * React UI has become a primary UI * Temporary disabled cuboid in cvat-core * Fixed: Failed to execute removChild.. (#1405) * Update CHANGELOG.md * Updated CHANGELOG (new version will be 1.0.0-beta.1) * Update CHANGELOG.md (next version is 1.0.0-beta.2) * Updated version of CVAT server till beta.2 * Fixed auto annotation, tf annotation and auto segmentation apps (#1409) * fixed code that uses FrameProvider, as the interface has changed * Update CHANGELOG.md * Update item "Types of shapes" (#1401) * React UI: ReID algorithm (#1406) * Initial commit * Connected storage * Added core API method * Done implementation * Removed rule * Removed double cancel * Updated changelog * Fixed: Cannot read property toFixed of undefined * Update CHANGELOG.md * Improve PR template (#1427) * Simplified PR template. * Remove a new line to make codacy happy. * fix: OSError:broken data stream (#1430) * [Datumaro] Fix duplicating keypoints in COCO export (#1435) * React UI: Fixed typos in remove annotations confirmation (#1450) * React UI: Batch of fixes (#1445) * Hide functionality (H) doesn't work * The highlighted attribute doesn't correspond to the chosen attribute in AAM * Inconvinient image shaking while drawing a polygon (hold Alt key during drawing/editing/grouping to drag an image) * Filter property "shape" doesn't work and extra operator in description * Block of text information doesn't disappear after deactivating for locked shapes * Annotation uploading fails in annotation view * UI freezes after canceling pasting with escape Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * Update item "Annotation mode (basics)" in User Guide (#1412) * Update item "annotation mode (basics)" in User Guide * Replacing a gif017 with an image * React UI: Added client versioning (#1448) * Adjusted antd import * Wrapped core and canvas * Added versioning * Updated changelog, adjusted installation guide a bit * Update item "Interpolation mode (basics)" in User Guide (#1455) * Update item interpolation mode in user guide fix typos and contents * Fix a typo * Fix a typo in the filename Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * React UI: batch of fixes (#1462) * CVAT new UI: add arrows on a mouse cursor * Delete point bug (in new UI) * fix auto annotation to not eat all RAM (#1328) Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * Fixed versioning for node 8 (#1466) * Update items from AAM (basics) to Vocabulary in User Guide (#1459) * Update items from AAM (basics) to Vocabulary * Delete unused images and gif * Fix apache startup (#1467) * Fix Network Error after PC Restart (#1035) * Update CHANGELOG.md * Remove deprecated utils (#1477) * removed deprecated convert scripts * updated changelog * Fixed 'Open task' button doesn't work (#1474) * Fixed uploading track annotations for multi-segment tasks (#1396) * fixed uploading annotation for overlapped segments * fixed dump of tracks for multisegment task * Update CHANGELOG.md * fixed comments * fixed comments * Update cvat/apps/engine/data_manager.py Co-Authored-By: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * drop start shapes with outside==True for splitted track * code cleanup * fixed typo * fix Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * React UI: cuboids (#1451) * Update changelog. * Update CHANGELOG and the version of CVAT server. * session.annotations.put() returns indexes of added objects (#1493) * session.annotations.put() returns indexes of added objects * Updated changelog * Updated README.md files (added info about versioning) (#1490) * Updated README.md files (added info about versioning) * Typos * Add coverage for python (#1483) * Fix coverage merging (#1504) * Updating instructions to serve Swagger documentation (#1502) * Update items Workspace and Types of shapes in User Guide (#1497) * Merge annotations and dataset_manager apps (#1352) * Fix coverage measurement (#1516) * fixed linter issues and store credentials cookie in the session object (#1526) * fixed issues * fixed issues and stored credentials cookies inside the session * fixed tests * React UI: batch of fixes (#1525) * Update item Settings in User Guide (#1508) * React UI: cookie policy drawer (#1511) * fixed linter issues (#1538) * fixed false tag activation (#1541) * Added item with npm package version increasing to the PR template (#1542) * [Datumaro] Fix coco import conflict with labels (#1548) * [Datumaro] Change alignment in mask parsing (#1547) * Include empty images in exported annotations (#1479) * Update item Bottom panel in User Guide (#1509) * Update item Bottom panel in User Guide rename the "Bottom panel" to the "Top panel" * Fixed typos, remove trailing spaces in user_guide.md * Fix user_guide.md and update image051.jpg * Ability to configure user agreements for the register user form (#1464) * Update item Side panel in User guide (#1513) * Update item Side panel in User guide * Removed trailing spaces in an Object sidebar item * Update cvat/apps/documentation/user_guide.md * fix typo * missing image correction Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * Layout styles fixes * React UI: cuboid interpolation and cuboid drawing from rectangles (#1560) * Added backend cuboid interpolation and cuboid drawing from rectangles * Added CHANELOG.md * Fixed cuboid front edges stroke width * PR fixes * Fixed auto_segmentation app (#1562) * disabled tf eager execution for auto_segmentation * Update CHANGELOG.md * Add VOC grayscale masks test and documentation (#1576) * Add a test for unpainted masks * Update format documentation * [Datumaro] Fix mask to polygons warning (#1581) * Fix message, add test * update changelog * Fix cuboid conversion (#1577) * Fix cuboid conversion * update changelog Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * [Datumaro] Simplify log level setting (#1583) * Simplify loglevel setting * update changelog * Fixed git synchronization (#1582) * fixed git synchronization * Update CHANGELOG.md Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * Fixed dextr, fixed moving of the canvas (#1573) * Fixed dextr, fixed moving of the canvas * Updated CONTRIBUTUNG.md, updated version Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * Added the ability to configure custom pageViewHit (may useful for web analytics) (#1566) * added the ability to configure custom pageViewHit (may useful for web analytics) * updated version and changelog * fixed comments * cvat-ui minor v++ * subscribe on history updates in the root component * updated Online Demo section of Readme (#1588) * updated Online Demo section of Readme * Change content of "online demo" section Co-authored-by: Nikita Manovich <nikita.manovich@intel.com> * Fixed task creation for videos with uneven dimensions. (#1594) * used yuv420p format for compressed and original chunks * updated changelog * added settings to reduce access to analytics component (#1592) * added settings to reduce access to analytics component * updated CHANGELOG * fixed typo * Add item Controls sidebar in User Guide (#1510) * Update text, images and gif in user_guide.md (#1558) * Update user_guide.md, images and gif (#1556) Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * update user_guide.md, images and gifs * Delete gif013 * add gif013 with correct name * Use z_order as a class property (#1589) * Use z_order as a class property * Fix z_order use in voc * Update changelog * Update item Annotation with polylines (#1596) * update user_guide.md and images * fix uppercase letters in images path in user_guide.md and remove trailing spaces * delete images and gifs containing uppercase letters in the name * add images with correct names * fix image paths in user_guide.md * Delete image133 * add image133 with correct name * Fix example yaml format (#1603) * delete duplicate item in user_guide.md (#1607) * update user_guide.md and add image (#1604) * update gifs, images and user_guide.md (#1605) * fix analytics permissions (#1608) * Update item Annotation with cuboids (#1598) * Update item Annotation with polygons in User guide v2 (#1612) * update user_guide.md * update images and gifs * Update Filter, Analytics and Shortcuts items in User Guide (#1606) * update user_guide.md and images * fix user_guide.md * delete unused image * delete unused images * Fix duplicate item in User Guide (#1617) * remove item Annotation with Auto Segmentation * fix link in user_guide.md * delete unused images * Slightly improve changelog * Update CVAT version Co-authored-by: Boris Sekachev <40690378+bsekachev@users.noreply.github.com> Co-authored-by: Tritin Truong <truongtritin98@gmail.com> Co-authored-by: Andrey Zhavoronkov <41117609+azhavoro@users.noreply.github.com> Co-authored-by: Dmitry Kalinin <dmitry.kalinin@intel.com> Co-authored-by: zhiltsov-max <zhiltsov.max35@gmail.com> Co-authored-by: Pratik Raj <Rajpratik71@gmail.com> Co-authored-by: Mathis Chenuet <artdevelopp@hotmail.fr> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Boris Sekachev <boris.sekachev@yandex.ru> Co-authored-by: Ben Hoff <hoff.benjamin.k@gmail.com> Co-authored-by: Andrey Zhavoronkov <andrey.zhavoronkov@intel.com> Co-authored-by: TOsmanov <54434686+TOsmanov@users.noreply.github.com> Co-authored-by: ranko r sredojevic <radoye@users.noreply.github.com> Co-authored-by: Thomas Albrecht <thomas.albrecht@gmx.net> Co-authored-by: Johannes222 <johannes.halaoui@alumni.fh-aachen.de> Co-authored-by: Gururaj Jeerge <gururaj@orangepro.in> Co-authored-by: timurx.osmanov <timurx.osmanov@intel.com> Co-authored-by: YutaYamazaki <37947061+yutayamazaki@users.noreply.github.com>main
parent
034268e87d
commit
07de7141ed
@ -1,4 +1,5 @@
|
||||
exclude_paths:
|
||||
- '**/3rdparty/**'
|
||||
- '**/engine/js/cvat-core.min.js'
|
||||
- '**/engine/js/unzip_imgs.js'
|
||||
- CHANGELOG.md
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
[run]
|
||||
branch = true
|
||||
# relative_files = true # does not work?
|
||||
|
||||
source =
|
||||
datumaro/datumaro/
|
||||
cvat/apps/
|
||||
utils/cli/
|
||||
|
||||
omit =
|
||||
datumaro/datumaro/__main__.py
|
||||
datumaro/datumaro/version.py
|
||||
cvat/settings/*
|
||||
*/tests/*
|
||||
*/test_*
|
||||
*/_test_*
|
||||
*/migrations/*
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
exclude_lines =
|
||||
# Have to re-enable the standard pragma
|
||||
pragma: no cover
|
||||
|
||||
# Don't complain about missing debug-only code:
|
||||
def __repr__
|
||||
if\s+[\w\.()]+\.isEnabledFor\(log\.DEBUG\):
|
||||
|
||||
# Don't complain if tests don't hit defensive assertion code:
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
if 0:
|
||||
if __name__ == .__main__.:
|
||||
|
||||
# don't fail on the code that can be found
|
||||
ignore_errors = true
|
||||
|
||||
skip_empty = true
|
||||
@ -0,0 +1,39 @@
|
||||
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
|
||||
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence, they will
|
||||
# be requested for review when someone opens a pull request.
|
||||
* @nmanovic
|
||||
|
||||
# Order is important; the last matching pattern takes the most
|
||||
# precedence. When someone opens a pull request that only
|
||||
# modifies components below, only the list of owners and not
|
||||
# the global owner(s) will be requested for a review.
|
||||
|
||||
# Component: Server
|
||||
/cvat/ @nmanovic
|
||||
|
||||
# Component: CVAT UI
|
||||
/cvat-ui/ @bsekachev
|
||||
/cvat-data/ @azhavoro
|
||||
/cvat-canvas/ @bsekachev
|
||||
/cvat-core/ @bsekachev
|
||||
|
||||
# Component: Datumaro
|
||||
/datumaro/ @zhiltsov-max
|
||||
/cvat/apps/dataset_manager/ @zhiltsov-max
|
||||
|
||||
# Advanced components (e.g. OpenVINO)
|
||||
/components/ @azhavoro
|
||||
|
||||
# Infrastructure
|
||||
Dockerfile* @azhavoro
|
||||
docker-compose* @azhavoro
|
||||
.* @azhavoro
|
||||
*.conf @azhavoro
|
||||
*.sh @azhavoro
|
||||
/cvat_proxy/ @azhavoro
|
||||
/tests/ @azhavoro
|
||||
/utils/ @azhavoro
|
||||
/LICENSE @nmanovic
|
||||
/.github/ @nmanovic
|
||||
@ -1,38 +0,0 @@
|
||||
# Core support team
|
||||
- **[Nikita Manovich](https://github.com/nmanovic)**
|
||||
|
||||
* Project lead
|
||||
* Developer
|
||||
* Author and maintainer
|
||||
|
||||
- **[Boris Sekachev](https://github.com/bsekachev)**
|
||||
|
||||
* Primary developer
|
||||
* Author and maintainer
|
||||
|
||||
- **[Andrey Zhavoronkov](https://github.com/azhavoro)**
|
||||
|
||||
* Developer
|
||||
* Author and maintainer
|
||||
|
||||
# Contributors
|
||||
|
||||
- **[Victor Salimonov](https://github.com/VikTorSalimonov)**
|
||||
|
||||
* Documentation, screencasts
|
||||
|
||||
- **[Dmitry Sidnev](https://github.com/DmitriySidnev)**
|
||||
|
||||
* [convert_to_coco.py](utils/coco) - an utility for converting annotation from CVAT to COCO data annotation format
|
||||
|
||||
- **[Sebastián Yonekura](https://github.com/syonekura)**
|
||||
|
||||
* [convert_to_voc.py](utils/voc) - an utility for converting CVAT XML to PASCAL VOC data annotation format.
|
||||
|
||||
- **[ITLab Team](https://github.com/itlab-vision/cvat):**
|
||||
**[Vasily Danilin](https://github.com/DanVev)**,
|
||||
**[Eugene Shashkin](https://github.com/EvgenyShashkin)**,
|
||||
**[Dmitry Silenko](https://github.com/DimaSilenko)**,
|
||||
**[Alina Bykovskaya](https://github.com/alinaut)**,
|
||||
**[Yanina Koltushkina](https://github.com/YaniKolt)**
|
||||
* Integrating CI tools as Travis CI, Codacy and Coveralls.io
|
||||
@ -0,0 +1,301 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import * as SVG from 'svg.js';
|
||||
|
||||
import consts from './consts';
|
||||
import { Geometry } from './canvasModel';
|
||||
|
||||
interface TransformedShape {
|
||||
points: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface AutoborderHandler {
|
||||
autoborder(enabled: boolean, currentShape?: SVG.Shape, ignoreCurrent?: boolean): void;
|
||||
transform(geometry: Geometry): void;
|
||||
updateObjects(): void;
|
||||
}
|
||||
|
||||
export class AutoborderHandlerImpl implements AutoborderHandler {
|
||||
private currentShape: SVG.Shape | null;
|
||||
private ignoreCurrent: boolean;
|
||||
private frameContent: SVGSVGElement;
|
||||
private enabled: boolean;
|
||||
private scale: number;
|
||||
private groups: SVGGElement[];
|
||||
private auxiliaryGroupID: number | null;
|
||||
private auxiliaryClicks: number[];
|
||||
private listeners: Record<number, Record<number, {
|
||||
click: (event: MouseEvent) => void;
|
||||
dblclick: (event: MouseEvent) => void;
|
||||
}>>;
|
||||
|
||||
public constructor(frameContent: SVGSVGElement) {
|
||||
this.frameContent = frameContent;
|
||||
this.ignoreCurrent = false;
|
||||
this.currentShape = null;
|
||||
this.enabled = false;
|
||||
this.scale = 1;
|
||||
this.groups = [];
|
||||
this.auxiliaryGroupID = null;
|
||||
this.auxiliaryClicks = [];
|
||||
this.listeners = {};
|
||||
}
|
||||
|
||||
private removeMarkers(): void {
|
||||
this.groups.forEach((group: SVGGElement): void => {
|
||||
const groupID = group.dataset.groupId;
|
||||
Array.from(group.children)
|
||||
.forEach((circle: SVGCircleElement, pointID: number): void => {
|
||||
circle.removeEventListener('click', this.listeners[+groupID][pointID].click);
|
||||
circle.removeEventListener('dblclick', this.listeners[+groupID][pointID].click);
|
||||
circle.remove();
|
||||
});
|
||||
|
||||
group.remove();
|
||||
});
|
||||
|
||||
this.groups = [];
|
||||
this.auxiliaryGroupID = null;
|
||||
this.auxiliaryClicks = [];
|
||||
this.listeners = {};
|
||||
}
|
||||
|
||||
private release(): void {
|
||||
this.removeMarkers();
|
||||
this.enabled = false;
|
||||
this.currentShape = null;
|
||||
}
|
||||
|
||||
private addPointToCurrentShape(x: number, y: number): void {
|
||||
const array: number[][] = (this.currentShape as any).array().valueOf();
|
||||
array.pop();
|
||||
|
||||
// need to append twice (specific of the library)
|
||||
array.push([x, y]);
|
||||
array.push([x, y]);
|
||||
|
||||
const paintHandler = this.currentShape.remember('_paintHandler');
|
||||
paintHandler.drawCircles();
|
||||
paintHandler.set.members.forEach((el: SVG.Circle): void => {
|
||||
el.attr('stroke-width', 1 / this.scale).attr('r', 2.5 / this.scale);
|
||||
});
|
||||
(this.currentShape as any).plot(array);
|
||||
}
|
||||
|
||||
private resetAuxiliaryShape(): void {
|
||||
if (this.auxiliaryGroupID !== null) {
|
||||
while (this.auxiliaryClicks.length > 0) {
|
||||
const resetID = this.auxiliaryClicks.pop();
|
||||
this.groups[this.auxiliaryGroupID]
|
||||
.children[resetID].classList.remove('cvat_canvas_autoborder_point_direction');
|
||||
}
|
||||
}
|
||||
|
||||
this.auxiliaryClicks = [];
|
||||
this.auxiliaryGroupID = null;
|
||||
}
|
||||
|
||||
// convert each shape to group of clicable points
|
||||
// save all groups
|
||||
private drawMarkers(transformedShapes: TransformedShape[]): void {
|
||||
const svgNamespace = 'http://www.w3.org/2000/svg';
|
||||
|
||||
this.groups = transformedShapes
|
||||
.map((shape: TransformedShape, groupID: number): SVGGElement => {
|
||||
const group = document.createElementNS(svgNamespace, 'g');
|
||||
group.setAttribute('data-group-id', `${groupID}`);
|
||||
|
||||
this.listeners[groupID] = this.listeners[groupID] || {};
|
||||
const circles = shape.points.split(/\s/).map((
|
||||
point: string, pointID: number, points: string[],
|
||||
): SVGCircleElement => {
|
||||
const [x, y] = point.split(',');
|
||||
|
||||
const circle = document.createElementNS(svgNamespace, 'circle');
|
||||
circle.classList.add('cvat_canvas_autoborder_point');
|
||||
circle.setAttribute('fill', shape.color);
|
||||
circle.setAttribute('stroke', 'black');
|
||||
circle.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.scale}`);
|
||||
circle.setAttribute('cx', x);
|
||||
circle.setAttribute('cy', y);
|
||||
circle.setAttribute('r', `${consts.BASE_POINT_SIZE / this.scale}`);
|
||||
|
||||
const click = (event: MouseEvent): void => {
|
||||
event.stopPropagation();
|
||||
|
||||
// another shape was clicked
|
||||
if (this.auxiliaryGroupID !== null
|
||||
&& this.auxiliaryGroupID !== groupID
|
||||
) {
|
||||
this.resetAuxiliaryShape();
|
||||
}
|
||||
|
||||
this.auxiliaryGroupID = groupID;
|
||||
// up clicked group for convenience
|
||||
this.frameContent.appendChild(group);
|
||||
|
||||
if (this.auxiliaryClicks[1] === pointID) {
|
||||
// the second point was clicked twice
|
||||
this.addPointToCurrentShape(+x, +y);
|
||||
this.resetAuxiliaryShape();
|
||||
return;
|
||||
}
|
||||
|
||||
// the first point can not be clicked twice
|
||||
// just ignore such a click if it is
|
||||
if (this.auxiliaryClicks[0] !== pointID) {
|
||||
this.auxiliaryClicks.push(pointID);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// it is the first click
|
||||
if (this.auxiliaryClicks.length === 1) {
|
||||
const handler = this.currentShape.remember('_paintHandler');
|
||||
// draw and remove initial point just to initialize data structures
|
||||
if (!handler || !handler.startPoint) {
|
||||
(this.currentShape as any).draw('point', event);
|
||||
(this.currentShape as any).draw('undo');
|
||||
}
|
||||
|
||||
this.addPointToCurrentShape(+x, +y);
|
||||
// is is the second click
|
||||
} else if (this.auxiliaryClicks.length === 2) {
|
||||
circle.classList.add('cvat_canvas_autoborder_point_direction');
|
||||
// it is the third click
|
||||
} else {
|
||||
// sign defines bypass direction
|
||||
const landmarks = this.auxiliaryClicks;
|
||||
const sign = Math.sign(landmarks[2] - landmarks[0])
|
||||
* Math.sign(landmarks[1] - landmarks[0])
|
||||
* Math.sign(landmarks[2] - landmarks[1]);
|
||||
|
||||
// go via a polygon and get vertexes
|
||||
// the first vertex has been already drawn
|
||||
const way = [];
|
||||
for (let i = landmarks[0] + sign; ; i += sign) {
|
||||
if (i < 0) {
|
||||
i = points.length - 1;
|
||||
} else if (i === points.length) {
|
||||
i = 0;
|
||||
}
|
||||
|
||||
way.push(points[i]);
|
||||
|
||||
if (i === this.auxiliaryClicks[this.auxiliaryClicks.length - 1]) {
|
||||
// put the last element twice
|
||||
// specific of svg.draw.js
|
||||
// way.push(points[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// remove the latest cursor position from drawing array
|
||||
for (const wayPoint of way) {
|
||||
const [_x, _y] = wayPoint.split(',')
|
||||
.map((coordinate: string): number => +coordinate);
|
||||
this.addPointToCurrentShape(_x, _y);
|
||||
}
|
||||
|
||||
this.resetAuxiliaryShape();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const dblclick = (event: MouseEvent): void => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
this.listeners[groupID][pointID] = {
|
||||
click,
|
||||
dblclick,
|
||||
};
|
||||
|
||||
circle.addEventListener('mousedown', this.listeners[groupID][pointID].click);
|
||||
circle.addEventListener('dblclick', this.listeners[groupID][pointID].click);
|
||||
return circle;
|
||||
});
|
||||
|
||||
group.append(...circles);
|
||||
return group;
|
||||
});
|
||||
|
||||
this.frameContent.append(...this.groups);
|
||||
}
|
||||
|
||||
public updateObjects(): void {
|
||||
if (!this.enabled) return;
|
||||
this.removeMarkers();
|
||||
|
||||
const currentClientID = this.currentShape.node.dataset.originClientId;
|
||||
const shapes = Array.from(this.frameContent.getElementsByClassName('cvat_canvas_shape'));
|
||||
const transformedShapes = shapes.map((shape: HTMLElement): TransformedShape | null => {
|
||||
const color = shape.getAttribute('fill');
|
||||
const clientID = shape.getAttribute('clientID');
|
||||
|
||||
if (color === null || clientID === null) return null;
|
||||
if (+clientID === +currentClientID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let points = '';
|
||||
if (shape.tagName === 'polyline' || shape.tagName === 'polygon') {
|
||||
points = shape.getAttribute('points');
|
||||
} else if (shape.tagName === 'rect') {
|
||||
const x = +shape.getAttribute('x');
|
||||
const y = +shape.getAttribute('y');
|
||||
const width = +shape.getAttribute('width');
|
||||
const height = +shape.getAttribute('height');
|
||||
|
||||
if (Number.isNaN(x) || Number.isNaN(y) || Number.isNaN(x) || Number.isNaN(x)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
points = `${x},${y} ${x + width},${y} ${x + width},${y + height} ${x},${y + height}`;
|
||||
} else if (shape.tagName === 'g') {
|
||||
const polylineID = shape.dataset.polylineId;
|
||||
const polyline = this.frameContent.getElementById(polylineID);
|
||||
if (polyline && polyline.getAttribute('points')) {
|
||||
points = polyline.getAttribute('points');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
color,
|
||||
points: points.trim(),
|
||||
};
|
||||
}).filter((state: TransformedShape | null): boolean => state !== null);
|
||||
|
||||
this.drawMarkers(transformedShapes);
|
||||
}
|
||||
|
||||
public autoborder(
|
||||
enabled: boolean,
|
||||
currentShape?: SVG.Shape,
|
||||
ignoreCurrent: boolean = false,
|
||||
): void {
|
||||
if (enabled && !this.enabled && currentShape) {
|
||||
this.enabled = true;
|
||||
this.currentShape = currentShape;
|
||||
this.ignoreCurrent = ignoreCurrent;
|
||||
this.updateObjects();
|
||||
} else {
|
||||
this.release();
|
||||
}
|
||||
}
|
||||
|
||||
public transform(geometry: Geometry): void {
|
||||
this.scale = geometry.scale;
|
||||
this.groups.forEach((group: SVGGElement): void => {
|
||||
Array.from(group.children).forEach((circle: SVGCircleElement): void => {
|
||||
circle.setAttribute('r', `${consts.BASE_POINT_SIZE / this.scale}`);
|
||||
circle.setAttribute('stroke-width', `${consts.BASE_STROKE_WIDTH / this.scale}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,494 @@
|
||||
/* eslint-disable func-names */
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/* eslint-disable curly */
|
||||
/*
|
||||
* Copyright (C) 2020 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import consts from './consts';
|
||||
|
||||
export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export enum Orientation {
|
||||
LEFT = 'left',
|
||||
RIGHT = 'right',
|
||||
}
|
||||
|
||||
function line(p1: Point, p2: Point): number[] {
|
||||
const a = p1.y - p2.y;
|
||||
const b = p2.x - p1.x;
|
||||
const c = b * p1.y + a * p1.x;
|
||||
return [a, b, c];
|
||||
}
|
||||
|
||||
function intersection(
|
||||
p1: Point, p2: Point, p3: Point, p4: Point,
|
||||
): Point | null {
|
||||
const L1 = line(p1, p2);
|
||||
const L2 = line(p3, p4);
|
||||
|
||||
const D = L1[0] * L2[1] - L1[1] * L2[0];
|
||||
const Dx = L1[2] * L2[1] - L1[1] * L2[2];
|
||||
const Dy = L1[0] * L2[2] - L1[2] * L2[0];
|
||||
|
||||
let x = null;
|
||||
let y = null;
|
||||
if (D !== 0) {
|
||||
x = Dx / D;
|
||||
y = Dy / D;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export class Equation {
|
||||
private a: number;
|
||||
private b: number;
|
||||
private c: number;
|
||||
|
||||
public constructor(p1: Point, p2: Point) {
|
||||
this.a = p1.y - p2.y;
|
||||
this.b = p2.x - p1.x;
|
||||
this.c = this.b * p1.y + this.a * p1.x;
|
||||
}
|
||||
|
||||
// get the line equation in actual coordinates
|
||||
public getY(x: number): number {
|
||||
return (this.c - this.a * x) / this.b;
|
||||
}
|
||||
}
|
||||
|
||||
export class Figure {
|
||||
private indices: number[];
|
||||
private allPoints: Point[];
|
||||
|
||||
public constructor(indices: number[], points: Point[]) {
|
||||
this.indices = indices;
|
||||
this.allPoints = points;
|
||||
}
|
||||
|
||||
public get points(): Point[] {
|
||||
const points = [];
|
||||
for (const index of this.indices) {
|
||||
points.push(this.allPoints[index]);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
// sets the point for a given edge, points must be given in
|
||||
// array form in the same ordering as the getter
|
||||
// if you only need to update a subset of the points,
|
||||
// simply put null for the points you want to keep
|
||||
public set points(newPoints) {
|
||||
const oldPoints = this.allPoints;
|
||||
for (let i = 0; i < newPoints.length; i += 1) {
|
||||
if (newPoints[i] !== null) {
|
||||
oldPoints[this.indices[i]] = { x: newPoints[i].x, y: newPoints[i].y };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Edge extends Figure {
|
||||
public getEquation(): Equation {
|
||||
return new Equation(this.points[0], this.points[1]);
|
||||
}
|
||||
}
|
||||
|
||||
export class CuboidModel {
|
||||
public points: Point[];
|
||||
private fr: Edge;
|
||||
private fl: Edge;
|
||||
private dr: Edge;
|
||||
private dl: Edge;
|
||||
private ft: Edge;
|
||||
private rt: Edge;
|
||||
private lt: Edge;
|
||||
private dt: Edge;
|
||||
private fb: Edge;
|
||||
private rb: Edge;
|
||||
private lb: Edge;
|
||||
private db: Edge;
|
||||
public edgeList: Edge[];
|
||||
private front: Figure;
|
||||
private right: Figure;
|
||||
private dorsal: Figure;
|
||||
private left: Figure;
|
||||
private top: Figure;
|
||||
private bot: Figure;
|
||||
public facesList: Figure[];
|
||||
public vpl: Point | null;
|
||||
public vpr: Point | null;
|
||||
public orientation: Orientation;
|
||||
|
||||
public constructor(points?: Point[]) {
|
||||
this.points = points;
|
||||
this.initEdges();
|
||||
this.initFaces();
|
||||
this.updateVanishingPoints(false);
|
||||
this.buildBackEdge(false);
|
||||
this.updatePoints();
|
||||
this.updateOrientation();
|
||||
}
|
||||
|
||||
public getPoints(): Point[] {
|
||||
return this.points;
|
||||
}
|
||||
|
||||
public setPoints(points: (Point | null)[]): void {
|
||||
points.forEach((point: Point | null, i: number): void => {
|
||||
if (point !== null) {
|
||||
this.points[i].x = point.x;
|
||||
this.points[i].y = point.y;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public updateOrientation(): void {
|
||||
if (this.dl.points[0].x > this.fl.points[0].x) {
|
||||
this.orientation = Orientation.LEFT;
|
||||
} else {
|
||||
this.orientation = Orientation.RIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
public updatePoints(): void {
|
||||
// making sure that the edges are vertical
|
||||
this.fr.points[0].x = this.fr.points[1].x;
|
||||
this.fl.points[0].x = this.fl.points[1].x;
|
||||
this.dr.points[0].x = this.dr.points[1].x;
|
||||
this.dl.points[0].x = this.dl.points[1].x;
|
||||
}
|
||||
|
||||
public computeSideEdgeConstraints(edge: any): any {
|
||||
const midLength = this.fr.points[1].y - this.fr.points[0].y - 1;
|
||||
|
||||
const minY = edge.points[1].y - midLength;
|
||||
const maxY = edge.points[0].y + midLength;
|
||||
|
||||
const y1 = edge.points[0].y;
|
||||
const y2 = edge.points[1].y;
|
||||
|
||||
const miny1 = y2 - midLength;
|
||||
const maxy1 = y2 - consts.MIN_EDGE_LENGTH;
|
||||
|
||||
const miny2 = y1 + consts.MIN_EDGE_LENGTH;
|
||||
const maxy2 = y1 + midLength;
|
||||
|
||||
return {
|
||||
constraint: {
|
||||
minY,
|
||||
maxY,
|
||||
},
|
||||
y1Range: {
|
||||
max: maxy1,
|
||||
min: miny1,
|
||||
},
|
||||
y2Range: {
|
||||
max: maxy2,
|
||||
min: miny2,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// boolean value parameter controls which edges should be used to recalculate vanishing points
|
||||
private updateVanishingPoints(buildright: boolean): void {
|
||||
let leftEdge = [];
|
||||
let rightEdge = [];
|
||||
let midEdge = [];
|
||||
if (buildright) {
|
||||
leftEdge = this.fr.points;
|
||||
rightEdge = this.dl.points;
|
||||
midEdge = this.fl.points;
|
||||
} else {
|
||||
leftEdge = this.fl.points;
|
||||
rightEdge = this.dr.points;
|
||||
midEdge = this.fr.points;
|
||||
}
|
||||
|
||||
this.vpl = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
|
||||
this.vpr = intersection(rightEdge[0], midEdge[0], rightEdge[1], midEdge[1]);
|
||||
if (this.vpl === null) {
|
||||
// shift the edge slightly to avoid edge case
|
||||
leftEdge[0].y -= 0.001;
|
||||
leftEdge[0].x += 0.001;
|
||||
leftEdge[1].x += 0.001;
|
||||
this.vpl = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
|
||||
}
|
||||
if (this.vpr === null) {
|
||||
// shift the edge slightly to avoid edge case
|
||||
rightEdge[0].y -= 0.001;
|
||||
rightEdge[0].x -= 0.001;
|
||||
rightEdge[1].x -= 0.001;
|
||||
this.vpr = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
|
||||
}
|
||||
}
|
||||
|
||||
private initEdges(): void {
|
||||
this.fl = new Edge([0, 1], this.points);
|
||||
this.fr = new Edge([2, 3], this.points);
|
||||
this.dr = new Edge([4, 5], this.points);
|
||||
this.dl = new Edge([6, 7], this.points);
|
||||
|
||||
this.ft = new Edge([0, 2], this.points);
|
||||
this.lt = new Edge([0, 6], this.points);
|
||||
this.rt = new Edge([2, 4], this.points);
|
||||
this.dt = new Edge([6, 4], this.points);
|
||||
|
||||
this.fb = new Edge([1, 3], this.points);
|
||||
this.lb = new Edge([1, 7], this.points);
|
||||
this.rb = new Edge([3, 5], this.points);
|
||||
this.db = new Edge([7, 5], this.points);
|
||||
|
||||
this.edgeList = [this.fl, this.fr, this.dl, this.dr, this.ft, this.lt,
|
||||
this.rt, this.dt, this.fb, this.lb, this.rb, this.db];
|
||||
}
|
||||
|
||||
private initFaces(): void {
|
||||
this.front = new Figure([0, 1, 3, 2], this.points);
|
||||
this.right = new Figure([2, 3, 5, 4], this.points);
|
||||
this.dorsal = new Figure([4, 5, 7, 6], this.points);
|
||||
this.left = new Figure([6, 7, 1, 0], this.points);
|
||||
this.top = new Figure([0, 2, 4, 6], this.points);
|
||||
this.bot = new Figure([1, 3, 5, 7], this.points);
|
||||
|
||||
this.facesList = [this.front, this.right, this.dorsal, this.left];
|
||||
}
|
||||
|
||||
private buildBackEdge(buildright: boolean): void {
|
||||
this.updateVanishingPoints(buildright);
|
||||
let leftPoints = [];
|
||||
let rightPoints = [];
|
||||
|
||||
let topIndex = 0;
|
||||
let botIndex = 0;
|
||||
|
||||
if (buildright) {
|
||||
leftPoints = this.dl.points;
|
||||
rightPoints = this.fr.points;
|
||||
topIndex = 4;
|
||||
botIndex = 5;
|
||||
} else {
|
||||
leftPoints = this.dr.points;
|
||||
rightPoints = this.fl.points;
|
||||
topIndex = 6;
|
||||
botIndex = 7;
|
||||
}
|
||||
|
||||
const vpLeft = this.vpl;
|
||||
const vpRight = this.vpr;
|
||||
|
||||
let p1 = intersection(vpLeft, leftPoints[0], vpRight, rightPoints[0]);
|
||||
let p2 = intersection(vpLeft, leftPoints[1], vpRight, rightPoints[1]);
|
||||
|
||||
if (p1 === null) {
|
||||
p1 = { x: p2.x, y: vpLeft.y };
|
||||
} else if (p2 === null) {
|
||||
p2 = { x: p1.x, y: vpLeft.y };
|
||||
}
|
||||
|
||||
this.points[topIndex] = { x: p1.x, y: p1.y };
|
||||
this.points[botIndex] = { x: p2.x, y: p2.y };
|
||||
|
||||
// Making sure that the vertical edges stay vertical
|
||||
this.updatePoints();
|
||||
}
|
||||
}
|
||||
|
||||
function sortPointsClockwise(points: any[]): any[] {
|
||||
points.sort((a, b): number => a.y - b.y);
|
||||
// Get center y
|
||||
const cy = (points[0].y + points[points.length - 1].y) / 2;
|
||||
|
||||
// Sort from right to left
|
||||
points.sort((a, b): number => b.x - a.x);
|
||||
|
||||
// Get center x
|
||||
const cx = (points[0].x + points[points.length - 1].x) / 2;
|
||||
|
||||
// Center point
|
||||
const center = {
|
||||
x: cx,
|
||||
y: cy,
|
||||
};
|
||||
|
||||
// Starting angle used to reference other angles
|
||||
let startAng: number | undefined;
|
||||
points.forEach((point): void => {
|
||||
let ang = Math.atan2(point.y - center.y, point.x - center.x);
|
||||
if (!startAng) {
|
||||
startAng = ang;
|
||||
// ensure that all points are clockwise of the start point
|
||||
} else if (ang < startAng) {
|
||||
ang += Math.PI * 2;
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
point.angle = ang; // add the angle to the point
|
||||
});
|
||||
|
||||
// first sort clockwise
|
||||
points.sort((a, b): number => a.angle - b.angle);
|
||||
return points.reverse();
|
||||
}
|
||||
|
||||
function setupCuboidPoints(points: Point[]): any[] {
|
||||
let left;
|
||||
let right;
|
||||
let left2;
|
||||
let right2;
|
||||
let p1;
|
||||
let p2;
|
||||
let p3;
|
||||
let p4;
|
||||
|
||||
const height = Math.abs(points[0].x - points[1].x)
|
||||
< Math.abs(points[1].x - points[2].x)
|
||||
? Math.abs(points[1].y - points[0].y)
|
||||
: Math.abs(points[1].y - points[2].y);
|
||||
|
||||
// seperate into left and right point
|
||||
// we pick the first and third point because we know assume they will be on
|
||||
// opposite corners
|
||||
if (points[0].x < points[2].x) {
|
||||
[left,, right] = points;
|
||||
} else {
|
||||
[right,, left] = points;
|
||||
}
|
||||
|
||||
// get other 2 points using the given height
|
||||
if (left.y < right.y) {
|
||||
left2 = { x: left.x, y: left.y + height };
|
||||
right2 = { x: right.x, y: right.y - height };
|
||||
} else {
|
||||
left2 = { x: left.x, y: left.y - height };
|
||||
right2 = { x: right.x, y: right.y + height };
|
||||
}
|
||||
|
||||
// get the vector for the last point relative to the previous point
|
||||
const vec = {
|
||||
x: points[3].x - points[2].x,
|
||||
y: points[3].y - points[2].y,
|
||||
};
|
||||
|
||||
if (left.y < left2.y) {
|
||||
p1 = left;
|
||||
p2 = left2;
|
||||
} else {
|
||||
p1 = left2;
|
||||
p2 = left;
|
||||
}
|
||||
|
||||
if (right.y < right2.y) {
|
||||
p3 = right;
|
||||
p4 = right2;
|
||||
} else {
|
||||
p3 = right2;
|
||||
p4 = right;
|
||||
}
|
||||
|
||||
const p5 = { x: p3.x + vec.x, y: p3.y + vec.y + 0.1 };
|
||||
const p6 = { x: p4.x + vec.x, y: p4.y + vec.y - 0.1 };
|
||||
const p7 = { x: p1.x + vec.x, y: p1.y + vec.y + 0.1 };
|
||||
const p8 = { x: p2.x + vec.x, y: p2.y + vec.y - 0.1 };
|
||||
|
||||
p1.y += 0.1;
|
||||
return [p1, p2, p3, p4, p5, p6, p7, p8];
|
||||
}
|
||||
|
||||
export function cuboidFrom4Points(flattenedPoints: any[]): any[] {
|
||||
const points: Point[] = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const [x, y] = flattenedPoints.slice(i * 2, i * 2 + 2);
|
||||
points.push({ x, y });
|
||||
}
|
||||
const unsortedPlanePoints = points.slice(0, 3);
|
||||
function rotate(array: any[], times: number): void{
|
||||
let t = times;
|
||||
while (t--) {
|
||||
const temp = array.shift();
|
||||
array.push(temp);
|
||||
}
|
||||
}
|
||||
|
||||
const plane2 = {
|
||||
p1: points[0],
|
||||
p2: points[0],
|
||||
p3: points[0],
|
||||
p4: points[0],
|
||||
};
|
||||
|
||||
// completing the plane
|
||||
const vector = {
|
||||
x: points[2].x - points[1].x,
|
||||
y: points[2].y - points[1].y,
|
||||
};
|
||||
|
||||
// sorting the first plane
|
||||
unsortedPlanePoints.push({
|
||||
x: points[0].x + vector.x,
|
||||
y: points[0].y + vector.y,
|
||||
});
|
||||
const sortedPlanePoints = sortPointsClockwise(unsortedPlanePoints);
|
||||
let leftIndex = 0;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
leftIndex = sortedPlanePoints[i].x < sortedPlanePoints[leftIndex].x ? i : leftIndex;
|
||||
}
|
||||
rotate(sortedPlanePoints, leftIndex);
|
||||
const plane1 = {
|
||||
p1: sortedPlanePoints[0],
|
||||
p2: sortedPlanePoints[1],
|
||||
p3: sortedPlanePoints[2],
|
||||
p4: sortedPlanePoints[3],
|
||||
};
|
||||
|
||||
const vec = {
|
||||
x: points[3].x - points[2].x,
|
||||
y: points[3].y - points[2].y,
|
||||
};
|
||||
// determine the orientation
|
||||
const angle = Math.atan2(vec.y, vec.x);
|
||||
|
||||
// making the other plane
|
||||
plane2.p1 = { x: plane1.p1.x + vec.x, y: plane1.p1.y + vec.y };
|
||||
plane2.p2 = { x: plane1.p2.x + vec.x, y: plane1.p2.y + vec.y };
|
||||
plane2.p3 = { x: plane1.p3.x + vec.x, y: plane1.p3.y + vec.y };
|
||||
plane2.p4 = { x: plane1.p4.x + vec.x, y: plane1.p4.y + vec.y };
|
||||
|
||||
|
||||
let cuboidPoints;
|
||||
// right
|
||||
if (Math.abs(angle) < Math.PI / 2 - 0.1) {
|
||||
cuboidPoints = setupCuboidPoints(points);
|
||||
// left
|
||||
} else if (Math.abs(angle) > Math.PI / 2 + 0.1) {
|
||||
cuboidPoints = setupCuboidPoints(points);
|
||||
// down
|
||||
} else if (angle > 0) {
|
||||
cuboidPoints = [
|
||||
plane1.p1, plane2.p1, plane1.p2, plane2.p2,
|
||||
plane1.p3, plane2.p3, plane1.p4, plane2.p4,
|
||||
];
|
||||
cuboidPoints[0].y += 0.1;
|
||||
cuboidPoints[4].y += 0.1;
|
||||
// up
|
||||
} else {
|
||||
cuboidPoints = [
|
||||
plane2.p1, plane1.p1, plane2.p2, plane1.p2,
|
||||
plane2.p3, plane1.p3, plane2.p4, plane1.p4,
|
||||
];
|
||||
cuboidPoints[0].y += 0.1;
|
||||
cuboidPoints[4].y += 0.1;
|
||||
}
|
||||
|
||||
return cuboidPoints.reduce((arr: number[], point: any): number[] => {
|
||||
arr.push(point.x);
|
||||
arr.push(point.y);
|
||||
return arr;
|
||||
}, []);
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
const Axios = require('axios');
|
||||
|
||||
Axios.defaults.withCredentials = true;
|
||||
Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN';
|
||||
Axios.defaults.xsrfCookieName = 'csrftoken';
|
||||
|
||||
|
||||
onmessage = (e) => {
|
||||
Axios.get(e.data.url, e.data.config)
|
||||
.then((response) => {
|
||||
postMessage({
|
||||
responseData: response.data,
|
||||
id: e.data.id,
|
||||
isSuccess: true,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
postMessage({
|
||||
id: e.data.id,
|
||||
error,
|
||||
isSuccess: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,247 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
const { detect } = require('detect-browser');
|
||||
const PluginRegistry = require('./plugins');
|
||||
const { ArgumentError } = require('./exceptions');
|
||||
const { LogType } = require('./enums');
|
||||
|
||||
/**
|
||||
* Class representing a single log
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class Log {
|
||||
constructor(logType, payload) {
|
||||
this.onCloseCallback = null;
|
||||
|
||||
this.type = logType;
|
||||
this.payload = { ...payload };
|
||||
this.time = new Date();
|
||||
}
|
||||
|
||||
onClose(callback) {
|
||||
this.onCloseCallback = callback;
|
||||
}
|
||||
|
||||
validatePayload() {
|
||||
if (typeof (this.payload) !== 'object') {
|
||||
throw new ArgumentError('Payload must be an object');
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.stringify(this.payload);
|
||||
} catch (error) {
|
||||
const message = `Log payload must be JSON serializable. ${error.toString()}`;
|
||||
throw new ArgumentError(message);
|
||||
}
|
||||
}
|
||||
|
||||
dump() {
|
||||
const payload = { ...this.payload };
|
||||
const body = {
|
||||
name: this.type,
|
||||
time: this.time.toISOString(),
|
||||
};
|
||||
|
||||
for (const field of ['client_id', 'job_id', 'task_id', 'is_active']) {
|
||||
if (field in payload) {
|
||||
body[field] = payload[field];
|
||||
delete payload[field];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...body,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Method saves a durable log in a storage <br>
|
||||
* Note then you can call close() multiple times <br>
|
||||
* Log duration will be computed based on the latest call <br>
|
||||
* All payloads will be shallowly combined (all top level properties will exist)
|
||||
* @method close
|
||||
* @memberof module:API.cvat.classes.Log
|
||||
* @param {object} [payload] part of payload can be added when close a log
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
async close(payload = {}) {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(this, Log.prototype.close, payload);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Log.prototype.close.implementation = function (payload) {
|
||||
this.payload.duration = Date.now() - this.time.getTime();
|
||||
this.payload = { ...this.payload, ...payload };
|
||||
|
||||
if (this.onCloseCallback) {
|
||||
this.onCloseCallback();
|
||||
}
|
||||
};
|
||||
|
||||
class LogWithCount extends Log {
|
||||
validatePayload() {
|
||||
Log.prototype.validatePayload.call(this);
|
||||
if (!Number.isInteger(this.payload.count) || this.payload.count < 1) {
|
||||
const message = `The field "count" is required for "${this.type}" log`
|
||||
+ 'It must be a positive integer';
|
||||
throw new ArgumentError(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LogWithObjectsInfo extends Log {
|
||||
validatePayload() {
|
||||
const generateError = (name, range) => {
|
||||
const message = `The field "${name}" is required for "${this.type}" log. ${range}`;
|
||||
throw new ArgumentError(message);
|
||||
};
|
||||
|
||||
if (!Number.isInteger(this.payload['track count']) || this.payload['track count'] < 0) {
|
||||
generateError('track count', 'It must be an integer not less than 0');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(this.payload['tag count']) || this.payload['tag count'] < 0) {
|
||||
generateError('tag count', 'It must be an integer not less than 0');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(this.payload['object count']) || this.payload['object count'] < 0) {
|
||||
generateError('object count', 'It must be an integer not less than 0');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(this.payload['frame count']) || this.payload['frame count'] < 1) {
|
||||
generateError('frame count', 'It must be an integer not less than 1');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(this.payload['box count']) || this.payload['box count'] < 0) {
|
||||
generateError('box count', 'It must be an integer not less than 0');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(this.payload['polygon count']) || this.payload['polygon count'] < 0) {
|
||||
generateError('polygon count', 'It must be an integer not less than 0');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(this.payload['polyline count']) || this.payload['polyline count'] < 0) {
|
||||
generateError('polyline count', 'It must be an integer not less than 0');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(this.payload['points count']) || this.payload['points count'] < 0) {
|
||||
generateError('points count', 'It must be an integer not less than 0');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LogWithWorkingTime extends Log {
|
||||
validatePayload() {
|
||||
Log.prototype.validatePayload.call(this);
|
||||
|
||||
if (!('working_time' in this.payload)
|
||||
|| !typeof (this.payload.working_time) === 'number'
|
||||
|| this.payload.working_time < 0
|
||||
) {
|
||||
const message = `The field "working_time" is required for ${this.type} log. `
|
||||
+ 'It must be a number not less than 0';
|
||||
throw new ArgumentError(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LogWithExceptionInfo extends Log {
|
||||
validatePayload() {
|
||||
Log.prototype.validatePayload.call(this);
|
||||
|
||||
if (typeof (this.payload.message) !== 'string') {
|
||||
const message = `The field "message" is required for ${this.type} log. `
|
||||
+ 'It must be a string';
|
||||
throw new ArgumentError(message);
|
||||
}
|
||||
|
||||
if (typeof (this.payload.filename) !== 'string') {
|
||||
const message = `The field "filename" is required for ${this.type} log. `
|
||||
+ 'It must be a string';
|
||||
throw new ArgumentError(message);
|
||||
}
|
||||
|
||||
if (typeof (this.payload.line) !== 'number') {
|
||||
const message = `The field "line" is required for ${this.type} log. `
|
||||
+ 'It must be a number';
|
||||
throw new ArgumentError(message);
|
||||
}
|
||||
|
||||
if (typeof (this.payload.column) !== 'number') {
|
||||
const message = `The field "column" is required for ${this.type} log. `
|
||||
+ 'It must be a number';
|
||||
throw new ArgumentError(message);
|
||||
}
|
||||
|
||||
if (typeof (this.payload.stack) !== 'string') {
|
||||
const message = `The field "stack" is required for ${this.type} log. `
|
||||
+ 'It must be a string';
|
||||
throw new ArgumentError(message);
|
||||
}
|
||||
}
|
||||
|
||||
dump() {
|
||||
let body = super.dump();
|
||||
const payload = body.payload;
|
||||
const client = detect();
|
||||
body = {
|
||||
...body,
|
||||
message: payload.message,
|
||||
filename: payload.filename,
|
||||
line: payload.line,
|
||||
column: payload.column,
|
||||
stack: payload.stack,
|
||||
system: client.os,
|
||||
client: client.name,
|
||||
version: client.version,
|
||||
};
|
||||
|
||||
delete payload.message;
|
||||
delete payload.filename;
|
||||
delete payload.line;
|
||||
delete payload.column;
|
||||
delete payload.stack;
|
||||
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
function logFactory(logType, payload) {
|
||||
const logsWithCount = [
|
||||
LogType.deleteObject, LogType.mergeObjects, LogType.copyObject,
|
||||
LogType.undoAction, LogType.redoAction,
|
||||
];
|
||||
|
||||
if (logsWithCount.includes(logType)) {
|
||||
return new LogWithCount(logType, payload);
|
||||
}
|
||||
if ([LogType.sendTaskInfo, LogType.loadJob, LogType.uploadAnnotations].includes(logType)) {
|
||||
return new LogWithObjectsInfo(logType, payload);
|
||||
}
|
||||
|
||||
if (logType === LogType.sendUserActivity) {
|
||||
return new LogWithWorkingTime(logType, payload);
|
||||
}
|
||||
|
||||
if (logType === LogType.sendException) {
|
||||
return new LogWithExceptionInfo(logType, payload);
|
||||
}
|
||||
|
||||
return new Log(logType, payload);
|
||||
}
|
||||
|
||||
module.exports = logFactory;
|
||||
@ -0,0 +1,177 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
const PluginRegistry = require('./plugins');
|
||||
const serverProxy = require('./server-proxy');
|
||||
const logFactory = require('./log');
|
||||
const { ArgumentError } = require('./exceptions');
|
||||
const { LogType } = require('./enums');
|
||||
|
||||
const WORKING_TIME_THRESHOLD = 100000; // ms, 1.66 min
|
||||
|
||||
class LoggerStorage {
|
||||
constructor() {
|
||||
this.clientID = Date.now().toString().substr(-6);
|
||||
this.lastLogTime = Date.now();
|
||||
this.workingTime = 0;
|
||||
this.collection = [];
|
||||
this.ignoreRules = {}; // by event
|
||||
this.isActiveChecker = null;
|
||||
|
||||
this.ignoreRules[LogType.zoomImage] = {
|
||||
lastLog: null,
|
||||
timeThreshold: 1000,
|
||||
ignore(previousLog) {
|
||||
return Date.now() - previousLog.time < this.timeThreshold;
|
||||
},
|
||||
};
|
||||
|
||||
this.ignoreRules[LogType.changeAttribute] = {
|
||||
lastLog: null,
|
||||
ignore(previousLog, currentPayload) {
|
||||
return currentPayload.object_id === previousLog.payload.object_id
|
||||
&& currentPayload.id === previousLog.payload.id;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
updateWorkingTime() {
|
||||
if (!this.isActiveChecker || this.isActiveChecker()) {
|
||||
const lastLogTime = Date.now();
|
||||
const diff = lastLogTime - this.lastLogTime;
|
||||
this.workingTime += diff < WORKING_TIME_THRESHOLD ? diff : 0;
|
||||
this.lastLogTime = lastLogTime;
|
||||
}
|
||||
}
|
||||
|
||||
async configure(isActiveChecker, activityHelper) {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(
|
||||
this, LoggerStorage.prototype.configure,
|
||||
isActiveChecker, activityHelper,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
async log(logType, payload = {}, wait = false) {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(this, LoggerStorage.prototype.log, logType, payload, wait);
|
||||
return result;
|
||||
}
|
||||
|
||||
async save() {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(this, LoggerStorage.prototype.save);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
LoggerStorage.prototype.configure.implementation = function (
|
||||
isActiveChecker,
|
||||
userActivityCallback,
|
||||
) {
|
||||
if (typeof (isActiveChecker) !== 'function') {
|
||||
throw new ArgumentError('isActiveChecker argument must be callable');
|
||||
}
|
||||
|
||||
if (!Array.isArray(userActivityCallback)) {
|
||||
throw new ArgumentError('userActivityCallback argument must be an array');
|
||||
}
|
||||
|
||||
this.isActiveChecker = () => !!isActiveChecker();
|
||||
userActivityCallback.push(this.updateWorkingTime.bind(this));
|
||||
};
|
||||
|
||||
LoggerStorage.prototype.log.implementation = function (logType, payload, wait) {
|
||||
if (typeof (payload) !== 'object') {
|
||||
throw new ArgumentError('Payload must be an object');
|
||||
}
|
||||
|
||||
if (typeof (wait) !== 'boolean') {
|
||||
throw new ArgumentError('Payload must be an object');
|
||||
}
|
||||
|
||||
if (logType in this.ignoreRules) {
|
||||
const ignoreRule = this.ignoreRules[logType];
|
||||
const { lastLog } = ignoreRule;
|
||||
if (lastLog && ignoreRule.ignore(lastLog, payload)) {
|
||||
lastLog.payload = {
|
||||
...lastLog.payload,
|
||||
...payload,
|
||||
};
|
||||
|
||||
this.updateWorkingTime();
|
||||
return ignoreRule.lastLog;
|
||||
}
|
||||
}
|
||||
|
||||
const logPayload = { ...payload };
|
||||
logPayload.client_id = this.clientID;
|
||||
if (this.isActiveChecker) {
|
||||
logPayload.is_active = this.isActiveChecker();
|
||||
}
|
||||
|
||||
const log = logFactory(logType, { ...logPayload });
|
||||
if (logType in this.ignoreRules) {
|
||||
this.ignoreRules[logType].lastLog = log;
|
||||
}
|
||||
|
||||
const pushEvent = () => {
|
||||
this.updateWorkingTime();
|
||||
log.validatePayload();
|
||||
log.onClose(null);
|
||||
this.collection.push(log);
|
||||
};
|
||||
|
||||
if (log.type === LogType.sendException) {
|
||||
serverProxy.server.exception(log.dump()).catch(() => {
|
||||
pushEvent();
|
||||
});
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
if (wait) {
|
||||
log.onClose(pushEvent);
|
||||
} else {
|
||||
pushEvent();
|
||||
}
|
||||
|
||||
return log;
|
||||
};
|
||||
|
||||
LoggerStorage.prototype.save.implementation = async function () {
|
||||
const collectionToSend = [...this.collection];
|
||||
const lastLog = this.collection[this.collection.length - 1];
|
||||
|
||||
const logPayload = {};
|
||||
logPayload.client_id = this.clientID;
|
||||
logPayload.working_time = this.workingTime;
|
||||
if (this.isActiveChecker) {
|
||||
logPayload.is_active = this.isActiveChecker();
|
||||
}
|
||||
|
||||
if (lastLog && lastLog.type === LogType.sendTaskInfo) {
|
||||
logPayload.job_id = lastLog.payload.job_id;
|
||||
logPayload.task_id = lastLog.payload.task_id;
|
||||
}
|
||||
|
||||
const userActivityLog = logFactory(LogType.sendUserActivity, logPayload);
|
||||
collectionToSend.push(userActivityLog);
|
||||
|
||||
await serverProxy.logs.save(collectionToSend.map((log) => log.dump()));
|
||||
|
||||
for (const rule of Object.values(this.ignoreRules)) {
|
||||
rule.lastLog = null;
|
||||
}
|
||||
this.collection = [];
|
||||
this.workingTime = 0;
|
||||
this.lastLogTime = Date.now();
|
||||
};
|
||||
|
||||
module.exports = new LoggerStorage();
|
||||
@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const PluginRegistry = require('./plugins');
|
||||
|
||||
/**
|
||||
* Class describe scheme of a log object
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class Log {
|
||||
constructor(logType, continuous, details) {
|
||||
this.type = logType;
|
||||
this.continuous = continuous;
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method closes a continue log
|
||||
* @method close
|
||||
* @memberof module:API.cvat.classes.Log
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async close() {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(this, Log.prototype.close);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Log;
|
||||
})();
|
||||
@ -0,0 +1 @@
|
||||
**/3rdparty/*.js
|
||||
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2018-2020 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
"env": {
|
||||
"node": false,
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"jquery": true,
|
||||
"qunit": true,
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint",
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2018,
|
||||
},
|
||||
"plugins": [
|
||||
"security",
|
||||
"no-unsanitized",
|
||||
"no-unsafe-innerhtml",
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:security/recommended",
|
||||
"plugin:no-unsanitized/DOM",
|
||||
"airbnb-base",
|
||||
],
|
||||
"rules": {
|
||||
"no-await-in-loop": [0],
|
||||
"global-require": [0],
|
||||
"no-new": [0],
|
||||
"class-methods-use-this": [0],
|
||||
"no-restricted-properties": [0, {
|
||||
"object": "Math",
|
||||
"property": "pow",
|
||||
}],
|
||||
"no-plusplus": [0],
|
||||
"no-param-reassign": [0],
|
||||
"no-underscore-dangle": ["error", { "allowAfterThis": true }],
|
||||
"no-restricted-syntax": [0, {"selector": "ForOfStatement"}],
|
||||
"no-continue": [0],
|
||||
"no-unsafe-innerhtml/no-unsafe-innerhtml": 1,
|
||||
// This rule actual for user input data on the node.js environment mainly.
|
||||
"security/detect-object-injection": 0,
|
||||
"indent": ["warn", 4],
|
||||
"no-useless-constructor": 0,
|
||||
"func-names": [0],
|
||||
"valid-typeof": [0],
|
||||
"no-console": [0], // this rule deprecates console.log, console.warn etc. because "it is not good in production code"
|
||||
"max-classes-per-file": [0],
|
||||
"quotes": ["warn", "single"],
|
||||
},
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
dist
|
||||
@ -0,0 +1,14 @@
|
||||
# cvat-data module
|
||||
|
||||
```bash
|
||||
npm run build # build with minification
|
||||
npm run build -- --mode=development # build without minification
|
||||
npm run server # run debug server
|
||||
```
|
||||
|
||||
## Versioning
|
||||
If you make changes in this package, please do following:
|
||||
|
||||
- After not important changes (typos, backward compatible bug fixes, refactoring) do: ``npm version patch``
|
||||
- After changing API (backward compatible new features) do: ``npm version minor``
|
||||
- After changing API (changes that break backward compatibility) do: ``npm version major``
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "cvat-data",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "src/js/cvat-data.js",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.4.4",
|
||||
"@babel/core": "^7.4.4",
|
||||
"@babel/preset-env": "^7.4.4",
|
||||
"babel-loader": "^8.0.6",
|
||||
"copy-webpack-plugin": "^5.0.5",
|
||||
"eslint": "^6.4.0",
|
||||
"eslint-config-airbnb-base": "^14.0.0",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-no-unsafe-innerhtml": "^1.0.16",
|
||||
"eslint-plugin-no-unsanitized": "^3.0.2",
|
||||
"eslint-plugin-security": "^1.4.0",
|
||||
"nodemon": "^1.19.2",
|
||||
"webpack": "^4.39.3",
|
||||
"webpack-cli": "^3.3.7",
|
||||
"worker-loader": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-mutex": "^0.1.4",
|
||||
"jszip": "3.1.5"
|
||||
},
|
||||
"scripts": {
|
||||
"patch": "cd src/js && patch --dry-run --forward -p0 < 3rdparty_patch.diff >> /dev/null && patch -p0 < 3rdparty_patch.diff; true",
|
||||
"build": "npm run patch; webpack --config ./webpack.config.js",
|
||||
"server": "npm run patch; nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'"
|
||||
},
|
||||
"author": "Intel",
|
||||
"license": "MIT"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,88 @@
|
||||
## 3rdparty components
|
||||
|
||||
These files are from the [Broadway.js](https://github.com/mbebenita/Broadway) repository:
|
||||
- Decoder.js
|
||||
- mp4.js
|
||||
- avc.wasm
|
||||
|
||||
### Why do we store them here?
|
||||
|
||||
Authors don't provide an npm package, so we need to store these components in our repository.
|
||||
We use this dependency to decode video chunks from a server and split them to frames on client side.
|
||||
|
||||
We need to run this package in node environent (for example for debug, or for running unit tests).
|
||||
But there aren't any ways to do that (even with syntetic environment, provided for example by the package ``browser-env``).
|
||||
For example there are issues with canvas using (webpack doesn't work with binary canvas package for node-js) and others.
|
||||
So, we have solved to write patch file for this library.
|
||||
It modifies source code a little to support our scenario of using.
|
||||
|
||||
### How to build awc.wasm and Decoder.js
|
||||
1. Clone Emscripten SDK, install and activate the latest fastcomp SDK:
|
||||
```sh
|
||||
git clone https://github.com/emscripten-core/emsdk.git && cd emsdk
|
||||
```
|
||||
```sh
|
||||
./emsdk install latest-fastcomp
|
||||
```
|
||||
```sh
|
||||
./emsdk activate latest-fastcomp
|
||||
```
|
||||
|
||||
1. Clone Broadway.js
|
||||
```sh
|
||||
git clone https://github.com/mbebenita/Broadway.git && cd Broadway/Decoder
|
||||
```
|
||||
|
||||
1. Edit `make.py`:
|
||||
- Remove or comment the following options:
|
||||
`'-s', 'NO_BROWSER=1',`\
|
||||
`'-s', 'PRECISE_I64_MATH=0',`
|
||||
- Remove `"HEAP8", "HEAP16", "HEAP32"` from the `EXPORTED_FUNCTIONS` list.
|
||||
- Increase total memory to make possible decode 4k videos
|
||||
(or try to enable `ALLOW_MEMORY_GROWTH`, but this option has not been tested):\
|
||||
`'-s', 'TOTAL_MEMORY=' + str(100*1024*1024),`
|
||||
- Add the following options:\
|
||||
`'-s', "ENVIRONMENT='worker'",`\
|
||||
`'-s', 'WASM=1',`
|
||||
|
||||
1. Activate emsdk environment and build Broadway.js:
|
||||
```sh
|
||||
. /tmp/emsdk/emsdk_env.sh
|
||||
```
|
||||
```sh
|
||||
python2 make.py
|
||||
```
|
||||
|
||||
1. Copy the following files to cvat-data 3rdparty source folder:
|
||||
```sh
|
||||
cd ..
|
||||
```
|
||||
```sh
|
||||
cp Player/avc.wasm Player/Decoder.js Player/mp4.js <CVAT_FOLDER>/cvat-data/src/
|
||||
```
|
||||
```sh
|
||||
js/3rdparty
|
||||
```
|
||||
|
||||
### How work with a patch file
|
||||
```bash
|
||||
# from cvat-data/src/js
|
||||
cp -r 3rdparty 3rdparty_edited
|
||||
# change 3rdparty edited as we need
|
||||
diff -u 3rdparty 3rdparty_edited/ > 3rdparty_patch.diff
|
||||
patch -p0 < 3rdparty_patch.diff # apply patch from cvat-data/src/js
|
||||
```
|
||||
|
||||
Also these files have been added to ignore for git in all future revisions:
|
||||
```bash
|
||||
# from cvat-data dir
|
||||
git update-index --skip-worktree src/js/3rdparty/*.js
|
||||
```
|
||||
|
||||
This behaviour can be reset with:
|
||||
```bash
|
||||
# from cvat-data dir
|
||||
git update-index --no-skip-worktree src/js/3rdparty/*.js
|
||||
```
|
||||
|
||||
[Stackoverflow issue](https://stackoverflow.com/questions/4348590/how-can-i-make-git-ignore-future-revisions-to-a-file)
|
||||
Binary file not shown.
@ -0,0 +1,977 @@
|
||||
module.exports = (function(){
|
||||
|
||||
'use strict';
|
||||
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
error(message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Represents a 2-dimensional size value.
|
||||
*/
|
||||
var Size = (function size() {
|
||||
function constructor(w, h) {
|
||||
this.w = w;
|
||||
this.h = h;
|
||||
}
|
||||
constructor.prototype = {
|
||||
toString: function () {
|
||||
return "(" + this.w + ", " + this.h + ")";
|
||||
},
|
||||
getHalfSize: function() {
|
||||
return new Size(this.w >>> 1, this.h >>> 1);
|
||||
},
|
||||
length: function() {
|
||||
return this.w * this.h;
|
||||
}
|
||||
};
|
||||
return constructor;
|
||||
})();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var Bytestream = (function BytestreamClosure() {
|
||||
function constructor(arrayBuffer, start, length) {
|
||||
this.bytes = new Uint8Array(arrayBuffer);
|
||||
this.start = start || 0;
|
||||
this.pos = this.start;
|
||||
this.end = (start + length) || this.bytes.length;
|
||||
}
|
||||
constructor.prototype = {
|
||||
get length() {
|
||||
return this.end - this.start;
|
||||
},
|
||||
get position() {
|
||||
return this.pos;
|
||||
},
|
||||
get remaining() {
|
||||
return this.end - this.pos;
|
||||
},
|
||||
readU8Array: function (length) {
|
||||
if (this.pos > this.end - length)
|
||||
return null;
|
||||
var res = this.bytes.subarray(this.pos, this.pos + length);
|
||||
this.pos += length;
|
||||
return res;
|
||||
},
|
||||
readU32Array: function (rows, cols, names) {
|
||||
cols = cols || 1;
|
||||
if (this.pos > this.end - (rows * cols) * 4)
|
||||
return null;
|
||||
if (cols == 1) {
|
||||
var array = new Uint32Array(rows);
|
||||
for (var i = 0; i < rows; i++) {
|
||||
array[i] = this.readU32();
|
||||
}
|
||||
return array;
|
||||
} else {
|
||||
var array = new Array(rows);
|
||||
for (var i = 0; i < rows; i++) {
|
||||
var row = null;
|
||||
if (names) {
|
||||
row = {};
|
||||
for (var j = 0; j < cols; j++) {
|
||||
row[names[j]] = this.readU32();
|
||||
}
|
||||
} else {
|
||||
row = new Uint32Array(cols);
|
||||
for (var j = 0; j < cols; j++) {
|
||||
row[j] = this.readU32();
|
||||
}
|
||||
}
|
||||
array[i] = row;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
},
|
||||
read8: function () {
|
||||
return this.readU8() << 24 >> 24;
|
||||
},
|
||||
readU8: function () {
|
||||
if (this.pos >= this.end)
|
||||
return null;
|
||||
return this.bytes[this.pos++];
|
||||
},
|
||||
read16: function () {
|
||||
return this.readU16() << 16 >> 16;
|
||||
},
|
||||
readU16: function () {
|
||||
if (this.pos >= this.end - 1)
|
||||
return null;
|
||||
var res = this.bytes[this.pos + 0] << 8 | this.bytes[this.pos + 1];
|
||||
this.pos += 2;
|
||||
return res;
|
||||
},
|
||||
read24: function () {
|
||||
return this.readU24() << 8 >> 8;
|
||||
},
|
||||
readU24: function () {
|
||||
var pos = this.pos;
|
||||
var bytes = this.bytes;
|
||||
if (pos > this.end - 3)
|
||||
return null;
|
||||
var res = bytes[pos + 0] << 16 | bytes[pos + 1] << 8 | bytes[pos + 2];
|
||||
this.pos += 3;
|
||||
return res;
|
||||
},
|
||||
peek32: function (advance) {
|
||||
var pos = this.pos;
|
||||
var bytes = this.bytes;
|
||||
if (pos > this.end - 4)
|
||||
return null;
|
||||
var res = bytes[pos + 0] << 24 | bytes[pos + 1] << 16 | bytes[pos + 2] << 8 | bytes[pos + 3];
|
||||
if (advance) {
|
||||
this.pos += 4;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
read32: function () {
|
||||
return this.peek32(true);
|
||||
},
|
||||
readU32: function () {
|
||||
return this.peek32(true) >>> 0;
|
||||
},
|
||||
read4CC: function () {
|
||||
var pos = this.pos;
|
||||
if (pos > this.end - 4)
|
||||
return null;
|
||||
var res = "";
|
||||
for (var i = 0; i < 4; i++) {
|
||||
res += String.fromCharCode(this.bytes[pos + i]);
|
||||
}
|
||||
this.pos += 4;
|
||||
return res;
|
||||
},
|
||||
readFP16: function () {
|
||||
return this.read32() / 65536;
|
||||
},
|
||||
readFP8: function () {
|
||||
return this.read16() / 256;
|
||||
},
|
||||
readISO639: function () {
|
||||
var bits = this.readU16();
|
||||
var res = "";
|
||||
for (var i = 0; i < 3; i++) {
|
||||
var c = (bits >>> (2 - i) * 5) & 0x1f;
|
||||
res += String.fromCharCode(c + 0x60);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
readUTF8: function (length) {
|
||||
var res = "";
|
||||
for (var i = 0; i < length; i++) {
|
||||
res += String.fromCharCode(this.readU8());
|
||||
}
|
||||
return res;
|
||||
},
|
||||
readPString: function (max) {
|
||||
var len = this.readU8();
|
||||
assert (len <= max);
|
||||
var res = this.readUTF8(len);
|
||||
this.reserved(max - len - 1, 0);
|
||||
return res;
|
||||
},
|
||||
skip: function (length) {
|
||||
this.seek(this.pos + length);
|
||||
},
|
||||
reserved: function (length, value) {
|
||||
for (var i = 0; i < length; i++) {
|
||||
assert (this.readU8() == value);
|
||||
}
|
||||
},
|
||||
seek: function (index) {
|
||||
if (index < 0 || index > this.end) {
|
||||
error("Index out of bounds (bounds: [0, " + this.end + "], index: " + index + ").");
|
||||
}
|
||||
this.pos = index;
|
||||
},
|
||||
subStream: function (start, length) {
|
||||
return new Bytestream(this.bytes.buffer, start, length);
|
||||
}
|
||||
};
|
||||
return constructor;
|
||||
})();
|
||||
|
||||
|
||||
var PARANOID = true; // Heavy-weight assertions.
|
||||
|
||||
/**
|
||||
* Reads an mp4 file and constructs a object graph that corresponds to the box/atom
|
||||
* structure of the file. Mp4 files are based on the ISO Base Media format, which in
|
||||
* turn is based on the Apple Quicktime format. The Quicktime spec is available at:
|
||||
* http://developer.apple.com/library/mac/#documentation/QuickTime/QTFF. An mp4 spec
|
||||
* also exists, but I cannot find it freely available.
|
||||
*
|
||||
* Mp4 files contain a tree of boxes (or atoms in Quicktime). The general structure
|
||||
* is as follows (in a pseudo regex syntax):
|
||||
*
|
||||
* Box / Atom Structure:
|
||||
*
|
||||
* [size type [version flags] field* box*]
|
||||
* <32> <4C> <--8--> <24-> <-?-> <?>
|
||||
* <------------- box size ------------>
|
||||
*
|
||||
* The box size indicates the entire size of the box and its children, we can use it
|
||||
* to skip over boxes that are of no interest. Each box has a type indicated by a
|
||||
* four character code (4C), this describes how the box should be parsed and is also
|
||||
* used as an object key name in the resulting box tree. For example, the expression:
|
||||
* "moov.trak[0].mdia.minf" can be used to access individual boxes in the tree based
|
||||
* on their 4C name. If two or more boxes with the same 4C name exist in a box, then
|
||||
* an array is built with that name.
|
||||
*
|
||||
*/
|
||||
var MP4Reader = (function reader() {
|
||||
var BOX_HEADER_SIZE = 8;
|
||||
var FULL_BOX_HEADER_SIZE = BOX_HEADER_SIZE + 4;
|
||||
|
||||
function constructor(stream) {
|
||||
this.stream = stream;
|
||||
this.tracks = {};
|
||||
}
|
||||
|
||||
constructor.prototype = {
|
||||
readBoxes: function (stream, parent) {
|
||||
while (stream.peek32()) {
|
||||
var child = this.readBox(stream);
|
||||
if (child.type in parent) {
|
||||
var old = parent[child.type];
|
||||
if (!(old instanceof Array)) {
|
||||
parent[child.type] = [old];
|
||||
}
|
||||
parent[child.type].push(child);
|
||||
} else {
|
||||
parent[child.type] = child;
|
||||
}
|
||||
}
|
||||
},
|
||||
readBox: function readBox(stream) {
|
||||
var box = { offset: stream.position };
|
||||
|
||||
function readHeader() {
|
||||
box.size = stream.readU32();
|
||||
box.type = stream.read4CC();
|
||||
}
|
||||
|
||||
function readFullHeader() {
|
||||
box.version = stream.readU8();
|
||||
box.flags = stream.readU24();
|
||||
}
|
||||
|
||||
function remainingBytes() {
|
||||
return box.size - (stream.position - box.offset);
|
||||
}
|
||||
|
||||
function skipRemainingBytes () {
|
||||
stream.skip(remainingBytes());
|
||||
}
|
||||
|
||||
var readRemainingBoxes = function () {
|
||||
var subStream = stream.subStream(stream.position, remainingBytes());
|
||||
this.readBoxes(subStream, box);
|
||||
stream.skip(subStream.length);
|
||||
}.bind(this);
|
||||
|
||||
readHeader();
|
||||
|
||||
switch (box.type) {
|
||||
case 'ftyp':
|
||||
box.name = "File Type Box";
|
||||
box.majorBrand = stream.read4CC();
|
||||
box.minorVersion = stream.readU32();
|
||||
box.compatibleBrands = new Array((box.size - 16) / 4);
|
||||
for (var i = 0; i < box.compatibleBrands.length; i++) {
|
||||
box.compatibleBrands[i] = stream.read4CC();
|
||||
}
|
||||
break;
|
||||
case 'moov':
|
||||
box.name = "Movie Box";
|
||||
readRemainingBoxes();
|
||||
break;
|
||||
case 'mvhd':
|
||||
box.name = "Movie Header Box";
|
||||
readFullHeader();
|
||||
assert (box.version == 0);
|
||||
box.creationTime = stream.readU32();
|
||||
box.modificationTime = stream.readU32();
|
||||
box.timeScale = stream.readU32();
|
||||
box.duration = stream.readU32();
|
||||
box.rate = stream.readFP16();
|
||||
box.volume = stream.readFP8();
|
||||
stream.skip(10);
|
||||
box.matrix = stream.readU32Array(9);
|
||||
stream.skip(6 * 4);
|
||||
box.nextTrackId = stream.readU32();
|
||||
break;
|
||||
case 'trak':
|
||||
box.name = "Track Box";
|
||||
readRemainingBoxes();
|
||||
this.tracks[box.tkhd.trackId] = new Track(this, box);
|
||||
break;
|
||||
case 'tkhd':
|
||||
box.name = "Track Header Box";
|
||||
readFullHeader();
|
||||
assert (box.version == 0);
|
||||
box.creationTime = stream.readU32();
|
||||
box.modificationTime = stream.readU32();
|
||||
box.trackId = stream.readU32();
|
||||
stream.skip(4);
|
||||
box.duration = stream.readU32();
|
||||
stream.skip(8);
|
||||
box.layer = stream.readU16();
|
||||
box.alternateGroup = stream.readU16();
|
||||
box.volume = stream.readFP8();
|
||||
stream.skip(2);
|
||||
box.matrix = stream.readU32Array(9);
|
||||
box.width = stream.readFP16();
|
||||
box.height = stream.readFP16();
|
||||
break;
|
||||
case 'mdia':
|
||||
box.name = "Media Box";
|
||||
readRemainingBoxes();
|
||||
break;
|
||||
case 'mdhd':
|
||||
box.name = "Media Header Box";
|
||||
readFullHeader();
|
||||
assert (box.version == 0);
|
||||
box.creationTime = stream.readU32();
|
||||
box.modificationTime = stream.readU32();
|
||||
box.timeScale = stream.readU32();
|
||||
box.duration = stream.readU32();
|
||||
box.language = stream.readISO639();
|
||||
stream.skip(2);
|
||||
break;
|
||||
case 'hdlr':
|
||||
box.name = "Handler Reference Box";
|
||||
readFullHeader();
|
||||
stream.skip(4);
|
||||
box.handlerType = stream.read4CC();
|
||||
stream.skip(4 * 3);
|
||||
var bytesLeft = box.size - 32;
|
||||
if (bytesLeft > 0) {
|
||||
box.name = stream.readUTF8(bytesLeft);
|
||||
}
|
||||
break;
|
||||
case 'minf':
|
||||
box.name = "Media Information Box";
|
||||
readRemainingBoxes();
|
||||
break;
|
||||
case 'stbl':
|
||||
box.name = "Sample Table Box";
|
||||
readRemainingBoxes();
|
||||
break;
|
||||
case 'stsd':
|
||||
box.name = "Sample Description Box";
|
||||
readFullHeader();
|
||||
box.sd = [];
|
||||
var entries = stream.readU32();
|
||||
readRemainingBoxes();
|
||||
break;
|
||||
case 'avc1':
|
||||
stream.reserved(6, 0);
|
||||
box.dataReferenceIndex = stream.readU16();
|
||||
assert (stream.readU16() == 0); // Version
|
||||
assert (stream.readU16() == 0); // Revision Level
|
||||
stream.readU32(); // Vendor
|
||||
stream.readU32(); // Temporal Quality
|
||||
stream.readU32(); // Spatial Quality
|
||||
box.width = stream.readU16();
|
||||
box.height = stream.readU16();
|
||||
box.horizontalResolution = stream.readFP16();
|
||||
box.verticalResolution = stream.readFP16();
|
||||
assert (stream.readU32() == 0); // Reserved
|
||||
box.frameCount = stream.readU16();
|
||||
box.compressorName = stream.readPString(32);
|
||||
box.depth = stream.readU16();
|
||||
assert (stream.readU16() == 0xFFFF); // Color Table Id
|
||||
readRemainingBoxes();
|
||||
break;
|
||||
case 'mp4a':
|
||||
stream.reserved(6, 0);
|
||||
box.dataReferenceIndex = stream.readU16();
|
||||
box.version = stream.readU16();
|
||||
stream.skip(2);
|
||||
stream.skip(4);
|
||||
box.channelCount = stream.readU16();
|
||||
box.sampleSize = stream.readU16();
|
||||
box.compressionId = stream.readU16();
|
||||
box.packetSize = stream.readU16();
|
||||
box.sampleRate = stream.readU32() >>> 16;
|
||||
|
||||
// TODO: Parse other version levels.
|
||||
assert (box.version == 0);
|
||||
readRemainingBoxes();
|
||||
break;
|
||||
case 'esds':
|
||||
box.name = "Elementary Stream Descriptor";
|
||||
readFullHeader();
|
||||
// TODO: Do we really need to parse this?
|
||||
skipRemainingBytes();
|
||||
break;
|
||||
case 'avcC':
|
||||
box.name = "AVC Configuration Box";
|
||||
box.configurationVersion = stream.readU8();
|
||||
box.avcProfileIndication = stream.readU8();
|
||||
box.profileCompatibility = stream.readU8();
|
||||
box.avcLevelIndication = stream.readU8();
|
||||
box.lengthSizeMinusOne = stream.readU8() & 3;
|
||||
assert (box.lengthSizeMinusOne == 3, "TODO");
|
||||
var count = stream.readU8() & 31;
|
||||
box.sps = [];
|
||||
for (var i = 0; i < count; i++) {
|
||||
box.sps.push(stream.readU8Array(stream.readU16()));
|
||||
}
|
||||
var count = stream.readU8() & 31;
|
||||
box.pps = [];
|
||||
for (var i = 0; i < count; i++) {
|
||||
box.pps.push(stream.readU8Array(stream.readU16()));
|
||||
}
|
||||
skipRemainingBytes();
|
||||
break;
|
||||
case 'btrt':
|
||||
box.name = "Bit Rate Box";
|
||||
box.bufferSizeDb = stream.readU32();
|
||||
box.maxBitrate = stream.readU32();
|
||||
box.avgBitrate = stream.readU32();
|
||||
break;
|
||||
case 'stts':
|
||||
box.name = "Decoding Time to Sample Box";
|
||||
readFullHeader();
|
||||
box.table = stream.readU32Array(stream.readU32(), 2, ["count", "delta"]);
|
||||
break;
|
||||
case 'stss':
|
||||
box.name = "Sync Sample Box";
|
||||
readFullHeader();
|
||||
box.samples = stream.readU32Array(stream.readU32());
|
||||
break;
|
||||
case 'stsc':
|
||||
box.name = "Sample to Chunk Box";
|
||||
readFullHeader();
|
||||
box.table = stream.readU32Array(stream.readU32(), 3,
|
||||
["firstChunk", "samplesPerChunk", "sampleDescriptionId"]);
|
||||
break;
|
||||
case 'stsz':
|
||||
box.name = "Sample Size Box";
|
||||
readFullHeader();
|
||||
box.sampleSize = stream.readU32();
|
||||
var count = stream.readU32();
|
||||
if (box.sampleSize == 0) {
|
||||
box.table = stream.readU32Array(count);
|
||||
}
|
||||
break;
|
||||
case 'stco':
|
||||
box.name = "Chunk Offset Box";
|
||||
readFullHeader();
|
||||
box.table = stream.readU32Array(stream.readU32());
|
||||
break;
|
||||
case 'smhd':
|
||||
box.name = "Sound Media Header Box";
|
||||
readFullHeader();
|
||||
box.balance = stream.readFP8();
|
||||
stream.reserved(2, 0);
|
||||
break;
|
||||
case 'mdat':
|
||||
box.name = "Media Data Box";
|
||||
assert (box.size >= 8, "Cannot parse large media data yet.");
|
||||
box.data = stream.readU8Array(remainingBytes());
|
||||
break;
|
||||
default:
|
||||
skipRemainingBytes();
|
||||
break;
|
||||
};
|
||||
return box;
|
||||
},
|
||||
read: function () {
|
||||
var start = (new Date).getTime();
|
||||
this.file = {};
|
||||
this.readBoxes(this.stream, this.file);
|
||||
console.info("Parsed stream in " + ((new Date).getTime() - start) + " ms");
|
||||
},
|
||||
traceSamples: function () {
|
||||
var video = this.tracks[1];
|
||||
var audio = this.tracks[2];
|
||||
|
||||
console.info("Video Samples: " + video.getSampleCount());
|
||||
console.info("Audio Samples: " + audio.getSampleCount());
|
||||
|
||||
var vi = 0;
|
||||
var ai = 0;
|
||||
|
||||
for (var i = 0; i < 100; i++) {
|
||||
var vo = video.sampleToOffset(vi);
|
||||
var ao = audio.sampleToOffset(ai);
|
||||
|
||||
var vs = video.sampleToSize(vi, 1);
|
||||
var as = audio.sampleToSize(ai, 1);
|
||||
|
||||
if (vo < ao) {
|
||||
console.info("V Sample " + vi + " Offset : " + vo + ", Size : " + vs);
|
||||
vi ++;
|
||||
} else {
|
||||
console.info("A Sample " + ai + " Offset : " + ao + ", Size : " + as);
|
||||
ai ++;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return constructor;
|
||||
})();
|
||||
|
||||
var Track = (function track () {
|
||||
function constructor(file, trak) {
|
||||
this.file = file;
|
||||
this.trak = trak;
|
||||
}
|
||||
|
||||
constructor.prototype = {
|
||||
getSampleSizeTable: function () {
|
||||
return this.trak.mdia.minf.stbl.stsz.table;
|
||||
},
|
||||
getSampleCount: function () {
|
||||
return this.getSampleSizeTable().length;
|
||||
},
|
||||
/**
|
||||
* Computes the size of a range of samples, returns zero if length is zero.
|
||||
*/
|
||||
sampleToSize: function (start, length) {
|
||||
var table = this.getSampleSizeTable();
|
||||
var size = 0;
|
||||
for (var i = start; i < start + length; i++) {
|
||||
size += table[i];
|
||||
}
|
||||
return size;
|
||||
},
|
||||
/**
|
||||
* Computes the chunk that contains the specified sample, as well as the offset of
|
||||
* the sample in the computed chunk.
|
||||
*/
|
||||
sampleToChunk: function (sample) {
|
||||
|
||||
/* Samples are grouped in chunks which may contain a variable number of samples.
|
||||
* The sample-to-chunk table in the stsc box describes how samples are arranged
|
||||
* in chunks. Each table row corresponds to a set of consecutive chunks with the
|
||||
* same number of samples and description ids. For example, the following table:
|
||||
*
|
||||
* +-------------+-------------------+----------------------+
|
||||
* | firstChunk | samplesPerChunk | sampleDescriptionId |
|
||||
* +-------------+-------------------+----------------------+
|
||||
* | 1 | 3 | 23 |
|
||||
* | 3 | 1 | 23 |
|
||||
* | 5 | 1 | 24 |
|
||||
* +-------------+-------------------+----------------------+
|
||||
*
|
||||
* describes 5 chunks with a total of (2 * 3) + (2 * 1) + (1 * 1) = 9 samples,
|
||||
* each chunk containing samples 3, 3, 1, 1, 1 in chunk order, or
|
||||
* chunks 1, 1, 1, 2, 2, 2, 3, 4, 5 in sample order.
|
||||
*
|
||||
* This function determines the chunk that contains a specified sample by iterating
|
||||
* over every entry in the table. It also returns the position of the sample in the
|
||||
* chunk which can be used to compute the sample's exact position in the file.
|
||||
*
|
||||
* TODO: Determine if we should memoize this function.
|
||||
*/
|
||||
|
||||
var table = this.trak.mdia.minf.stbl.stsc.table;
|
||||
|
||||
if (table.length === 1) {
|
||||
var row = table[0];
|
||||
assert (row.firstChunk === 1);
|
||||
return {
|
||||
index: Math.floor(sample / row.samplesPerChunk),
|
||||
offset: sample % row.samplesPerChunk
|
||||
};
|
||||
}
|
||||
|
||||
var totalChunkCount = 0;
|
||||
for (var i = 0; i < table.length; i++) {
|
||||
var row = table[i];
|
||||
if (i > 0) {
|
||||
var previousRow = table[i - 1];
|
||||
var previousChunkCount = row.firstChunk - previousRow.firstChunk;
|
||||
var previousSampleCount = previousRow.samplesPerChunk * previousChunkCount;
|
||||
if (sample >= previousSampleCount) {
|
||||
sample -= previousSampleCount;
|
||||
if (i == table.length - 1) {
|
||||
return {
|
||||
index: totalChunkCount + previousChunkCount + Math.floor(sample / row.samplesPerChunk),
|
||||
offset: sample % row.samplesPerChunk
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
index: totalChunkCount + Math.floor(sample / previousRow.samplesPerChunk),
|
||||
offset: sample % previousRow.samplesPerChunk
|
||||
};
|
||||
}
|
||||
totalChunkCount += previousChunkCount;
|
||||
}
|
||||
}
|
||||
assert(false);
|
||||
},
|
||||
chunkToOffset: function (chunk) {
|
||||
var table = this.trak.mdia.minf.stbl.stco.table;
|
||||
return table[chunk];
|
||||
},
|
||||
sampleToOffset: function (sample) {
|
||||
var res = this.sampleToChunk(sample);
|
||||
var offset = this.chunkToOffset(res.index);
|
||||
return offset + this.sampleToSize(sample - res.offset, res.offset);
|
||||
},
|
||||
/**
|
||||
* Computes the sample at the specified time.
|
||||
*/
|
||||
timeToSample: function (time) {
|
||||
/* In the time-to-sample table samples are grouped by their duration. The count field
|
||||
* indicates the number of consecutive samples that have the same duration. For example,
|
||||
* the following table:
|
||||
*
|
||||
* +-------+-------+
|
||||
* | count | delta |
|
||||
* +-------+-------+
|
||||
* | 4 | 3 |
|
||||
* | 2 | 1 |
|
||||
* | 3 | 2 |
|
||||
* +-------+-------+
|
||||
*
|
||||
* describes 9 samples with a total time of (4 * 3) + (2 * 1) + (3 * 2) = 20.
|
||||
*
|
||||
* This function determines the sample at the specified time by iterating over every
|
||||
* entry in the table.
|
||||
*
|
||||
* TODO: Determine if we should memoize this function.
|
||||
*/
|
||||
var table = this.trak.mdia.minf.stbl.stts.table;
|
||||
var sample = 0;
|
||||
for (var i = 0; i < table.length; i++) {
|
||||
var delta = table[i].count * table[i].delta;
|
||||
if (time >= delta) {
|
||||
time -= delta;
|
||||
sample += table[i].count;
|
||||
} else {
|
||||
return sample + Math.floor(time / table[i].delta);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Gets the total time of the track.
|
||||
*/
|
||||
getTotalTime: function () {
|
||||
if (PARANOID) {
|
||||
var table = this.trak.mdia.minf.stbl.stts.table;
|
||||
var duration = 0;
|
||||
for (var i = 0; i < table.length; i++) {
|
||||
duration += table[i].count * table[i].delta;
|
||||
}
|
||||
assert (this.trak.mdia.mdhd.duration == duration);
|
||||
}
|
||||
return this.trak.mdia.mdhd.duration;
|
||||
},
|
||||
getTotalTimeInSeconds: function () {
|
||||
return this.timeToSeconds(this.getTotalTime());
|
||||
},
|
||||
getTimeScale: function () {
|
||||
return this.trak.mdia.mdhd.timeScale;
|
||||
},
|
||||
/**
|
||||
* Converts time units to real time (seconds).
|
||||
*/
|
||||
timeToSeconds: function (time) {
|
||||
return time / this.getTimeScale();
|
||||
},
|
||||
/**
|
||||
* Converts real time (seconds) to time units.
|
||||
*/
|
||||
secondsToTime: function (seconds) {
|
||||
return seconds * this.getTimeScale();
|
||||
},
|
||||
foo: function () {
|
||||
/*
|
||||
for (var i = 0; i < this.getSampleCount(); i++) {
|
||||
var res = this.sampleToChunk(i);
|
||||
console.info("Sample " + i + " -> " + res.index + " % " + res.offset +
|
||||
" @ " + this.chunkToOffset(res.index) +
|
||||
" @@ " + this.sampleToOffset(i));
|
||||
}
|
||||
console.info("Total Time: " + this.timeToSeconds(this.getTotalTime()));
|
||||
var total = this.getTotalTimeInSeconds();
|
||||
for (var i = 50; i < total; i += 0.1) {
|
||||
// console.info("Time: " + i.toFixed(2) + " " + this.secondsToTime(i));
|
||||
|
||||
console.info("Time: " + i.toFixed(2) + " " + this.timeToSample(this.secondsToTime(i)));
|
||||
}
|
||||
*/
|
||||
},
|
||||
/**
|
||||
* AVC samples contain one or more NAL units each of which have a length prefix.
|
||||
* This function returns an array of NAL units without their length prefixes.
|
||||
*/
|
||||
getSampleNALUnits: function (sample) {
|
||||
var bytes = this.file.stream.bytes;
|
||||
var offset = this.sampleToOffset(sample);
|
||||
var end = offset + this.sampleToSize(sample, 1);
|
||||
var nalUnits = [];
|
||||
while(end - offset > 0) {
|
||||
var length = (new Bytestream(bytes.buffer, offset)).readU32();
|
||||
nalUnits.push(bytes.subarray(offset + 4, offset + length + 4));
|
||||
offset = offset + length + 4;
|
||||
}
|
||||
return nalUnits;
|
||||
}
|
||||
};
|
||||
return constructor;
|
||||
})();
|
||||
|
||||
|
||||
// Only add setZeroTimeout to the window object, and hide everything
|
||||
// else in a closure. (http://dbaron.org/log/20100309-faster-timeouts)
|
||||
(function() {
|
||||
var timeouts = [];
|
||||
var messageName = "zero-timeout-message";
|
||||
|
||||
// Like setTimeout, but only takes a function argument. There's
|
||||
// no time argument (always zero) and no arguments (you have to
|
||||
// use a closure).
|
||||
function setZeroTimeout(fn) {
|
||||
timeouts.push(fn);
|
||||
window.postMessage(messageName, "*");
|
||||
}
|
||||
|
||||
function handleMessage(event) {
|
||||
if (event.source == window && event.data == messageName) {
|
||||
event.stopPropagation();
|
||||
if (timeouts.length > 0) {
|
||||
var fn = timeouts.shift();
|
||||
fn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage, true);
|
||||
|
||||
// Add the one thing we want added to the window object.
|
||||
window.setZeroTimeout = setZeroTimeout;
|
||||
})();
|
||||
|
||||
var MP4Player = (function reader() {
|
||||
var defaultConfig = {
|
||||
filter: "original",
|
||||
filterHorLuma: "optimized",
|
||||
filterVerLumaEdge: "optimized",
|
||||
getBoundaryStrengthsA: "optimized"
|
||||
};
|
||||
|
||||
function constructor(stream, useWorkers, webgl, render) {
|
||||
this.stream = stream;
|
||||
this.useWorkers = useWorkers;
|
||||
this.webgl = webgl;
|
||||
this.render = render;
|
||||
|
||||
this.statistics = {
|
||||
videoStartTime: 0,
|
||||
videoPictureCounter: 0,
|
||||
windowStartTime: 0,
|
||||
windowPictureCounter: 0,
|
||||
fps: 0,
|
||||
fpsMin: 1000,
|
||||
fpsMax: -1000,
|
||||
webGLTextureUploadTime: 0
|
||||
};
|
||||
|
||||
this.onStatisticsUpdated = function () {};
|
||||
|
||||
this.avc = new Player({
|
||||
useWorker: useWorkers,
|
||||
reuseMemory: true,
|
||||
webgl: webgl,
|
||||
size: {
|
||||
width: 640,
|
||||
height: 368
|
||||
}
|
||||
});
|
||||
|
||||
this.webgl = this.avc.webgl;
|
||||
|
||||
var self = this;
|
||||
this.avc.onPictureDecoded = function(){
|
||||
updateStatistics.call(self);
|
||||
};
|
||||
|
||||
this.canvas = this.avc.canvas;
|
||||
}
|
||||
|
||||
function updateStatistics() {
|
||||
var s = this.statistics;
|
||||
s.videoPictureCounter += 1;
|
||||
s.windowPictureCounter += 1;
|
||||
var now = Date.now();
|
||||
if (!s.videoStartTime) {
|
||||
s.videoStartTime = now;
|
||||
}
|
||||
var videoElapsedTime = now - s.videoStartTime;
|
||||
s.elapsed = videoElapsedTime / 1000;
|
||||
if (videoElapsedTime < 1000) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!s.windowStartTime) {
|
||||
s.windowStartTime = now;
|
||||
return;
|
||||
} else if ((now - s.windowStartTime) > 1000) {
|
||||
var windowElapsedTime = now - s.windowStartTime;
|
||||
var fps = (s.windowPictureCounter / windowElapsedTime) * 1000;
|
||||
s.windowStartTime = now;
|
||||
s.windowPictureCounter = 0;
|
||||
|
||||
if (fps < s.fpsMin) s.fpsMin = fps;
|
||||
if (fps > s.fpsMax) s.fpsMax = fps;
|
||||
s.fps = fps;
|
||||
}
|
||||
|
||||
var fps = (s.videoPictureCounter / videoElapsedTime) * 1000;
|
||||
s.fpsSinceStart = fps;
|
||||
this.onStatisticsUpdated(this.statistics);
|
||||
return;
|
||||
}
|
||||
|
||||
constructor.prototype = {
|
||||
readAll: function(callback) {
|
||||
console.info("MP4Player::readAll()");
|
||||
this.stream.readAll(null, function (buffer) {
|
||||
this.reader = new MP4Reader(new Bytestream(buffer));
|
||||
this.reader.read();
|
||||
var video = this.reader.tracks[1];
|
||||
this.size = new Size(video.trak.tkhd.width, video.trak.tkhd.height);
|
||||
console.info("MP4Player::readAll(), length: " + this.reader.stream.length);
|
||||
if (callback) callback();
|
||||
}.bind(this));
|
||||
},
|
||||
play: function() {
|
||||
var reader = this.reader;
|
||||
|
||||
if (!reader) {
|
||||
this.readAll(this.play.bind(this));
|
||||
return;
|
||||
};
|
||||
|
||||
var video = reader.tracks[1];
|
||||
var audio = reader.tracks[2];
|
||||
|
||||
var avc = reader.tracks[1].trak.mdia.minf.stbl.stsd.avc1.avcC;
|
||||
var sps = avc.sps[0];
|
||||
var pps = avc.pps[0];
|
||||
|
||||
/* Decode Sequence & Picture Parameter Sets */
|
||||
this.avc.decode(sps);
|
||||
this.avc.decode(pps);
|
||||
|
||||
/* Decode Pictures */
|
||||
var pic = 0;
|
||||
setTimeout(function foo() {
|
||||
var avc = this.avc;
|
||||
video.getSampleNALUnits(pic).forEach(function (nal) {
|
||||
avc.decode(nal);
|
||||
});
|
||||
pic ++;
|
||||
if (pic < 3000) {
|
||||
setTimeout(foo.bind(this), 1);
|
||||
};
|
||||
}.bind(this), 1);
|
||||
}
|
||||
};
|
||||
|
||||
return constructor;
|
||||
})();
|
||||
|
||||
var Broadway = (function broadway() {
|
||||
function constructor(div) {
|
||||
var src = div.attributes.src ? div.attributes.src.value : undefined;
|
||||
var width = div.attributes.width ? div.attributes.width.value : 640;
|
||||
var height = div.attributes.height ? div.attributes.height.value : 480;
|
||||
|
||||
var controls = document.createElement('div');
|
||||
controls.setAttribute('style', "z-index: 100; position: absolute; bottom: 0px; background-color: rgba(0,0,0,0.8); height: 30px; width: 100%; text-align: left;");
|
||||
this.info = document.createElement('div');
|
||||
this.info.setAttribute('style', "font-size: 14px; font-weight: bold; padding: 6px; color: lime;");
|
||||
controls.appendChild(this.info);
|
||||
div.appendChild(controls);
|
||||
|
||||
var useWorkers = div.attributes.workers ? div.attributes.workers.value == "true" : false;
|
||||
var render = div.attributes.render ? div.attributes.render.value == "true" : false;
|
||||
|
||||
var webgl = "auto";
|
||||
if (div.attributes.webgl){
|
||||
if (div.attributes.webgl.value == "true"){
|
||||
webgl = true;
|
||||
};
|
||||
if (div.attributes.webgl.value == "false"){
|
||||
webgl = false;
|
||||
};
|
||||
};
|
||||
|
||||
var infoStrPre = "Click canvas to load and play - ";
|
||||
var infoStr = "";
|
||||
if (useWorkers){
|
||||
infoStr += "worker thread ";
|
||||
}else{
|
||||
infoStr += "main thread ";
|
||||
};
|
||||
|
||||
this.player = new MP4Player(new Stream(src), useWorkers, webgl, render);
|
||||
this.canvas = this.player.canvas;
|
||||
this.canvas.onclick = function () {
|
||||
this.play();
|
||||
}.bind(this);
|
||||
div.appendChild(this.canvas);
|
||||
|
||||
|
||||
infoStr += " - webgl: " + this.player.webgl;
|
||||
this.info.innerHTML = infoStrPre + infoStr;
|
||||
|
||||
|
||||
this.score = null;
|
||||
this.player.onStatisticsUpdated = function (statistics) {
|
||||
if (statistics.videoPictureCounter % 10 != 0) {
|
||||
return;
|
||||
}
|
||||
var info = "";
|
||||
if (statistics.fps) {
|
||||
info += " fps: " + statistics.fps.toFixed(2);
|
||||
}
|
||||
if (statistics.fpsSinceStart) {
|
||||
info += " avg: " + statistics.fpsSinceStart.toFixed(2);
|
||||
}
|
||||
var scoreCutoff = 1200;
|
||||
if (statistics.videoPictureCounter < scoreCutoff) {
|
||||
this.score = scoreCutoff - statistics.videoPictureCounter;
|
||||
} else if (statistics.videoPictureCounter == scoreCutoff) {
|
||||
this.score = statistics.fpsSinceStart.toFixed(2);
|
||||
}
|
||||
// info += " score: " + this.score;
|
||||
|
||||
this.info.innerHTML = infoStr + info;
|
||||
}.bind(this);
|
||||
}
|
||||
constructor.prototype = {
|
||||
play: function () {
|
||||
this.player.play();
|
||||
}
|
||||
};
|
||||
return constructor;
|
||||
})();
|
||||
|
||||
|
||||
return {
|
||||
Size,
|
||||
Track,
|
||||
MP4Reader,
|
||||
MP4Player,
|
||||
Bytestream,
|
||||
Broadway,
|
||||
}
|
||||
|
||||
})();
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,350 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:true
|
||||
*/
|
||||
|
||||
const { Mutex } = require('async-mutex');
|
||||
// eslint-disable-next-line max-classes-per-file
|
||||
const { MP4Reader, Bytestream } = require('./3rdparty/mp4');
|
||||
const ZipDecoder = require('./unzip_imgs.worker');
|
||||
const H264Decoder = require('./3rdparty/Decoder.worker');
|
||||
|
||||
const BlockType = Object.freeze({
|
||||
MP4VIDEO: 'mp4video',
|
||||
ARCHIVE: 'archive',
|
||||
});
|
||||
|
||||
class FrameProvider {
|
||||
constructor(blockType, blockSize, cachedBlockCount,
|
||||
decodedBlocksCacheSize = 5, maxWorkerThreadCount = 2) {
|
||||
this._frames = {};
|
||||
this._cachedBlockCount = Math.max(1, cachedBlockCount); // number of stored blocks
|
||||
this._decodedBlocksCacheSize = decodedBlocksCacheSize;
|
||||
this._blocksRanges = [];
|
||||
this._blocks = {};
|
||||
this._blockSize = blockSize;
|
||||
this._running = false;
|
||||
this._blockType = blockType;
|
||||
this._currFrame = -1;
|
||||
this._requestedBlockDecode = null;
|
||||
this._width = null;
|
||||
this._height = null;
|
||||
this._decodingBlocks = {};
|
||||
this._decodeThreadCount = 0;
|
||||
this._timerId = setTimeout(this._worker.bind(this), 100);
|
||||
this._mutex = new Mutex();
|
||||
this._promisedFrames = {};
|
||||
this._maxWorkerThreadCount = maxWorkerThreadCount;
|
||||
}
|
||||
|
||||
async _worker() {
|
||||
if (this._requestedBlockDecode !== null
|
||||
&& this._decodeThreadCount < this._maxWorkerThreadCount) {
|
||||
await this.startDecode();
|
||||
}
|
||||
this._timerId = setTimeout(this._worker.bind(this), 100);
|
||||
}
|
||||
|
||||
isChunkCached(start, end) {
|
||||
return (`${start}:${end}` in this._blocksRanges);
|
||||
}
|
||||
|
||||
/* This method removes extra data from a cache when memory overflow */
|
||||
async _cleanup() {
|
||||
if (this._blocksRanges.length > this._cachedBlockCount) {
|
||||
const shifted = this._blocksRanges.shift(); // get the oldest block
|
||||
const [start, end] = shifted.split(':').map((el) => +el);
|
||||
delete this._blocks[start / this._blockSize];
|
||||
for (let i = start; i <= end; i++) {
|
||||
delete this._frames[i];
|
||||
}
|
||||
}
|
||||
|
||||
// delete frames whose are not in areas of current frame
|
||||
const distance = Math.floor(this._decodedBlocksCacheSize / 2);
|
||||
for (let i = 0; i < this._blocksRanges.length; i++) {
|
||||
const [start, end] = this._blocksRanges[i].split(':').map((el) => +el);
|
||||
if (end < this._currFrame - distance * this._blockSize
|
||||
|| start > this._currFrame + distance * this._blockSize) {
|
||||
for (let j = start; j <= end; j++) {
|
||||
delete this._frames[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async requestDecodeBlock(block, start, end, resolveCallback, rejectCallback) {
|
||||
const release = await this._mutex.acquire();
|
||||
try {
|
||||
if (this._requestedBlockDecode !== null) {
|
||||
if (start === this._requestedBlockDecode.start
|
||||
&& end === this._requestedBlockDecode.end) {
|
||||
this._requestedBlockDecode.resolveCallback = resolveCallback;
|
||||
this._requestedBlockDecode.rejectCallback = rejectCallback;
|
||||
} else if (this._requestedBlockDecode.rejectCallback) {
|
||||
this._requestedBlockDecode.rejectCallback();
|
||||
}
|
||||
}
|
||||
if (!(`${start}:${end}` in this._decodingBlocks)) {
|
||||
this._requestedBlockDecode = {
|
||||
block: block || this._blocks[Math.floor(start / this._blockSize)],
|
||||
start,
|
||||
end,
|
||||
resolveCallback,
|
||||
rejectCallback,
|
||||
};
|
||||
} else {
|
||||
this._decodingBlocks[`${start}:${end}`].rejectCallback = rejectCallback;
|
||||
this._decodingBlocks[`${start}:${end}`].resolveCallback = resolveCallback;
|
||||
}
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
isRequestExist() {
|
||||
return this._requestedBlockDecode !== null;
|
||||
}
|
||||
|
||||
setRenderSize(width, height) {
|
||||
this._width = width;
|
||||
this._height = height;
|
||||
}
|
||||
|
||||
/* Method returns frame from collection. Else method returns 0 */
|
||||
async frame(frameNumber) {
|
||||
this._currFrame = frameNumber;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (frameNumber in this._frames) {
|
||||
if (this._frames[frameNumber] !== null) {
|
||||
resolve(this._frames[frameNumber]);
|
||||
} else {
|
||||
this._promisedFrames[frameNumber] = {
|
||||
resolve,
|
||||
reject,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isNextChunkExists(frameNumber) {
|
||||
const nextChunkNum = Math.floor(frameNumber / this._blockSize) + 1;
|
||||
if (this._blocks[nextChunkNum] === 'loading') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return nextChunkNum in this._blocks;
|
||||
}
|
||||
|
||||
/*
|
||||
Method start asynchronic decode a block of data
|
||||
|
||||
@param block - is a data from a server as is (ts file or archive)
|
||||
@param start {number} - is the first frame of a block
|
||||
@param end {number} - is the last frame of a block + 1
|
||||
@param callback - callback)
|
||||
|
||||
*/
|
||||
|
||||
setReadyToLoading(chunkNumber) {
|
||||
this._blocks[chunkNumber] = 'loading';
|
||||
}
|
||||
|
||||
static cropImage(imageBuffer, imageWidth, imageHeight, xOffset, yOffset, width, height) {
|
||||
if (xOffset === 0 && width === imageWidth
|
||||
&& yOffset === 0 && height === imageHeight) {
|
||||
return new ImageData(new Uint8ClampedArray(imageBuffer), width, height);
|
||||
}
|
||||
const source = new Uint32Array(imageBuffer);
|
||||
|
||||
const bufferSize = width * height * 4;
|
||||
const buffer = new ArrayBuffer(bufferSize);
|
||||
const rgbaInt32 = new Uint32Array(buffer);
|
||||
const rgbaInt8Clamped = new Uint8ClampedArray(buffer);
|
||||
|
||||
if (imageWidth === width) {
|
||||
return new ImageData(
|
||||
new Uint8ClampedArray(imageBuffer, yOffset * 4, bufferSize),
|
||||
width,
|
||||
height,
|
||||
);
|
||||
}
|
||||
|
||||
let writeIdx = 0;
|
||||
for (let row = yOffset; row < height; row++) {
|
||||
const start = row * imageWidth + xOffset;
|
||||
rgbaInt32.set(source.subarray(start, start + width), writeIdx);
|
||||
writeIdx += width;
|
||||
}
|
||||
|
||||
return new ImageData(rgbaInt8Clamped, width, height);
|
||||
}
|
||||
|
||||
async startDecode() {
|
||||
const release = await this._mutex.acquire();
|
||||
try {
|
||||
const height = this._height;
|
||||
const width = this._width;
|
||||
const { start, end, block } = this._requestedBlockDecode;
|
||||
|
||||
this._blocksRanges.push(`${start}:${end}`);
|
||||
this._decodingBlocks[`${start}:${end}`] = this._requestedBlockDecode;
|
||||
this._requestedBlockDecode = null;
|
||||
this._blocks[Math.floor((start + 1) / this._blockSize)] = block;
|
||||
for (let i = start; i <= end; i++) {
|
||||
this._frames[i] = null;
|
||||
}
|
||||
this._cleanup();
|
||||
if (this._blockType === BlockType.MP4VIDEO) {
|
||||
const worker = new H264Decoder();
|
||||
let index = start;
|
||||
|
||||
worker.onmessage = (e) => {
|
||||
if (e.data.consoleLog) { // ignore initialization message
|
||||
return;
|
||||
}
|
||||
|
||||
const scaleFactor = Math.ceil(this._height / e.data.height);
|
||||
this._frames[index] = FrameProvider.cropImage(
|
||||
e.data.buf, e.data.width, e.data.height, 0, 0,
|
||||
Math.floor(width / scaleFactor), Math.floor(height / scaleFactor),
|
||||
);
|
||||
|
||||
if (this._decodingBlocks[`${start}:${end}`].resolveCallback) {
|
||||
this._decodingBlocks[`${start}:${end}`].resolveCallback(index);
|
||||
}
|
||||
|
||||
if (index in this._promisedFrames) {
|
||||
this._promisedFrames[index].resolve(this._frames[index]);
|
||||
delete this._promisedFrames[index];
|
||||
}
|
||||
if (index === end) {
|
||||
this._decodeThreadCount--;
|
||||
delete this._decodingBlocks[`${start}:${end}`];
|
||||
worker.terminate();
|
||||
}
|
||||
index++;
|
||||
};
|
||||
|
||||
worker.onerror = (e) => {
|
||||
worker.terminate();
|
||||
this._decodeThreadCount--;
|
||||
|
||||
for (let i = index; i <= end; i++) {
|
||||
if (i in this._promisedFrames) {
|
||||
this._promisedFrames[i].reject();
|
||||
delete this._promisedFrames[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (this._decodingBlocks[`${start}:${end}`].rejectCallback) {
|
||||
this._decodingBlocks[`${start}:${end}`].rejectCallback(Error(e));
|
||||
}
|
||||
delete this._decodingBlocks[`${start}:${end}`];
|
||||
};
|
||||
|
||||
worker.postMessage({
|
||||
type: 'Broadway.js - Worker init',
|
||||
options: {
|
||||
rgb: true,
|
||||
reuseMemory: false,
|
||||
},
|
||||
});
|
||||
|
||||
const reader = new MP4Reader(new Bytestream(block));
|
||||
reader.read();
|
||||
const video = reader.tracks[1];
|
||||
|
||||
const avc = reader.tracks[1].trak.mdia.minf.stbl.stsd.avc1.avcC;
|
||||
const sps = avc.sps[0];
|
||||
const pps = avc.pps[0];
|
||||
|
||||
/* Decode Sequence & Picture Parameter Sets */
|
||||
worker.postMessage({ buf: sps, offset: 0, length: sps.length });
|
||||
worker.postMessage({ buf: pps, offset: 0, length: pps.length });
|
||||
|
||||
/* Decode Pictures */
|
||||
for (let sample = 0; sample < video.getSampleCount(); sample++) {
|
||||
video.getSampleNALUnits(sample).forEach((nal) => {
|
||||
worker.postMessage({ buf: nal, offset: 0, length: nal.length });
|
||||
});
|
||||
}
|
||||
this._decodeThreadCount++;
|
||||
} else {
|
||||
const worker = new ZipDecoder();
|
||||
let index = start;
|
||||
|
||||
worker.onerror = (e) => {
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (i in this._promisedFrames) {
|
||||
this._promisedFrames[i].reject();
|
||||
delete this._promisedFrames[i];
|
||||
}
|
||||
}
|
||||
if (this._decodingBlocks[`${start}:${end}`].rejectCallback) {
|
||||
this._decodingBlocks[`${start}:${end}`].rejectCallback(Error(e));
|
||||
}
|
||||
this._decodeThreadCount--;
|
||||
worker.terminate();
|
||||
};
|
||||
|
||||
worker.onmessage = (event) => {
|
||||
this._frames[event.data.index] = event.data.data;
|
||||
|
||||
if (this._decodingBlocks[`${start}:${end}`].resolveCallback) {
|
||||
this._decodingBlocks[`${start}:${end}`].resolveCallback(event.data.index);
|
||||
}
|
||||
|
||||
if (event.data.index in this._promisedFrames) {
|
||||
this._promisedFrames[event.data.index].resolve(
|
||||
this._frames[event.data.index],
|
||||
);
|
||||
delete this._promisedFrames[event.data.index];
|
||||
}
|
||||
|
||||
if (index === end) {
|
||||
worker.terminate();
|
||||
delete this._decodingBlocks[`${start}:${end}`];
|
||||
this._decodeThreadCount--;
|
||||
}
|
||||
index++;
|
||||
};
|
||||
|
||||
worker.postMessage({ block, start, end });
|
||||
this._decodeThreadCount++;
|
||||
}
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
get decodeThreadCount() {
|
||||
return this._decodeThreadCount;
|
||||
}
|
||||
|
||||
get decodedBlocksCacheSize() {
|
||||
return this._decodedBlocksCacheSize;
|
||||
}
|
||||
|
||||
/*
|
||||
Method returns a list of cached ranges
|
||||
Is an array of strings like "start:end"
|
||||
*/
|
||||
get cachedFrames() {
|
||||
return [...this._blocksRanges].sort(
|
||||
(a, b) => a.split(':')[0] - b.split(':')[0],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FrameProvider,
|
||||
BlockType,
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:true
|
||||
*/
|
||||
|
||||
const JSZip = require('jszip');
|
||||
|
||||
onmessage = (e) => {
|
||||
const zip = new JSZip();
|
||||
if (e.data) {
|
||||
const { start, end, block } = e.data;
|
||||
|
||||
zip.loadAsync(block).then((_zip) => {
|
||||
let index = start;
|
||||
_zip.forEach((relativePath) => {
|
||||
const fileIndex = index++;
|
||||
if (fileIndex <= end) {
|
||||
_zip.file(relativePath).async('blob').then((fileData) => {
|
||||
createImageBitmap(fileData).then((img) => {
|
||||
postMessage({
|
||||
fileName: relativePath,
|
||||
index: fileIndex,
|
||||
data: img,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,64 @@
|
||||
/* global
|
||||
require:true,
|
||||
__dirname:true,
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
const cvatData = {
|
||||
target: 'web',
|
||||
mode: 'production',
|
||||
entry: './src/js/cvat-data.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'cvat-data.min.js',
|
||||
library: 'cvatData',
|
||||
libraryTarget: 'window',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /.js?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
['@babel/preset-env', {
|
||||
targets: '> 2.5%', // https://github.com/browserslist/browserslist
|
||||
}],
|
||||
],
|
||||
sourceType: 'unambiguous',
|
||||
},
|
||||
},
|
||||
}, {
|
||||
test: /\.worker\.js$/,
|
||||
exclude: /3rdparty/,
|
||||
use: {
|
||||
loader: 'worker-loader',
|
||||
options: {
|
||||
publicPath: '/',
|
||||
name: '[name].js',
|
||||
},
|
||||
},
|
||||
}, {
|
||||
test: /3rdparty\/.*\.worker\.js$/,
|
||||
use: {
|
||||
loader: 'worker-loader',
|
||||
options: {
|
||||
publicPath: '/3rdparty/',
|
||||
name: '3rdparty/[name].js',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new CopyPlugin([
|
||||
'./src/js/3rdparty/avc.wasm',
|
||||
]),
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = cvatData;
|
||||
@ -0,0 +1,37 @@
|
||||
# cvat-ui module
|
||||
|
||||
## Description
|
||||
This is a client UI for Computer Vision Annotation Tool based on React, Redux and Antd
|
||||
|
||||
## Versioning
|
||||
If you make changes in this package, please do following:
|
||||
|
||||
- After not important changes (typos, bug fixes, refactoring) do: ``npm version patch``
|
||||
- After adding new features do: ``npm version minor``
|
||||
- After significant UI redesign do: ``npm version major``
|
||||
|
||||
Important: If you have changed versions for ``cvat-core``, ``cvat-canvas``, ``cvat-data``,
|
||||
you also need to do ``npm install`` to update ``package-lock.json``
|
||||
|
||||
## Commands
|
||||
- Installing dependencies:
|
||||
|
||||
```bash
|
||||
cd ../cvat-core && npm install && cd - && npm install
|
||||
```
|
||||
|
||||
- Running development UI server with autorebuild on change
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
- Building the module from sources in the ```dist``` directory:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run build -- --mode=development # without a minification
|
||||
```
|
||||
|
||||
Important: You also have to run CVAT REST API server (please read ``CONTRIBUTING.md``)
|
||||
to correct working since UI gets all necessary data (tasks, users, annotations) from there
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,88 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import {
|
||||
ActionUnion,
|
||||
createAction,
|
||||
ThunkAction,
|
||||
ThunkDispatch,
|
||||
} from 'utils/redux';
|
||||
import getCore from 'cvat-core-wrapper';
|
||||
import { LogType } from 'cvat-logger';
|
||||
import { computeZRange } from './annotation-actions';
|
||||
|
||||
const cvat = getCore();
|
||||
|
||||
export enum BoundariesActionTypes {
|
||||
RESET_AFTER_ERROR = 'RESET_AFTER_ERROR',
|
||||
THROW_RESET_ERROR = 'THROW_RESET_ERROR',
|
||||
}
|
||||
|
||||
export const boundariesActions = {
|
||||
resetAfterError: (
|
||||
job: any,
|
||||
states: any[],
|
||||
frameNumber: number,
|
||||
frameData: any | null,
|
||||
minZ: number,
|
||||
maxZ: number,
|
||||
colors: string[],
|
||||
) => createAction(BoundariesActionTypes.RESET_AFTER_ERROR, {
|
||||
job,
|
||||
states,
|
||||
frameNumber,
|
||||
frameData,
|
||||
minZ,
|
||||
maxZ,
|
||||
colors,
|
||||
}),
|
||||
throwResetError: () => createAction(BoundariesActionTypes.THROW_RESET_ERROR),
|
||||
};
|
||||
|
||||
export function resetAfterErrorAsync(): ThunkAction {
|
||||
return async (dispatch: ThunkDispatch, getState): Promise<void> => {
|
||||
try {
|
||||
const state = getState();
|
||||
const job = state.annotation.job.instance;
|
||||
|
||||
if (job) {
|
||||
const currentFrame = state.annotation.player.frame.number;
|
||||
const { showAllInterpolationTracks } = state.settings.workspace;
|
||||
const frameNumber = Math.max(Math.min(job.stopFrame, currentFrame), job.startFrame);
|
||||
|
||||
const states = await job.annotations
|
||||
.get(frameNumber, showAllInterpolationTracks, []);
|
||||
const frameData = await job.frames.get(frameNumber);
|
||||
const [minZ, maxZ] = computeZRange(states);
|
||||
const colors = [...cvat.enums.colors];
|
||||
|
||||
await job.logger.log(LogType.restoreJob);
|
||||
|
||||
dispatch(boundariesActions.resetAfterError(
|
||||
job,
|
||||
states,
|
||||
frameNumber,
|
||||
frameData,
|
||||
minZ,
|
||||
maxZ,
|
||||
colors,
|
||||
));
|
||||
} else {
|
||||
dispatch(boundariesActions.resetAfterError(
|
||||
null,
|
||||
[],
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
[],
|
||||
));
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(boundariesActions.throwResetError());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type boundariesActions = ActionUnion<typeof boundariesActions>;
|
||||
@ -0,0 +1,38 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
|
||||
import getCore from 'cvat-core-wrapper';
|
||||
import { UserAgreement } from 'reducers/interfaces'
|
||||
|
||||
const core = getCore();
|
||||
|
||||
export enum UserAgreementsActionTypes {
|
||||
GET_USER_AGREEMENTS = 'GET_USER_AGREEMENTS',
|
||||
GET_USER_AGREEMENTS_SUCCESS = 'GET_USER_AGREEMENTS_SUCCESS',
|
||||
GET_USER_AGREEMENTS_FAILED = 'GET_USER_AGREEMENTS_FAILED',
|
||||
}
|
||||
|
||||
const userAgreementsActions = {
|
||||
getUserAgreements: () => createAction(UserAgreementsActionTypes.GET_USER_AGREEMENTS),
|
||||
getUserAgreementsSuccess: (userAgreements: UserAgreement[]) =>
|
||||
createAction(UserAgreementsActionTypes.GET_USER_AGREEMENTS_SUCCESS, userAgreements),
|
||||
getUserAgreementsFailed: (error: any) =>
|
||||
createAction(UserAgreementsActionTypes.GET_USER_AGREEMENTS_FAILED, { error }),
|
||||
};
|
||||
|
||||
export type UserAgreementsActions = ActionUnion<typeof userAgreementsActions>;
|
||||
|
||||
export const getUserAgreementsAsync = (): ThunkAction => async (dispatch): Promise<void> => {
|
||||
dispatch(userAgreementsActions.getUserAgreements());
|
||||
|
||||
try {
|
||||
const userAgreements = await core.server.userAgreements();
|
||||
dispatch(
|
||||
userAgreementsActions.getUserAgreementsSuccess(userAgreements),
|
||||
);
|
||||
} catch (error) {
|
||||
dispatch(userAgreementsActions.getUserAgreementsFailed(error));
|
||||
}
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 13L2.34921 12.2407L2 12.5401V13H3ZM3 33H2V34H3V33ZM30 33V34H30.3699L30.6508 33.7593L30 33ZM37 27L37.6508 27.7593L38 27.4599V27H37ZM37 7H38V6H37V7ZM10 7V6H9.63008L9.34921 6.24074L10 7ZM2 13V33H4V13H2ZM3 34H30V32H3V34ZM30.6508 33.7593L37.6508 27.7593L36.3492 26.2407L29.3492 32.2407L30.6508 33.7593ZM38 27V7H36V27H38ZM36.3492 6.24074L29.3492 12.2407L30.6508 13.7593L37.6508 7.75926L36.3492 6.24074ZM30 12H3V14H30V12ZM31 33V13H29V33H31ZM3.65079 13.7593L10.6508 7.75926L9.34921 6.24074L2.34921 12.2407L3.65079 13.7593ZM10 8H37V6H10V8Z" fill="black"/></svg>
|
||||
|
After Width: | Height: | Size: 660 B |
@ -0,0 +1,189 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Select, { SelectValue, LabeledValue } from 'antd/lib/select';
|
||||
import Title from 'antd/lib/typography/Title';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import Paragraph from 'antd/lib/typography/Paragraph';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Icon from 'antd/lib/icon';
|
||||
|
||||
import {
|
||||
changeAnnotationsFilters as changeAnnotationsFiltersAction,
|
||||
fetchAnnotationsAsync,
|
||||
} from 'actions/annotation-actions';
|
||||
import { CombinedState } from 'reducers/interfaces';
|
||||
|
||||
interface StateToProps {
|
||||
annotationsFilters: string[];
|
||||
annotationsFiltersHistory: string[];
|
||||
searchForwardShortcut: string;
|
||||
searchBackwardShortcut: string;
|
||||
}
|
||||
|
||||
interface DispatchToProps {
|
||||
changeAnnotationsFilters(value: SelectValue): void;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: CombinedState): StateToProps {
|
||||
const {
|
||||
annotation: {
|
||||
annotations: {
|
||||
filters: annotationsFilters,
|
||||
filtersHistory: annotationsFiltersHistory,
|
||||
},
|
||||
},
|
||||
shortcuts: {
|
||||
normalizedKeyMap,
|
||||
},
|
||||
} = state;
|
||||
|
||||
return {
|
||||
annotationsFilters,
|
||||
annotationsFiltersHistory,
|
||||
searchForwardShortcut: normalizedKeyMap.SEARCH_FORWARD,
|
||||
searchBackwardShortcut: normalizedKeyMap.SEARCH_BACKWARD,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||
return {
|
||||
changeAnnotationsFilters(value: SelectValue) {
|
||||
if (typeof (value) === 'string') {
|
||||
dispatch(changeAnnotationsFiltersAction([value]));
|
||||
dispatch(fetchAnnotationsAsync());
|
||||
} else if (Array.isArray(value)
|
||||
&& value.every((element: string | number | LabeledValue): boolean => (
|
||||
typeof (element) === 'string'
|
||||
))
|
||||
) {
|
||||
dispatch(changeAnnotationsFiltersAction(value as string[]));
|
||||
dispatch(fetchAnnotationsAsync());
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function filtersHelpModalContent(
|
||||
searchForwardShortcut: string,
|
||||
searchBackwardShortcut: string,
|
||||
): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Paragraph>
|
||||
<Title level={3}>General</Title>
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
You can use filters to display only subset of objects on a frame
|
||||
or to search objects that satisfy the filters using hotkeys
|
||||
<Text strong>
|
||||
{` ${searchForwardShortcut} `}
|
||||
</Text>
|
||||
and
|
||||
<Text strong>
|
||||
{` ${searchBackwardShortcut} `}
|
||||
</Text>
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Text strong>Supported properties: </Text>
|
||||
width, height, label, serverID, clientID, type, shape, occluded
|
||||
<br />
|
||||
<Text strong>Supported operators: </Text>
|
||||
==, !=, >, >=, <, <=, (), & and |
|
||||
<br />
|
||||
<Text strong>
|
||||
If you have double quotes in your query string,
|
||||
please escape them using back slash: \" (see the latest example)
|
||||
</Text>
|
||||
<br />
|
||||
All properties and values are case-sensitive.
|
||||
CVAT uses json queries to perform search.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Title level={3}>Examples</Title>
|
||||
<ul>
|
||||
<li>label=="car" | label==["road sign"]</li>
|
||||
<li>shape == "polygon"</li>
|
||||
<li>width >= height</li>
|
||||
<li>attr["Attribute 1"] == attr["Attribute 2"]</li>
|
||||
<li>clientID == 50</li>
|
||||
<li>
|
||||
(label=="car" & attr["parked"]==true)
|
||||
| (label=="pedestrian" & width > 150)
|
||||
</li>
|
||||
<li>
|
||||
(( label==["car \"mazda\""])
|
||||
& (attr["sunglasses"]==true
|
||||
| (width > 150 | height > 150 & (clientID == serverID)))))
|
||||
</li>
|
||||
</ul>
|
||||
</Paragraph>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AnnotationsFiltersInput(props: StateToProps & DispatchToProps): JSX.Element {
|
||||
const {
|
||||
annotationsFilters,
|
||||
annotationsFiltersHistory,
|
||||
searchForwardShortcut,
|
||||
searchBackwardShortcut,
|
||||
changeAnnotationsFilters,
|
||||
} = props;
|
||||
|
||||
const [underCursor, setUnderCursor] = useState(false);
|
||||
|
||||
return (
|
||||
<Select
|
||||
className='cvat-annotations-filters-input'
|
||||
allowClear
|
||||
value={annotationsFilters}
|
||||
mode='tags'
|
||||
style={{ width: '100%' }}
|
||||
placeholder={
|
||||
underCursor ? (
|
||||
<>
|
||||
<Tooltip title='Click to open help'>
|
||||
<Icon
|
||||
type='filter'
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
Modal.info({
|
||||
width: 700,
|
||||
title: 'How to use filters?',
|
||||
content: filtersHelpModalContent(
|
||||
searchForwardShortcut,
|
||||
searchBackwardShortcut,
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon style={{ transform: 'scale(0.9)' }} type='filter' />
|
||||
<span style={{ marginLeft: 5 }}>Annotations filters</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
onChange={changeAnnotationsFilters}
|
||||
onMouseEnter={() => setUnderCursor(true)}
|
||||
onMouseLeave={() => setUnderCursor(false)}
|
||||
>
|
||||
{annotationsFiltersHistory.map((element: string): JSX.Element => (
|
||||
<Select.Option key={element} value={element}>{element}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(AnnotationsFiltersInput);
|
||||
@ -0,0 +1,320 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { GlobalHotKeys, ExtendedKeyMapOptions } from 'react-hotkeys';
|
||||
import { connect } from 'react-redux';
|
||||
import { Action } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import Layout, { SiderProps } from 'antd/lib/layout';
|
||||
import { SelectValue } from 'antd/lib/select';
|
||||
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import Icon from 'antd/lib/icon';
|
||||
|
||||
import { LogType } from 'cvat-logger';
|
||||
import {
|
||||
activateObject as activateObjectAction,
|
||||
updateAnnotationsAsync,
|
||||
} from 'actions/annotation-actions';
|
||||
import { CombinedState } from 'reducers/interfaces';
|
||||
import AnnotationsFiltersInput from 'components/annotation-page/annotations-filters-input';
|
||||
import ObjectSwitcher from './object-switcher';
|
||||
import AttributeSwitcher from './attribute-switcher';
|
||||
import ObjectBasicsEditor from './object-basics-edtior';
|
||||
import AttributeEditor from './attribute-editor';
|
||||
|
||||
|
||||
interface StateToProps {
|
||||
activatedStateID: number | null;
|
||||
activatedAttributeID: number | null;
|
||||
states: any[];
|
||||
labels: any[];
|
||||
jobInstance: any;
|
||||
keyMap: Record<string, ExtendedKeyMapOptions>;
|
||||
normalizedKeyMap: Record<string, string>;
|
||||
}
|
||||
|
||||
interface DispatchToProps {
|
||||
activateObject(clientID: number | null, attrID: number | null): void;
|
||||
updateAnnotations(statesToUpdate: any[]): void;
|
||||
}
|
||||
|
||||
interface LabelAttrMap {
|
||||
[index: number]: any;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: CombinedState): StateToProps {
|
||||
const {
|
||||
annotation: {
|
||||
annotations: {
|
||||
activatedStateID,
|
||||
activatedAttributeID,
|
||||
states,
|
||||
},
|
||||
job: {
|
||||
instance: jobInstance,
|
||||
labels,
|
||||
},
|
||||
},
|
||||
shortcuts: {
|
||||
keyMap,
|
||||
normalizedKeyMap,
|
||||
},
|
||||
} = state;
|
||||
|
||||
return {
|
||||
jobInstance,
|
||||
labels,
|
||||
activatedStateID,
|
||||
activatedAttributeID,
|
||||
states,
|
||||
keyMap,
|
||||
normalizedKeyMap,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: ThunkDispatch<CombinedState, {}, Action>): DispatchToProps {
|
||||
return {
|
||||
activateObject(clientID: number, attrID: number): void {
|
||||
dispatch(activateObjectAction(clientID, attrID));
|
||||
},
|
||||
updateAnnotations(states): void {
|
||||
dispatch(updateAnnotationsAsync(states));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Element {
|
||||
const {
|
||||
labels,
|
||||
states,
|
||||
activatedStateID,
|
||||
activatedAttributeID,
|
||||
jobInstance,
|
||||
updateAnnotations,
|
||||
activateObject,
|
||||
keyMap,
|
||||
normalizedKeyMap,
|
||||
} = props;
|
||||
|
||||
const [labelAttrMap, setLabelAttrMap] = useState(
|
||||
labels.reduce((acc, label): LabelAttrMap => {
|
||||
acc[label.id] = label.attributes.length ? label.attributes[0] : null;
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
|
||||
const [activeObjectState] = activatedStateID === null
|
||||
? [null] : states.filter((objectState: any): boolean => (
|
||||
objectState.clientID === activatedStateID
|
||||
));
|
||||
const activeAttribute = activeObjectState
|
||||
? labelAttrMap[activeObjectState.label.id]
|
||||
: null;
|
||||
|
||||
if (activeObjectState) {
|
||||
const attribute = labelAttrMap[activeObjectState.label.id];
|
||||
if (attribute && attribute.id !== activatedAttributeID) {
|
||||
activateObject(activatedStateID, attribute ? attribute.id : null);
|
||||
}
|
||||
} else if (states.length) {
|
||||
const attribute = labelAttrMap[states[0].label.id];
|
||||
activateObject(states[0].clientID, attribute ? attribute.id : null);
|
||||
}
|
||||
|
||||
const nextObject = (step: number): void => {
|
||||
if (states.length) {
|
||||
const index = states.indexOf(activeObjectState);
|
||||
let nextIndex = index + step;
|
||||
if (nextIndex > states.length - 1) {
|
||||
nextIndex = 0;
|
||||
} else if (nextIndex < 0) {
|
||||
nextIndex = states.length - 1;
|
||||
}
|
||||
if (nextIndex !== index) {
|
||||
const attribute = labelAttrMap[states[nextIndex].label.id];
|
||||
activateObject(states[nextIndex].clientID, attribute ? attribute.id : null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const nextAttribute = (step: number): void => {
|
||||
if (activeObjectState) {
|
||||
const { label } = activeObjectState;
|
||||
const { attributes } = label;
|
||||
if (attributes.length) {
|
||||
const index = attributes.indexOf(activeAttribute);
|
||||
let nextIndex = index + step;
|
||||
if (nextIndex > attributes.length - 1) {
|
||||
nextIndex = 0;
|
||||
} else if (nextIndex < 0) {
|
||||
nextIndex = attributes.length - 1;
|
||||
}
|
||||
if (index !== nextIndex) {
|
||||
const updatedLabelAttrMap = { ...labelAttrMap };
|
||||
updatedLabelAttrMap[label.id] = attributes[nextIndex];
|
||||
setLabelAttrMap(updatedLabelAttrMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const siderProps: SiderProps = {
|
||||
className: 'attribute-annotation-sidebar',
|
||||
theme: 'light',
|
||||
width: 300,
|
||||
collapsedWidth: 0,
|
||||
reverseArrow: true,
|
||||
collapsible: true,
|
||||
trigger: null,
|
||||
collapsed: sidebarCollapsed,
|
||||
};
|
||||
|
||||
const subKeyMap = {
|
||||
NEXT_ATTRIBUTE: keyMap.NEXT_ATTRIBUTE,
|
||||
PREVIOUS_ATTRIBUTE: keyMap.PREVIOUS_ATTRIBUTE,
|
||||
NEXT_OBJECT: keyMap.NEXT_OBJECT,
|
||||
PREVIOUS_OBJECT: keyMap.PREVIOUS_OBJECT,
|
||||
};
|
||||
|
||||
const handlers = {
|
||||
NEXT_ATTRIBUTE: (event: KeyboardEvent | undefined) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
nextAttribute(1);
|
||||
},
|
||||
PREVIOUS_ATTRIBUTE: (event: KeyboardEvent | undefined) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
nextAttribute(-1);
|
||||
},
|
||||
NEXT_OBJECT: (event: KeyboardEvent | undefined) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
nextObject(1);
|
||||
},
|
||||
PREVIOUS_OBJECT: (event: KeyboardEvent | undefined) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
nextObject(-1);
|
||||
},
|
||||
};
|
||||
|
||||
if (activeObjectState) {
|
||||
return (
|
||||
<Layout.Sider {...siderProps}>
|
||||
{/* eslint-disable-next-line */}
|
||||
<span
|
||||
className={`cvat-objects-sidebar-sider
|
||||
ant-layout-sider-zero-width-trigger
|
||||
ant-layout-sider-zero-width-trigger-left`}
|
||||
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||
>
|
||||
{sidebarCollapsed ? <Icon type='menu-fold' title='Show' />
|
||||
: <Icon type='menu-unfold' title='Hide' />}
|
||||
</span>
|
||||
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} allowChanges />
|
||||
<Row className='cvat-objects-sidebar-filter-input'>
|
||||
<Col>
|
||||
<AnnotationsFiltersInput />
|
||||
</Col>
|
||||
</Row>
|
||||
<ObjectSwitcher
|
||||
currentLabel={activeObjectState.label.name}
|
||||
clientID={activeObjectState.clientID}
|
||||
occluded={activeObjectState.occluded}
|
||||
objectsCount={states.length}
|
||||
currentIndex={states.indexOf(activeObjectState)}
|
||||
normalizedKeyMap={normalizedKeyMap}
|
||||
nextObject={nextObject}
|
||||
/>
|
||||
<ObjectBasicsEditor
|
||||
currentLabel={activeObjectState.label.name}
|
||||
labels={labels}
|
||||
occluded={activeObjectState.occluded}
|
||||
changeLabel={(value: SelectValue): void => {
|
||||
const labelName = value as string;
|
||||
const [newLabel] = labels
|
||||
.filter((_label): boolean => _label.name === labelName);
|
||||
activeObjectState.label = newLabel;
|
||||
updateAnnotations([activeObjectState]);
|
||||
}}
|
||||
setOccluded={(event: CheckboxChangeEvent): void => {
|
||||
activeObjectState.occluded = event.target.checked;
|
||||
updateAnnotations([activeObjectState]);
|
||||
}}
|
||||
/>
|
||||
{
|
||||
activeAttribute
|
||||
? (
|
||||
<>
|
||||
<AttributeSwitcher
|
||||
currentAttribute={activeAttribute.name}
|
||||
currentIndex={activeObjectState.label.attributes
|
||||
.indexOf(activeAttribute)}
|
||||
attributesCount={activeObjectState.label.attributes.length}
|
||||
normalizedKeyMap={normalizedKeyMap}
|
||||
nextAttribute={nextAttribute}
|
||||
/>
|
||||
<AttributeEditor
|
||||
attribute={activeAttribute}
|
||||
currentValue={activeObjectState.attributes[activeAttribute.id]}
|
||||
onChange={(value: string) => {
|
||||
const { attributes } = activeObjectState;
|
||||
jobInstance.logger.log(
|
||||
LogType.changeAttribute, {
|
||||
id: activeAttribute.id,
|
||||
object_id: activeObjectState.clientID,
|
||||
value,
|
||||
},
|
||||
);
|
||||
attributes[activeAttribute.id] = value;
|
||||
activeObjectState.attributes = attributes;
|
||||
updateAnnotations([activeObjectState]);
|
||||
}}
|
||||
/>
|
||||
|
||||
</>
|
||||
) : (
|
||||
<div className='attribute-annotations-sidebar-not-found-wrapper'>
|
||||
<Text strong>No attributes found</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Layout.Sider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout.Sider {...siderProps}>
|
||||
<div className='attribute-annotations-sidebar-not-found-wrapper'>
|
||||
<Text strong>No objects found</Text>
|
||||
</div>
|
||||
</Layout.Sider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(AttributeAnnotationSidebar);
|
||||
@ -0,0 +1,284 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
|
||||
import Select, { SelectValue } from 'antd/lib/select';
|
||||
import Radio, { RadioChangeEvent } from 'antd/lib/radio';
|
||||
import Input from 'antd/lib/input';
|
||||
|
||||
import consts from 'consts';
|
||||
|
||||
interface InputElementParameters {
|
||||
attrID: number;
|
||||
inputType: string;
|
||||
values: string[];
|
||||
currentValue: string;
|
||||
onChange(value: string): void;
|
||||
}
|
||||
|
||||
function renderInputElement(parameters: InputElementParameters): JSX.Element {
|
||||
const {
|
||||
inputType,
|
||||
attrID,
|
||||
values,
|
||||
currentValue,
|
||||
onChange,
|
||||
} = parameters;
|
||||
|
||||
const renderCheckbox = (): JSX.Element => (
|
||||
<>
|
||||
<Text strong>Checkbox: </Text>
|
||||
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
|
||||
<Checkbox
|
||||
onChange={(event: CheckboxChangeEvent): void => (
|
||||
onChange(event.target.checked ? 'true' : 'false')
|
||||
)}
|
||||
checked={currentValue === 'true'}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderSelect = (): JSX.Element => (
|
||||
<>
|
||||
<Text strong>Values: </Text>
|
||||
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
|
||||
<Select
|
||||
value={currentValue}
|
||||
style={{ width: '80%' }}
|
||||
onChange={(value: SelectValue) => (
|
||||
onChange(value as string)
|
||||
)}
|
||||
>
|
||||
{values.map((value: string): JSX.Element => (
|
||||
<Select.Option key={value} value={value}>
|
||||
{value === consts.UNDEFINED_ATTRIBUTE_VALUE
|
||||
? consts.NO_BREAK_SPACE : value}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderRadio = (): JSX.Element => (
|
||||
<>
|
||||
<Text strong>Values: </Text>
|
||||
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
|
||||
<Radio.Group
|
||||
value={currentValue}
|
||||
onChange={(event: RadioChangeEvent) => (
|
||||
onChange(event.target.value)
|
||||
)}
|
||||
>
|
||||
{values.map((value: string): JSX.Element => (
|
||||
<Radio style={{ display: 'block' }} key={value} value={value}>
|
||||
{value === consts.UNDEFINED_ATTRIBUTE_VALUE
|
||||
? consts.NO_BREAK_SPACE : value}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const handleKeydown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (['ArrowDown', 'ArrowUp', 'ArrowLeft',
|
||||
'ArrowRight', 'Tab', 'Shift', 'Control']
|
||||
.includes(event.key)
|
||||
) {
|
||||
event.preventDefault();
|
||||
const copyEvent = new KeyboardEvent('keydown', event);
|
||||
window.document.dispatchEvent(copyEvent);
|
||||
}
|
||||
};
|
||||
|
||||
const renderText = (): JSX.Element => (
|
||||
<>
|
||||
{inputType === 'number' ? <Text strong>Number: </Text> : <Text strong>Text: </Text>}
|
||||
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
|
||||
<Input
|
||||
autoFocus
|
||||
key={attrID}
|
||||
defaultValue={currentValue}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target;
|
||||
if (inputType === 'number') {
|
||||
if (value !== '') {
|
||||
const numberValue = +value;
|
||||
if (!Number.isNaN(numberValue)) {
|
||||
onChange(`${numberValue}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onChange(value);
|
||||
}
|
||||
}}
|
||||
onKeyDown={handleKeydown}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
let element = null;
|
||||
if (inputType === 'checkbox') {
|
||||
element = renderCheckbox();
|
||||
} else if (inputType === 'select') {
|
||||
element = renderSelect();
|
||||
} else if (inputType === 'radio') {
|
||||
element = renderRadio();
|
||||
} else {
|
||||
element = renderText();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='attribute-annotation-sidebar-attr-editor'>
|
||||
{element}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ListParameters {
|
||||
inputType: string;
|
||||
values: string[];
|
||||
onChange(value: string): void;
|
||||
}
|
||||
|
||||
function renderList(parameters: ListParameters): JSX.Element | null {
|
||||
const { inputType, values, onChange } = parameters;
|
||||
|
||||
if (inputType === 'checkbox') {
|
||||
const sortedValues = ['true', 'false'];
|
||||
if (values[0].toLowerCase() !== 'true') {
|
||||
sortedValues.reverse();
|
||||
}
|
||||
|
||||
const keyMap: KeyMap = {};
|
||||
const handlers: {
|
||||
[key: string]: (keyEvent?: KeyboardEvent) => void;
|
||||
} = {};
|
||||
|
||||
sortedValues.forEach((value: string, index: number): void => {
|
||||
const key = `SET_${index}_VALUE`;
|
||||
keyMap[key] = {
|
||||
name: `Set value "${value}"`,
|
||||
description: `Change current value for the attribute to "${value}"`,
|
||||
sequence: `${index}`,
|
||||
action: 'keydown',
|
||||
};
|
||||
|
||||
handlers[key] = (event: KeyboardEvent | undefined) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
onChange(value);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
|
||||
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers} allowChanges />
|
||||
<div>
|
||||
<Text strong>0:</Text>
|
||||
<Text>{` ${sortedValues[0]}`}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>1:</Text>
|
||||
<Text>{` ${sortedValues[1]}`}</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (inputType === 'radio' || inputType === 'select') {
|
||||
const keyMap: KeyMap = {};
|
||||
const handlers: {
|
||||
[key: string]: (keyEvent?: KeyboardEvent) => void;
|
||||
} = {};
|
||||
|
||||
const filteredValues = values
|
||||
.filter((value: string): boolean => value !== consts.UNDEFINED_ATTRIBUTE_VALUE);
|
||||
filteredValues.slice(0, 10).forEach((value: string, index: number): void => {
|
||||
const key = `SET_${index}_VALUE`;
|
||||
keyMap[key] = {
|
||||
name: `Set value "${value}"`,
|
||||
description: `Change current value for the attribute to "${value}"`,
|
||||
sequence: `${index}`,
|
||||
action: 'keydown',
|
||||
};
|
||||
|
||||
handlers[key] = (event: KeyboardEvent | undefined) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
onChange(value);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
|
||||
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers} allowChanges />
|
||||
{filteredValues.map((value: string, index: number): JSX.Element => (
|
||||
<div key={value}>
|
||||
<Text strong>{`${index}:`}</Text>
|
||||
<Text>{` ${value}`}</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (inputType === 'number') {
|
||||
return (
|
||||
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
|
||||
<div>
|
||||
<Text strong>From:</Text>
|
||||
<Text>{` ${values[0]}`}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>To:</Text>
|
||||
<Text>{` ${values[1]}`}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>Step:</Text>
|
||||
<Text>{` ${values[2]}`}</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
attribute: any;
|
||||
currentValue: string;
|
||||
onChange(value: string): void;
|
||||
}
|
||||
|
||||
function AttributeEditor(props: Props): JSX.Element {
|
||||
const { attribute, currentValue, onChange } = props;
|
||||
const { inputType, values, id: attrID } = attribute;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{renderList({ values, inputType, onChange })}
|
||||
<hr />
|
||||
{renderInputElement({
|
||||
attrID,
|
||||
inputType,
|
||||
currentValue,
|
||||
values,
|
||||
onChange,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(AttributeEditor);
|
||||
@ -0,0 +1,49 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import Button from 'antd/lib/button';
|
||||
|
||||
interface Props {
|
||||
currentAttribute: string;
|
||||
currentIndex: number;
|
||||
attributesCount: number;
|
||||
normalizedKeyMap: Record<string, string>;
|
||||
nextAttribute(step: number): void;
|
||||
}
|
||||
|
||||
function AttributeSwitcher(props: Props): JSX.Element {
|
||||
const {
|
||||
currentAttribute,
|
||||
currentIndex,
|
||||
attributesCount,
|
||||
nextAttribute,
|
||||
normalizedKeyMap,
|
||||
} = props;
|
||||
|
||||
const title = `${currentAttribute} [${currentIndex + 1}/${attributesCount}]`;
|
||||
return (
|
||||
<div className='attribute-annotation-sidebar-switcher'>
|
||||
<Tooltip title={`Previous attribute ${normalizedKeyMap.PREVIOUS_ATTRIBUTE}`}>
|
||||
<Button disabled={attributesCount <= 1} onClick={() => nextAttribute(-1)}>
|
||||
<Icon type='left' />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={title}>
|
||||
<Text className='cvat-text'>{currentAttribute}</Text>
|
||||
<Text strong>{` [${currentIndex + 1}/${attributesCount}]`}</Text>
|
||||
</Tooltip>
|
||||
<Tooltip title={`Next attribute ${normalizedKeyMap.NEXT_ATTRIBUTE}`}>
|
||||
<Button disabled={attributesCount <= 1} onClick={() => nextAttribute(1)}>
|
||||
<Icon type='right' />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(AttributeSwitcher);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue