From 4f3d3cb85fa7b10bd0cc6a2fd649f6846e3e2abd Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov <41117609+azhavoro@users.noreply.github.com> Date: Tue, 24 Mar 2020 13:30:55 +0300 Subject: [PATCH] Az/update data streaming (#1308) * Changed version number (0, 5, 'final', 0). * Updated changelog file. * fixed default attribute values for tracked shapes (#703) * Updated CHANGELOG with information about Zenodo * Updated CHANGELOG with information about Zenodo (#777) * Updated version of the project. * Hotfix: fixed skikit-image version (#965) * Fixed skikit-image version * Updated changelog * Increased CVAT version (0.5.2) * Release 0.6.0 (#1238) * Release 0.5 (#705) * Changed version number (0, 5, 'final', 0). * Updated changelog file. * fixed default attribute values for tracked shapes (#703) * typo ? Should not this be cvat_redis -> redis ? * Fixed labels regex for non-latin characters (#708) * Update README.md * Update README.md * Don't save shapes with keyframe==False * Selecting non images leads to 400 error (#734) * Fix HTTP 400 error if together with vision data the user submit non-vision data (e.g. text files) * Ignore SVG images because Pillow doesn't work with them. * Fix the problem with duplicated frames in case of "share" (#735) * Fix the problem with duplicated frames in case of "share". * Fix a case when the code works incorrectly /a/b/c /a/b/c0 Previously only /a/b/c will be in output but should be both. * added method docs to Auto Annotation inference.py (#725) * remove deprecated method call `from_ir` (#726) * New command line tool for working with tasks (#732) * Adding new command line tool for performing common task related operations (create, list, delete, etc.) * Replaced @exception decorator with try/except in main() * Replaced optional --name with positional name and removed default * Added license text to files * Added django units to cover future API changes * Refactored into submodules to better support tests * Fix an issue with permissions (observer can change annotations) (#745) * Fixed a problem with observer (check_object_permissions method was not called) * Added a test case to cover issue #712. * COCO Annotation IDs should begin with 1 (#748) Currently the annotation ID begins with 0 which is interpreted by cocoapi as a false detection. The array dtm saves the matches via the ground truth annotation ID. The variable dtm is initialized as an array of zeros. https://github.com/cocodataset/cocoapi/blob/636becdc73d54283b3aac6d4ec363cffbb6f9b20/PythonAPI/pycocotools/cocoeval.py#L269 https://github.com/cocodataset/cocoapi/blob/636becdc73d54283b3aac6d4ec363cffbb6f9b20/PythonAPI/pycocotools/cocoeval.py#L295 https://github.com/cocodataset/cocoapi/blob/636becdc73d54283b3aac6d4ec363cffbb6f9b20/PythonAPI/pycocotools/cocoeval.py#L375 * Slightly enhance command line interface feature (#746) * Slightly enhance command line interface feature. Added README.md, run tests using travis, run CLI tests from VS code. * Removed formatted string due to a limitation on our python version inside the container. * Add information about command line interface to the main page. * Projects (server only, REST API) (#754) * Initial version of projects * Added tests for Projects REST API. * Added information about projects into CHANGELOG * Updating string format for case missed in PR #746. (#757) * add robust JSON handeling for auto annotation runner (#758) * Basic user information (#761) * Fix https://github.com/opencv/cvat/issues/750 * Updated CHANGELOG * Added more tests for /api/v1/users* REST API. * Disable fix_segments_intersections for now (#751) * Disable fix_segments_intersections for now When the bounding boxes had intersections and were exported with the COCO JSON format they were often cut off. I commented out the line with the function fix_segments_intersections and replaced it with lines of that function. This helped with the bounding boxes and keeps the masks as they are created with CVAT. It is probably inconvenient for the user to get something fixed in the export without an active agreement of the user. Secondly letting a function automatically fix segments could result in a bad fix. * Use fix_segments_intersections only with z-order The fix_segments_intersections will only be used when the z-order flag is set. This is useful for bounding boxes or masks which don't need to be fixed. This fix was created according to Andrey Zhavoronkov's (@azhavoro) advice. * Added information about a fixed issue. (#765) * Add more information into questions section (#766) * User interface with react and antd (#755) * Login page, router * Registration * Tasks view * add in serializing check in auto annotation model runner (#770) * allow security segmentation models to be used in auto annotation (#759) * Integration with Zenodo (#779) * Updated CHANGELOG with information about Zenodo * Updated version of the project. * Fixed a case when a task's owner can be undefined. (#782) * Added `restart` tag to docker-compose for `cvat_ui` (#789) * User interface with React and antd (#785) * Dump & refactoring * Upload annotations, cvat-core from sources * Added download icon * Added icon * Update documentation to point to OpenVino component documentation (#752) * Change the version of OpenVINO compatibility (#797) * Change the version of OpenVINO compatibility * added mask RCNN script (#780) * added in yolo auto annotation sciprt (#794) * Annotation formats documentation (#719) * added handling of truncated and difficult attributes for pascal voc loader/dumper added descriptions of supported annotation formats * added YOLO example * made match_frame as Annotations method changed 'image/source_id' field TF feature from int64 to string (according to TF OD API dataset utlis) * updated README improved match_frame function * added unit tests for dump/load * added in semantic segmentation instructions to README (#804) * fix off by one error in mask rcnn (#801) * Fix Yolo: swap width, height; Change box coord order; parsing fix (#802) * Auto segmentation using Mask_RCNN (#767) * Update CHANGELOG.md * Bump pillow from 5.1.0 to 6.2.0 in /cvat/requirements (#808) Bumps [pillow](https://github.com/python-pillow/Pillow) from 5.1.0 to 6.2.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/5.1.0...6.2.0) Signed-off-by: dependabot[bot] * Bump pillow from 5.3.0 to 6.2.0 in /utils/cli (#807) Bumps [pillow](https://github.com/python-pillow/Pillow) from 5.3.0 to 6.2.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/5.3.0...6.2.0) Signed-off-by: dependabot[bot] * Bump eslint-utils from 1.4.0 to 1.4.3 in /cvat-canvas (#809) Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.4.0 to 1.4.3. - [Release notes](https://github.com/mysticatea/eslint-utils/releases) - [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.4.0...v1.4.3) Signed-off-by: dependabot[bot] * fix serialize bug when using AutoAnnotation runner (#810) * User interface with React and antd (#811) * Fixed links for analytics and help * Delete task functionality * Added navigation for create and open task * Added icon for help * Added easy plugin checker * Header dependes on installed plugins * Menu depends on installed plugins * Shared actions menu component, base layout for task page * Task page based (relations with redux, base layout) * Added attribute form * Finished label creator * Added jobs table * Added job assignee * Save updated labels on server * Added imports plugin, updated webpack * Editable bug tracker * Clean task update * Change assignee * Fix login problem (unathorized user cannot login). (#812) * Fix upload anno for COCO (#788) * COCO: load bbox as rectangle if segmentation field is empty * added unit test for coco format (case: object segment field is empty) * Add support for ip git repo urls (#827) * Add support for ip v4 git repo urls * Add tests for git urls * React & Antd UI: Create task (#840) * Separated component user selector * Change job assignee * Basic create task window * Bug fixes and refactoring * Create task connected with a server * Loading status for a button * Reset loading on error response * UI improvements * Github/feedback/share window * added in new interp files for pixel link v0004 (#852) * Add LabelMe format support (#844) * Add labelme export * Add LabelMe import * Add labelme format to readme * Updated CHANGELOG.md * Adding dump and load support for MOT CSV format. (#830) * Adding dump and load support for MOT CSV format. * Updated test cases to use correct track annotations for MOT format. * Removed behaviour of MOT loader which would duplicate the last track shape prior to setting outside=True. * Add dataset export facility (#813) * Add datumaro django application * Add cvat task datumaro bindings * Add REST api for task export * Add scheduler service * Updated CHANGELOG.md * Mit license for pixellink and changelog (#862) * React & Antd UI: Model manager (#856) * Supported git to create and sync * Updated antd * Updated icons * Improved header * Top bar for models & empty models list * Removed one extra reducer and actions * Removed one extra reducer and actions * Crossplatform css * Models reducers, some models actions, base for model list, imrovements * Models list, ability to delete models * Added ability to upload models * Improved form, reinit models after create * Removed some importants in css * Model running dialog window, a lot of fixes * Add a dataset export button for tasks (#834) * Add dataset export button for tasks in dashboard * Fix downloading, shrink list of export formats * Add strict export format check * Add strict export format check * Change REST api paths * Move formats declarations to server, * Coco converter updates (#864) * [Datumaro] Fix coco images export (#875) * Update test * Fix export * Support several image paths in coco extractor * [Datumaro] Disable lazy image caching by default (#876) * Disable lazy image caching by default * Deterministic cache test * Add displacing image cache * React & Antd UI: Export dataset, refactoring & fixes (#872) * Automatic label matching (by the same name) in model running window * Improved create task window * Improved upload model window * Fixed: error window showed twice * Updated CONTRIBUTING.md * Removed token before login, fixed dump submenu (adjustment), fixed case when empty models list displayed * Export as dataset, better error showing system * Removed extra requests, improved UI * Fixed a name of a format * Show inference progress * Fixed model loading after a model was uploaded * Fix redirect (#878) * Add cvat cli to datumaro project export (#870) * Configurable REST for UI, minor improvements (#880) * [Datumaro] Pip installation (#881) * Add version file * Remove unnecessary dependencies * Add lxml use motivation * Add pip setup script * Reduce opencv dependency * Fix cli command * Codacy * page_size parameter for all REST API methods (#884) * Added page_size parameter for all REST API methods which returns list of objects. Also it is possible to specify page_size=all to return all elements. * Updated changelog.md * VOC converter: Use depth from CVAT XML if available (#885) * Token auth for non-REST API apps (#889) * Token authorization for non REST API apps (e.g. git, tf annotation, tf segmentation) * set CORS_REPLACE_HTTPS_REFERER option to True (#895) * Fix some spelling (#897) * React & Antd: Dashboard migration (#892) * Removed old dashboard * Getting all users * Updated changelog * Reimplemented login decorator * Implicit host, scheme in docker-compose * Fixed issue with pagination * Implicit page size parameter for tasks * Fixed linkedin icon, added links to tasks in notifications * Configurable method for check plugin * Bump django from 2.2.4 to 2.2.8 in /cvat/requirements (#902) Bumps [django](https://github.com/django/django) from 2.2.4 to 2.2.8. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/2.2.4...2.2.8) Signed-off-by: dependabot[bot] * Az/fix meta requests (#903) * fixed processing of meta requests * Fixed some issues with dump (#904) * Changed method for downloading annotations * Initial commit * Initial commit * Updated download method for dataset * fixed eslint error * Restore session id (#905) * Restore session id when we use token authorization. * UI eslint fixes (#908) * Installed airbnb fullsettings * Fixed actions menu * Create model/task page * File manager, header * Labels editor * Login, register * Models page & model runner * Tasks page * Feedback and base app * Tasks page * Containers * Reducers * Fixed additional issues * Small pagination fix * implemented adas semantic segmentation * Copy JOB info to clibpard * Yolov3 interpretration script fix for 'Annotation failed' and changes to mapping.json (#896) (#912) * [Datumaro] Add YOLO converter (#906) * Add YOLO converter * Added yolo extractor * Added YOLO format test * Add YOLO export in UI * Added padding * Remove deprecated html attributes (#924) * Updated message * Improved some hints * Added 3rdparty library to clipboard * Updated doc * Added ability to copy labels without IDs * Removed extra lines * Updated contributing * Updated contributing * Task name displayed better * Improved tasks routing * Ability to show hidden task * Destroy messages before getting new tasks * Fixed eslint * Names of selected files when creating a new task * [Datumaro] Added tf detection api tfrecord import and export (#894) * Added tf detection api tfrecord import and export * Added export button in dashboard * Add tf to requirements * Extend test * Add tf dependency * Require images in tfrecord export * Add video task case handling * Maintain image order in CVAT export * Fix Task image id-path conversions * Update tfrecord tests * Extend image utilities * Update tfrecord format * Fix image loading bug * Add some logs * Add 'copy' option to project import command * Reduce default cache size * Improve UX with creating new shape by shortkey (#941) * Fixed command in CONTRIBUTING.md (#947) * Fixed command in CONTRIBUTING.md * Removed daemon, updated command * [Datumaro] COCO 'merge instance polygons' option (#938) * Add polygon merging option to coco converter * Add test, refactor coco, add support for cli args * Drop colormap application in datumaro format * Add cli support in voc converter * Add cli support in yolo converter * Add converter cli options in project cli * Add image data type conversion in image saving * [Datumaro] Fix voc colormap (#945) * Add polygon merging option to coco converter * Add test, refactor coco, add support for cli args * Drop colormap application in datumaro format * Add cli support in voc converter * Add cli support in yolo converter * Add converter cli options in project cli * Add image data type conversion in image saving * Add image data type conversion in image saving * Update mask support in voc * Replace null with quotes in coco export * Improve cli * Enable Datumaro intellisense in vs cde * Adjust fields in voc detection export * Return pylint to config (#951) * Update docker base images (#950) Don't fix minor/patch version to get security updates and bug fixes. * Fixed git plugin (#961) * Add upload annotation function to cli (#958) * add upload annotation function to cli * Update core.py Removing whitespace * React, Antd, Redux: Left sidebar and top for annotation page (#963) * Rebased from develop * Improved getting icons method * Added more icons * Left menu * Initial commit * Setup SVGO, added some buttons to top * Top bar progress * Top bar for annotation page * Updated styles * added in label visualization to auto annotation runner (#931) * Bump tensorflow from 1.13.1 to 1.15.0 in /utils/tfrecords (#967) Bumps [tensorflow](https://github.com/tensorflow/tensorflow) from 1.13.1 to 1.15.0. - [Release notes](https://github.com/tensorflow/tensorflow/releases) - [Changelog](https://github.com/tensorflow/tensorflow/blob/master/RELEASE.md) - [Commits](https://github.com/tensorflow/tensorflow/compare/v1.13.1...v1.15.0) Signed-off-by: dependabot[bot] * Fixed number attribute (#972) * CSS Enhancement (#971) * Removed vendor/specific rules * Sass for CVAT, less for Antd, added autoprefixer and css polyfills * Removed extra line * Changed update state * [Datumaro] VOC labelmap support (#957) * Add import result checks and options to skip * Add label-specific attributes * Overwrite option for export * Add labelmap file support in voc * Add labelmap tests * Little refactoring * Bump tensorflow from 1.12.3 to 1.15.0 in /cvat/requirements (#968) * Bump tensorflow from 1.12.3 to 1.15.0 in /cvat/requirements Bumps [tensorflow](https://github.com/tensorflow/tensorflow) from 1.12.3 to 1.15.0. - [Release notes](https://github.com/tensorflow/tensorflow/releases) - [Changelog](https://github.com/tensorflow/tensorflow/blob/master/RELEASE.md) - [Commits](https://github.com/tensorflow/tensorflow/compare/v1.12.3...v1.15.0) Signed-off-by: dependabot[bot] * Update pip because tensorflow 1.15 cannot not be found. * Fix a typo (pip -> pip3) * Replaced pip3 by python3 -m pip. * Change-submit-button-style (#976) * UI/UX improvement. Changed buttons type for create task / upload model * Added documentation for swagger page (#936) * Styles refactoring (#977) * Add polygon point count checks (#975) * User Guide update (#953) * Swagger documentation (#978) * Fix swagger problems (exceptions, /api/swagger.json, /api/docs/) * [Datumaro] CVAT format import (#974) * Add label-specific attributes * Add CVAT format import * Register CVAT format * Add little more logs * Little refactoring for tests * Cvat format checks * Add missing check * Refactor datumaro format * Little refactoring * Regularize dataset importer logic * Fix project import issue * Refactor coco extractor * Refactor tests * Codacy * Fix label for mask rcnn (#980) * UI Enhancements (#985) * Single import of basic styles * A little bit redesigned header * Specified min resolution 1280x768 * Getting a job instance * Improved handling when task doesn't exist * Adding dump for VOC instance mask. (#859) * Add mask instance dumper * Fix bug * Merge mask instance into mask * Merge the change into mask * Create MaskColorizer * Add dump method * Updating the Model Manager section of the CVAT User Guide (#991) * Added Code Climate, CodeBeat badges. (#995) * [Datumaro] Fix TFrecord converter constructor (#993) * Resolved performance bottleneck in merge function (#999) * Fixed issue: Unknown shape type found (#998) * Automatic bordering feature during drawing/editing (#997) * Change Modal submit button okType (#1001) * Fixed comparison of shapes (#1000) * Add test code for cli upload function (#986) * pass in model name and task id to run auto annotation script (#934) * fix dockerfile for PDF (#939) * Updating the Auto Annotation section of the CVAT User Guide (#996) * Updating the Task synchronization with a repository section of the CVAT User Guide (#1006) * Fix timezone bug (#1010) * [Datumaro] Fix project loading (#1013) * Fix occasional infinite loop in project loading * Fix project import source options saving * Fix project import .git dir placement * Make code aware of grayscale images * Added root folder for share functionality (#1005) * Improved feature: common borders (#1016) * Auto borders -> common borders, invisible when do not edit or draw, don't reset state * Reset sticker after clicking outside * Update AWS-Deployment-Guide.md (#1019) Fixed documentation typo for file extension * Correct link to #automatic-annotation in README (#1029) * AWS deployment guide updated #1009 (#1031) * Add info about auto segmentation to advanced topics of the installation guide (#1033) * correct path to eula.cfg (#1037) * Update README.md (#1040) * Removed patool package with GPL license (it is not used) (#1045) * Removed VIM package (it isn't necessary) (#1046) * Trim possible attribute values like attribute values setup by a user (#1044) * React UI: Player in annotation view & settings page (#1018) * Active player controls * Setup packages * Playing * Fold/unfold sidebar, minor issues * Improved cvat-canvas integration * Resolved some issues * Added cvat-canvas to Dockerfile.ui * Fit canvas method * Added annotation reducer * Added annotation actions * Added containers * Added components * cvat-canvas removed from dockerignore * Added settings page * Minor improvements * Container for canvas wrapper * Configurable grid * Rotation * fitCanvas added to readme * Aligned table * Changed CharField(64) -> CharField(4096) for attribute value (#1048) * [Datumaro] Add cvat format export (#1034) * Add cvat format export * Remove wrong items in test * [Datumaro] Instance polygon-mask conversions in COCO format (#1008) * Microoptimizations * Mask conversion functions * Add mask-polygon conversions * Add mask-polygon conversions in coco * Add mask-polygon conversions in coco * Update requirements * Option to disable crop * Fix cli parameter passing * Fix test * Fixes in COCO * [Datumaro] Dataset annotations filter (#1053) * Fix deprecation message * Update launcher interface * Add dataset entity, anno filter, remove filter from project config, update transform * Update project and source cli * Fix help message * Refactor tests * Added ability to match many model labels to one task labels (#1051) * Added ability to match many model labels to one task labels * Fixed grammar * React UI: Player updates (#1058) * Move, zoom integration * Moving, zooming, additional canvas handler * Activating & changing for objects * Improved colors * Saving annotations on the server * Fixed size * Refactoring * Added couple of notifications * Basic shape drawing * Cancel previous drawing * Refactoring * Minor draw improvings * Merge, group, split * Improved colors * Fixed: Uncaught TypeError: Cannot read property 'nodeValue' of undefined (#1068) * Add about CVAT (#1024) * Fix typos in xml_format.md (#1069) typo fixes * Update CONTRIBUTING.md (#1072) * align serializer max length of attribute value with the model (#1074) * Cleanup Dockerfiles for CVAT (#1060) * Replaced wget by curl * Moved CI stuff into Dockerfile.ci * Use docker-compose to run commnands inside docker (need environment variables) * Added patool again (to support different archive formats) * Roll back tensorflow version: 1.15 -> 1.13.1 Fixed https://github.com/opencv/cvat/issues/982 Fixed https://github.com/opencv/cvat/issues/1017 * datumaro install tensorflow 2.x now. It breaks automatic annotation using TF. * Follow redirects in curl (auto_segmentation) * Update method call (#1085) * React UI: Sidebar with objects and optimizations for annotation view (#1089) * Basic layout for objects panel * Objects header * A little name refactoring * Side panel base layout * Firefox specific exceptions * Some minor fixes * React & canvas optimizations * Icons refactoring * Little style refactoring * Some style fixes * Improved side panel with objects * Actual attribute values * Actual icons * Hidden > visible * hidden -> __internal * Fixed hidden in ui * Fixed some issues in canvas * Fixed list height * Color picker for labels * A bit fixed design * Actual header icons * Changing attributes and switchable buttons * Removed react memo (will reoptimize better) * Sorting methods, removed cache from cvat-core (a lot of bugs related with it) * Label switchers * Fixed bug with update timestamp for shapes * Annotation state refactoring * Removed old resetCache calls * Optimized top & left panels. Number of renders significantly decreased * Optimized some extra renders * Accelerated performance * Fixed two minor issues * Canvas improvements * Minor fixes * Removed extra code * resolving import error caused by pip 20.0 (#1094) * [Datumaro] CLI updates + better documentation (#1057) * Optimize mask conversions (#1097) * Update base.py (#1099) Modification necessary for using CVAT from remote machines when accessing with FQDNs See https://github.com/opencv/cvat/issues/1011#issue-542817055 and https://github.com/opencv/cvat/pull/1098 "I believe the reason for this is that sometimes if the port number is :80 and the URL is not in the LAN (:port), but instead it is a Fully Qualified Domain Name (:port), the port 80 is redundant (mydomain.com:80) and the errors arise." * fixed dump of interpolation points object && statistics calculation (#1108) * Add extreme clicking feature to draw box by 4 points (#1111) * Add extreme clicking feature to draw box by 4 points * Add documentation for extreme clicking * React UI: Annotation view enhancements (#1106) * Keyframes navigation * Synchronized objects on canvas and in side panel * Fixed minor bug with collapse * Fixed css property 'pointer-events' * Drawn appearance block * Removed extra force reflow * Finished appearance block, fixed couple bugs * Improved save() in cvat-core, changed approach to highlight shapes * Fixed exception in edit function, fixed filling for polylines and points, fixed wrong image navigation, remove and copy * Added lock * Some fixes with points * Minor appearance fixes * Fixed insert for points * Fixed unit tests * Fixed control * Fixed list size * Added propagate * Minor fix with attr saving * Some div changed to buttons * Locked some buttons for unimplemented functionalities * Statistics modal, changing a job status * Minor fix with shapes counting * Couple of fixes to improve visibility * Added fullscreen * SVG Canvas -> HTML Canvas frame (#1113) * SVG Frame -> HTML Canvas frame * React UI: Added annotation menus, added shape context menu, added some confirmations before dangerous actions (#1123) * Annotation menu, modified tasks menu * Removed extra styles * Context menu using side panel * Mousewheel on draw * Added more cursor icons * Do not check .svg & .scss by eslint * [Datumaro] Plugins and transforms (#1126) * Fix model run command * Rename annotation types, update class interfaces * Fix random cvat format test fails * Mask operations and dataset format fixes * Update tests, extract format testing functions * Add transform interface * Implement plugin system * Update tests with plugins * Fix logging * Add transfroms * Update cvat integration * Fix tensorflow installation (#1129) * Make tf dependency optional * Reduce opencv dependency * Import tf eagerly as it is a plugin * Do not install TF with Datumaro * Add plugin system documentation (#1131) * React UI: Improved mouse behaviour during draw/merge/edit/group/split (#1130) * Moving image with mouse during drawing, paste, group, split, merge * Babel plugin to dev deps * Move mouse during editing * Minor issues * [Datumaro] fixes (#1137) * Fix import command * Fix project name for spawned projects * Fix voc and coco converter parameters * Fix voc colormap color interpretation * Change order of image search for cvat extractor * fix CVAT image search paths * Bump django from 2.2.8 to 2.2.10 in /cvat/requirements (#1139) Bumps [django](https://github.com/django/django) from 2.2.8 to 2.2.10. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/2.2.8...2.2.10) Signed-off-by: dependabot[bot] * Add extreme clicking method in cvat-canvas and cvat-ui (#1127) * Add extreme clicking method in cvat-canvas and cvat-ui * Fix bugs and issues, update readme * Fix error after rebasing develop * updated CUDA to version 10 (#1138) * updated CUDA to version 10 * updated tensorflow * added comment about NVIDIA_REQUIRE_CUDA env varOF * React UI: Undo/redo (#1135) * Typed reducers (#1136) * Added typed actions/reducers * Added commands to check types / eslint issues * Added redux dev tools * Bump gitpython version (#1146) * Fix postgres startup. * React UI: Objects filtering & search (#1155) * Initial filter function * Updated method for filtering * Updated documentation * Added annotations filter file * Updated some comments * Added filter to UI * Implemented search alorithm * Removed extra code * Fixed typos * Added frame URL * Object URL * Removed extra encoding/decoding * Fixed dump for cases when special URL characters in task name (#1162) * Add offline subset remapping and bbox conversion (#1147) * Avoid tf deprecation warning (#1148) * [Datumaro] Pretty output folder names (#1149) * Generate output dir name from operation parameters * Fix failing command * Update changelog (#1165) * [Datumaro] Introduce image info (#1140) * Employ transforms and item wrapper * Add image class and tests * Add image info support to formats * Fix cli * Fix merge and voc converte * Update remote images extractor * Codacy * Remove item name, require path in Image * Merge images of dataset items * Update tests * Add image dir converter * Update Datumaro format * Update COCO format with image info * Update CVAT format with image info * Update TFrecord format with image info * Update VOC formar with image info * Update YOLO format with image info * Update dataset manager bindings with image info * Add image name to id transform * Fix coco export * More types in actions and reducers (#1166) * [Datumaro] Add masks to tfrecord format (#1156) * Employ transforms and item wrapper * Add image class and tests * Add image info support to formats * Fix cli * Fix merge and voc converte * Update remote images extractor * Codacy * Remove item name, require path in Image * Merge images of dataset items * Update tests * Add image dir converter * Update Datumaro format * Update COCO format with image info * Update CVAT format with image info * Update TFrecord format with image info * Update VOC formar with image info * Update YOLO format with image info * Update dataset manager bindings with image info * Add image name to id transform * Fix coco export * Add masks support for tfrecord * Refactor coco * Fix comparison * Remove dead code * Extract common code for instances * Replace YOLO format support in CVAT with Datumaro (#1151) * Employ transforms and item wrapper * Add image class and tests * Add image info support to formats * Fix cli * Fix merge and voc converte * Update remote images extractor * Codacy * Remove item name, require path in Image * Merge images of dataset items * Update tests * Add image dir converter * Update Datumaro format * Update COCO format with image info * Update CVAT format with image info * Update TFrecord format with image info * Update VOC formar with image info * Update YOLO format with image info * Update dataset manager bindings with image info * Add image name to id transform * Replace YOLO export and import in CVAT with Datumaro * Add editorconfig (#1142) * Add editorconfig * Update indent value * Cuboid annotation (#678) * Cuboid feature * migration files * Refactored cuboidShape Fixed a bug where coloring by label would not update cuboids properly Fixed a bug where the select points would not scale properly on initialization * Removed math.js dependency Implemented custom line intersection function * new cvat formatting with labelled points * Added MIT License to js files that were missing it * Added simple constraints to the cuboids * reverted commit for settings for vscode to hide local path * fixed locking for cuboids * fixed cuboid View when locked * fixed occlusion view for cuboids * Allow cuboid points to be outside the frame dimensions. Signed-off-by: Tritin Truong * Added stricter constraints on cuboid edges. * Slightly stricter restrictions for edge case * Cleaned up unused imports * removed dashed lines on cuboids * Moved projection lines to settings tab * Fixed Cuboid shape buffer \ * Fix migrations (two 022 migrations after merge with the develop branch). * Fix compatibility issues with auto segmentation. * Grab points and update control scheme * Greatly improved control scheme, fixed shape merging Fixed Cuboid upload * Fixed slight visual bug when dragging faces * Some optimizations * Hiding the grab point on creation Small refactoring * Fixed some cases where cuboid breaks * Fixed upload for videos * Removed perspective effects * Made left back edge editable * left back edge resizable * fix statistics bug * added toggles for the back edges * Constraints for the back edges * Fix creation bug * Tightened creation constraints * Fixing the code style * updated message for invalid cuboids * Code style * More style fixes * Codacy fixes * added shift control for edges * More Codacy fixes * More Codacy fixes * Double arrows for cursor * Fix Drag bug * More Codacy fixes * Fix double quotes * Fix camel case * More camelcase fixes * Generic object sink fixes * Various codacy fixes * Codacy * Double quotes * Fix migrations * Updated shape creation Fix jittering * Adjusted constraints * Codacy fixes * Codacy fixes again * Drawing cuboids from the top and bottom * Codacy * Resetting perspective on cuboids * Choosing orientation of cuboids. * Codacy fix * Merge cleanup * revert vs-code settings * Update settings.json Co-authored-by: timbowl <54648082+timbowl@users.noreply.github.com> Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> * Update yolo format description (#1173) * Replace tfrecord format support in CVAT with Datumaro (#1157) * Replace mask format support with Datumaro (#1163) * Add box to mask transform * Fix 'source' labelmap mode in voc converter * Import groups * Replace mask format support * Update mask format documentation * codacy * Fix tests * Fix dataset * Fix segments grouping * Merge instances in mask export * Update Onepanel demo information and link (#1189) * Added displayed versions of core, canvas, and ui in about (#1191) * Added displayed versions of core, canvas, and ui in about * Removed extra method * React UI: ZOrder implementation (#1176) * Drawn z-order switcher * Z layer was added to state * Added ZLayer API method cvat-canvas * Added sorting by Z * Displaying points in top * Removed old code * Improved sort function * Drawn a couple of icons * Send to foreground / send to background * Updated unit tests * Added unit tests for filter parser * Removed extra code * Updated README.md * Replace VOC format support in CVAT with Datumaro (#1167) * Add image meta reading to voc * Replace voc support in cvat * Bump format version * Materialize lazy transforms in voc export * Store voc instance id as group id * Add flat format import * Add documentation * Fix format name in doc * [Datumaro] Remote project export fixes (#1193) * Export project with trask name * Do not expose server paths * Fix tfrecord mask reading in tf>1.14 * Setuptools compatibility * Replace COCO implementation (#1195) * Fixed lags (#1197) * React UI: Changing color for a shape (#1194) * Minimized size of an element in side panel * To background / to foreground like in legacy UI * Added color changer for a shape * Adjusted color updating * React-UI: settings (#1164) * Image filters: brightness, contrast, saturation * Auto saving * Frame auto fit * Player speed * Leave confirmation for unsaved changes * React UI: Changing color for a group (#1205) * Added license headers (#1208) * Added licenser * Added license headers for cvat-canvas and cvat-ui * Move project dir to .datumaro (#1207) * Updated svg.js version (#1212) * React UI: Batch of fixes (#1211) * Disabled tracks for polyshapes in UI * RectDrawingMethod enum pushed to cvat-canvas, fixed some code issues * Optional arguments * Draw a text for locked shapes, some fixes with not keyframe shapes * Fixed zooming & batch grouping * Reset zoom for tasks with images * Fixed putting shapes out of canvas * Fixed grid opacity, little refactoring of componentDidUpdate in canvas-wrapper component * Fixed corner cases for drawing * Fixed putting shapes out of canvas * Improved drawing * Removed extra event handler * Auto-generate labelmap for voc from task (#1214) * Add random split transform (#1213) * React UI: Improved rotation feature (#1206) Co-authored-by: Boris Sekachev <40690378+bsekachev@users.noreply.github.com> * Az/cvat proxy (#1177) * added nginx proxy * removed unnecessary port configuration & build arg * updated installation guide * Add tags to cvat xml (#1200) * Extend cvat format test * Add tags to cvat for images * Add tags to cvat format in dm * Add import of tags from datumaro * React UI: Pinned option was added (#1202) * Fix remainder logic for subset splitting (#1222) * Add tags support for VOC (#1201) * Extend voc format test with tags * Add import and export of voc labels * Fix voc and yolo format version numbers * React UI: batch of fixes (#1227) * Fix: keyframes navigation * Fix: handled removing of the latest keyframe * Fix: activating a shape when another shape is being changed * Fix: up points in the side bar on points click * Fix: editable shape isn't transformed when change zoom * Updated message * React UI: Filters history (#1225) * Added filters history * Fixed unclosed dropdown * Added saving filters to localStrorage * Added button to cancel started automatic annotation (#1198) * [WIP] Cuboid feature user guide (#1218) * Initial cuboid description * Added Gifs * Added gifs to descriptions * Formatting fixes * Codacy Fixes * Az/fix annotation dump upload (#1229) * fixed upload annotation in case of frame step != 1 * fixed upload annotation in case of attribute value is empty * React UI: Added shortcuts (#1230) * [Datumaro] Label remapping transform (#1233) * Add label remapping transform * Apply transforms before project saving * Refactor voc converter * [Datumaro] Optimize mask operations (#1232) * Optimize mask to rle * Optimize mask operations * Fix dm format cmdline * Use RLE masks in datumaro format * Fixed date in CHANGELOG.md * sort frame shapes by z_order (#1258) Co-authored-by: vfdev Co-authored-by: Boris Sekachev <40690378+bsekachev@users.noreply.github.com> Co-authored-by: Ben Hoff Co-authored-by: Boris Sekachev Co-authored-by: Ben Hoff Co-authored-by: telenachos <54951461+telenachos@users.noreply.github.com> Co-authored-by: Johannes222 Co-authored-by: RS Nikhil Krishna Co-authored-by: Andrey Zhavoronkov <41117609+azhavoro@users.noreply.github.com> Co-authored-by: Reza Malek Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: zhiltsov-max Co-authored-by: a-andre Co-authored-by: Maksim Markelov Co-authored-by: himalayanZephyr <42401082+himalayanZephyr@users.noreply.github.com> Co-authored-by: Seungwon Jeong Co-authored-by: Maya <49038720+Marishka17@users.noreply.github.com> Co-authored-by: TOsmanov <54434686+TOsmanov@users.noreply.github.com> Co-authored-by: vugia truong Co-authored-by: roho Co-authored-by: Christian Co-authored-by: provider161 Co-authored-by: Radhika <43014570+radhika1601@users.noreply.github.com> Co-authored-by: Tanvi Anand Co-authored-by: Lisa <38404726+LiSa20120@users.noreply.github.com> Co-authored-by: Josh Bradley Co-authored-by: Priya4607 <59498234+Priya4607@users.noreply.github.com> Co-authored-by: LukeAI <43993778+LukeAI@users.noreply.github.com> Co-authored-by: Jijoong Kim Co-authored-by: Nikita Glazov Co-authored-by: Tritin Truong Co-authored-by: timbowl <54648082+timbowl@users.noreply.github.com> Co-authored-by: Rush Tehrani Co-authored-by: Dmitry Kalinin Co-authored-by: Tritin Truong * [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 * React UI: Added logging (#1288) * OpenVino 2020 (#1269) * added support for OpenVINO 2020 * fixed dextr and tf_annotation Co-authored-by: Andrey Zhavoronkov * 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 * Release v0.6.1 (#1267) * Change the version and updated CHANGELOG.md * 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] * Updated CHANGELOG.md Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Boris Sekachev * 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] 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 * Fix release date. Co-authored-by: Boris Sekachev <40690378+bsekachev@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Boris Sekachev Co-authored-by: zhiltsov-max * Add information about v0.6.1 release. * React UI: Better exception handling (#1297) Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com> Co-authored-by: Nikita Manovich Co-authored-by: Boris Sekachev <40690378+bsekachev@users.noreply.github.com> Co-authored-by: vfdev Co-authored-by: Ben Hoff Co-authored-by: Boris Sekachev Co-authored-by: Ben Hoff Co-authored-by: telenachos <54951461+telenachos@users.noreply.github.com> Co-authored-by: Johannes222 Co-authored-by: RS Nikhil Krishna Co-authored-by: Reza Malek Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: zhiltsov-max Co-authored-by: a-andre Co-authored-by: Maksim Markelov Co-authored-by: himalayanZephyr <42401082+himalayanZephyr@users.noreply.github.com> Co-authored-by: Seungwon Jeong Co-authored-by: Maya <49038720+Marishka17@users.noreply.github.com> Co-authored-by: TOsmanov <54434686+TOsmanov@users.noreply.github.com> Co-authored-by: vugia truong Co-authored-by: roho Co-authored-by: Christian Co-authored-by: provider161 Co-authored-by: Radhika <43014570+radhika1601@users.noreply.github.com> Co-authored-by: Tanvi Anand Co-authored-by: Lisa <38404726+LiSa20120@users.noreply.github.com> Co-authored-by: Josh Bradley Co-authored-by: Priya4607 <59498234+Priya4607@users.noreply.github.com> Co-authored-by: LukeAI <43993778+LukeAI@users.noreply.github.com> Co-authored-by: Jijoong Kim Co-authored-by: Nikita Glazov Co-authored-by: Tritin Truong Co-authored-by: timbowl <54648082+timbowl@users.noreply.github.com> Co-authored-by: Rush Tehrani Co-authored-by: Dmitry Kalinin Co-authored-by: Tritin Truong --- CHANGELOG.md | 23 +- cvat-canvas/README.md | 21 +- cvat-canvas/src/typescript/canvasView.ts | 26 +- cvat-canvas/src/typescript/drawHandler.ts | 21 +- cvat-canvas/src/typescript/mergeHandler.ts | 9 +- cvat-core/package.json | 1 + cvat-core/src/api.js | 73 ++- cvat-core/src/config.js | 3 - cvat-core/src/enums.js | 125 ++--- cvat-core/src/log.js | 252 ++++++++++ cvat-core/src/logger-storage.js | 177 +++++++ cvat-core/src/logging.js | 42 -- cvat-core/src/server-proxy.js | 22 + cvat-core/src/session.js | 59 ++- cvat-ui/package-lock.json | 19 +- cvat-ui/package.json | 1 + cvat-ui/src/actions/annotation-actions.ts | 141 +++++- cvat-ui/src/actions/boundaries-actions.ts | 88 ++++ .../annotation-page/annotation-page.tsx | 10 +- .../attribute-annotation-sidebar.tsx | 16 +- .../standard-workspace/canvas-wrapper.tsx | 59 ++- cvat-ui/src/components/cvat-app.tsx | 91 ++-- .../global-error-boundary.tsx | 223 +++++++++ .../global-error-boundary/styles.scss | 17 + .../annotation-page/annotation-page.tsx | 6 +- .../objects-side-bar/object-item.tsx | 15 +- cvat-ui/src/cvat-logger.ts | 14 + cvat-ui/src/index.tsx | 50 +- cvat-ui/src/reducers/about-reducer.ts | 6 +- cvat-ui/src/reducers/annotation-reducer.ts | 20 +- cvat-ui/src/reducers/auth-reducer.ts | 6 +- cvat-ui/src/reducers/formats-reducer.ts | 8 +- cvat-ui/src/reducers/interfaces.ts | 4 + cvat-ui/src/reducers/models-reducer.ts | 11 +- cvat-ui/src/reducers/notifications-reducer.ts | 40 +- cvat-ui/src/reducers/settings-reducer.ts | 7 + cvat-ui/src/reducers/share-reducer.ts | 8 +- cvat-ui/src/reducers/shortcuts-reducer.ts | 11 +- cvat-ui/src/reducers/tasks-reducer.ts | 6 +- cvat-ui/src/reducers/users-reducer.ts | 8 +- cvat-ui/src/utils/redux.ts | 5 +- cvat-ui/webpack.config.js | 4 +- cvat/apps/annotation/labelme.py | 8 +- cvat/apps/auto_annotation/inference_engine.py | 17 +- cvat/apps/auto_annotation/model_loader.py | 26 +- cvat/apps/dataset_manager/bindings.py | 12 +- cvat/apps/dextr_segmentation/dextr.py | 9 +- cvat/apps/engine/tests/test_rest_api.py | 15 + cvat/apps/tf_annotation/views.py | 9 +- datumaro/datumaro/components/extractor.py | 1 + datumaro/datumaro/components/project.py | 4 +- .../datumaro/plugins/coco_format/converter.py | 66 +-- .../datumaro/plugins/coco_format/importer.py | 9 +- .../datumaro/plugins/cvat_format/converter.py | 11 +- .../datumaro/plugins/cvat_format/importer.py | 8 +- .../plugins/datumaro_format/converter.py | 35 +- datumaro/datumaro/plugins/labelme_format.py | 472 ++++++++++++++++++ datumaro/datumaro/plugins/mot_format.py | 341 +++++++++++++ .../tf_detection_api_format/importer.py | 3 +- datumaro/datumaro/plugins/transforms.py | 3 +- .../datumaro/plugins/yolo_format/extractor.py | 4 +- .../datumaro/plugins/yolo_format/importer.py | 2 +- datumaro/datumaro/util/__init__.py | 27 +- datumaro/datumaro/util/mask_tools.py | 1 - .../labelme_dataset/Masks/img1_mask_1.png | Bin 0 -> 211 bytes .../labelme_dataset/Masks/img1_mask_5.png | Bin 0 -> 388 bytes .../Scribbles/img1_scribble_1.png | Bin 0 -> 206 bytes .../Scribbles/img1_scribble_5.png | Bin 0 -> 387 bytes .../tests/assets/labelme_dataset/img1.png | Bin 0 -> 215 bytes .../tests/assets/labelme_dataset/img1.xml | 1 + datumaro/tests/test_coco_format.py | 5 +- datumaro/tests/test_labelme_format.py | 214 ++++++++ datumaro/tests/test_mot_format.py | 146 ++++++ 73 files changed, 2795 insertions(+), 402 deletions(-) create mode 100644 cvat-core/src/log.js create mode 100644 cvat-core/src/logger-storage.js delete mode 100644 cvat-core/src/logging.js create mode 100644 cvat-ui/src/actions/boundaries-actions.ts create mode 100644 cvat-ui/src/components/global-error-boundary/global-error-boundary.tsx create mode 100644 cvat-ui/src/components/global-error-boundary/styles.scss create mode 100644 cvat-ui/src/cvat-logger.ts create mode 100644 datumaro/datumaro/plugins/labelme_format.py create mode 100644 datumaro/datumaro/plugins/mot_format.py create mode 100644 datumaro/tests/assets/labelme_dataset/Masks/img1_mask_1.png create mode 100644 datumaro/tests/assets/labelme_dataset/Masks/img1_mask_5.png create mode 100644 datumaro/tests/assets/labelme_dataset/Scribbles/img1_scribble_1.png create mode 100644 datumaro/tests/assets/labelme_dataset/Scribbles/img1_scribble_5.png create mode 100644 datumaro/tests/assets/labelme_dataset/img1.png create mode 100644 datumaro/tests/assets/labelme_dataset/img1.xml create mode 100644 datumaro/tests/test_labelme_format.py create mode 100644 datumaro/tests/test_mot_format.py diff --git a/CHANGELOG.md b/CHANGELOG.md index eb69400edd8..3f15c960877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0-beta] - Unreleased +## [1.0.0-alpha] - Unreleased ### Added - ### Changed -- VOC task export now does not use official label map by default, but takes one - from the source task to avoid primary-class and class part name - clashing ([#1275](https://github.com/opencv/cvat/issues/1275)) +- ### Deprecated - @@ -22,6 +20,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - +### Security +- + +## [0.6.1] - 2020-03-21 +### Changed +- VOC task export now does not use official label map by default, but takes one + from the source task to avoid primary-class and class part name + clashing ([#1275](https://github.com/opencv/cvat/issues/1275)) + +### Fixed +- File names in LabelMe format export are no longer truncated ([#1259](https://github.com/opencv/cvat/issues/1259)) +- `occluded` and `z_order` annotation attributes are now correctly passed to Datumaro ([#1271](https://github.com/opencv/cvat/pull/1271)) +- Annotation-less tasks now can be exported as empty datasets in COCO ([#1277](https://github.com/opencv/cvat/issues/1277)) +- Frame name matching for video annotations import - + allowed `frame_XXXXXX[.ext]` format ([#1274](https://github.com/opencv/cvat/pull/1274)) + ### Security - Bump acorn from 6.3.0 to 6.4.1 in /cvat-ui ([#1270](https://github.com/opencv/cvat/pull/1270)) @@ -48,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - React & Redux & Antd based dashboard - Yolov3 interpretation script fix and changes to mapping.json - YOLO format support ([#1151](https://github.com/opencv/cvat/pull/1151)) +- Added support for OpenVINO 2020 ### Fixed - Exception in Git plugin [#826](https://github.com/opencv/cvat/issues/826) diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md index 2b9f9201ba7..7fe6049176e 100644 --- a/cvat-canvas/README.md +++ b/cvat-canvas/README.md @@ -37,6 +37,19 @@ Canvas itself handles: EXTREME_POINTS = 'By 4 points' } + enum Mode { + IDLE = 'idle', + DRAG = 'drag', + RESIZE = 'resize', + DRAW = 'draw', + EDIT = 'edit', + MERGE = 'merge', + SPLIT = 'split', + GROUP = 'group', + DRAG_CANVAS = 'drag_canvas', + ZOOM_CANVAS = 'zoom_canvas', + } + interface DrawData { enabled: boolean; shapeType?: string; @@ -70,6 +83,7 @@ Canvas itself handles: } interface Canvas { + mode(): Mode; html(): HTMLDivElement; setZLayer(zLayer: number | null): void; setup(frameData: any, objectStates: any[]): void; @@ -128,6 +142,10 @@ Standard JS events are used. - canvas.dragstop - canvas.zoomstart - canvas.zoomstop + - canvas.zoom + - canvas.fit + - canvas.dragshape => {id: number} + - canvas.resizeshape => {id: number} ``` ### WEB @@ -135,7 +153,8 @@ Standard JS events are used. // Create an instance of a canvas const canvas = new window.canvas.Canvas(); - console.log('Version', window.canvas.CanvasVersion); + console.log('Version ', window.canvas.CanvasVersion); + console.log('Current mode is ', window.canvas.mode()); // Put canvas to a html container htmlContainer.appendChild(canvas.html()); diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index f6f68d1f977..b372015fdb6 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -74,7 +74,7 @@ export class CanvasViewImpl implements CanvasView, Listener { return this.controller.mode; } - private onDrawDone(data: object, continueDraw?: boolean): void { + private onDrawDone(data: object | null, duration: number, continueDraw?: boolean): void { if (data) { const { zLayer } = this.controller; const event: CustomEvent = new CustomEvent('canvas.drawn', { @@ -87,6 +87,7 @@ export class CanvasViewImpl implements CanvasView, Listener { zOrder: zLayer || 0, }, continue: continueDraw, + duration, }, }); @@ -137,12 +138,13 @@ export class CanvasViewImpl implements CanvasView, Listener { this.mode = Mode.IDLE; } - private onMergeDone(objects: any[]): void { + private onMergeDone(objects: any[]| null, duration?: number): void { if (objects) { const event: CustomEvent = new CustomEvent('canvas.merged', { bubbles: false, cancelable: true, detail: { + duration, states: objects, }, }); @@ -708,6 +710,12 @@ export class CanvasViewImpl implements CanvasView, Listener { } else if ([UpdateReasons.IMAGE_ZOOMED, UpdateReasons.IMAGE_FITTED].includes(reason)) { this.moveCanvas(); this.transformCanvas(); + if (reason === UpdateReasons.IMAGE_FITTED) { + this.canvas.dispatchEvent(new CustomEvent('canvas.fit', { + bubbles: false, + cancelable: true, + })); + } } else if (reason === UpdateReasons.IMAGE_MOVED) { this.moveCanvas(); } else if ([UpdateReasons.OBJECTS_UPDATED, UpdateReasons.SET_Z_LAYER].includes(reason)) { @@ -1166,6 +1174,13 @@ export class CanvasViewImpl implements CanvasView, Listener { ).map((x: number): number => x - offset); this.drawnStates[state.clientID].points = points; + this.canvas.dispatchEvent(new CustomEvent('canvas.dragshape', { + bubbles: false, + cancelable: true, + detail: { + id: state.clientID, + }, + })); this.onEditDone(state, points); } }); @@ -1216,6 +1231,13 @@ export class CanvasViewImpl implements CanvasView, Listener { ).map((x: number): number => x - offset); this.drawnStates[state.clientID].points = points; + this.canvas.dispatchEvent(new CustomEvent('canvas.resizeshape', { + bubbles: false, + cancelable: true, + detail: { + id: state.clientID, + }, + })); this.onEditDone(state, points); } }); diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index 60745f9f3ec..4afe75ec259 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -31,7 +31,8 @@ export interface DrawHandler { export class DrawHandlerImpl implements DrawHandler { // callback is used to notify about creating new shape - private onDrawDone: (data: object, continueDraw?: boolean) => void; + private onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void; + private startTimestamp: number; private canvas: SVG.Container; private text: SVG.Container; private cursorPosition: { @@ -180,7 +181,7 @@ export class DrawHandlerImpl implements DrawHandler { this.onDrawDone({ shapeType, points: [xtl, ytl, xbr, ybr], - }); + }, Date.now() - this.startTimestamp); } }).on('drawupdate', (): void => { this.shapeSizeElement.update(this.drawInstance); @@ -213,7 +214,7 @@ export class DrawHandlerImpl implements DrawHandler { this.onDrawDone({ shapeType, points: [xtl, ytl, xbr, ybr], - }); + }, Date.now() - this.startTimestamp); } } }).on('undopoint', (): void => { @@ -300,7 +301,7 @@ export class DrawHandlerImpl implements DrawHandler { this.onDrawDone({ shapeType, points, - }); + }, Date.now() - this.startTimestamp); } else if (shapeType === 'polyline' && ((box.xbr - box.xtl) >= consts.SIZE_THRESHOLD || (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD) @@ -308,13 +309,13 @@ export class DrawHandlerImpl implements DrawHandler { this.onDrawDone({ shapeType, points, - }); + }, Date.now() - this.startTimestamp); } else if (shapeType === 'points' && (e.target as any).getAttribute('points') !== '0,0') { this.onDrawDone({ shapeType, points, - }); + }, Date.now() - this.startTimestamp); } }); } @@ -365,7 +366,7 @@ export class DrawHandlerImpl implements DrawHandler { attributes: { ...this.drawData.initialState.attributes }, label: this.drawData.initialState.label, color: this.drawData.initialState.color, - }, e.detail.originalEvent.ctrlKey); + }, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey); }); } @@ -405,7 +406,7 @@ export class DrawHandlerImpl implements DrawHandler { attributes: { ...this.drawData.initialState.attributes }, label: this.drawData.initialState.label, color: this.drawData.initialState.color, - }, e.detail.originalEvent.ctrlKey); + }, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey); }); } @@ -583,14 +584,16 @@ export class DrawHandlerImpl implements DrawHandler { this.setupDrawEvents(); } + this.startTimestamp = Date.now(); this.initialized = true; } public constructor( - onDrawDone: (data: object, continueDraw?: boolean) => void, + onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void, canvas: SVG.Container, text: SVG.Container, ) { + this.startTimestamp = Date.now(); this.onDrawDone = onDrawDone; this.canvas = canvas; this.text = text; diff --git a/cvat-canvas/src/typescript/mergeHandler.ts b/cvat-canvas/src/typescript/mergeHandler.ts index efaa4ac09d7..cfb3f78c43d 100644 --- a/cvat-canvas/src/typescript/mergeHandler.ts +++ b/cvat-canvas/src/typescript/mergeHandler.ts @@ -15,8 +15,9 @@ export interface MergeHandler { export class MergeHandlerImpl implements MergeHandler { // callback is used to notify about merging end - private onMergeDone: (objects: any[]) => void; + private onMergeDone: (objects: any[] | null, duration?: number) => void; private onFindObject: (event: MouseEvent) => void; + private startTimestamp: number; private canvas: SVG.Container; private initialized: boolean; private statesToBeMerged: any[]; // are being merged @@ -57,6 +58,7 @@ export class MergeHandlerImpl implements MergeHandler { private initMerging(): void { this.canvas.node.addEventListener('click', this.onFindObject); + this.startTimestamp = Date.now(); this.initialized = true; } @@ -66,7 +68,7 @@ export class MergeHandlerImpl implements MergeHandler { this.release(); if (statesToBeMerged.length > 1) { - this.onMergeDone(statesToBeMerged); + this.onMergeDone(statesToBeMerged, Date.now() - this.startTimestamp); } else { this.onMergeDone(null); // here is a cycle @@ -77,12 +79,13 @@ export class MergeHandlerImpl implements MergeHandler { } public constructor( - onMergeDone: (objects: any[]) => void, + onMergeDone: (objects: any[] | null, duration?: number) => void, onFindObject: (event: MouseEvent) => void, canvas: SVG.Container, ) { this.onMergeDone = onMergeDone; this.onFindObject = onFindObject; + this.startTimestamp = Date.now(); this.canvas = canvas; this.statesToBeMerged = []; this.highlightedShapes = {}; diff --git a/cvat-core/package.json b/cvat-core/package.json index d07e81aa12a..8859adc59bd 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -34,6 +34,7 @@ "dependencies": { "axios": "^0.18.0", "browser-or-node": "^1.2.1", + "detect-browser": "^5.0.0", "error-stack-parser": "^2.0.2", "form-data": "^2.5.0", "jest-config": "^24.8.0", diff --git a/cvat-core/src/api.js b/cvat-core/src/api.js index 7f9ad9c7448..4eb4e99af00 100644 --- a/cvat-core/src/api.js +++ b/cvat-core/src/api.js @@ -14,7 +14,8 @@ function build() { const PluginRegistry = require('./plugins'); - const User = require('./user'); + const loggerStorage = require('./logger-storage'); + const Log = require('./log'); const ObjectState = require('./object-state'); const Statistics = require('./statistics'); const { Job, Task } = require('./session'); @@ -41,6 +42,7 @@ function build() { ServerError, } = require('./exceptions'); + const User = require('./user'); const pjson = require('../package.json'); const config = require('./config'); @@ -419,6 +421,53 @@ function build() { return result; }, }, + /** + * Namespace to working with logs + * @namespace logger + * @memberof module:API.cvat + */ + /** + * Method to logger configuration + * @method configure + * @memberof module:API.cvat.logger + * @param {function} isActiveChecker - callback to know if logger + * should increase working time or not + * @param {object} userActivityCallback - container for a callback
+ * Logger put here a callback to update user activity timer
+ * You can call it outside + * @instance + * @async + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + + /** + * Append log to a log collection
+ * Durable logs will have been added after "close" method is called for them
+ * Ignore rules exist for some logs (e.g. zoomImage, changeAttribute)
+ * Payload of ignored logs are shallowly combined to previous logs of the same type + * @method log + * @memberof module:API.cvat.logger + * @param {module:API.cvat.enums.LogType | string} type - log type + * @param {Object} [payload = {}] - any other data that will be appended to the log + * @param {boolean} [wait = false] - specifies if log is durable + * @returns {module:API.cvat.classes.Log} + * @instance + * @async + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + + /** + * Save accumulated logs on a server + * @method save + * @memberof module:API.cvat.logger + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} + * @instance + * @async + */ + logger: loggerStorage, /** * Namespace contains some changeable configurations * @namespace config @@ -432,12 +481,6 @@ function build() { * @property {string} proxy Axios proxy settings. * For more details please read here * @memberof module:API.cvat.config - * @property {integer} taskID this value is displayed in a logs if available - * @memberof module:API.cvat.config - * @property {integer} jobID this value is displayed in a logs if available - * @memberof module:API.cvat.config - * @property {integer} clientID read only auto-generated - * value which is displayed in a logs * @memberof module:API.cvat.config */ get backendAPI() { @@ -452,21 +495,6 @@ function build() { set proxy(value) { config.proxy = value; }, - get taskID() { - return config.taskID; - }, - set taskID(value) { - config.taskID = value; - }, - get jobID() { - return config.jobID; - }, - set jobID(value) { - config.jobID = value; - }, - get clientID() { - return config.clientID; - }, }, /** * Namespace contains some library information e.g. api version @@ -524,6 +552,7 @@ function build() { Task, User, Job, + Log, Attribute, Label, Statistics, diff --git a/cvat-core/src/config.js b/cvat-core/src/config.js index e940a214c95..3b9eade8f34 100644 --- a/cvat-core/src/config.js +++ b/cvat-core/src/config.js @@ -6,7 +6,4 @@ module.exports = { backendAPI: 'http://localhost:7000/api/v1', proxy: false, - taskID: undefined, - jobID: undefined, - clientID: +Date.now().toString().substr(-6), }; diff --git a/cvat-core/src/enums.js b/cvat-core/src/enums.js index 2c34290876b..feef4825bdb 100644 --- a/cvat-core/src/enums.js +++ b/cvat-core/src/enums.js @@ -103,68 +103,76 @@ }); /** - * Event types - * @enum {number} + * Logger event types + * @enum {string} * @name LogType * @memberof module:API.cvat.enums - * @property {number} pasteObject 0 - * @property {number} changeAttribute 1 - * @property {number} dragObject 2 - * @property {number} deleteObject 3 - * @property {number} pressShortcut 4 - * @property {number} resizeObject 5 - * @property {number} sendLogs 6 - * @property {number} saveJob 7 - * @property {number} jumpFrame 8 - * @property {number} drawObject 9 - * @property {number} changeLabel 10 - * @property {number} sendTaskInfo 11 - * @property {number} loadJob 12 - * @property {number} moveImage 13 - * @property {number} zoomImage 14 - * @property {number} lockObject 15 - * @property {number} mergeObjects 16 - * @property {number} copyObject 17 - * @property {number} propagateObject 18 - * @property {number} undoAction 19 - * @property {number} redoAction 20 - * @property {number} sendUserActivity 21 - * @property {number} sendException 22 - * @property {number} changeFrame 23 - * @property {number} debugInfo 24 - * @property {number} fitImage 25 - * @property {number} rotateImage 26 + * @property {string} loadJob Load job + * @property {string} saveJob Save job + * @property {string} restoreJob Restore job + * @property {string} uploadAnnotations Upload annotations + * @property {string} sendUserActivity Send user activity + * @property {string} sendException Send exception + * @property {string} sendTaskInfo Send task info + + * @property {string} drawObject Draw object + * @property {string} pasteObject Paste object + * @property {string} copyObject Copy object + * @property {string} propagateObject Propagate object + * @property {string} dragObject Drag object + * @property {string} resizeObject Resize object + * @property {string} deleteObject Delete object + * @property {string} lockObject Lock object + * @property {string} mergeObjects Merge objects + * @property {string} changeAttribute Change attribute + * @property {string} changeLabel Change label + + * @property {string} changeFrame Change frame + * @property {string} moveImage Move image + * @property {string} zoomImage Zoom image + * @property {string} fitImage Fit image + * @property {string} rotateImage Rotate image + + * @property {string} undoAction Undo action + * @property {string} redoAction Redo action + + * @property {string} pressShortcut Press shortcut + * @property {string} debugInfo Debug info * @readonly */ - const LogType = { - pasteObject: 0, - changeAttribute: 1, - dragObject: 2, - deleteObject: 3, - pressShortcut: 4, - resizeObject: 5, - sendLogs: 6, - saveJob: 7, - jumpFrame: 8, - drawObject: 9, - changeLabel: 10, - sendTaskInfo: 11, - loadJob: 12, - moveImage: 13, - zoomImage: 14, - lockObject: 15, - mergeObjects: 16, - copyObject: 17, - propagateObject: 18, - undoAction: 19, - redoAction: 20, - sendUserActivity: 21, - sendException: 22, - changeFrame: 23, - debugInfo: 24, - fitImage: 25, - rotateImage: 26, - }; + const LogType = Object.freeze({ + loadJob: 'Load job', + saveJob: 'Save job', + restoreJob: 'Restore job', + uploadAnnotations: 'Upload annotations', + sendUserActivity: 'Send user activity', + sendException: 'Send exception', + sendTaskInfo: 'Send task info', + + drawObject: 'Draw object', + pasteObject: 'Paste object', + copyObject: 'Copy object', + propagateObject: 'Propagate object', + dragObject: 'Drag object', + resizeObject: 'Resize object', + deleteObject: 'Delete object', + lockObject: 'Lock object', + mergeObjects: 'Merge objects', + changeAttribute: 'Change attribute', + changeLabel: 'Change label', + + changeFrame: 'Change frame', + moveImage: 'Move image', + zoomImage: 'Zoom image', + fitImage: 'Fit image', + rotateImage: 'Rotate image', + + undoAction: 'Undo action', + redoAction: 'Redo action', + + pressShortcut: 'Press shortcut', + debugInfo: 'Debug info', + }); /** * Types of actions with annotations @@ -208,7 +216,6 @@ /** * Array of hex colors - * @type {module:API.cvat.classes.Loader[]} values * @name colors * @memberof module:API.cvat.enums * @type {string[]} diff --git a/cvat-core/src/log.js b/cvat-core/src/log.js new file mode 100644 index 00000000000..68cce5c5d5a --- /dev/null +++ b/cvat-core/src/log.js @@ -0,0 +1,252 @@ +// 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
+ * Note then you can call close() multiple times
+ * Log duration will be computed based on the latest call
+ * 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() { + const payload = { ...this.payload }; + const client = detect(); + const body = { + client_id: payload.client_id, + name: this.type, + time: this.time.toISOString(), + 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.client_id; + delete payload.message; + delete payload.filename; + delete payload.line; + delete payload.column; + delete payload.stack; + + return { + ...body, + payload, + }; + } +} + +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; diff --git a/cvat-core/src/logger-storage.js b/cvat-core/src/logger-storage.js new file mode 100644 index 00000000000..ca6860af898 --- /dev/null +++ b/cvat-core/src/logger-storage.js @@ -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(); diff --git a/cvat-core/src/logging.js b/cvat-core/src/logging.js deleted file mode 100644 index f6b52c4ec32..00000000000 --- a/cvat-core/src/logging.js +++ /dev/null @@ -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; -})(); diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index da2675ce4cf..8251328223d 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -643,6 +643,21 @@ }); } + async function saveLogs(logs) { + const { backendAPI } = config; + + try { + await Axios.post(`${backendAPI}/server/logs`, JSON.stringify(logs), { + proxy: config.proxy, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (errorData) { + throw generateError(errorData); + } + } + Object.defineProperties(this, Object.freeze({ server: { value: Object.freeze({ @@ -705,6 +720,13 @@ }), writable: false, }, + + logs: { + value: Object.freeze({ + save: saveLogs, + }), + writable: false, + }, })); } } diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 0adfbcf072d..7d66fac9f41 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -9,6 +9,7 @@ (() => { const PluginRegistry = require('./plugins'); + const loggerStorage = require('./logger-storage'); const serverProxy = require('./server-proxy'); const { getFrame, getRanges, getPreview } = require('./frames'); const { ArgumentError } = require('./exceptions'); @@ -130,16 +131,11 @@ }, writable: true, }), - logs: Object.freeze({ + logger: Object.freeze({ value: { - async put(logType, details) { + async log(logType, payload = {}, wait = false) { const result = await PluginRegistry - .apiWrapper.call(this, prototype.logs.put, logType, details); - return result; - }, - async save(onUpdate) { - const result = await PluginRegistry - .apiWrapper.call(this, prototype.logs.save, onUpdate); + .apiWrapper.call(this, prototype.logger.log, logType, payload, wait); return result; }, }, @@ -451,33 +447,28 @@ /** * Namespace is used for an interaction with logs - * @namespace logs + * @namespace logger * @memberof Session */ /** - * Append log to a log collection. - * Continue logs will have been added after "close" method is called - * @method put - * @memberof Session.logs - * @param {module:API.cvat.enums.LogType} type a type of a log - * @param {boolean} continuous log is a continuous log - * @param {Object} details any others data which will be append to log data + * Create a log and add it to a log collection
+ * Durable logs will be added after "close" method is called for them
+ * The fields "task_id" and "job_id" automatically added when add logs + * throught a task or a job
+ * Ignore rules exist for some logs (e.g. zoomImage, changeAttribute)
+ * Payload of ignored logs are shallowly combined to previous logs of the same type + * @method log + * @memberof Session.logger + * @param {module:API.cvat.enums.LogType | string} type - log type + * @param {Object} [payload = {}] - any other data that will be appended to the log + * @param {boolean} [wait = false] - specifies if log is durable * @returns {module:API.cvat.classes.Log} * @instance * @async * @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.ArgumentError} */ - /** - * Save accumulated logs on a server - * @method save - * @memberof Session.logs - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ServerError} - * @instance - * @async - */ /** * Namespace is used for an interaction with actions @@ -718,6 +709,10 @@ ranges: Object.getPrototypeOf(this).frames.ranges.bind(this), preview: Object.getPrototypeOf(this).frames.preview.bind(this), }; + + this.logger = { + log: Object.getPrototypeOf(this).logger.log.bind(this), + }; } /** @@ -1266,6 +1261,10 @@ ranges: Object.getPrototypeOf(this).frames.ranges.bind(this), preview: Object.getPrototypeOf(this).frames.preview.bind(this), }; + + this.logger = { + log: Object.getPrototypeOf(this).logger.log.bind(this), + }; } /** @@ -1518,6 +1517,11 @@ return result; }; + Job.prototype.logger.log.implementation = async function (logType, payload, wait) { + const result = await this.task.logger.log(logType, { ...payload, job_id: this.id }, wait); + return result; + }; + Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) { // TODO: Add ability to change an owner and an assignee if (typeof (this.id) !== 'undefined') { @@ -1756,4 +1760,9 @@ const result = getActions(this); return result; }; + + Task.prototype.logger.log.implementation = async function (logType, payload, wait) { + const result = await loggerStorage.log(logType, { ...payload, task_id: this.id }, wait); + return result; + }; })(); diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 5ee86576af3..5c0d7ae1715 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -3900,6 +3900,14 @@ "is-arrayish": "^0.2.1" } }, + "error-stack-parser": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz", + "integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==", + "requires": { + "stackframe": "^1.1.1" + } + }, "es-abstract": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.0.tgz", @@ -11291,6 +11299,11 @@ "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", "dev": true }, + "stackframe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.1.1.tgz", + "integrity": "sha512-0PlYhdKh6AfFxRyK/v+6/k+/mMfyiEBbTM5L94D0ZytQnJ166wuwoTYLHFWGbs2dpA8Rgq763KGWmN1EQEYHRQ==" + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -12075,9 +12088,9 @@ "dev": true }, "ua-parser-js": { - "version": "0.7.20", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.20.tgz", - "integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw==" + "version": "0.7.21", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz", + "integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==" }, "uglify-js": { "version": "3.4.10", diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 9204b37a6a5..6b6907d2e9f 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -59,6 +59,7 @@ "antd": "^3.25.2", "copy-to-clipboard": "^3.2.0", "dotenv-webpack": "^1.7.0", + "error-stack-parser": "^2.0.6", "moment": "^2.24.0", "prop-types": "^15.7.2", "react": "^16.9.0", diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 8040d5e05aa..0080d48538f 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -22,6 +22,7 @@ import { } from 'reducers/interfaces'; import getCore from 'cvat-core'; +import logger, { LogType } from 'cvat-logger'; import { RectDrawingMethod } from 'cvat-canvas'; import { getCVATStore } from 'cvat-store'; @@ -77,10 +78,14 @@ function receiveAnnotationsParameters(): AnnotationsParameters { }; } -function computeZRange(states: any[]): number[] { +export function computeZRange(states: any[]): number[] { let minZ = states.length ? states[0].zOrder : 0; let maxZ = states.length ? states[0].zOrder : 0; states.forEach((state: any): void => { + if (state.objectType === ObjectType.TAG) { + return; + } + minZ = Math.min(minZ, state.zOrder); maxZ = Math.max(maxZ, state.zOrder); }); @@ -88,6 +93,23 @@ function computeZRange(states: any[]): number[] { return [minZ, maxZ]; } +async function jobInfoGenerator(job: any): Promise> { + const { total } = await job.annotations.statistics(); + return { + 'frame count': job.stopFrame - job.startFrame + 1, + 'track count': total.rectangle.shape + total.rectangle.track + + total.polygon.shape + total.polygon.track + + total.polyline.shape + total.polyline.track + + total.points.shape + total.points.track, + 'object count': total.total, + 'box count': total.rectangle.shape + total.rectangle.track, + 'polygon count': total.polygon.shape + total.polygon.track, + 'polyline count': total.polyline.shape + total.polyline.track, + 'points count': total.points.shape + total.points.track, + 'tag count': total.tags, + }; +} + export enum AnnotationActionTypes { GET_JOB = 'GET_JOB', GET_JOB_SUCCESS = 'GET_JOB_SUCCESS', @@ -165,6 +187,28 @@ export enum AnnotationActionTypes { ADD_Z_LAYER = 'ADD_Z_LAYER', SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED', CHANGE_WORKSPACE = 'CHANGE_WORKSPACE', + SAVE_LOGS_SUCCESS = 'SAVE_LOGS_SUCCESS', + SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED', +} + +export function saveLogsAsync(): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator) => { + try { + await logger.save(); + dispatch({ + type: AnnotationActionTypes.SAVE_LOGS_SUCCESS, + payload: {}, + }); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.SAVE_LOGS_FAILED, + payload: { + error, + }, + }); + } + }; } export function changeWorkspace(workspace: Workspace): AnyAction { @@ -192,8 +236,7 @@ export function switchZLayer(cur: number): AnyAction { }; } -export function fetchAnnotationsAsync(): -ThunkAction, {}, {}, AnyAction> { +export function fetchAnnotationsAsync(): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { const { @@ -250,14 +293,21 @@ export function undoActionAsync(sessionInstance: any, frame: number): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { + const state = getStore().getState(); const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); // TODO: use affected IDs as an optimization + const [undoName] = state.annotation.annotations.history.undo.slice(-1); + const undoLog = await sessionInstance.logger.log(LogType.undoAction, { + name: undoName, + count: 1, + }, true); await sessionInstance.actions.undo(); const history = await sessionInstance.actions.get(); const states = await sessionInstance.annotations .get(frame, showAllInterpolationTracks, filters); const [minZ, maxZ] = computeZRange(states); + await undoLog.close(); dispatch({ type: AnnotationActionTypes.UNDO_ACTION_SUCCESS, @@ -283,14 +333,21 @@ export function redoActionAsync(sessionInstance: any, frame: number): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { + const state = getStore().getState(); const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); // TODO: use affected IDs as an optimization + const [redoName] = state.annotation.annotations.history.redo.slice(-1); + const redoLog = await sessionInstance.logger.log(LogType.redoAction, { + name: redoName, + count: 1, + }, true); await sessionInstance.actions.redo(); const history = await sessionInstance.actions.get(); const states = await sessionInstance.annotations .get(frame, showAllInterpolationTracks, filters); const [minZ, maxZ] = computeZRange(states); + await redoLog.close(); dispatch({ type: AnnotationActionTypes.REDO_ACTION_SUCCESS, @@ -373,6 +430,12 @@ ThunkAction, {}, {}, AnyAction> { const frame = state.annotation.player.frame.number; await job.annotations.upload(file, loader); + await job.logger.log( + LogType.uploadAnnotations, { + ...(await jobInfoGenerator(job)), + }, + ); + // One more update to escape some problems // in canvas when shape with the same // clientID has different type (polygon, rectangle) for example @@ -499,6 +562,9 @@ export function propagateObjectAsync( frame: from, }; + await sessionInstance.logger.log( + LogType.propagateObject, { count: to - from + 1 }, + ); const states = []; for (let frame = from; frame <= to; frame++) { copy.frame = frame; @@ -549,6 +615,7 @@ export function removeObjectAsync(sessionInstance: any, objectState: any, force: ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { + await sessionInstance.logger.log(LogType.deleteObject, { count: 1 }); const removed = await objectState.delete(force); const history = await sessionInstance.actions.get(); @@ -584,6 +651,9 @@ export function editShape(enabled: boolean): AnyAction { } export function copyShape(objectState: any): AnyAction { + const job = getStore().getState().annotation.job.instance; + job.logger.log(LogType.copyObject, { count: 1 }); + return { type: AnnotationActionTypes.COPY_SHAPE, payload: { @@ -687,6 +757,12 @@ ThunkAction, {}, {}, AnyAction> { payload: {}, }); + await job.logger.log( + LogType.changeFrame, { + from: frame, + to: toFrame, + }, + ); const data = await job.frames.get(toFrame, fillBuffer, frameStep); const states = await job.annotations.get(toFrame, showAllInterpolationTracks, filters); const [minZ, maxZ] = computeZRange(states); @@ -707,6 +783,7 @@ ThunkAction, {}, {}, AnyAction> { } const delay = Math.max(0, Math.round(1000 / frameSpeed) - currentTime + (state.annotation.player.frame.changeTime as number)); + dispatch({ type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS, payload: { @@ -734,14 +811,33 @@ ThunkAction, {}, {}, AnyAction> { export function rotateCurrentFrame(rotation: Rotation): AnyAction { const state: CombinedState = getStore().getState(); - const { number: frameNumber } = state.annotation.player.frame; - const { startFrame } = state.annotation.job.instance; - const { frameAngles } = state.annotation.player; - const { rotateAll } = state.settings.player; + const { + annotation: { + player: { + frame: { + number: frameNumber, + }, + frameAngles, + }, + job: { + instance: job, + instance: { + startFrame, + }, + }, + }, + settings: { + player: { + rotateAll, + }, + }, + } = state; const frameAngle = (frameAngles[frameNumber - startFrame] + (rotation === Rotation.CLOCKWISE90 ? 90 : 270)) % 360; + job.logger.log(LogType.rotateImage, { angle: frameAngle }); + return { type: AnnotationActionTypes.ROTATE_FRAME, payload: { @@ -791,11 +887,6 @@ export function getJobAsync( initialFilters: string[], ): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { - dispatch({ - type: AnnotationActionTypes.GET_JOB, - payload: {}, - }); - try { const state: CombinedState = getStore().getState(); const filters = initialFilters; @@ -808,6 +899,18 @@ export function getJobAsync( }); } + dispatch({ + type: AnnotationActionTypes.GET_JOB, + payload: {}, + }); + + const loadJobEvent = await logger.log( + LogType.loadJob, { + task_id: tid, + job_id: jid, + }, true, + ); + // Check state if the task is already there let task = state.tasks.current .filter((_task: Task) => _task.instance.id === tid) @@ -832,6 +935,8 @@ export function getJobAsync( const [minZ, maxZ] = computeZRange(states); const colors = [...cvat.enums.colors]; + loadJobEvent.close(await jobInfoGenerator(job)); + dispatch({ type: AnnotationActionTypes.GET_JOB_SUCCESS, payload: { @@ -865,6 +970,10 @@ ThunkAction, {}, {}, AnyAction> { }); try { + const saveJobEvent = await sessionInstance.logger.log( + LogType.saveJob, {}, true, + ); + await sessionInstance.annotations.save((status: string) => { dispatch({ type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS, @@ -874,6 +983,13 @@ ThunkAction, {}, {}, AnyAction> { }); }); + await saveJobEvent.close(); + await sessionInstance.logger.log( + LogType.sendTaskInfo, + await jobInfoGenerator(sessionInstance), + ); + dispatch(saveLogsAsync()); + dispatch({ type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS, payload: {}, @@ -889,6 +1005,7 @@ ThunkAction, {}, {}, AnyAction> { }; } +// used to reproduce the latest drawing (in case of tags just creating) by using N export function rememberObject( objectType: ObjectType, labelID: number, diff --git a/cvat-ui/src/actions/boundaries-actions.ts b/cvat-ui/src/actions/boundaries-actions.ts new file mode 100644 index 00000000000..fa2faba0308 --- /dev/null +++ b/cvat-ui/src/actions/boundaries-actions.ts @@ -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'; +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 => { + 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; diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index a096afd2fde..db5b6500e9b 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT import './styles.scss'; -import React from 'react'; +import React, { useEffect } from 'react'; import { Layout, @@ -21,18 +21,24 @@ interface Props { job: any | null | undefined; fetching: boolean; getJob(): void; + saveLogs(): void; workspace: Workspace; } - export default function AnnotationPageComponent(props: Props): JSX.Element { const { job, fetching, getJob, + saveLogs, workspace, } = props; + useEffect(() => { + saveLogs(); + return saveLogs; + }, []); + if (job === null) { if (!fetching) { getJob(); diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx index d19b94ef9de..74cd0beca38 100644 --- a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx @@ -5,12 +5,15 @@ import React, { useState, useEffect } from 'react'; import { GlobalHotKeys, KeyMap } 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 { LogType } from 'cvat-logger'; import { activateObject as activateObjectAction, updateAnnotationsAsync, @@ -28,6 +31,7 @@ interface StateToProps { activatedAttributeID: number | null; states: any[]; labels: any[]; + jobInstance: any; } interface DispatchToProps { @@ -48,12 +52,14 @@ function mapStateToProps(state: CombinedState): StateToProps { states, }, job: { + instance: jobInstance, labels, }, }, } = state; return { + jobInstance, labels, activatedStateID, activatedAttributeID, @@ -61,7 +67,7 @@ function mapStateToProps(state: CombinedState): StateToProps { }; } -function mapDispatchToProps(dispatch: any): DispatchToProps { +function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps { return { activateObject(clientID: number, attrID: number): void { dispatch(activateObjectAction(clientID, attrID)); @@ -78,6 +84,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. states, activatedStateID, activatedAttributeID, + jobInstance, updateAnnotations, activateObject, } = props; @@ -267,6 +274,13 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. 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]); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index 91a2e20e698..16bc2e3ec14 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -9,6 +9,7 @@ import Layout from 'antd/lib/layout'; import Icon from 'antd/lib/icon'; import Tooltip from 'antd/lib/tooltip'; +import { LogType } from 'cvat-logger'; import { Canvas } from 'cvat-canvas'; import getCore from 'cvat-core'; import { @@ -214,6 +215,10 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().removeEventListener('canvas.deactivated', this.onCanvasShapeDeactivated); canvasInstance.html().removeEventListener('canvas.moved', this.onCanvasCursorMoved); + canvasInstance.html().removeEventListener('canvas.zoom', this.onCanvasZoomChanged); + canvasInstance.html().removeEventListener('canvas.fit', this.onCanvasImageFitted); + canvasInstance.html().removeEventListener('canvas.dragshape', this.onCanvasShapeDragged); + canvasInstance.html().removeEventListener('canvas.resizeshape', this.onCanvasShapeResized); canvasInstance.html().removeEventListener('canvas.clicked', this.onCanvasShapeClicked); canvasInstance.html().removeEventListener('canvas.drawn', this.onCanvasShapeDrawn); canvasInstance.html().removeEventListener('canvas.merged', this.onCanvasObjectsMerged); @@ -237,20 +242,18 @@ export default class CanvasWrapperComponent extends React.PureComponent { onShapeDrawn(); } - const { state } = event.detail; - if (!state.objectType) { - state.objectType = activeObjectType; - } - - if (!state.label) { - [state.label] = jobInstance.task.labels - .filter((label: any) => label.id === activeLabelID); - } - - if (typeof (state.occluded) === 'undefined') { - state.occluded = false; + const { state, duration } = event.detail; + const isDrawnFromScratch = !state.label; + if (isDrawnFromScratch) { + jobInstance.logger.log(LogType.drawObject, { count: 1, duration }); + } else { + jobInstance.logger.log(LogType.pasteObject, { count: 1, duration }); } + state.objectType = state.objectType || activeObjectType; + state.label = state.label || jobInstance.task.labels + .filter((label: any) => label.id === activeLabelID)[0]; + state.occluded = state.occluded || false; state.frame = frame; const objectState = new cvat.classes.ObjectState(state); onCreateAnnotations(jobInstance, frame, [objectState]); @@ -266,7 +269,11 @@ export default class CanvasWrapperComponent extends React.PureComponent { onMergeObjects(false); - const { states } = event.detail; + const { states, duration } = event.detail; + jobInstance.logger.log(LogType.mergeObjects, { + duration, + count: states.length, + }); onMergeAnnotations(jobInstance, frame, states); }; @@ -324,6 +331,28 @@ export default class CanvasWrapperComponent extends React.PureComponent { onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY); }; + private onCanvasShapeDragged = (e: any): void => { + const { jobInstance } = this.props; + const { id } = e.detail; + jobInstance.logger.log(LogType.dragObject, { id }); + }; + + private onCanvasShapeResized = (e: any): void => { + const { jobInstance } = this.props; + const { id } = e.detail; + jobInstance.logger.log(LogType.resizeObject, { id }); + }; + + private onCanvasImageFitted = (): void => { + const { jobInstance } = this.props; + jobInstance.logger.log(LogType.fitImage); + }; + + private onCanvasZoomChanged = (): void => { + const { jobInstance } = this.props; + jobInstance.logger.log(LogType.zoomImage); + }; + private onCanvasShapeClicked = (e: any): void => { const { clientID } = e.detail.state; const sidebarItem = window.document @@ -581,6 +610,10 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().addEventListener('canvas.deactivated', this.onCanvasShapeDeactivated); canvasInstance.html().addEventListener('canvas.moved', this.onCanvasCursorMoved); + canvasInstance.html().addEventListener('canvas.zoom', this.onCanvasZoomChanged); + canvasInstance.html().addEventListener('canvas.fit', this.onCanvasImageFitted); + canvasInstance.html().addEventListener('canvas.dragshape', this.onCanvasShapeDragged); + canvasInstance.html().addEventListener('canvas.resizeshape', this.onCanvasShapeResized); canvasInstance.html().addEventListener('canvas.clicked', this.onCanvasShapeClicked); canvasInstance.html().addEventListener('canvas.drawn', this.onCanvasShapeDrawn); canvasInstance.html().addEventListener('canvas.merged', this.onCanvasObjectsMerged); diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 91795797980..c168a0288ee 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -8,13 +8,11 @@ import React from 'react'; import { Switch, Route, Redirect } from 'react-router'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { GlobalHotKeys, KeyMap, configure } from 'react-hotkeys'; +import Spin from 'antd/lib/spin'; +import Layout from 'antd/lib/layout'; +import notification from 'antd/lib/notification'; -import { - Spin, - Layout, - notification, -} from 'antd'; - +import GlobalErrorBoundary from 'components/global-error-boundary/global-error-boundary'; import ShorcutsDialog from 'components/shortcuts-dialog/shortcuts-dialog'; import SettingsPageContainer from 'containers/settings-page/settings-page'; import TasksPageContainer from 'containers/tasks-page/tasks-page'; @@ -27,6 +25,7 @@ import LoginPageContainer from 'containers/login-page/login-page'; import RegisterPageContainer from 'containers/register-page/register-page'; import HeaderContainer from 'containers/header/header'; +import getCore from 'cvat-core'; import { NotificationsState } from 'reducers/interfaces'; interface CVATAppProps { @@ -39,6 +38,7 @@ interface CVATAppProps { resetMessages: () => void; switchShortcutsDialog: () => void; userInitialized: boolean; + userFetching: boolean; pluginsInitialized: boolean; pluginsFetching: boolean; formatsInitialized: boolean; @@ -56,18 +56,29 @@ interface CVATAppProps { class CVATApplication extends React.PureComponent { public componentDidMount(): void { + const core = getCore(); const { verifyAuthorized } = this.props; configure({ ignoreRepeatedEventsWhenKeyHeldDown: false }); + + // Logger configuration + const userActivityCallback: (() => void)[] = []; + window.addEventListener('click', () => { + userActivityCallback.forEach((handler) => handler()); + }); + core.logger.configure(() => window.document.hasFocus, userActivityCallback); + verifyAuthorized(); } public componentDidUpdate(): void { const { + verifyAuthorized, loadFormats, loadUsers, loadAbout, initPlugins, userInitialized, + userFetching, formatsInitialized, formatsFetching, usersInitialized, @@ -82,8 +93,12 @@ class CVATApplication extends React.PureComponent - - - - - - - - - - - { withModels - && } - { installedAutoAnnotation - && } - - - - {/* eslint-disable-next-line */} - - - + + + + + + + + + + + + + {withModels + && } + {installedAutoAnnotation + && } + + + + {/* eslint-disable-next-line */} + + + + ); } return ( - - - - - + + + + + + + ); } diff --git a/cvat-ui/src/components/global-error-boundary/global-error-boundary.tsx b/cvat-ui/src/components/global-error-boundary/global-error-boundary.tsx new file mode 100644 index 00000000000..3c132fd9f9b --- /dev/null +++ b/cvat-ui/src/components/global-error-boundary/global-error-boundary.tsx @@ -0,0 +1,223 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React from 'react'; +import { connect } from 'react-redux'; +import { Action } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import Result from 'antd/lib/result'; +import Text from 'antd/lib/typography/Text'; +import Paragraph from 'antd/lib/typography/Paragraph'; +import Collapse from 'antd/lib/collapse'; +import TextArea from 'antd/lib/input/TextArea'; +import Tooltip from 'antd/lib/tooltip'; +import copy from 'copy-to-clipboard'; +import ErrorStackParser from 'error-stack-parser'; + +import { resetAfterErrorAsync } from 'actions/boundaries-actions'; +import { CombinedState } from 'reducers/interfaces'; +import logger, { LogType } from 'cvat-logger'; + +interface StateToProps { + job: any | null; + serverVersion: string; + coreVersion: string; + canvasVersion: string; + uiVersion: string; +} + +interface DispatchToProps { + restore(): void; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { + annotation: { + job: { + instance: job, + }, + }, + about: { + server, + packageVersion, + }, + } = state; + + return { + job, + serverVersion: server.version as string, + coreVersion: packageVersion.core, + canvasVersion: packageVersion.canvas, + uiVersion: packageVersion.ui, + }; +} + +function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps { + return { + restore(): void { + dispatch(resetAfterErrorAsync()); + }, + }; +} + + +type Props = StateToProps & DispatchToProps; +class GlobalErrorBoundary extends React.PureComponent { + public constructor(props: Props) { + super(props); + this.state = { + hasError: false, + error: null, + }; + } + + static getDerivedStateFromError(error: Error): State { + return { + hasError: true, + error, + }; + } + + public componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { + const { job } = this.props; + const parsed = ErrorStackParser.parse(error); + + const logPayload = { + filename: parsed[0].fileName, + line: parsed[0].lineNumber, + message: error.message, + column: parsed[0].columnNumber, + stack: error.stack, + componentStack: errorInfo.componentStack, + }; + + if (job) { + job.logger.log(LogType.sendException, logPayload); + } else { + logger.log(LogType.sendException, logPayload); + } + } + + public render(): React.ReactNode { + const { + restore, + job, + serverVersion, + coreVersion, + canvasVersion, + uiVersion, + } = this.props; + + const { hasError, error } = this.state; + + const restoreGlobalState = (): void => { + this.setState({ + error: null, + hasError: false, + }); + + restore(); + }; + + if (hasError && error) { + const message = `${error.name}\n${error.message}\n\n${error.stack}`; + return ( +
+ +
+ + What has happened? + Program error has just occured + + + +