diff --git a/.coveragerc b/.coveragerc index 7804da58abc..f174f8461b3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,6 @@ [run] branch = true +# relative_files = true # does not work? source = datumaro/datumaro/ @@ -36,4 +37,4 @@ exclude_lines = # don't fail on the code that can be found ignore_errors = true -skip_empty = true \ No newline at end of file +skip_empty = true diff --git a/.travis.yml b/.travis.yml index 7ea575eb2a9..a8715db3d1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,22 +10,17 @@ services: env: - CONTAINER_COVERAGE_DATA_DIR="/coverage_data" - HOST_COVERAGE_DATA_DIR="${TRAVIS_BUILD_DIR}/coverage_data" - -install: - - gem install coveralls-lcov - - pip install coveralls + HOST_COVERAGE_DATA_DIR="${TRAVIS_BUILD_DIR}" before_script: - docker-compose -f docker-compose.yml -f docker-compose.ci.yml build - - mkdir -m a=rwx -p ${HOST_COVERAGE_DATA_DIR} + - chmod a+rwx ${HOST_COVERAGE_DATA_DIR} script: - - docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'coverage run -a manage.py test cvat/apps utils/cli && coverage run -a --source cvat/apps/ manage.py test --pattern="_tests.py" cvat/apps/dataset_manager && mv .coverage ${CONTAINER_COVERAGE_DATA_DIR}' - - docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-data && npm install && cd ../cvat-core && npm install && npm run test && mv ./reports/coverage/lcov.info ${CONTAINER_COVERAGE_DATA_DIR}' + # FIXME: Git package and application name conflict in PATH and try to leave only one python test execution + - docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'coverage run -a manage.py test cvat/apps && coverage run -a manage.py test --pattern="_test*.py" cvat/apps/dataset_manager/tests cvat/apps/engine/tests utils/cli && coverage run -a manage.py test datumaro/ && mv .coverage ${CONTAINER_COVERAGE_DATA_DIR}' + - docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-data && npm install && cd ../cvat-core && npm install && npm run test && coveralls-lcov -v -n ./reports/coverage/lcov.info > ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json' after_success: # https://coveralls-python.readthedocs.io/en/latest/usage/multilang.html - - mv ${HOST_COVERAGE_DATA_DIR}/* . - - coveralls-lcov -v -n lcov.info > coverage.json - - coveralls --merge=coverage.json \ No newline at end of file + - docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'ln -s ${CONTAINER_COVERAGE_DATA_DIR}/.git . && ln -s ${CONTAINER_COVERAGE_DATA_DIR}/.coverage . && ln -s ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json . && coveralls --merge=coverage.json' diff --git a/.vscode/settings.json b/.vscode/settings.json index d7f4a5984ff..3b796a0be54 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -33,5 +33,6 @@ "./datumaro", ], "licenser.license": "Custom", - "licenser.customHeader": "Copyright (C) @YEAR@ Intel Corporation\n\nSPDX-License-Identifier: MIT" + "licenser.customHeader": "Copyright (C) @YEAR@ Intel Corporation\n\nSPDX-License-Identifier: MIT", + "files.trimTrailingWhitespace": true } diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a6a475b66..e6b333bcc73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,18 +6,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0] - Unreleased ### Added -- +- Added `datumaro_project` export format (https://github.com/opencv/cvat/pull/1352) ### Changed -- cvat-core: session.annotations.put() now returns identificators of added objects () +- Downloaded file name in annotations export became more informative (https://github.com/opencv/cvat/pull/1352) +- Added auto trimming for trailing whitespaces style enforsement (https://github.com/opencv/cvat/pull/1352) +- REST API: updated `GET /task//annotations`: parameters are `format`, `filename` (now optional), `action` (optional) (https://github.com/opencv/cvat/pull/1352) +- REST API: removed `dataset/formats`, changed format of `annotation/formats` (https://github.com/opencv/cvat/pull/1352) +- Exported annotations are stored for N hours instead of indefinitely (https://github.com/opencv/cvat/pull/1352) +- Formats: CVAT format now accepts ZIP and XML (https://github.com/opencv/cvat/pull/1352) +- Formats: COCO format now accepts ZIP and JSON (https://github.com/opencv/cvat/pull/1352) +- Formats: most of formats renamed, no extension in title (https://github.com/opencv/cvat/pull/1352) +- Formats: definitions are changed, are not stored in DB anymore (https://github.com/opencv/cvat/pull/1352) +- cvat-core: session.annotations.put() now returns identificators of added objects (https://github.com/opencv/cvat/pull/1493) ### Deprecated - ### Removed -- +- `annotation` application is replaced with `dataset_manager` (https://github.com/opencv/cvat/pull/1352) ### Fixed +- Categories for empty projects with no sources are taken from own dataset (https://github.com/opencv/cvat/pull/1352) +- Added directory removal on error during `extract` command (https://github.com/opencv/cvat/pull/1352) +- Added debug error message on incorrect XPath (https://github.com/opencv/cvat/pull/1352) +- Exporting frame stepped task (https://github.com/opencv/cvat/issues/1294, https://github.com/opencv/cvat/issues/1334) +- Fixed broken command line interface for `cvat` export format in Datumaro (https://github.com/opencv/cvat/issues/1494) - Updated Rest API document, Swagger document serving instruction issue (https://github.com/opencv/cvat/issues/1495) ### Security diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 56accad6a1a..1b02c62cc65 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,6 +75,7 @@ for development - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - [vscode-remark-lint](https://marketplace.visualstudio.com/items?itemName=drewbourne.vscode-remark-lint) - [licenser](https://marketplace.visualstudio.com/items?itemName=ymotongpoo.licenser) + - [Trailing Spaces](https://marketplace.visualstudio.com/items?itemName=shardulm94.trailing-spaces) - Reload Visual Studio Code from virtual environment diff --git a/Dockerfile.ci b/Dockerfile.ci index e53839a307a..809c1d785d1 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -12,10 +12,16 @@ RUN curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - & build-essential \ google-chrome-stable \ nodejs \ - python3-dev && \ + python3-dev \ + ruby \ + && \ rm -rf /var/lib/apt/lists/*; -RUN python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt +RUN python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt && \ + python3 -m pip install --no-cache-dir coveralls +RUN gem install coveralls-lcov + +COPY .coveragerc . # RUN all commands below as 'django' user USER ${USER} diff --git a/README.md b/README.md index c71325551f0..ccbebd4c42b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,10 @@ [![codebeat badge](https://codebeat.co/badges/53cd0d16-fddc-46f8-903c-f43ed9abb6dd)](https://codebeat.co/projects/github-com-opencv-cvat-develop) [![DOI](https://zenodo.org/badge/139156354.svg)](https://zenodo.org/badge/latestdoi/139156354) -CVAT is free, online, interactive video and image annotation tool for computer vision. It is being used by our team to annotate million of objects with different properties. Many UI and UX decisions are based on feedbacks from professional data annotation team. +CVAT is free, online, interactive video and image annotation +tool for computer vision. It is being used by our team to +annotate million of objects with different properties. Many UI +and UX decisions are based on feedbacks from professional data annotation team. ![CVAT screenshot](cvat/apps/documentation/static/documentation/images/cvat.jpg) @@ -34,21 +37,23 @@ CVAT is free, online, interactive video and image annotation tool for computer v ## Supported annotation formats -Format selection is possible after clicking on the Upload annotation / Dump annotation button. -[Datumaro](datumaro/README.md) dataset framework allows additional dataset transformations -via its command line tool. +Format selection is possible after clicking on the Upload annotation +and Dump annotation buttons. [Datumaro](datumaro/README.md) dataset +framework allows additional dataset transformations +via its command line tool and Python library. -| Annotation format | Dumper | Loader | +| Annotation format | Import | Export | | ------------------------------------------------------------------------------------------ | ------ | ------ | -| [CVAT XML v1.1 for images](cvat/apps/documentation/xml_format.md#annotation) | X | X | -| [CVAT XML v1.1 for a video](cvat/apps/documentation/xml_format.md#interpolation) | X | X | -| [Pascal VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X | +| [CVAT for images](cvat/apps/documentation/xml_format.md#annotation) | X | X | +| [CVAT for a video](cvat/apps/documentation/xml_format.md#interpolation) | X | X | +| [Datumaro](datumaro/README.md) | | X | +| [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X | +| Segmentation masks from [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X | | [YOLO](https://pjreddie.com/darknet/yolo/) | X | X | | [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X | -| PNG class mask + instance mask as in [Pascal VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X | | [TFrecord](https://www.tensorflow.org/tutorials/load_data/tf_records) | X | X | | [MOT](https://motchallenge.net/) | X | X | -| [LabelMe](http://labelme.csail.mit.edu/Release3.0) | X | X | +| [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | X | X | ## Links - [Intel AI blog: New Computer Vision Tool Accelerates Annotation of Digital Images and Video](https://www.intel.ai/introducing-cvat) @@ -57,14 +62,19 @@ via its command line tool. ## Online Demo -[Onepanel](https://www.onepanel.io/) has added CVAT as an environment into their platform and a running demo of CVAT can be accessed at [CVAT Public Demo](https://c.onepanel.io/onepanel-demo/projects/cvat-public-demo/workspaces?utm_source=cvat). +[Onepanel](https://www.onepanel.io/) has added CVAT as an environment +into their platform and a running demo of CVAT can be accessed at +[CVAT Public Demo](https://c.onepanel.io/onepanel-demo/projects/cvat-public-demo/workspaces?utm_source=cvat). -If you have any questions, please contact Onepanel directly at support@onepanel.io. If you are in the Onepanel application, you can also use the chat icon in the bottom right corner. +If you have any questions, please contact Onepanel directly at +support@onepanel.io. If you are in the Onepanel application, you can also +use the chat icon in the bottom right corner. ## REST API Automatically generated Swagger documentation for Django REST API is -available on ``/api/swagger`` (default: ``localhost:8080/api/swagger``). +available on ``/api/swagger`` +(default: ``localhost:8080/api/swagger``). Swagger documentation is visiable on allowed hostes, Update environement variable in docker-compose.yml file with cvat hosted machine IP or domain name. Example - ``ALLOWED_HOSTS: 'localhost, 127.0.0.1'``) diff --git a/cvat-core/package.json b/cvat-core/package.json index 0a1dd9de0d7..de54337f9e8 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "1.0.0", + "version": "2.0.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "babel.config.js", "scripts": { diff --git a/cvat-core/src/annotation-format.js b/cvat-core/src/annotation-formats.js similarity index 50% rename from cvat-core/src/annotation-format.js rename to cvat-core/src/annotation-formats.js index 5505ecf1a32..641f7bc0616 100644 --- a/cvat-core/src/annotation-format.js +++ b/cvat-core/src/annotation-formats.js @@ -12,9 +12,8 @@ class Loader { constructor(initialData) { const data = { - display_name: initialData.display_name, - format: initialData.format, - handler: initialData.handler, + name: initialData.name, + format: initialData.ext, version: initialData.version, }; @@ -27,7 +26,7 @@ * @readonly * @instance */ - get: () => data.display_name, + get: () => data.name, }, format: { /** @@ -39,16 +38,6 @@ */ get: () => data.format, }, - handler: { - /** - * @name handler - * @type {string} - * @memberof module:API.cvat.classes.Loader - * @readonly - * @instance - */ - get: () => data.handler, - }, version: { /** * @name version @@ -71,9 +60,8 @@ class Dumper { constructor(initialData) { const data = { - display_name: initialData.display_name, - format: initialData.format, - handler: initialData.handler, + name: initialData.name, + format: initialData.ext, version: initialData.version, }; @@ -86,7 +74,7 @@ * @readonly * @instance */ - get: () => data.display_name, + get: () => data.name, }, format: { /** @@ -98,16 +86,6 @@ */ get: () => data.format, }, - handler: { - /** - * @name handler - * @type {string} - * @memberof module:API.cvat.classes.Dumper - * @readonly - * @instance - */ - get: () => data.handler, - }, version: { /** * @name version @@ -127,108 +105,41 @@ * @memberof module:API.cvat.classes * @hideconstructor */ - class AnnotationFormat { + class AnnotationFormats { constructor(initialData) { const data = { - created_date: initialData.created_date, - updated_date: initialData.updated_date, - id: initialData.id, - owner: initialData.owner, - name: initialData.name, - handler_file: initialData.handler_file, + exporters: initialData.exporters.map((el) => new Dumper(el)), + importers: initialData.importers.map((el) => new Loader(el)), }; - data.dumpers = initialData.dumpers.map((el) => new Dumper(el)); - data.loaders = initialData.loaders.map((el) => new Loader(el)); - // Now all fields are readonly Object.defineProperties(this, { - id: { - /** - * @name id - * @type {integer} - * @memberof module:API.cvat.classes.AnnotationFormat - * @readonly - * @instance - */ - get: () => data.id, - }, - owner: { - /** - * @name owner - * @type {integer} - * @memberof module:API.cvat.classes.AnnotationFormat - * @readonly - * @instance - */ - get: () => data.owner, - }, - name: { - /** - * @name name - * @type {string} - * @memberof module:API.cvat.classes.AnnotationFormat - * @readonly - * @instance - */ - get: () => data.name, - }, - createdDate: { - /** - * @name createdDate - * @type {string} - * @memberof module:API.cvat.classes.AnnotationFormat - * @readonly - * @instance - */ - get: () => data.created_date, - }, - updatedDate: { - /** - * @name updatedDate - * @type {string} - * @memberof module:API.cvat.classes.AnnotationFormat - * @readonly - * @instance - */ - get: () => data.updated_date, - }, - handlerFile: { - /** - * @name handlerFile - * @type {string} - * @memberof module:API.cvat.classes.AnnotationFormat - * @readonly - * @instance - */ - get: () => data.handler_file, - }, loaders: { /** * @name loaders * @type {module:API.cvat.classes.Loader[]} - * @memberof module:API.cvat.classes.AnnotationFormat + * @memberof module:API.cvat.classes.AnnotationFormats * @readonly * @instance */ - get: () => [...data.loaders], + get: () => [...data.importers], }, dumpers: { /** * @name dumpers * @type {module:API.cvat.classes.Dumper[]} - * @memberof module:API.cvat.classes.AnnotationFormat + * @memberof module:API.cvat.classes.AnnotationFormats * @readonly * @instance */ - get: () => [...data.dumpers], + get: () => [...data.exporters], }, }); } } module.exports = { - AnnotationFormat, + AnnotationFormats, Loader, Dumper, }; diff --git a/cvat-core/src/annotations.js b/cvat-core/src/annotations.js index 608faf1eee9..e2f452a7137 100644 --- a/cvat-core/src/annotations.js +++ b/cvat-core/src/annotations.js @@ -17,7 +17,7 @@ const { Loader, Dumper, - } = require('./annotation-format.js'); + } = require('./annotation-formats.js'); const { ScriptingError, DataError, diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index 4b7e2872719..2147c19c9cb 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -26,7 +26,7 @@ } = require('./enums'); const User = require('./user'); - const { AnnotationFormat } = require('./annotation-format.js'); + const { AnnotationFormats } = require('./annotation-formats.js'); const { ArgumentError } = require('./exceptions'); const { Task } = require('./session'); @@ -66,12 +66,7 @@ cvat.server.formats.implementation = async () => { const result = await serverProxy.server.formats(); - return result.map((el) => new AnnotationFormat(el)); - }; - - cvat.server.datasetFormats.implementation = async () => { - const result = await serverProxy.server.datasetFormats(); - return result; + return new AnnotationFormats(result); }; cvat.server.register.implementation = async (username, firstName, lastName, diff --git a/cvat-core/src/api.js b/cvat-core/src/api.js index 4eb4e99af00..777d3fc0187 100644 --- a/cvat-core/src/api.js +++ b/cvat-core/src/api.js @@ -109,7 +109,7 @@ function build() { * @method formats * @async * @memberof module:API.cvat.server - * @returns {module:API.cvat.classes.AnnotationFormat[]} + * @returns {module:API.cvat.classes.AnnotationFormats} * @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.ServerError} */ @@ -118,20 +118,6 @@ function build() { .apiWrapper(cvat.server.formats); return result; }, - /** - * Method returns available dataset export formats - * @method exportFormats - * @async - * @memberof module:API.cvat.server - * @returns {module:String[]} - * @throws {module:API.cvat.exceptions.PluginError} - * @throws {module:API.cvat.exceptions.ServerError} - */ - async datasetFormats() { - const result = await PluginRegistry - .apiWrapper(cvat.server.datasetFormats); - return result; - }, /** * Method allows to register on a server * @method register diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index 8251328223d..2c28ea27d98 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -154,22 +154,6 @@ return response.data; } - async function datasetFormats() { - const { backendAPI } = config; - - let response = null; - try { - response = await Axios.get(`${backendAPI}/server/dataset/formats`, { - proxy: config.proxy, - }); - response = JSON.parse(response.data); - } catch (errorData) { - throw generateError(errorData); - } - - return response; - } - async function register(username, firstName, lastName, email, password1, password2) { let response = null; try { @@ -617,9 +601,12 @@ // Session is 'task' or 'job' async function dumpAnnotations(id, name, format) { const { backendAPI } = config; - const filename = name.replace(/\//g, '_'); - const baseURL = `${backendAPI}/tasks/${id}/annotations/${encodeURIComponent(filename)}`; + const baseURL = `${backendAPI}/tasks/${id}/annotations`; let query = `format=${encodeURIComponent(format)}`; + if (name) { + const filename = name.replace(/\//g, '_'); + query += `&filename=${encodeURIComponent(filename)}`; + } let url = `${baseURL}?${query}`; return new Promise((resolve, reject) => { @@ -664,7 +651,6 @@ about, share, formats, - datasetFormats, exception, login, logout, diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 1726766d328..2af76d13758 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -39,9 +39,9 @@ return result; }, - async dump(name, dumper) { + async dump(dumper, name = null) { const result = await PluginRegistry - .apiWrapper.call(this, prototype.annotations.dump, name, dumper); + .apiWrapper.call(this, prototype.annotations.dump, dumper, name); return result; }, @@ -255,8 +255,8 @@ * Method always dumps annotations for a whole task. * @method dump * @memberof Session.annotations - * @param {string} name - a name of a file with annotations * @param {module:API.cvat.classes.Dumper} dumper - a dumper + * @param {string} [name = null] - a name of a file with annotations * which will be used to dump * @returns {string} URL which can be used in order to get a dump file * @throws {module:API.cvat.exceptions.PluginError} @@ -1541,7 +1541,7 @@ return result; }; - Job.prototype.annotations.dump.implementation = async function (name, dumper) { + Job.prototype.annotations.dump.implementation = async function (dumper, name) { const result = await dumpAnnotations(this, name, dumper); return result; }; @@ -1785,7 +1785,7 @@ return result; }; - Task.prototype.annotations.dump.implementation = async function (name, dumper) { + Task.prototype.annotations.dump.implementation = async function (dumper, name) { const result = await dumpAnnotations(this, name, dumper); return result; }; diff --git a/cvat-core/tests/api/server.js b/cvat-core/tests/api/server.js index 6220e158dd5..80fb88685d8 100644 --- a/cvat-core/tests/api/server.js +++ b/cvat-core/tests/api/server.js @@ -18,10 +18,10 @@ jest.mock('../../src/server-proxy', () => { // Initialize api window.cvat = require('../../src/api'); const { - AnnotationFormat, + AnnotationFormats, Loader, Dumper, -} = require('../../src/annotation-format'); +} = require('../../src/annotation-formats'); // Test cases describe('Feature: get info about cvat', () => { @@ -58,24 +58,18 @@ describe('Feature: get share storage info', () => { describe('Feature: get annotation formats', () => { test('get annotation formats from a server', async () => { const result = await window.cvat.server.formats(); - expect(Array.isArray(result)).toBeTruthy(); - for (const format of result) { - expect(format).toBeInstanceOf(AnnotationFormat); - } + expect(result).toBeInstanceOf(AnnotationFormats); }); }); describe('Feature: get annotation loaders', () => { test('get annotation formats from a server', async () => { const result = await window.cvat.server.formats(); - expect(Array.isArray(result)).toBeTruthy(); - for (const format of result) { - expect(format).toBeInstanceOf(AnnotationFormat); - const { loaders } = format; - expect(Array.isArray(loaders)).toBeTruthy(); - for (const loader of loaders) { - expect(loader).toBeInstanceOf(Loader); - } + expect(result).toBeInstanceOf(AnnotationFormats); + const { loaders } = result; + expect(Array.isArray(loaders)).toBeTruthy(); + for (const loader of loaders) { + expect(loader).toBeInstanceOf(Loader); } }); }); @@ -83,14 +77,11 @@ describe('Feature: get annotation loaders', () => { describe('Feature: get annotation dumpers', () => { test('get annotation formats from a server', async () => { const result = await window.cvat.server.formats(); - expect(Array.isArray(result)).toBeTruthy(); - for (const format of result) { - expect(format).toBeInstanceOf(AnnotationFormat); - const { dumpers } = format; - expect(Array.isArray(dumpers)).toBeTruthy(); - for (const dumper of dumpers) { - expect(dumper).toBeInstanceOf(Dumper); - } + expect(result).toBeInstanceOf(AnnotationFormats); + const { dumpers } = result; + expect(Array.isArray(dumpers)).toBeTruthy(); + for (const dumper of dumpers) { + expect(dumper).toBeInstanceOf(Dumper); } }); }); \ No newline at end of file diff --git a/cvat-core/tests/mocks/dummy-data.mock.js b/cvat-core/tests/mocks/dummy-data.mock.js index d5c65e9bade..dd688e14b8b 100644 --- a/cvat-core/tests/mocks/dummy-data.mock.js +++ b/cvat-core/tests/mocks/dummy-data.mock.js @@ -6,84 +6,47 @@ const aboutDummyData = { "version": "0.5.dev20190516142240" } -const formatsDummyData = [{ - "id": 1, - "dumpers": [ +const formatsDummyData = { + "exporters": [ { - "display_name": "CVAT XML 1.1 for videos", - "format": "XML", + "name": "CVAT for video 1.1", + "ext": "XML", "version": "1.1", - "handler": "dump_as_cvat_interpolation" }, { - "display_name": "CVAT XML 1.1 for images", - "format": "XML", + "name": "CVAT for images 1.1", + "ext": "XML", "version": "1.1", - "handler": "dump_as_cvat_annotation" - } - ], - "loaders": [ - { - "display_name": "CVAT XML 1.1", - "format": "XML", - "version": "1.1", - "handler": "load" - } - ], - "name": "CVAT", - "created_date": "2019-08-08T12:18:56.571488+03:00", - "updated_date": "2019-08-08T12:18:56.571533+03:00", - "handler_file": "cvat/apps/annotation/cvat.py", - "owner": null -}, -{ - "id": 2, - "dumpers": [ + }, { - "display_name": "PASCAL VOC ZIP 1.0", - "format": "ZIP", + "name": "PASCAL VOC 1.0", + "ext": "ZIP", "version": "1.0", - "handler": "dump" - } - ], - "loaders": [ + }, { - "display_name": "PASCAL VOC ZIP 1.0", - "format": "ZIP", + "name": "YOLO 1.0", + "ext": "ZIP", "version": "1.0", - "handler": "load" - } + }, ], - "name": "PASCAL VOC", - "created_date": "2019-08-08T12:18:56.625025+03:00", - "updated_date": "2019-08-08T12:18:56.625071+03:00", - "handler_file": "cvat/apps/annotation/pascal_voc.py", - "owner": null -}, -{ - "id": 3, - "dumpers": [ + "importers": [ + { + "name": "CVAT 1.1", + "ext": "XML, ZIP", + "version": "1.1", + }, { - "display_name": "YOLO ZIP 1.0", - "format": "ZIP", + "name": "PASCAL VOC 1.0", + "ext": "ZIP", "version": "1.0", - "handler": "dump" - } - ], - "loaders": [ + }, { - "display_name": "YOLO ZIP 1.0", - "format": "ZIP", + "name": "MYFORMAT 1.0", + "ext": "TXT", "version": "1.0", - "handler": "load" } ], - "name": "YOLO", - "created_date": "2019-08-08T12:18:56.667534+03:00", - "updated_date": "2019-08-08T12:18:56.667578+03:00", - "handler_file": "cvat/apps/annotation/yolo.py", - "owner": null -}]; +}; const usersDummyData = { "count": 2, diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 634b919ea46..a799df3272d 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -16327,22 +16327,26 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "optional": true }, "aproba": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "optional": true, "requires": { "delegates": "^1.0.0", @@ -16351,12 +16355,14 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "optional": true }, "brace-expansion": { "version": "1.1.11", - "resolved": false, + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "optional": true, "requires": { "balanced-match": "^1.0.0", @@ -16365,32 +16371,38 @@ }, "chownr": { "version": "1.1.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "optional": true }, "code-point-at": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "optional": true }, "concat-map": { "version": "0.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "optional": true }, "console-control-strings": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "optional": true }, "core-util-is": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "optional": true }, "debug": { "version": "3.2.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "optional": true, "requires": { "ms": "^2.1.1" @@ -16398,17 +16410,20 @@ }, "deep-extend": { "version": "0.6.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "optional": true }, "delegates": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "optional": true }, "fs-minipass": { @@ -16422,7 +16437,8 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "optional": true }, "gauge": { @@ -16457,12 +16473,14 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "optional": true }, "iconv-lite": { "version": "0.4.24", - "resolved": false, + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "optional": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" @@ -16470,7 +16488,8 @@ }, "ignore-walk": { "version": "3.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", "optional": true, "requires": { "minimatch": "^3.0.4" @@ -16488,17 +16507,20 @@ }, "inherits": { "version": "2.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "optional": true }, "ini": { "version": "1.3.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "optional": true, "requires": { "number-is-nan": "^1.0.0" @@ -16506,12 +16528,14 @@ }, "isarray": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "optional": true, "requires": { "brace-expansion": "^1.1.7" @@ -16519,7 +16543,8 @@ }, "minimist": { "version": "1.2.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "optional": true }, "minipass": { @@ -16543,7 +16568,8 @@ }, "mkdirp": { "version": "0.5.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", "optional": true, "requires": { "minimist": "^1.2.5" @@ -16551,12 +16577,14 @@ }, "ms": { "version": "2.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "optional": true }, "needle": { "version": "2.3.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.3.tgz", + "integrity": "sha512-EkY0GeSq87rWp1hoq/sH/wnTWgFVhYlnIkbJ0YJFfRgEFlz2RraCjBpFQ+vrEgEdp0ThfyHADmkChEhcb7PKyw==", "optional": true, "requires": { "debug": "^3.2.6", @@ -16584,7 +16612,8 @@ }, "nopt": { "version": "4.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", "optional": true, "requires": { "abbrev": "1", @@ -16593,7 +16622,8 @@ }, "npm-bundled": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", "optional": true, "requires": { "npm-normalize-package-bin": "^1.0.1" @@ -16601,12 +16631,14 @@ }, "npm-normalize-package-bin": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", "optional": true }, "npm-packlist": { "version": "1.4.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", "optional": true, "requires": { "ignore-walk": "^3.0.1", @@ -16628,12 +16660,14 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "optional": true }, "object-assign": { "version": "4.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "optional": true }, "once": { @@ -16647,17 +16681,20 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "optional": true }, "osenv": { "version": "0.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "optional": true, "requires": { "os-homedir": "^1.0.0", @@ -16666,17 +16703,20 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "optional": true }, "process-nextick-args": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "optional": true }, "rc": { "version": "1.2.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "optional": true, "requires": { "deep-extend": "^0.6.0", @@ -16687,7 +16727,8 @@ }, "readable-stream": { "version": "2.3.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "optional": true, "requires": { "core-util-is": "~1.0.0", @@ -16710,37 +16751,44 @@ }, "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "optional": true }, "safer-buffer": { "version": "2.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "optional": true }, "sax": { "version": "1.2.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "optional": true }, "semver": { "version": "5.7.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "optional": true }, "string-width": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "optional": true, "requires": { "code-point-at": "^1.0.0", @@ -16750,7 +16798,8 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "optional": true, "requires": { "safe-buffer": "~5.1.0" @@ -16758,7 +16807,8 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "optional": true, "requires": { "ansi-regex": "^2.0.0" @@ -16766,7 +16816,8 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "optional": true }, "tar": { @@ -16786,12 +16837,14 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "optional": true }, "wide-align": { "version": "1.1.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "optional": true, "requires": { "string-width": "^1.0.2 || 2" diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 6608aa56dca..9a038b6496d 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.0.0", + "version": "1.0.1", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/formats-actions.ts b/cvat-ui/src/actions/formats-actions.ts index d8e5a6568d0..0644d26b8c0 100644 --- a/cvat-ui/src/actions/formats-actions.ts +++ b/cvat-ui/src/actions/formats-actions.ts @@ -15,10 +15,9 @@ export enum FormatsActionTypes { const formatsActions = { getFormats: () => createAction(FormatsActionTypes.GET_FORMATS), - getFormatsSuccess: (annotationFormats: any[], datasetFormats: any[]) => ( + getFormatsSuccess: (annotationFormats: any) => ( createAction(FormatsActionTypes.GET_FORMATS_SUCCESS, { annotationFormats, - datasetFormats, }) ), getFormatsFailed: (error: any) => ( @@ -32,14 +31,12 @@ export function getFormatsAsync(): ThunkAction { return async (dispatch): Promise => { dispatch(formatsActions.getFormats()); let annotationFormats = null; - let datasetFormats = null; try { annotationFormats = await cvat.server.formats(); - datasetFormats = await cvat.server.datasetFormats(); dispatch( - formatsActions.getFormatsSuccess(annotationFormats, datasetFormats), + formatsActions.getFormatsSuccess(annotationFormats), ); } catch (error) { dispatch(formatsActions.getFormatsFailed(error)); diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index 7291ffa8de6..fd1bfbc48ff 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -169,7 +169,7 @@ ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { dispatch(dumpAnnotation(task, dumper)); - const url = await task.annotations.dump(task.name, dumper); + const url = await task.annotations.dump(dumper); const downloadAnchor = (window.document.getElementById('downloadAnchor') as HTMLAnchorElement); downloadAnchor.href = url; downloadAnchor.click(); @@ -280,7 +280,7 @@ ThunkAction, {}, {}, AnyAction> { dispatch(exportDataset(task, exporter)); try { - const url = await task.annotations.exportDataset(exporter.tag); + const url = await task.annotations.exportDataset(exporter.name); const downloadAnchor = (window.document.getElementById('downloadAnchor') as HTMLAnchorElement); downloadAnchor.href = url; downloadAnchor.click(); diff --git a/cvat-ui/src/components/actions-menu/actions-menu.tsx b/cvat-ui/src/components/actions-menu/actions-menu.tsx index 31845d48d56..7d4f305bd50 100644 --- a/cvat-ui/src/components/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/components/actions-menu/actions-menu.tsx @@ -18,7 +18,6 @@ interface Props { loaders: string[]; dumpers: string[]; - exporters: string[]; loadActivity: string | null; dumpActivities: string[] | null; exportActivities: string[] | null; @@ -53,7 +52,6 @@ export default function ActionsMenuComponent(props: Props): JSX.Element { dumpers, loaders, - exporters, onClickMenu, dumpActivities, exportActivities, @@ -133,7 +131,7 @@ export default function ActionsMenuComponent(props: Props): JSX.Element { } { ExportSubmenu({ - exporters, + exporters: dumpers, exportActivities, menuKey: Actions.EXPORT_TASK_DATASET, }) diff --git a/cvat-ui/src/components/actions-menu/dump-submenu.tsx b/cvat-ui/src/components/actions-menu/dump-submenu.tsx index d443e192e26..23a6ab6c147 100644 --- a/cvat-ui/src/components/actions-menu/dump-submenu.tsx +++ b/cvat-ui/src/components/actions-menu/dump-submenu.tsx @@ -8,8 +8,8 @@ import Icon from 'antd/lib/icon'; import Text from 'antd/lib/typography/Text'; function isDefaultFormat(dumperName: string, taskMode: string): boolean { - return (dumperName === 'CVAT XML 1.1 for videos' && taskMode === 'interpolation') - || (dumperName === 'CVAT XML 1.1 for images' && taskMode === 'annotation'); + return (dumperName === 'CVAT for video 1.1' && taskMode === 'interpolation') + || (dumperName === 'CVAT for images 1.1' && taskMode === 'annotation'); } interface Props { diff --git a/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx b/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx index 3fb1c192722..7039abff7fc 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx @@ -15,7 +15,6 @@ interface Props { taskMode: string; loaders: string[]; dumpers: string[]; - exporters: string[]; loadActivity: string | null; dumpActivities: string[] | null; exportActivities: string[] | null; @@ -36,7 +35,6 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element { taskMode, loaders, dumpers, - exporters, onClickMenu, loadActivity, dumpActivities, @@ -111,7 +109,7 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element { } { ExportSubmenu({ - exporters, + exporters: dumpers, exportActivities, menuKey: Actions.EXPORT_TASK_DATASET, }) diff --git a/cvat-ui/src/containers/actions-menu/actions-menu.tsx b/cvat-ui/src/containers/actions-menu/actions-menu.tsx index 77b1c6cd52b..c502eece148 100644 --- a/cvat-ui/src/containers/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/containers/actions-menu/actions-menu.tsx @@ -24,8 +24,7 @@ interface OwnProps { } interface StateToProps { - annotationFormats: any[]; - exporters: any[]; + annotationFormats: any; loadActivity: string | null; dumpActivities: string[] | null; exportActivities: string[] | null; @@ -53,7 +52,6 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { const { formats: { annotationFormats, - datasetFormats, }, plugins: { list: { @@ -79,7 +77,6 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { exportActivities: tid in activeExports ? activeExports[tid] : null, loadActivity: tid in loads ? loads[tid] : null, annotationFormats, - exporters: datasetFormats, inferenceIsActive: tid in state.models.inferences, }; } @@ -107,8 +104,10 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps): JSX.Element { const { taskInstance, - annotationFormats, - exporters, + annotationFormats: { + loaders, + dumpers, + }, loadActivity, dumpActivities, exportActivities, @@ -124,13 +123,6 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps): openRunModelWindow, } = props; - - const loaders = annotationFormats - .map((format: any): any[] => format.loaders).flat(); - - const dumpers = annotationFormats - .map((format: any): any[] => format.dumpers).flat(); - function onClickMenu(params: ClickParam, file?: File): void { if (params.keyPath.length > 1) { const [additionalKey, action] = params.keyPath; @@ -150,7 +142,7 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps): } } else if (action === Actions.EXPORT_TASK_DATASET) { const format = additionalKey; - const [exporter] = exporters + const [exporter] = dumpers .filter((_exporter: any): boolean => _exporter.name === format); if (exporter) { exportDataset(taskInstance, exporter); @@ -176,7 +168,6 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps): bugTracker={taskInstance.bugTracker} loaders={loaders.map((loader: any): string => `${loader.name}::${loader.format}`)} dumpers={dumpers.map((dumper: any): string => dumper.name)} - exporters={exporters.map((exporter: any): string => exporter.name)} loadActivity={loadActivity} dumpActivities={dumpActivities} exportActivities={exportActivities} diff --git a/cvat-ui/src/containers/annotation-page/top-bar/annotation-menu.tsx b/cvat-ui/src/containers/annotation-page/top-bar/annotation-menu.tsx index 8c73137d46f..20648a2b023 100644 --- a/cvat-ui/src/containers/annotation-page/top-bar/annotation-menu.tsx +++ b/cvat-ui/src/containers/annotation-page/top-bar/annotation-menu.tsx @@ -21,8 +21,7 @@ import { } from 'actions/annotation-actions'; interface StateToProps { - annotationFormats: any[]; - exporters: any[]; + annotationFormats: any; jobInstance: any; loadActivity: string | null; dumpActivities: string[] | null; @@ -49,7 +48,6 @@ function mapStateToProps(state: CombinedState): StateToProps { }, formats: { annotationFormats, - datasetFormats: exporters, }, tasks: { activities: { @@ -73,7 +71,6 @@ function mapStateToProps(state: CombinedState): StateToProps { ? loads[taskID] || jobLoads[jobID] : null, jobInstance, annotationFormats, - exporters, installedReID: list.REID, }; } @@ -100,8 +97,10 @@ type Props = StateToProps & DispatchToProps & RouteComponentProps; function AnnotationMenuContainer(props: Props): JSX.Element { const { jobInstance, - annotationFormats, - exporters, + annotationFormats: { + loaders, + dumpers, + }, loadAnnotations, dumpAnnotations, exportDataset, @@ -113,12 +112,6 @@ function AnnotationMenuContainer(props: Props): JSX.Element { installedReID, } = props; - const loaders = annotationFormats - .map((format: any): any[] => format.loaders).flat(); - - const dumpers = annotationFormats - .map((format: any): any[] => format.dumpers).flat(); - const onClickMenu = (params: ClickParam, file?: File): void => { if (params.keyPath.length > 1) { const [additionalKey, action] = params.keyPath; @@ -138,7 +131,7 @@ function AnnotationMenuContainer(props: Props): JSX.Element { } } else if (action === Actions.EXPORT_TASK_DATASET) { const format = additionalKey; - const [exporter] = exporters + const [exporter] = dumpers .filter((_exporter: any): boolean => _exporter.name === format); if (exporter) { exportDataset(jobInstance.task, exporter); @@ -159,7 +152,6 @@ function AnnotationMenuContainer(props: Props): JSX.Element { taskMode={jobInstance.task.mode} loaders={loaders.map((loader: any): string => loader.name)} dumpers={dumpers.map((dumper: any): string => dumper.name)} - exporters={exporters.map((exporter: any): string => exporter.name)} loadActivity={loadActivity} dumpActivities={dumpActivities} exportActivities={exportActivities} diff --git a/cvat-ui/src/reducers/formats-reducer.ts b/cvat-ui/src/reducers/formats-reducer.ts index f807b809966..3429fc2fa75 100644 --- a/cvat-ui/src/reducers/formats-reducer.ts +++ b/cvat-ui/src/reducers/formats-reducer.ts @@ -9,8 +9,7 @@ import { AuthActionTypes, AuthActions } from 'actions/auth-actions'; import { FormatsState } from './interfaces'; const defaultState: FormatsState = { - annotationFormats: [], - datasetFormats: [], + annotationFormats: null, initialized: false, fetching: false, }; @@ -33,7 +32,6 @@ export default ( initialized: true, fetching: false, annotationFormats: action.payload.annotationFormats, - datasetFormats: action.payload.datasetFormats, }; case FormatsActionTypes.GET_FORMATS_FAILED: return { diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 63d46154c9e..dad3cdb8f46 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -63,8 +63,7 @@ export interface TasksState { } export interface FormatsState { - annotationFormats: any[]; - datasetFormats: any[]; + annotationFormats: any; fetching: boolean; initialized: boolean; } diff --git a/cvat/apps/annotation/README.md b/cvat/apps/annotation/README.md deleted file mode 100644 index 101a28ac2f2..00000000000 --- a/cvat/apps/annotation/README.md +++ /dev/null @@ -1,628 +0,0 @@ - - -## Description - -The purpose of this application is to add support for multiple annotation formats for CVAT. -It allows to download and upload annotations in different formats and easily add support for new. - -## How to add a new annotation format support - -1. Write a python script that will be executed via exec() function. Following items must be defined inside at code: - - **format_spec** - a dictionary with the following structure: - ```python - format_spec = { - "name": "CVAT", - "dumpers": [ - { - "display_name": "{name} {format} {version} for videos", - "format": "XML", - "version": "1.1", - "handler": "dump_as_cvat_interpolation" - }, - { - "display_name": "{name} {format} {version} for images", - "format": "XML", - "version": "1.1", - "handler": "dump_as_cvat_annotation" - } - ], - "loaders": [ - { - "display_name": "{name} {format} {version}", - "format": "XML", - "version": "1.1", - "handler": "load", - } - ], - } - ``` - - **name** - unique name for each format - - **dumpers and loaders** - lists of objects that describes exposed dumpers and loaders and must - have following keys: - 1. display_name - **unique** string used as ID for dumpers and loaders. - Also this string is displayed in CVAT UI. - Possible to use a named placeholders like the python format function - (supports only name, format and version variables). - 1. format - a string, used as extension for a dumped annotation. - 1. version - just string with version. - 1. handler - function that will be called and should be defined at top scope. - - dumper/loader handler functions. Each function should have the following signature: - ```python - def dump_handler(file_object, annotations): - ``` - - Inside of the script environment 2 variables are available: - - **file_object** - python's standard file object returned by open() function and exposing a file-oriented API - (with methods such as read() or write()) to an underlying resource. - - **annotations** - instance of [Annotation](annotation.py#L106) class. - - Annotation class expose API and some additional pre-defined types that allow to get/add shapes inside - a loader/dumper code. - - Short description of the public methods: - - **Annotation.shapes** - property, returns a generator of Annotation.LabeledShape objects - - **Annotation.tracks** - property, returns a generator of Annotation.Track objects - - **Annotation.tags** - property, returns a generator of Annotation.Tag objects - - **Annotation.group_by_frame()** - method, returns an iterator on Annotation.Frame object, - which groups annotation objects by frame. Note that TrackedShapes will be represented as Annotation.LabeledShape. - - **Annotation.meta** - property, returns dictionary which represent a task meta information, - for example - video source name, number of frames, number of jobs, etc - - **Annotation.add_tag(tag)** - tag should be a instance of the Annotation.Tag class - - **Annotation.add_shape(shape)** - shape should be a instance of the Annotation.Shape class - - **Annotation.add_track(track)** - track should be a instance of the Annotation.Track class - - **Annotation.Attribute** = namedtuple('Attribute', 'name, value') - - name - String, name of the attribute - - value - String, value of the attribute - - **Annotation.LabeledShape** = namedtuple('LabeledShape', 'type, frame, label, points, occluded, attributes, - group, z_order') - LabeledShape.\__new\__.\__defaults\__ = (0, None) - - **TrackedShape** = namedtuple('TrackedShape', 'type, points, occluded, frame, attributes, outside, - keyframe, z_order') - TrackedShape.\__new\__.\__defaults\__ = (None, ) - - **Track** = namedtuple('Track', 'label, group, shapes') - - **Tag** = namedtuple('Tag', 'frame, label, attributes, group') - Tag.\__new\__.\__defaults\__ = (0, ) - - **Frame** = namedtuple('Frame', 'frame, name, width, height, labeled_shapes, tags') - - Pseudocode for a dumper script - ```python - ... - # dump meta info if necessary - ... - - # iterate over all frames - for frame_annotation in annotations.group_by_frame(): - # get frame info - image_name = frame_annotation.name - image_width = frame_annotation.width - image_height = frame_annotation.height - - # iterate over all shapes on the frame - for shape in frame_annotation.labeled_shapes: - label = shape.label - xtl = shape.points[0] - ytl = shape.points[1] - xbr = shape.points[2] - ybr = shape.points[3] - - # iterate over shape attributes - for attr in shape.attributes: - attr_name = attr.name - attr_value = attr.value - ... - # dump annotation code - file_object.write(...) - ... - ``` - Pseudocode for a loader code - ```python - ... - #read file_object - ... - - for parsed_shape in parsed_shapes: - shape = annotations.LabeledShape( - type="rectangle", - points=[0, 0, 100, 100], - occluded=False, - attributes=[], - label="car", - outside=False, - frame=99, - ) - - annotations.add_shape(shape) - ``` - Full examples can be found in corrseponding *.py files (cvat.py, coco.py, yolo.py, etc.). -1. Add path to a new python script to the annotation app settings: - - ```python - BUILTIN_FORMATS = ( - os.path.join(path_prefix, 'cvat.py'), - os.path.join(path_prefix,'pascal_voc.py'), - ) - ``` - -## Ideas for improvements - -- Annotation format manager like DL Model manager with which the user can add custom format support by - writing dumper/loader scripts. -- Often a custom loader/dumper requires additional python packages and it would be useful if CVAT provided some API - that allows the user to install a python dependencies from their own code without changing the source code. - Possible solutions: install additional modules via pip call to a separate directory for each Annotation Format - to reduce version conflicts, etc. Thus, custom code can be run in an extended environment, and core CVAT modules - should not be affected. As well, this functionality can be useful for Auto Annotation module. - -## Format specifications - -### CVAT -This is native CVAT annotation format. -[Detailed format description](cvat/apps/documentation/xml_format.md) - -#### CVAT XML for images dumper -- downloaded file: Single unpacked XML -- supported shapes - Rectangles, Polygons, Polylines, Points - -#### CVAT XML for videos dumper -- downloaded file: Single unpacked XML -- supported shapes - Rectangles, Polygons, Polylines, Points - -#### CVAT XML Loader -- uploaded file: Single unpacked XML -- supported shapes - Rectangles, Polygons, Polylines, Points - -### [Pascal VOC](http://host.robots.ox.ac.uk/pascal/VOC/) -- [Format specification](http://host.robots.ox.ac.uk/pascal/VOC/voc2012/devkit_doc.pdf) - -#### Pascal dumper description -- downloaded file: a zip archive of the following structure: - ```bash - taskname.zip/ - ├── Annotations/ - │   ├── .xml - │   ├── .xml - │   └── .xml - ├── ImageSets/ - │   └── Main/ - │   └── default.txt - └── labelmap.txt - ``` - -- supported shapes: Rectangles -- additional comments: If you plan to use `truncated` and `difficult` attributes please add the corresponding - items to the CVAT label attributes: - `~checkbox=difficult:false ~checkbox=truncated:false` - -#### Pascal loader description -- uploaded file: a zip archive of the structure declared above or the following: - ```bash - taskname.zip/ - ├── .xml - ├── .xml - ├── .xml - └── labelmap.txt # optional - ``` - - The `labelmap.txt` file contains dataset labels. It **must** be included - if dataset labels **differ** from VOC default labels. The file structure: - ```bash - # label : color_rgb : 'body' parts : actions - background::: - aeroplane::: - bicycle::: - bird::: - ``` - - It must be possible for CVAT to match the frame (image name) and file name from annotation \*.xml - file (the tag filename, e.g. `2008_004457.jpg`). There are 2 options: - 1. full match between image name and filename from annotation \*.xml - (in cases when task was created from images or image archive). - 1. match by frame number (if CVAT cannot match by name). File name should - be in the following format `.jpg`. - It should be used when task was created from a video. - -- supported shapes: Rectangles -- limitations: Support of Pascal VOC object detection format -- additional comments: the CVAT task should be created with the full label set that may be in the annotation files - -#### How to create a task from Pascal VOC dataset -1. Download the Pascal Voc dataset (Can be downloaded from the - [PASCAL VOC website](http://host.robots.ox.ac.uk/pascal/VOC/)) -1. Create a CVAT task with the following labels: - ```bash - aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person pottedplant sheep sofa train tvmonitor - ``` - You can add `~checkbox=difficult:false ~checkbox=truncated:false` attributes for each label if you want to use them. - - Select interesting image files - (See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task) - guide for details) -1. zip the corresponding annotation files -1. click `Upload annotation` button, choose `Pascal VOC ZIP 1.1` -and select the *.zip file with annotations from previous step. -It may take some time. - -### [YOLO](https://pjreddie.com/darknet/yolo/) -#### Yolo dumper description -- downloaded file: a zip archive with following structure: - [Format specification](https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects) - ```bash - archive.zip/ - ├── obj.data - ├── obj.names - ├── obj__data - │   ├── image1.txt - │   └── image2.txt - └── train.txt # list of subset image paths - - # the only valid subsets are: train, valid - # train.txt and valid.txt: - obj__data/image1.jpg - obj__data/image2.jpg - - # obj.data: - classes = 3 # optional - names = obj.names - train = train.txt - valid = valid.txt # optional - backup = backup/ # optional - - # obj.names: - cat - dog - airplane - - # image_name.txt: - # label_id - id from obj.names - # cx, cy - relative coordinates of the bbox center - # rw, rh - relative size of the bbox - # label_id cx cy rw rh - 1 0.3 0.8 0.1 0.3 - 2 0.7 0.2 0.3 0.1 - ``` - Each annotation `*.txt` file has a name that corresponds to the name of the image file - (e.g. `frame_000001.txt` is the annotation for the `frame_000001.jpg` image). - The `*.txt` file structure: each line describes label and bounding box - in the following format `label_id cx cy w h`. - `obj.names` contains the ordered list of label names. -- supported shapes - Rectangles - -#### Yolo loader description -- uploaded file: a zip archive of the same structure as above - It must be possible to match the CVAT frame (image name) and annotation file name - There are 2 options: - 1. full match between image name and name of annotation `*.txt` file - (in cases when a task was created from images or archive of images). - 1. match by frame number (if CVAT cannot match by name). File name should be in the following format `.jpg`. - It should be used when task was created from a video. - -- supported shapes: Rectangles -- additional comments: the CVAT task should be created with the full label set that may be in the annotation files - -#### How to create a task from YOLO formatted dataset (from VOC for example) -1. Follow the official [guide](https://pjreddie.com/darknet/yolo/)(see Training YOLO on VOC section) - and prepare the YOLO formatted annotation files. -1. Zip train images - ```bash - zip images.zip -j -@ < train.txt - ``` -1. Create a CVAT task with the following labels: - ```bash - aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person pottedplant sheep sofa train tvmonitor - ``` - Select images.zip as data. Most likely you should use `share` - functionality because size of images.zip is more than 500Mb. - See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task) - guide for details. -1. Create `obj.names` with the following content: - ```bash - aeroplane - bicycle - bird - boat - bottle - bus - car - cat - chair - cow - diningtable - dog - horse - motorbike - person - pottedplant - sheep - sofa - train - tvmonitor - ``` -1. Zip all label files together (we need to add only label files that correspond to the train subset) - ```bash - cat train.txt | while read p; do echo ${p%/*/*}/labels/${${p##*/}%%.*}.txt; done | zip labels.zip -j -@ obj.names - ``` -1. Click `Upload annotation` button, choose `YOLO ZIP 1.1` and select the *.zip file with labels from previous step. - It may take some time. - -### [MS COCO Object Detection](http://cocodataset.org/#format-data) -#### COCO dumper description -- downloaded file: single unpacked `json`. Detailed description of the MS COCO format can be found [here](http://cocodataset.org/#format-data) -- supported shapes - Polygons, Rectangles (interpreted as polygons) - -#### COCO loader description -- uploaded file: single unpacked `*.json`. -- supported shapes: object is interpreted as Polygon if the `segmentation` field of annotation is not empty - else as Rectangle with coordinates from `bbox` field. -- additional comments: the CVAT task should be created with the full label set that may be in the annotation files - -#### How to create a task from MS COCO dataset -1. Download the [MS COCO dataset](http://cocodataset.org/#download). - For example [2017 Val images](http://images.cocodataset.org/zips/val2017.zip) - and [2017 Train/Val annotations](http://images.cocodataset.org/annotations/annotations_trainval2017.zip). -1. Create a CVAT task with the following labels: - ```bash - person bicycle car motorcycle airplane bus train truck boat "traffic light" "fire hydrant" "stop sign" "parking meter" bench bird cat dog horse sheep cow elephant bear zebra giraffe backpack umbrella handbag tie suitcase frisbee skis snowboard "sports ball" kite "baseball bat" "baseball glove" skateboard surfboard "tennis racket" bottle "wine glass" cup fork knife spoon bowl banana apple sandwich orange broccoli carrot "hot dog" pizza donut cake chair couch "potted plant" bed "dining table" toilet tv laptop mouse remote keyboard "cell phone" microwave oven toaster sink refrigerator book clock vase scissors "teddy bear" "hair drier" toothbrush - ``` - - Select val2017.zip as data - (See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task) - guide for details) -1. unpack annotations_trainval2017.zip -1. click `Upload annotation` button, - choose `COCO JSON 1.0` and select `instances_val2017.json.json` annotation file. It may take some time. - -### [TFRecord](https://www.tensorflow.org/tutorials/load_data/tf_records) -TFRecord is a very flexible format, but we try to correspond the format that used in -[TF object detection](https://github.com/tensorflow/models/tree/master/research/object_detection) -with minimal modifications. -Used feature description: -```python -image_feature_description = { - 'image/filename': tf.io.FixedLenFeature([], tf.string), - 'image/source_id': tf.io.FixedLenFeature([], tf.string), - 'image/height': tf.io.FixedLenFeature([], tf.int64), - 'image/width': tf.io.FixedLenFeature([], tf.int64), - # Object boxes and classes. - 'image/object/bbox/xmin': tf.io.VarLenFeature(tf.float32), - 'image/object/bbox/xmax': tf.io.VarLenFeature(tf.float32), - 'image/object/bbox/ymin': tf.io.VarLenFeature(tf.float32), - 'image/object/bbox/ymax': tf.io.VarLenFeature(tf.float32), - 'image/object/class/label': tf.io.VarLenFeature(tf.int64), - 'image/object/class/text': tf.io.VarLenFeature(tf.string), -} -``` -#### TFRecord dumper description -- downloaded file: a zip archive with following structure: - ```bash - taskname.zip - ├── task2.tfrecord - └── label_map.pbtxt - ``` -- supported shapes - Rectangles - -#### TFRecord loader description -- uploaded file: a zip archive with following structure: - ```bash - taskname.zip - └── task2.tfrecord - ``` -- supported shapes: Rectangles -- additional comments: the CVAT task should be created with the full label set that may be in the annotation files - -#### How to create a task from TFRecord dataset (from VOC2007 for example) -1. Create label_map.pbtxt file with the following content: -```js -item { - id: 1 - name: 'aeroplane' -} -item { - id: 2 - name: 'bicycle' -} -item { - id: 3 - name: 'bird' -} -item { - id: 4 - name: 'boat' -} -item { - id: 5 - name: 'bottle' -} -item { - id: 6 - name: 'bus' -} -item { - id: 7 - name: 'car' -} -item { - id: 8 - name: 'cat' -} -item { - id: 9 - name: 'chair' -} -item { - id: 10 - name: 'cow' -} -item { - id: 11 - name: 'diningtable' -} -item { - id: 12 - name: 'dog' -} -item { - id: 13 - name: 'horse' -} -item { - id: 14 - name: 'motorbike' -} -item { - id: 15 - name: 'person' -} -item { - id: 16 - name: 'pottedplant' -} -item { - id: 17 - name: 'sheep' -} -item { - id: 18 - name: 'sofa' -} -item { - id: 19 - name: 'train' -} -item { - id: 20 - name: 'tvmonitor' -} -``` -1. Use [create_pascal_tf_record.py](https://github.com/tensorflow/models/blob/master/research/object_detection/dataset_tools/create_pascal_tf_record.py) -to convert VOC2007 dataset to TFRecord format. -As example: -```bash -python create_pascal_tf_record.py --data_dir --set train --year VOC2007 --output_path pascal.tfrecord --label_map_path label_map.pbtxt -``` -1. Zip train images - ```bash - cat /VOC2007/ImageSets/Main/train.txt | while read p; do echo /VOC2007/JPEGImages/${p}.jpg ; done | zip images.zip -j -@ - ``` -1. Create a CVAT task with the following labels: - ```bash - aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person pottedplant sheep sofa train tvmonitor - ``` - Select images.zip as data. - See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task) - guide for details. -1. Zip pascal.tfrecord and label_map.pbtxt files together - ```bash - zip anno.zip -j - ``` -1. Click `Upload annotation` button, choose `TFRecord ZIP 1.0` and select the *.zip file - with labels from previous step. It may take some time. - -### PNG mask -#### Mask dumper description -- downloaded file: a zip archive with the following structure: - ```bash - taskname.zip - ├── labelmap.txt # optional, required for non-VOC labels - ├── ImageSets/ - │   └── Segmentation/ - │   └── default.txt # list of image names without extension - ├── SegmentationClass/ # merged class masks - │   ├── image1.png - │   └── image2.png - └── SegmentationObject/ # merged instance masks - ├── image1.png - └── image2.png - ``` - Mask is a png image with several (RGB) channels where each pixel has own color which corresponds to a label. - Color generation correspond to the Pascal VOC color generation - [algorithm](http://host.robots.ox.ac.uk/pascal/VOC/voc2012/htmldoc/devkit_doc.html#sec:voclabelcolormap). - (0, 0, 0) is used for background. - `labelmap.txt` file contains the values of the used colors in RGB format. The file structure: - ```bash - # label:color_rgb:parts:actions - background:0,128,0:: - aeroplane:10,10,128:: - bicycle:10,128,0:: - bird:0,108,128:: - boat:108,0,100:: - bottle:18,0,8:: - bus:12,28,0:: - ``` -- supported shapes - Rectangles, Polygons - -#### Mask loader description -- uploaded file: a zip archive of the following structure: - ```bash - name.zip - ├── labelmap.txt # optional, required for non-VOC labels - ├── ImageSets/ - │   └── Segmentation/ - │   └── .txt - ├── SegmentationClass/ - │   ├── image1.png - │   └── image2.png - └── SegmentationObject/ - ├── image1.png - └── image2.png - ``` -- supported shapes: Polygons -- additional comments: the CVAT task should be created with the full label set that may be in the annotation files - -### [MOT sequence](https://arxiv.org/pdf/1906.04567.pdf) -#### Dumper -- downloaded file: a zip archive of the following structure: - ```bash - taskname.zip/ - ├── img1/ - | ├── imgage1.jpg - | └── imgage2.jpg - └── gt/ - ├── labels.txt - └── gt.txt - - # labels.txt - cat - dog - person - ... - - # gt.txt - # frame_id, track_id, x, y, w, h, "not ignored", class_id, visibility, - 1,1,1363,569,103,241,1,1,0.86014 - ... - - ``` -- supported annotations: Rectangle shapes and tracks -- supported attributes: `visibility` (number), `ignored` (checkbox) - -#### Loader -- uploaded file: a zip archive of the structure above or: - ```bash - taskname.zip/ - ├── labels.txt # optional, mandatory for non-official labels - └── gt.txt - ``` -- supported annotations: Rectangle tracks - -### [LabelMe](http://labelme.csail.mit.edu/Release3.0) -#### Dumper -- downloaded file: a zip archive of the following structure: - ```bash - taskname.zip/ - ├── img1.jpg - └── img1.xml - ``` -- supported annotations: Rectangles, Polygons (with attributes) - -#### Loader -- uploaded file: a zip archive of the following structure: - ```bash - taskname.zip/ - ├── Masks/ - | ├── img1_mask1.png - | └── img1_mask2.png - ├── img1.xml - ├── img2.xml - └── img3.xml - ``` -- supported annotations: Rectangles, Polygons, Masks (as polygons) diff --git a/cvat/apps/annotation/__init__.py b/cvat/apps/annotation/__init__.py deleted file mode 100644 index a6b8e925df1..00000000000 --- a/cvat/apps/annotation/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2018 Intel Corporation -# -# SPDX-License-Identifier: MIT -default_app_config = 'cvat.apps.annotation.apps.AnnotationConfig' diff --git a/cvat/apps/annotation/admin.py b/cvat/apps/annotation/admin.py deleted file mode 100644 index b66dde17a5c..00000000000 --- a/cvat/apps/annotation/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2018 Intel Corporation -# -# SPDX-License-Identifier: MIT diff --git a/cvat/apps/annotation/annotation.py b/cvat/apps/annotation/annotation.py deleted file mode 100644 index 86cdeb52aa5..00000000000 --- a/cvat/apps/annotation/annotation.py +++ /dev/null @@ -1,518 +0,0 @@ -# Copyright (C) 2018 Intel Corporation -# -# SPDX-License-Identifier: MIT - -import os -import copy -from collections import OrderedDict, namedtuple - -from django.utils import timezone - -from cvat.apps.engine.data_manager import DataManager, TrackManager -from cvat.apps.engine.serializers import LabeledDataSerializer - -class AnnotationIR: - def __init__(self, data=None): - self.reset() - if data: - self._tags = getattr(data, 'tags', []) or data['tags'] - self._shapes = getattr(data, 'shapes', []) or data['shapes'] - self._tracks = getattr(data, 'tracks', []) or data['tracks'] - - def add_tag(self, tag): - self._tags.append(tag) - - def add_shape(self, shape): - self._shapes.append(shape) - - def add_track(self, track): - self._tracks.append(track) - - @property - def tags(self): - return self._tags - - @property - def shapes(self): - return self._shapes - - @property - def tracks(self): - return self._tracks - - @property - def version(self): - return self._version - - @tags.setter - def tags(self, tags): - self._tags = tags - - @shapes.setter - def shapes(self, shapes): - self._shapes = shapes - - @tracks.setter - def tracks(self, tracks): - self._tracks = tracks - - @version.setter - def version(self, version): - self._version = version - - def __getitem__(self, key): - return getattr(self, key) - - @property - def data(self): - return { - 'version': self.version, - 'tags': self.tags, - 'shapes': self.shapes, - 'tracks': self.tracks, - } - - def serialize(self): - serializer = LabeledDataSerializer(data=self.data) - if serializer.is_valid(raise_exception=True): - return serializer.data - - @staticmethod - def _is_shape_inside(shape, start, stop): - return start <= int(shape['frame']) <= stop - - @staticmethod - def _is_track_inside(track, start, stop): - # a <= b - def has_overlap(a, b): - return 0 <= min(b, stop) - max(a, start) - - prev_shape = None - for shape in track['shapes']: - if prev_shape and not prev_shape['outside'] and \ - has_overlap(prev_shape['frame'], shape['frame']): - return True - prev_shape = shape - - if not prev_shape['outside'] and prev_shape['frame'] <= stop: - return True - - return False - - @staticmethod - def _slice_track(track_, start, stop): - def filter_track_shapes(shapes): - shapes = [s for s in shapes if AnnotationIR._is_shape_inside(s, start, stop)] - drop_count = 0 - for s in shapes: - if s['outside']: - drop_count += 1 - else: - break - # Need to leave the last shape if all shapes are outside - if drop_count == len(shapes): - drop_count -= 1 - - return shapes[drop_count:] - - track = copy.deepcopy(track_) - segment_shapes = filter_track_shapes(track['shapes']) - - if len(segment_shapes) < len(track['shapes']): - interpolated_shapes = TrackManager.get_interpolated_shapes(track, start, stop) - scoped_shapes = filter_track_shapes(interpolated_shapes) - - if scoped_shapes: - if not scoped_shapes[0]['keyframe']: - segment_shapes.insert(0, scoped_shapes[0]) - if not scoped_shapes[-1]['keyframe']: - segment_shapes.append(scoped_shapes[-1]) - - # Should delete 'interpolation_shapes' and 'keyframe' keys because - # Track and TrackedShape models don't expect these fields - del track['interpolated_shapes'] - for shape in segment_shapes: - del shape['keyframe'] - - track['shapes'] = segment_shapes - track['frame'] = track['shapes'][0]['frame'] - return track - - #makes a data copy from specified frame interval - def slice(self, start, stop): - splitted_data = AnnotationIR() - splitted_data.tags = [copy.deepcopy(t) for t in self.tags if self._is_shape_inside(t, start, stop)] - splitted_data.shapes = [copy.deepcopy(s) for s in self.shapes if self._is_shape_inside(s, start, stop)] - splitted_data.tracks = [self._slice_track(t, start, stop) for t in self.tracks if self._is_track_inside(t, start, stop)] - - return splitted_data - - @data.setter - def data(self, data): - self.version = data['version'] - self.tags = data['tags'] - self.shapes = data['shapes'] - self.tracks = data['tracks'] - - def reset(self): - self._version = 0 - self._tags = [] - self._shapes = [] - self._tracks = [] - -class Annotation: - Attribute = namedtuple('Attribute', 'name, value') - LabeledShape = namedtuple('LabeledShape', 'type, frame, label, points, occluded, attributes, group, z_order') - LabeledShape.__new__.__defaults__ = (0, 0) - TrackedShape = namedtuple('TrackedShape', 'type, frame, points, occluded, outside, keyframe, attributes, group, z_order, label, track_id') - TrackedShape.__new__.__defaults__ = (0, 0, None, 0) - Track = namedtuple('Track', 'label, group, shapes') - Tag = namedtuple('Tag', 'frame, label, attributes, group') - Tag.__new__.__defaults__ = (0, ) - Frame = namedtuple('Frame', 'frame, name, width, height, labeled_shapes, tags') - - def __init__(self, annotation_ir, db_task, scheme='', host='', create_callback=None): - self._annotation_ir = annotation_ir - self._db_task = db_task - self._scheme = scheme - self._host = host - self._create_callback=create_callback - self._MAX_ANNO_SIZE=30000 - self._frame_info = {} - self._frame_mapping = {} - self._frame_step = db_task.data.get_frame_step() - - db_labels = self._db_task.label_set.all().prefetch_related('attributespec_set').order_by('pk') - - self._label_mapping = OrderedDict((db_label.id, db_label) for db_label in db_labels) - - self._attribute_mapping = {db_label.id: {'mutable': {}, 'immutable': {}} for db_label in db_labels} - - for db_label in db_labels: - for db_attribute in db_label.attributespec_set.all(): - if db_attribute.mutable: - self._attribute_mapping[db_label.id]['mutable'][db_attribute.id] = db_attribute.name - else: - self._attribute_mapping[db_label.id]['immutable'][db_attribute.id] = db_attribute.name - - self._attribute_mapping_merged = {} - for label_id, attr_mapping in self._attribute_mapping.items(): - self._attribute_mapping_merged[label_id] = { - **attr_mapping['mutable'], - **attr_mapping['immutable'], - } - - self._init_frame_info() - self._init_meta() - - def _get_label_id(self, label_name): - for db_label in self._label_mapping.values(): - if label_name == db_label.name: - return db_label.id - return None - - def _get_label_name(self, label_id): - return self._label_mapping[label_id].name - - def _get_attribute_name(self, attribute_id): - for attribute_mapping in self._attribute_mapping_merged.values(): - if attribute_id in attribute_mapping: - return attribute_mapping[attribute_id] - - def _get_attribute_id(self, label_id, attribute_name, attribute_type=None): - if attribute_type: - container = self._attribute_mapping[label_id][attribute_type] - else: - container = self._attribute_mapping_merged[label_id] - - for attr_id, attr_name in container.items(): - if attribute_name == attr_name: - return attr_id - return None - - def _get_mutable_attribute_id(self, label_id, attribute_name): - return self._get_attribute_id(label_id, attribute_name, 'mutable') - - def _get_immutable_attribute_id(self, label_id, attribute_name): - return self._get_attribute_id(label_id, attribute_name, 'immutable') - - def _init_frame_info(self): - if hasattr(self._db_task.data, 'video'): - self._frame_info = { - frame: { - "path": "frame_{:06d}".format(frame), - "width": self._db_task.data.video.width, - "height": self._db_task.data.video.height, - } for frame in range(self._db_task.data.size) - } - else: - self._frame_info = {db_image.frame: { - "path": db_image.path, - "width": db_image.width, - "height": db_image.height, - } for db_image in self._db_task.data.images.all()} - - self._frame_mapping = { - self._get_filename(info["path"]): frame for frame, info in self._frame_info.items() - } - - def _init_meta(self): - db_segments = self._db_task.segment_set.all().prefetch_related('job_set') - self._meta = OrderedDict([ - ("task", OrderedDict([ - ("id", str(self._db_task.id)), - ("name", self._db_task.name), - ("size", str(self._db_task.data.size)), - ("mode", self._db_task.mode), - ("overlap", str(self._db_task.overlap)), - ("bugtracker", self._db_task.bug_tracker), - ("created", str(timezone.localtime(self._db_task.created_date))), - ("updated", str(timezone.localtime(self._db_task.updated_date))), - ("start_frame", str(self._db_task.data.start_frame)), - ("stop_frame", str(self._db_task.data.stop_frame)), - ("frame_filter", self._db_task.data.frame_filter), - ("z_order", str(self._db_task.z_order)), - - ("labels", [ - ("label", OrderedDict([ - ("name", db_label.name), - ("attributes", [ - ("attribute", OrderedDict([ - ("name", db_attr.name), - ("mutable", str(db_attr.mutable)), - ("input_type", db_attr.input_type), - ("default_value", db_attr.default_value), - ("values", db_attr.values)])) - for db_attr in db_label.attributespec_set.all()]) - ])) for db_label in self._label_mapping.values() - ]), - - ("segments", [ - ("segment", OrderedDict([ - ("id", str(db_segment.id)), - ("start", str(db_segment.start_frame)), - ("stop", str(db_segment.stop_frame)), - ("url", "{0}://{1}/?id={2}".format( - self._scheme, self._host, db_segment.job_set.all()[0].id))] - )) for db_segment in db_segments - ]), - - ("owner", OrderedDict([ - ("username", self._db_task.owner.username), - ("email", self._db_task.owner.email) - ]) if self._db_task.owner else ""), - - ("assignee", OrderedDict([ - ("username", self._db_task.assignee.username), - ("email", self._db_task.assignee.email) - ]) if self._db_task.assignee else ""), - ])), - ("dumped", str(timezone.localtime(timezone.now()))) - ]) - - if hasattr(self._db_task.data, "video"): - self._meta["task"]["original_size"] = OrderedDict([ - ("width", str(self._db_task.data.video.width)), - ("height", str(self._db_task.data.video.height)) - ]) - # Add source to dumped file - self._meta["source"] = str(os.path.basename(self._db_task.data.video.path)) - - def _export_attributes(self, attributes): - exported_attributes = [] - for attr in attributes: - attribute_name = self._get_attribute_name(attr["spec_id"]) - exported_attributes.append(Annotation.Attribute( - name=attribute_name, - value=attr["value"], - )) - return exported_attributes - - def _export_tracked_shape(self, shape): - return Annotation.TrackedShape( - type=shape["type"], - frame=self._db_task.data.start_frame + shape["frame"] * self._frame_step, - label=self._get_label_name(shape["label_id"]), - points=shape["points"], - occluded=shape["occluded"], - z_order=shape.get("z_order", 0), - group=shape.get("group", 0), - outside=shape.get("outside", False), - keyframe=shape.get("keyframe", True), - track_id=shape["track_id"], - attributes=self._export_attributes(shape["attributes"]), - ) - - def _export_labeled_shape(self, shape): - return Annotation.LabeledShape( - type=shape["type"], - label=self._get_label_name(shape["label_id"]), - frame=self._db_task.data.start_frame + shape["frame"] * self._frame_step, - points=shape["points"], - occluded=shape["occluded"], - z_order=shape.get("z_order", 0), - group=shape.get("group", 0), - attributes=self._export_attributes(shape["attributes"]), - ) - - def _export_tag(self, tag): - return Annotation.Tag( - frame=self._db_task.data.start_frame + tag["frame"] * self._frame_step, - label=self._get_label_name(tag["label_id"]), - group=tag.get("group", 0), - attributes=self._export_attributes(tag["attributes"]), - ) - - def group_by_frame(self): - def _get_frame(annotations, shape): - db_image = self._frame_info[shape["frame"]] - frame = self._db_task.data.start_frame + shape["frame"] * self._frame_step - if frame not in annotations: - annotations[frame] = Annotation.Frame( - frame=frame, - name=db_image['path'], - height=db_image["height"], - width=db_image["width"], - labeled_shapes=[], - tags=[], - ) - return annotations[frame] - - annotations = {} - data_manager = DataManager(self._annotation_ir) - for shape in sorted(data_manager.to_shapes(self._db_task.data.size), key=lambda shape: shape.get("z_order", 0)): - if 'track_id' in shape: - exported_shape = self._export_tracked_shape(shape) - else: - exported_shape = self._export_labeled_shape(shape) - _get_frame(annotations, shape).labeled_shapes.append(exported_shape) - - for tag in self._annotation_ir.tags: - _get_frame(annotations, tag).tags.append(self._export_tag(tag)) - - return iter(annotations.values()) - - @property - def shapes(self): - for shape in self._annotation_ir.shapes: - yield self._export_labeled_shape(shape) - - @property - def tracks(self): - for idx, track in enumerate(self._annotation_ir.tracks): - tracked_shapes = TrackManager.get_interpolated_shapes(track, 0, self._db_task.data.size) - for tracked_shape in tracked_shapes: - tracked_shape["attributes"] += track["attributes"] - tracked_shape["track_id"] = idx - tracked_shape["group"] = track["group"] - tracked_shape["label_id"] = track["label_id"] - - yield Annotation.Track( - label=self._get_label_name(track["label_id"]), - group=track["group"], - shapes=[self._export_tracked_shape(shape) for shape in tracked_shapes], - ) - - @property - def tags(self): - for tag in self._annotation_ir.tags: - yield self._export_tag(tag) - - @property - def meta(self): - return self._meta - - def _import_tag(self, tag): - _tag = tag._asdict() - label_id = self._get_label_id(_tag.pop('label')) - _tag['frame'] = (int(_tag['frame']) - self._db_task.data.start_frame) // self._frame_step - _tag['label_id'] = label_id - _tag['attributes'] = [self._import_attribute(label_id, attrib) for attrib in _tag['attributes'] - if self._get_attribute_id(label_id, attrib.name)] - return _tag - - def _import_attribute(self, label_id, attribute): - return { - 'spec_id': self._get_attribute_id(label_id, attribute.name), - 'value': attribute.value, - } - - def _import_shape(self, shape): - _shape = shape._asdict() - label_id = self._get_label_id(_shape.pop('label')) - _shape['frame'] = (int(_shape['frame']) - self._db_task.data.start_frame) // self._frame_step - _shape['label_id'] = label_id - _shape['attributes'] = [self._import_attribute(label_id, attrib) for attrib in _shape['attributes'] - if self._get_attribute_id(label_id, attrib.name)] - return _shape - - def _import_track(self, track): - _track = track._asdict() - label_id = self._get_label_id(_track.pop('label')) - _track['frame'] = (min(int(shape.frame) for shape in _track['shapes']) - \ - self._db_task.data.start_frame) // self._frame_step - _track['label_id'] = label_id - _track['attributes'] = [] - _track['shapes'] = [shape._asdict() for shape in _track['shapes']] - for shape in _track['shapes']: - shape['frame'] = (int(shape['frame']) - self._db_task.data.start_frame) // self._frame_step - _track['attributes'] = [self._import_attribute(label_id, attrib) for attrib in shape['attributes'] - if self._get_immutable_attribute_id(label_id, attrib.name)] - shape['attributes'] = [self._import_attribute(label_id, attrib) for attrib in shape['attributes'] - if self._get_mutable_attribute_id(label_id, attrib.name)] - - return _track - - def _call_callback(self): - if self._len() > self._MAX_ANNO_SIZE: - self._create_callback(self._annotation_ir.serialize()) - self._annotation_ir.reset() - - def add_tag(self, tag): - imported_tag = self._import_tag(tag) - if imported_tag['label_id']: - self._annotation_ir.add_tag(imported_tag) - self._call_callback() - - def add_shape(self, shape): - imported_shape = self._import_shape(shape) - if imported_shape['label_id']: - self._annotation_ir.add_shape(imported_shape) - self._call_callback() - - def add_track(self, track): - imported_track = self._import_track(track) - if imported_track['label_id']: - self._annotation_ir.add_track(imported_track) - self._call_callback() - - @property - def data(self): - return self._annotation_ir - - def _len(self): - track_len = 0 - for track in self._annotation_ir.tracks: - track_len += len(track['shapes']) - - return len(self._annotation_ir.tags) + len(self._annotation_ir.shapes) + track_len - - @property - def frame_info(self): - return self._frame_info - - @property - def frame_step(self): - return self._frame_step - - @staticmethod - def _get_filename(path): - return os.path.splitext(os.path.basename(path))[0] - - def match_frame(self, filename): - # try to match by filename - _filename = self._get_filename(filename) - if _filename in self._frame_mapping: - return self._frame_mapping[_filename] - - raise Exception("Cannot match filename or determinate framenumber for {} filename".format(filename)) diff --git a/cvat/apps/annotation/apps.py b/cvat/apps/annotation/apps.py deleted file mode 100644 index 6a14bfef6b8..00000000000 --- a/cvat/apps/annotation/apps.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (C) 2018 Intel Corporation -# -# SPDX-License-Identifier: MIT - -from django.apps import AppConfig -from django.db.models.signals import post_migrate -from cvat.apps.annotation.settings import BUILTIN_FORMATS - -def register_builtins_callback(sender, **kwargs): - from .format import register_format - for builtin_format in BUILTIN_FORMATS: - register_format(builtin_format) - -class AnnotationConfig(AppConfig): - name = 'cvat.apps.annotation' - - def ready(self): - post_migrate.connect(register_builtins_callback, sender=self) diff --git a/cvat/apps/annotation/format.py b/cvat/apps/annotation/format.py deleted file mode 100644 index 9ac2a00ca1b..00000000000 --- a/cvat/apps/annotation/format.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (C) 2018 Intel Corporation -# -# SPDX-License-Identifier: MIT - -from cvat.apps.annotation import models -from django.core.exceptions import ObjectDoesNotExist -from cvat.apps.annotation.serializers import AnnotationFormatSerializer -from django.core.files import File - -from copy import deepcopy - -def register_format(format_file): - source_code = open(format_file, 'r').read() - global_vars = {} - exec(source_code, global_vars) - if "format_spec" not in global_vars or not isinstance(global_vars["format_spec"], dict): - raise Exception("Could not find 'format_spec' definition in format file specification") - - format_spec = deepcopy(global_vars["format_spec"]) - format_spec["handler_file"] = File(open(format_file)) - for spec in format_spec["loaders"] + format_spec["dumpers"]: - spec["display_name"] = spec["display_name"].format( - name=format_spec["name"], - format=spec["format"], - version=spec["version"], - ) - - try: - annotation_format = models.AnnotationFormat.objects.get(name=format_spec["name"]) - serializer = AnnotationFormatSerializer(annotation_format, data=format_spec) - if serializer.is_valid(raise_exception=True): - serializer.save() - except ObjectDoesNotExist: - serializer = AnnotationFormatSerializer(data=format_spec) - if serializer.is_valid(raise_exception=True): - serializer.save() - -def get_annotation_formats(): - return AnnotationFormatSerializer( - models.AnnotationFormat.objects.all(), - many=True).data diff --git a/cvat/apps/annotation/migrations/0001_initial.py b/cvat/apps/annotation/migrations/0001_initial.py deleted file mode 100644 index 9c331fee941..00000000000 --- a/cvat/apps/annotation/migrations/0001_initial.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 2.1.9 on 2019-07-31 15:20 - -import cvat.apps.annotation.models -import cvat.apps.engine.models -from django.conf import settings -import django.core.files.storage -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='AnnotationFormat', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', cvat.apps.engine.models.SafeCharField(max_length=256)), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('updated_date', models.DateTimeField(auto_now_add=True)), - ('handler_file', models.FileField(storage=django.core.files.storage.FileSystemStorage(location=settings.BASE_DIR), upload_to=cvat.apps.annotation.models.upload_file_handler)), - ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'default_permissions': (), - }, - ), - migrations.CreateModel( - name='AnnotationHandler', - fields=[ - ('type', models.CharField(choices=[('dumper', 'DUMPER'), ('loader', 'LOADER')], max_length=16)), - ('display_name', cvat.apps.engine.models.SafeCharField(max_length=256, primary_key=True, serialize=False)), - ('format', models.CharField(max_length=16)), - ('version', models.CharField(max_length=16)), - ('handler', models.CharField(max_length=256)), - ('annotation_format', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='annotation.AnnotationFormat')), - ], - options={ - 'default_permissions': (), - }, - ), - ] diff --git a/cvat/apps/annotation/migrations/0002_auto_20190805_0927.py b/cvat/apps/annotation/migrations/0002_auto_20190805_0927.py deleted file mode 100644 index 6b289864020..00000000000 --- a/cvat/apps/annotation/migrations/0002_auto_20190805_0927.py +++ /dev/null @@ -1,74 +0,0 @@ -# Generated by Django 2.1.9 on 2019-08-05 06:27 - -import cvat.apps.engine.models -from django.db import migrations, models -import django.db.models.deletion - -def split_handlers(apps, schema_editor): - db_alias = schema_editor.connection.alias - handler_model = apps.get_model('annotation', 'AnnotationHandler') - dumper_model = apps.get_model('annotation', "AnnotationDumper") - loader_model = apps.get_model('annotation', 'AnnotationLoader') - - - for db_handler in handler_model.objects.all(): - if db_handler.type == "dumper": - new_handler = dumper_model() - else: - new_handler = loader_model() - - new_handler.display_name = db_handler.display_name - new_handler.format = db_handler.format - new_handler.version = db_handler.version - new_handler.handler = db_handler.handler - new_handler.annotation_format = db_handler.annotation_format - - new_handler.save() - db_handler.delete() - -class Migration(migrations.Migration): - - dependencies = [ - ('annotation', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='AnnotationDumper', - fields=[ - ('display_name', cvat.apps.engine.models.SafeCharField(max_length=256, primary_key=True, serialize=False)), - ('format', models.CharField(max_length=16)), - ('version', models.CharField(max_length=16)), - ('handler', models.CharField(max_length=256)), - ('annotation_format', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='annotation.AnnotationFormat')), - ], - options={ - 'abstract': False, - 'default_permissions': (), - }, - ), - migrations.CreateModel( - name='AnnotationLoader', - fields=[ - ('display_name', cvat.apps.engine.models.SafeCharField(max_length=256, primary_key=True, serialize=False)), - ('format', models.CharField(max_length=16)), - ('version', models.CharField(max_length=16)), - ('handler', models.CharField(max_length=256)), - ('annotation_format', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='annotation.AnnotationFormat')), - ], - options={ - 'abstract': False, - 'default_permissions': (), - }, - ), - migrations.RunPython( - code=split_handlers, - ), - migrations.RemoveField( - model_name='annotationhandler', - name='annotation_format', - ), - migrations.DeleteModel( - name='AnnotationHandler', - ), - ] diff --git a/cvat/apps/annotation/migrations/__init__.py b/cvat/apps/annotation/migrations/__init__.py deleted file mode 100644 index b66dde17a5c..00000000000 --- a/cvat/apps/annotation/migrations/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2018 Intel Corporation -# -# SPDX-License-Identifier: MIT diff --git a/cvat/apps/annotation/models.py b/cvat/apps/annotation/models.py deleted file mode 100644 index 3595327dca5..00000000000 --- a/cvat/apps/annotation/models.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (C) 2018 Intel Corporation -# -# SPDX-License-Identifier: MIT - -import os - -from django.db import models -from django.conf import settings -from django.core.files.storage import FileSystemStorage -from django.contrib.auth.models import User - -from cvat.apps.engine.models import SafeCharField - -def upload_file_handler(instance, filename): - return os.path.join('formats', str(instance.id), filename) - -class AnnotationFormat(models.Model): - name = SafeCharField(max_length=256) - owner = models.ForeignKey(User, null=True, blank=True, - on_delete=models.SET_NULL) - created_date = models.DateTimeField(auto_now_add=True) - updated_date = models.DateTimeField(auto_now_add=True) - handler_file = models.FileField( - upload_to=upload_file_handler, - storage=FileSystemStorage(location=os.path.join(settings.BASE_DIR)), - ) - - class Meta: - default_permissions = () - -class AnnotationHandler(models.Model): - display_name = SafeCharField(max_length=256, primary_key=True) - format = models.CharField(max_length=16) - version = models.CharField(max_length=16) - handler = models.CharField(max_length=256) - annotation_format = models.ForeignKey(AnnotationFormat, on_delete=models.CASCADE) - - class Meta: - default_permissions = () - abstract = True - -class AnnotationDumper(AnnotationHandler): - pass - -class AnnotationLoader(AnnotationHandler): - pass diff --git a/cvat/apps/annotation/serializers.py b/cvat/apps/annotation/serializers.py deleted file mode 100644 index 7284c0414a0..00000000000 --- a/cvat/apps/annotation/serializers.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (C) 2018-2020 Intel Corporation -# -# SPDX-License-Identifier: MIT - -from django.utils import timezone -from rest_framework import serializers - -from cvat.apps.annotation import models - -class AnnotationDumperSerializer(serializers.ModelSerializer): - class Meta: - model = models.AnnotationDumper - exclude = ('annotation_format',) - # https://www.django-rest-framework.org/api-guide/validators/#updating-nested-serializers - extra_kwargs = { - 'display_name': { - 'validators': [], - }, - } - -class AnnotationLoaderSerializer(serializers.ModelSerializer): - class Meta: - model = models.AnnotationLoader - exclude = ('annotation_format',) - # https://www.django-rest-framework.org/api-guide/validators/#updating-nested-serializers - extra_kwargs = { - 'display_name': { - 'validators': [], - }, - } - -class AnnotationFormatSerializer(serializers.ModelSerializer): - dumpers = AnnotationDumperSerializer(many=True, source="annotationdumper_set") - loaders = AnnotationLoaderSerializer(many=True, source="annotationloader_set") - - class Meta: - model = models.AnnotationFormat - fields = "__all__" - - # pylint: disable=no-self-use - def create(self, validated_data): - dumpers = validated_data.pop("annotationdumper_set") - loaders = validated_data.pop("annotationloader_set") - - annotation_format = models.AnnotationFormat() - annotation_format.name = validated_data["name"] - annotation_format.handler_file = validated_data["handler_file"].name - annotation_format.save() - - for dumper in dumpers: - models.AnnotationDumper(annotation_format=annotation_format, **dumper).save() - - for loader in loaders: - models.AnnotationLoader(annotation_format=annotation_format, **loader).save() - - return annotation_format - - # pylint: disable=no-self-use - def update(self, instance, validated_data): - dumper_names = [handler["display_name"] for handler in validated_data["annotationdumper_set"]] - loader_names = [handler["display_name"] for handler in validated_data["annotationloader_set"]] - instance.handler_file = validated_data.get('handler_file', instance.handler_file) - instance.owner = validated_data.get('owner', instance.owner) - instance.updated_date = timezone.localtime(timezone.now()) - - handlers_to_delete = [d for d in instance.annotationdumper_set.all() if d.display_name not in dumper_names] + \ - [l for l in instance.annotationloader_set.all() if l.display_name not in loader_names] - - for db_handler in handlers_to_delete: - db_handler.delete() - - for dumper in validated_data["annotationdumper_set"]: - models.AnnotationDumper(annotation_format=instance, **dumper).save() - for loader in validated_data["annotationloader_set"]: - models.AnnotationLoader(annotation_format=instance, **loader).save() - - instance.save() - return instance - -class AnnotationFileSerializer(serializers.Serializer): - annotation_file = serializers.FileField() diff --git a/cvat/apps/annotation/settings.py b/cvat/apps/annotation/settings.py deleted file mode 100644 index 9099a387c05..00000000000 --- a/cvat/apps/annotation/settings.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (C) 2018 Intel Corporation -# -# SPDX-License-Identifier: MIT - -import os - -path_prefix = os.path.join('cvat', 'apps', 'dataset_manager', 'formats') -BUILTIN_FORMATS = ( - os.path.join(path_prefix, 'cvat.py'), - os.path.join(path_prefix, 'pascal_voc.py'), - os.path.join(path_prefix, 'yolo.py'), - os.path.join(path_prefix, 'coco.py'), - os.path.join(path_prefix, 'mask.py'), - os.path.join(path_prefix, 'tfrecord.py'), - os.path.join(path_prefix, 'mot.py'), - os.path.join(path_prefix, 'labelme.py'), -) diff --git a/cvat/apps/annotation/tests.py b/cvat/apps/annotation/tests.py deleted file mode 100644 index b66dde17a5c..00000000000 --- a/cvat/apps/annotation/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2018 Intel Corporation -# -# SPDX-License-Identifier: MIT diff --git a/cvat/apps/annotation/views.py b/cvat/apps/annotation/views.py deleted file mode 100644 index b66dde17a5c..00000000000 --- a/cvat/apps/annotation/views.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2018 Intel Corporation -# -# SPDX-License-Identifier: MIT diff --git a/cvat/apps/auto_annotation/model_manager.py b/cvat/apps/auto_annotation/model_manager.py index 66467100a14..37f6cc059bc 100644 --- a/cvat/apps/auto_annotation/model_manager.py +++ b/cvat/apps/auto_annotation/model_manager.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2019 Intel Corporation +# Copyright (C) 2018-2020 Intel Corporation # # SPDX-License-Identifier: MIT @@ -17,7 +17,7 @@ from cvat.apps.engine.models import Task as TaskModel from cvat.apps.authentication.auth import has_admin_role from cvat.apps.engine.serializers import LabeledDataSerializer -from cvat.apps.engine.annotation import put_task_data, patch_task_data +from cvat.apps.dataset_manager.task import put_task_data, patch_task_data from cvat.apps.engine.frame_provider import FrameProvider from .models import AnnotationModel, FrameworkChoice @@ -248,9 +248,9 @@ def update_progress(job, progress): serializer = LabeledDataSerializer(data = result) if serializer.is_valid(raise_exception=True): if reset: - put_task_data(tid, user, result) + put_task_data(tid, result) else: - patch_task_data(tid, user, result, "create") + patch_task_data(tid, result, "create") slogger.glob.info("auto annotation for task {} done".format(tid)) except Exception as e: diff --git a/cvat/apps/auto_segmentation/views.py b/cvat/apps/auto_segmentation/views.py index e6ff702513d..1a9ce062581 100644 --- a/cvat/apps/auto_segmentation/views.py +++ b/cvat/apps/auto_segmentation/views.py @@ -1,5 +1,5 @@ -# Copyright (C) 2018-2019 Intel Corporation +# Copyright (C) 2018-2020 Intel Corporation # # SPDX-License-Identifier: MIT @@ -8,9 +8,9 @@ from rest_framework.decorators import api_view from rules.contrib.views import permission_required, objectgetter from cvat.apps.authentication.decorators import login_required +from cvat.apps.dataset_manager.task import put_task_data from cvat.apps.engine.models import Task as TaskModel from cvat.apps.engine.serializers import LabeledDataSerializer -from cvat.apps.engine.annotation import put_task_data from cvat.apps.engine.frame_provider import FrameProvider import django_rq @@ -161,7 +161,7 @@ def create_thread(tid, labels_mapping, user): result = convert_to_cvat_format(result) serializer = LabeledDataSerializer(data = result) if serializer.is_valid(raise_exception=True): - put_task_data(tid, user, result) + put_task_data(tid, result) slogger.glob.info('auto segmentation for task {} done'.format(tid)) except Exception as ex: try: diff --git a/cvat/apps/dataset_manager/__init__.py b/cvat/apps/dataset_manager/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/cvat/apps/engine/data_manager.py b/cvat/apps/dataset_manager/annotation.py similarity index 75% rename from cvat/apps/engine/data_manager.py rename to cvat/apps/dataset_manager/annotation.py index 7da117c1f04..d0ed61f523a 100644 --- a/cvat/apps/engine/data_manager.py +++ b/cvat/apps/dataset_manager/annotation.py @@ -2,16 +2,138 @@ # # SPDX-License-Identifier: MIT -import copy +from copy import copy, deepcopy import numpy as np from scipy.optimize import linear_sum_assignment from shapely import geometry -from . import models +from cvat.apps.engine.models import ShapeType +from cvat.apps.engine.serializers import LabeledDataSerializer -class DataManager: +class AnnotationIR: + def __init__(self, data=None): + self.reset() + if data: + self.tags = getattr(data, 'tags', []) or data['tags'] + self.shapes = getattr(data, 'shapes', []) or data['shapes'] + self.tracks = getattr(data, 'tracks', []) or data['tracks'] + + def add_tag(self, tag): + self.tags.append(tag) + + def add_shape(self, shape): + self.shapes.append(shape) + + def add_track(self, track): + self.tracks.append(track) + + @property + def data(self): + return { + 'version': self.version, + 'tags': self.tags, + 'shapes': self.shapes, + 'tracks': self.tracks, + } + + def __getitem__(self, key): + return getattr(self, key) + + @data.setter + def data(self, data): + self.version = data['version'] + self.tags = data['tags'] + self.shapes = data['shapes'] + self.tracks = data['tracks'] + + def serialize(self): + serializer = LabeledDataSerializer(data=self.data) + if serializer.is_valid(raise_exception=True): + return serializer.data + + @staticmethod + def _is_shape_inside(shape, start, stop): + return start <= int(shape['frame']) <= stop + + @staticmethod + def _is_track_inside(track, start, stop): + def has_overlap(a, b): + # a <= b + return 0 <= min(b, stop) - max(a, start) + + prev_shape = None + for shape in track['shapes']: + if prev_shape and not prev_shape['outside'] and \ + has_overlap(prev_shape['frame'], shape['frame']): + return True + prev_shape = shape + + if not prev_shape['outside'] and prev_shape['frame'] <= stop: + return True + + return False + + @classmethod + def _slice_track(cls, track_, start, stop): + def filter_track_shapes(shapes): + shapes = [s for s in shapes if cls._is_shape_inside(s, start, stop)] + drop_count = 0 + for s in shapes: + if s['outside']: + drop_count += 1 + else: + break + # Need to leave the last shape if all shapes are outside + if drop_count == len(shapes): + drop_count -= 1 + + return shapes[drop_count:] + + track = deepcopy(track_) + segment_shapes = filter_track_shapes(track['shapes']) + + if len(segment_shapes) < len(track['shapes']): + interpolated_shapes = TrackManager.get_interpolated_shapes( + track, start, stop) + scoped_shapes = filter_track_shapes(interpolated_shapes) + + if scoped_shapes: + if not scoped_shapes[0]['keyframe']: + segment_shapes.insert(0, scoped_shapes[0]) + if not scoped_shapes[-1]['keyframe']: + segment_shapes.append(scoped_shapes[-1]) + + # Should delete 'interpolation_shapes' and 'keyframe' keys because + # Track and TrackedShape models don't expect these fields + del track['interpolated_shapes'] + for shape in segment_shapes: + del shape['keyframe'] + + track['shapes'] = segment_shapes + track['frame'] = track['shapes'][0]['frame'] + return track + + def slice(self, start, stop): + #makes a data copy from specified frame interval + splitted_data = AnnotationIR() + splitted_data.tags = [deepcopy(t) + for t in self.tags if self._is_shape_inside(t, start, stop)] + splitted_data.shapes = [deepcopy(s) + for s in self.shapes if self._is_shape_inside(s, start, stop)] + splitted_data.tracks = [self._slice_track(t, start, stop) + for t in self.tracks if self._is_track_inside(t, start, stop)] + + return splitted_data + + def reset(self): + self.version = 0 + self.tags = [] + self.shapes = [] + self.tracks = [] + +class AnnotationManager: def __init__(self, data): self.data = data @@ -164,13 +286,13 @@ class ShapeManager(ObjectManager): def to_tracks(self): tracks = [] for shape in self.objects: - shape0 = copy.copy(shape) + shape0 = copy(shape) shape0["keyframe"] = True shape0["outside"] = False # TODO: Separate attributes on mutable and unmutable shape0["attributes"] = [] shape0.pop("group", None) - shape1 = copy.copy(shape0) + shape1 = copy(shape0) shape1["outside"] = True shape1["frame"] += 1 @@ -198,12 +320,12 @@ def _calc_polygons_similarity(p0, p1): has_same_type = obj0["type"] == obj1["type"] has_same_label = obj0.get("label_id") == obj1.get("label_id") if has_same_type and has_same_label: - if obj0["type"] == models.ShapeType.RECTANGLE: + if obj0["type"] == ShapeType.RECTANGLE: p0 = geometry.box(*obj0["points"]) p1 = geometry.box(*obj1["points"]) return _calc_polygons_similarity(p0, p1) - elif obj0["type"] == models.ShapeType.POLYGON: + elif obj0["type"] == ShapeType.POLYGON: p0 = geometry.Polygon(pairwise(obj0["points"])) p1 = geometry.Polygon(pairwise(obj0["points"])) @@ -286,7 +408,7 @@ def _calc_objects_similarity(obj0, obj1, start_frame, overlap): def _modify_unmached_object(obj, end_frame): shape = obj["shapes"][-1] if not shape["outside"]: - shape = copy.deepcopy(shape) + shape = deepcopy(shape) shape["frame"] = end_frame shape["outside"] = True obj["shapes"].append(shape) @@ -295,7 +417,7 @@ def _modify_unmached_object(obj, end_frame): if obj.get("interpolated_shapes"): last_interpolated_shape = obj["interpolated_shapes"][-1] for frame in range(last_interpolated_shape["frame"] + 1, end_frame): - last_interpolated_shape = copy.deepcopy(last_interpolated_shape) + last_interpolated_shape = deepcopy(last_interpolated_shape) last_interpolated_shape["frame"] = frame obj["interpolated_shapes"].append(last_interpolated_shape) obj["interpolated_shapes"].append(shape) @@ -313,7 +435,7 @@ def normalize_shape(shape): points.append(p.x) points.append(p.y) - shape = copy.copy(shape) + shape = copy(shape) shape["points"] = points return shape @@ -323,8 +445,8 @@ def get_interpolated_shapes(track, start_frame, end_frame): def interpolate(shape0, shape1): shapes = [] is_same_type = shape0["type"] == shape1["type"] - is_polygon = shape0["type"] == models.ShapeType.POLYGON - is_polyline = shape0["type"] == models.ShapeType.POLYLINE + is_polygon = shape0["type"] == ShapeType.POLYGON + is_polyline = shape0["type"] == ShapeType.POLYLINE is_same_size = len(shape0["points"]) == len(shape1["points"]) if not is_same_type or is_polygon or is_polyline or not is_same_size: shape0 = TrackManager.normalize_shape(shape0) @@ -338,7 +460,7 @@ def interpolate(shape0, shape1): points = np.asarray(shape0["points"]).reshape(-1, 2) else: points = (shape0["points"] + step * off).reshape(-1, 2) - shape = copy.deepcopy(shape0) + shape = deepcopy(shape0) if len(points) == 1: shape["points"] = points.flatten() else: @@ -362,7 +484,7 @@ def interpolate(shape0, shape1): assert shape["frame"] > curr_frame for attr in prev_shape["attributes"]: if attr["spec_id"] not in map(lambda el: el["spec_id"], shape["attributes"]): - shape["attributes"].append(copy.deepcopy(attr)) + shape["attributes"].append(deepcopy(attr)) if not prev_shape["outside"]: shapes.extend(interpolate(prev_shape, shape)) @@ -372,9 +494,9 @@ def interpolate(shape0, shape1): prev_shape = shape # TODO: Need to modify a client and a database (append "outside" shapes for polytracks) - if not prev_shape["outside"] and (prev_shape["type"] == models.ShapeType.RECTANGLE - or prev_shape["type"] == models.ShapeType.POINTS): - shape = copy.copy(prev_shape) + if not prev_shape["outside"] and (prev_shape["type"] == ShapeType.RECTANGLE + or prev_shape["type"] == ShapeType.POINTS): + shape = copy(prev_shape) shape["frame"] = end_frame shapes.extend(interpolate(prev_shape, shape)) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 53a103f02a6..b598cac3e8d 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -3,79 +3,439 @@ # # SPDX-License-Identifier: MIT -from collections import OrderedDict +import os.path as osp +from collections import OrderedDict, namedtuple -from django.db import transaction - -from cvat.apps.annotation.annotation import Annotation -from cvat.apps.engine.annotation import TaskAnnotation -from cvat.apps.engine.models import ShapeType, AttributeType +from django.utils import timezone import datumaro.components.extractor as datumaro +from cvat.apps.engine.frame_provider import FrameProvider +from cvat.apps.engine.models import AttributeType, ShapeType from datumaro.util.image import Image +from .annotation import AnnotationManager, TrackManager + + +class TaskData: + Attribute = namedtuple('Attribute', 'name, value') + LabeledShape = namedtuple( + 'LabeledShape', 'type, frame, label, points, occluded, attributes, group, z_order') + LabeledShape.__new__.__defaults__ = (0, 0) + TrackedShape = namedtuple( + 'TrackedShape', 'type, frame, points, occluded, outside, keyframe, attributes, group, z_order, label, track_id') + TrackedShape.__new__.__defaults__ = (0, 0, None, 0) + Track = namedtuple('Track', 'label, group, shapes') + Tag = namedtuple('Tag', 'frame, label, attributes, group') + Tag.__new__.__defaults__ = (0, ) + Frame = namedtuple( + 'Frame', 'idx, frame, name, width, height, labeled_shapes, tags') + + def __init__(self, annotation_ir, db_task, host='', create_callback=None): + self._annotation_ir = annotation_ir + self._db_task = db_task + self._host = host + self._create_callback = create_callback + self._MAX_ANNO_SIZE = 30000 + self._frame_info = {} + self._frame_mapping = {} + self._frame_step = db_task.data.get_frame_step() + + db_labels = self._db_task.label_set.all().prefetch_related( + 'attributespec_set').order_by('pk') + + self._label_mapping = OrderedDict( + (db_label.id, db_label) for db_label in db_labels) + + self._attribute_mapping = {db_label.id: { + 'mutable': {}, 'immutable': {}} for db_label in db_labels} + + for db_label in db_labels: + for db_attribute in db_label.attributespec_set.all(): + if db_attribute.mutable: + self._attribute_mapping[db_label.id]['mutable'][db_attribute.id] = db_attribute.name + else: + self._attribute_mapping[db_label.id]['immutable'][db_attribute.id] = db_attribute.name + + self._attribute_mapping_merged = {} + for label_id, attr_mapping in self._attribute_mapping.items(): + self._attribute_mapping_merged[label_id] = { + **attr_mapping['mutable'], + **attr_mapping['immutable'], + } + + self._init_frame_info() + self._init_meta() + + def _get_label_id(self, label_name): + for db_label in self._label_mapping.values(): + if label_name == db_label.name: + return db_label.id + return None + + def _get_label_name(self, label_id): + return self._label_mapping[label_id].name + + def _get_attribute_name(self, attribute_id): + for attribute_mapping in self._attribute_mapping_merged.values(): + if attribute_id in attribute_mapping: + return attribute_mapping[attribute_id] + + def _get_attribute_id(self, label_id, attribute_name, attribute_type=None): + if attribute_type: + container = self._attribute_mapping[label_id][attribute_type] + else: + container = self._attribute_mapping_merged[label_id] + + for attr_id, attr_name in container.items(): + if attribute_name == attr_name: + return attr_id + return None + + def _get_mutable_attribute_id(self, label_id, attribute_name): + return self._get_attribute_id(label_id, attribute_name, 'mutable') + + def _get_immutable_attribute_id(self, label_id, attribute_name): + return self._get_attribute_id(label_id, attribute_name, 'immutable') + + def _init_frame_info(self): + if hasattr(self._db_task.data, 'video'): + self._frame_info = {frame: { + "path": "frame_{:06d}".format( + self._db_task.data.start_frame + frame * self._frame_step), + "width": self._db_task.data.video.width, + "height": self._db_task.data.video.height, + } for frame in range(self._db_task.data.size)} + else: + self._frame_info = {db_image.frame: { + "path": db_image.path, + "width": db_image.width, + "height": db_image.height, + } for db_image in self._db_task.data.images.all()} + + self._frame_mapping = { + self._get_filename(info["path"]): frame + for frame, info in self._frame_info.items() + } -class CvatImagesExtractor(datumaro.Extractor): - def __init__(self, url, frame_provider): - super().__init__() + def _init_meta(self): + db_segments = self._db_task.segment_set.all().prefetch_related('job_set') + self._meta = OrderedDict([ + ("task", OrderedDict([ + ("id", str(self._db_task.id)), + ("name", self._db_task.name), + ("size", str(self._db_task.data.size)), + ("mode", self._db_task.mode), + ("overlap", str(self._db_task.overlap)), + ("bugtracker", self._db_task.bug_tracker), + ("created", str(timezone.localtime(self._db_task.created_date))), + ("updated", str(timezone.localtime(self._db_task.updated_date))), + ("start_frame", str(self._db_task.data.start_frame)), + ("stop_frame", str(self._db_task.data.stop_frame)), + ("frame_filter", self._db_task.data.frame_filter), + ("z_order", str(self._db_task.z_order)), + + ("labels", [ + ("label", OrderedDict([ + ("name", db_label.name), + ("attributes", [ + ("attribute", OrderedDict([ + ("name", db_attr.name), + ("mutable", str(db_attr.mutable)), + ("input_type", db_attr.input_type), + ("default_value", db_attr.default_value), + ("values", db_attr.values)])) + for db_attr in db_label.attributespec_set.all()]) + ])) for db_label in self._label_mapping.values() + ]), + + ("segments", [ + ("segment", OrderedDict([ + ("id", str(db_segment.id)), + ("start", str(db_segment.start_frame)), + ("stop", str(db_segment.stop_frame)), + ("url", "{}/?id={}".format( + self._host, db_segment.job_set.all()[0].id))] + )) for db_segment in db_segments + ]), + + ("owner", OrderedDict([ + ("username", self._db_task.owner.username), + ("email", self._db_task.owner.email) + ]) if self._db_task.owner else ""), + + ("assignee", OrderedDict([ + ("username", self._db_task.assignee.username), + ("email", self._db_task.assignee.email) + ]) if self._db_task.assignee else ""), + ])), + ("dumped", str(timezone.localtime(timezone.now()))) + ]) + + if hasattr(self._db_task.data, "video"): + self._meta["task"]["original_size"] = OrderedDict([ + ("width", str(self._db_task.data.video.width)), + ("height", str(self._db_task.data.video.height)) + ]) + # Add source to dumped file + self._meta["source"] = str( + osp.basename(self._db_task.data.video.path)) + + def _export_attributes(self, attributes): + exported_attributes = [] + for attr in attributes: + attribute_name = self._get_attribute_name(attr["spec_id"]) + exported_attributes.append(TaskData.Attribute( + name=attribute_name, + value=attr["value"], + )) + return exported_attributes + + def _export_tracked_shape(self, shape): + return TaskData.TrackedShape( + type=shape["type"], + frame=self._db_task.data.start_frame + + shape["frame"] * self._frame_step, + label=self._get_label_name(shape["label_id"]), + points=shape["points"], + occluded=shape["occluded"], + z_order=shape.get("z_order", 0), + group=shape.get("group", 0), + outside=shape.get("outside", False), + keyframe=shape.get("keyframe", True), + track_id=shape["track_id"], + attributes=self._export_attributes(shape["attributes"]), + ) - self._frame_provider = frame_provider - self._subsets = None + def _export_labeled_shape(self, shape): + return TaskData.LabeledShape( + type=shape["type"], + label=self._get_label_name(shape["label_id"]), + frame=self._db_task.data.start_frame + + shape["frame"] * self._frame_step, + points=shape["points"], + occluded=shape["occluded"], + z_order=shape.get("z_order", 0), + group=shape.get("group", 0), + attributes=self._export_attributes(shape["attributes"]), + ) - def __iter__(self): - frames = self._frame_provider.get_frames( - self._frame_provider.Quality.ORIGINAL, - self._frame_provider.Type.NUMPY_ARRAY) - for item_id, (image, _) in enumerate(frames): - yield datumaro.DatasetItem( - id=item_id, - image=Image(image), + def _export_tag(self, tag): + return TaskData.Tag( + frame=self._db_task.data.start_frame + + tag["frame"] * self._frame_step, + label=self._get_label_name(tag["label_id"]), + group=tag.get("group", 0), + attributes=self._export_attributes(tag["attributes"]), + ) + + def group_by_frame(self, include_empty=False): + frames = {} + def get_frame(idx): + frame_info = self._frame_info[idx] + frame = self._db_task.data.start_frame + idx * self._frame_step + if frame not in frames: + frames[frame] = TaskData.Frame( + idx=idx, + frame=frame, + name=frame_info['path'], + height=frame_info["height"], + width=frame_info["width"], + labeled_shapes=[], + tags=[], + ) + return frames[frame] + + if include_empty: + for idx in self._frame_info: + get_frame(idx) + + anno_manager = AnnotationManager(self._annotation_ir) + for shape in sorted(anno_manager.to_shapes(self._db_task.data.size), + key=lambda shape: shape.get("z_order", 0)): + if 'track_id' in shape: + exported_shape = self._export_tracked_shape(shape) + else: + exported_shape = self._export_labeled_shape(shape) + get_frame(shape['frame']).labeled_shapes.append( + exported_shape) + + for tag in self._annotation_ir.tags: + get_frame(tag['frame']).tags.append(self._export_tag(tag)) + + return iter(frames.values()) + + @property + def shapes(self): + for shape in self._annotation_ir.shapes: + yield self._export_labeled_shape(shape) + + @property + def tracks(self): + for idx, track in enumerate(self._annotation_ir.tracks): + tracked_shapes = TrackManager.get_interpolated_shapes( + track, 0, self._db_task.data.size) + for tracked_shape in tracked_shapes: + tracked_shape["attributes"] += track["attributes"] + tracked_shape["track_id"] = idx + tracked_shape["group"] = track["group"] + tracked_shape["label_id"] = track["label_id"] + + yield TaskData.Track( + label=self._get_label_name(track["label_id"]), + group=track["group"], + shapes=[self._export_tracked_shape(shape) + for shape in tracked_shapes], ) - def __len__(self): - return len(self._frame_provider) + @property + def tags(self): + for tag in self._annotation_ir.tags: + yield self._export_tag(tag) + + @property + def meta(self): + return self._meta + + def _import_tag(self, tag): + _tag = tag._asdict() + label_id = self._get_label_id(_tag.pop('label')) + _tag['frame'] = (int(_tag['frame']) - + self._db_task.data.start_frame) // self._frame_step + _tag['label_id'] = label_id + _tag['attributes'] = [self._import_attribute(label_id, attrib) + for attrib in _tag['attributes'] + if self._get_attribute_id(label_id, attrib.name)] + return _tag + + def _import_attribute(self, label_id, attribute): + return { + 'spec_id': self._get_attribute_id(label_id, attribute.name), + 'value': attribute.value, + } - def subsets(self): - return self._subsets + def _import_shape(self, shape): + _shape = shape._asdict() + label_id = self._get_label_id(_shape.pop('label')) + _shape['frame'] = (int(_shape['frame']) - + self._db_task.data.start_frame) // self._frame_step + _shape['label_id'] = label_id + _shape['attributes'] = [self._import_attribute(label_id, attrib) + for attrib in _shape['attributes'] + if self._get_attribute_id(label_id, attrib.name)] + return _shape + + def _import_track(self, track): + _track = track._asdict() + label_id = self._get_label_id(_track.pop('label')) + _track['frame'] = (min(int(shape.frame) for shape in _track['shapes']) - + self._db_task.data.start_frame) // self._frame_step + _track['label_id'] = label_id + _track['attributes'] = [] + _track['shapes'] = [shape._asdict() for shape in _track['shapes']] + for shape in _track['shapes']: + shape['frame'] = (int(shape['frame']) - \ + self._db_task.data.start_frame) // self._frame_step + _track['attributes'] = [self._import_attribute(label_id, attrib) + for attrib in shape['attributes'] + if self._get_immutable_attribute_id(label_id, attrib.name)] + shape['attributes'] = [self._import_attribute(label_id, attrib) + for attrib in shape['attributes'] + if self._get_mutable_attribute_id(label_id, attrib.name)] + + return _track + + def _call_callback(self): + if self._len() > self._MAX_ANNO_SIZE: + self._create_callback(self._annotation_ir.serialize()) + self._annotation_ir.reset() + + def add_tag(self, tag): + imported_tag = self._import_tag(tag) + if imported_tag['label_id']: + self._annotation_ir.add_tag(imported_tag) + self._call_callback() + + def add_shape(self, shape): + imported_shape = self._import_shape(shape) + if imported_shape['label_id']: + self._annotation_ir.add_shape(imported_shape) + self._call_callback() + + def add_track(self, track): + imported_track = self._import_track(track) + if imported_track['label_id']: + self._annotation_ir.add_track(imported_track) + self._call_callback() + + @property + def data(self): + return self._annotation_ir + + def _len(self): + track_len = 0 + for track in self._annotation_ir.tracks: + track_len += len(track['shapes']) + + return len(self._annotation_ir.tags) + len(self._annotation_ir.shapes) + track_len + + @property + def frame_info(self): + return self._frame_info + + @property + def frame_step(self): + return self._frame_step + + @property + def db_task(self): + return self._db_task - def get(self, item_id, subset=None, path=None): - if path or subset: - raise KeyError() - return datumaro.DatasetItem( - id=item_id, - image=self._frame_provider[item_id].getvalue() - ) + @staticmethod + def _get_filename(path): + return osp.splitext(osp.basename(path))[0] + + def match_frame(self, filename): + # try to match by filename + _filename = self._get_filename(filename) + if _filename in self._frame_mapping: + return self._frame_mapping[_filename] + + raise Exception( + "Cannot match filename or determine frame number for {} filename".format(filename)) -class CvatAnnotationsExtractor(datumaro.Extractor): - def __init__(self, url, cvat_annotations): - self._categories = self._load_categories(cvat_annotations) +class CvatTaskDataExtractor(datumaro.SourceExtractor): + def __init__(self, task_data, include_images=False): + super().__init__() + self._categories = self._load_categories(task_data) + + dm_items = [] - dm_annotations = [] + if include_images: + frame_provider = FrameProvider(task_data.db_task.data) - for cvat_frame_anno in cvat_annotations.group_by_frame(): - dm_anno = self._read_cvat_anno(cvat_frame_anno, cvat_annotations) - dm_image = Image(path=cvat_frame_anno.name, size=( - cvat_frame_anno.height, cvat_frame_anno.width) + for frame_data in task_data.group_by_frame(include_empty=include_images): + loader = None + if include_images: + loader = lambda p, i=frame_data.idx: frame_provider.get_frame(i, + quality=frame_provider.Quality.ORIGINAL, + out_type=frame_provider.Type.NUMPY_ARRAY)[0] + dm_image = Image(path=frame_data.name, loader=loader, + size=(frame_data.height, frame_data.width) ) - dm_item = datumaro.DatasetItem(id=cvat_frame_anno.frame, + dm_anno = self._read_cvat_anno(frame_data, task_data) + dm_item = datumaro.DatasetItem(id=frame_data.frame, annotations=dm_anno, image=dm_image) - dm_annotations.append((dm_item.id, dm_item)) + dm_items.append(dm_item) - dm_annotations = sorted(dm_annotations, key=lambda e: int(e[0])) - self._items = OrderedDict(dm_annotations) + self._items = dm_items def __iter__(self): - for item in self._items.values(): + for item in self._items: yield item def __len__(self): return len(self._items) - # pylint: disable=no-self-use - def subsets(self): - return [] - # pylint: enable=no-self-use - def categories(self): return self._categories @@ -95,15 +455,15 @@ def _load_categories(cvat_anno): return categories - def _read_cvat_anno(self, cvat_frame_anno, cvat_task_anno): + def _read_cvat_anno(self, cvat_frame_anno, task_data): item_anno = [] categories = self.categories() label_cat = categories[datumaro.AnnotationType.label] - map_label = lambda name: label_cat.find(name)[0] + def map_label(name): return label_cat.find(name)[0] label_attrs = { label['name']: label['attributes'] - for _, label in cvat_task_anno.meta['task']['labels'] + for _, label in task_data.meta['task']['labels'] } def convert_attrs(label, cvat_attrs): @@ -165,27 +525,18 @@ def convert_attrs(label, cvat_attrs): return item_anno -class CvatTaskExtractor(CvatAnnotationsExtractor): - def __init__(self, url, db_task, user): - cvat_annotations = TaskAnnotation(db_task.id, user) - with transaction.atomic(): - cvat_annotations.init_from_db() - cvat_annotations = Annotation(cvat_annotations.ir_data, db_task) - super().__init__(url, cvat_annotations) - - -def match_frame(item, cvat_task_anno): - is_video = cvat_task_anno.meta['task']['mode'] == 'interpolation' +def match_frame(item, task_data): + is_video = task_data.meta['task']['mode'] == 'interpolation' frame_number = None if frame_number is None: try: - frame_number = cvat_task_anno.match_frame(item.id) + frame_number = task_data.match_frame(item.id) except Exception: pass if frame_number is None and item.has_image: try: - frame_number = cvat_task_anno.match_frame(item.image.filename) + frame_number = task_data.match_frame(item.image.filename) except Exception: pass if frame_number is None: @@ -195,12 +546,12 @@ def match_frame(item, cvat_task_anno): pass if frame_number is None and is_video and item.id.startswith('frame_'): frame_number = int(item.id[len('frame_'):]) - if not frame_number in cvat_task_anno.frame_info: + if not frame_number in task_data.frame_info: raise Exception("Could not match item id: '%s' with any task frame" % item.id) return frame_number -def import_dm_annotations(dm_dataset, cvat_task_anno): +def import_dm_annotations(dm_dataset, task_data): shapes = { datumaro.AnnotationType.bbox: ShapeType.RECTANGLE, datumaro.AnnotationType.polygon: ShapeType.POLYGON, @@ -211,11 +562,11 @@ def import_dm_annotations(dm_dataset, cvat_task_anno): label_cat = dm_dataset.categories()[datumaro.AnnotationType.label] for item in dm_dataset: - frame_number = match_frame(item, cvat_task_anno) + frame_number = match_frame(item, task_data) # do not store one-item groups - group_map = { 0: 0 } - group_size = { 0: 0 } + group_map = {0: 0} + group_size = {0: 0} for ann in item.annotations: if ann.type in shapes: group = group_map.get(ann.group) @@ -231,21 +582,21 @@ def import_dm_annotations(dm_dataset, cvat_task_anno): for ann in item.annotations: if ann.type in shapes: - cvat_task_anno.add_shape(cvat_task_anno.LabeledShape( + task_data.add_shape(task_data.LabeledShape( type=shapes[ann.type], frame=frame_number, label=label_cat.items[ann.label].name, points=ann.points, occluded=ann.attributes.get('occluded') == True, group=group_map.get(ann.group, 0), - attributes=[cvat_task_anno.Attribute(name=n, value=str(v)) + attributes=[task_data.Attribute(name=n, value=str(v)) for n, v in ann.attributes.items()], )) elif ann.type == datumaro.AnnotationType.label: - cvat_task_anno.add_tag(cvat_task_anno.Tag( + task_data.add_tag(task_data.Tag( frame=frame_number, label=label_cat.items[ann.label].name, group=group_map.get(ann.group, 0), - attributes=[cvat_task_anno.Attribute(name=n, value=str(v)) + attributes=[task_data.Attribute(name=n, value=str(v)) for n, v in ann.attributes.items()], )) diff --git a/cvat/apps/dataset_manager/formats/README.md b/cvat/apps/dataset_manager/formats/README.md new file mode 100644 index 00000000000..56a5dfad50e --- /dev/null +++ b/cvat/apps/dataset_manager/formats/README.md @@ -0,0 +1,753 @@ + + + + + + +# Dataset and annotation formats + +## Contents + +- [How to add a format](#how-to-add) +- [Format descriptions](#formats) + - [CVAT](#cvat) + - [LabelMe](#labelme) + - [MOT](#mot) + - [COCO](#coco) + - [PASCAL VOC and mask](#voc) + - [YOLO](#yolo) + - [TF detection API](#tfrecord) + +## How to add a new annotation format support + +1. Add a python script to `dataset_manager/formats` +1. Add an import statement to [registry.py](./registry.py). +1. Implement some importers and exporters as the format requires. + +Each format is supported by an importer and exporter. + +It can be a function or a class decorated with +`importer` or `exporter` from [registry.py](./registry.py). Examples: + +``` python +@importer(name="MyFormat", version="1.0", ext="ZIP") +def my_importer(file_object, task_data, **options): + ... + +@importer(name="MyFormat", version="2.0", ext="XML") +class my_importer(file_object, task_data, **options): + def __call__(self, file_object, task_data, **options): + ... + +@exporter(name="MyFormat", version="1.0", ext="ZIP"): +def my_exporter(file_object, task_data, **options): + ... +``` + +Each decorator defines format parameters such as: + +- *name* + +- *version* + +- *file extension*. For the `importer` it can be a comma-separated list. + These parameters are combined to produce a visible name. It can be + set explicitly by the `display_name` argument. + +Importer arguments: + +- *file_object* - a file with annotations or dataset +- *task_data* - an instance of `TaskData` class. + +Exporter arguments: + +- *file_object* - a file for annotations or dataset + +- *task_data* - an instance of `TaskData` class. + +- *options* - format-specific options. `save_images` is the option to +distinguish if dataset or just annotations are requested. + +[`TaskData`](../bindings.py) provides many task properties and interfaces +to add and read task annotations. + +Public members: + +- **TaskData. Attribute** - class, `namedtuple('Attribute', 'name, value')` + +- **TaskData. LabeledShape** - class, `namedtuple('LabeledShape', + 'type, frame, label, points, occluded, attributes, group, z_order')` + +- **TrackedShape** - `namedtuple('TrackedShape', + 'type, points, occluded, frame, attributes, outside, keyframe, z_order')` + +- **Track** - class, `namedtuple('Track', 'label, group, shapes')` + +- **Tag** - class, `namedtuple('Tag', 'frame, label, attributes, group')` + +- **Frame** - class, `namedtuple('Frame', + 'frame, name, width, height, labeled_shapes, tags')` + +- **TaskData. shapes** - property, an iterator over `LabeledShape` objects + +- **TaskData. tracks** - property, an iterator over `Track` objects + +- **TaskData. tags** - property, an iterator over `Tag` objects + +- **TaskData. meta** - property, a dictionary with task information + +- **TaskData. group_by_frame()** - method, returns + an iterator over `Frame` objects, which groups annotation objects by frame. + Note that `TrackedShape` s will be represented as `LabeledShape` s. + +- **TaskData. add_tag(tag)** - method, + tag should be an instance of the `Tag` class + +- **TaskData. add_shape(shape)** - method, + shape should be an instance of the `Shape` class + +- **TaskData. add_track(track)** - method, + track should be an instance of the `Track` class + +Sample exporter code: + +``` python +... +# dump meta info if necessary +... +# iterate over all frames +for frame_annotation in task_data.group_by_frame(): + # get frame info + image_name = frame_annotation.name + image_width = frame_annotation.width + image_height = frame_annotation.height + # iterate over all shapes on the frame + for shape in frame_annotation.labeled_shapes: + label = shape.label + xtl = shape.points[0] + ytl = shape.points[1] + xbr = shape.points[2] + ybr = shape.points[3] + # iterate over shape attributes + for attr in shape.attributes: + attr_name = attr.name + attr_value = attr.value +... +# dump annotation code +file_object.write(...) +... +``` + +Sample importer code: + +``` python +... +#read file_object +... +for parsed_shape in parsed_shapes: + shape = task_data.LabeledShape( + type="rectangle", + points=[0, 0, 100, 100], + occluded=False, + attributes=[], + label="car", + outside=False, + frame=99, + ) +task_data.add_shape(shape) +``` + +## Format specifications + +### CVAT + +This is the native CVAT annotation format. It supports all CVAT annotations +features, so it can be used to make data backups. + +- supported annotations: Rectangles, Polygons, Polylines, + Points, Cuboids, Tags, Tracks + +- attributes are supported + +- [Format specification](/cvat/apps/documentation/xml_format.md) + +#### CVAT for images dumper + +Downloaded file: a ZIP file of the following structure: + +``` bash +taskname.zip/ +├── images/ +| ├── img1.png +| └── img2.jpg +└── annotations.xml +``` + +- tracks are split by frames + +#### CVAT for videos dumper + +Downloaded file: a ZIP file of the following structure: + +``` bash +taskname.zip/ +├── images/ +| ├── frame_000000.png +| └── frame_000001.png +└── annotations.xml +``` + +- shapes are exported as single-frame tracks + +#### CVAT loader + +Uploaded file: an XML file or a ZIP file of the structures above + +### [Pascal VOC](http://host.robots.ox.ac.uk/pascal/VOC/) + +- [Format specification](http://host.robots.ox.ac.uk/pascal/VOC/voc2012/devkit_doc.pdf) + +- supported annotations: + + - Rectangles (detection and layout tasks) + - Tags (action- and classification tasks) + - Polygons (segmentation task) + +- supported attributes: + + - `occluded` + - `truncated` and `difficult` (should be defined for labels as `checkbox` -es) + - action attributes (import only, should be defined as `checkbox` -es) + +#### Pascal VOC export + +Downloaded file: a zip archive of the following structure: + +``` bash +taskname.zip/ +├── JpegImages/ +│   ├── .jpg +│   ├── .jpg +│   └── .jpg +├── Annotations/ +│   ├── .xml +│   ├── .xml +│   └── .xml +├── ImageSets/ +│   └── Main/ +│   └── default.txt +└── labelmap.txt + +# labelmap.txt +# label : color_rgb : 'body' parts : actions +background::: +aeroplane::: +bicycle::: +bird::: +``` + +#### Pascal VOC import + +Uploaded file: a zip archive of the structure declared above or the following: + +``` bash +taskname.zip/ +├── .xml +├── .xml +└── .xml +``` + +It must be possible for CVAT to match the frame name and file name +from annotation `.xml` file (the `filename` tag, e. g. +`2008_004457.jpg` ). + +There are 2 options: + +1. full match between frame name and file name from annotation `.xml` + (in cases when task was created from images or image archive). + +1. match by frame number. File name should be `.jpg` + or `frame_000000.jpg`. It should be used when task was created from video. + +#### Segmentation mask export + +Downloaded file: a zip archive of the following structure: + +``` bash +taskname.zip/ +├── labelmap.txt # optional, required for non-VOC labels +├── ImageSets/ +│   └── Segmentation/ +│   └── default.txt # list of image names without extension +├── SegmentationClass/ # merged class masks +│   ├── image1.png +│   └── image2.png +└── SegmentationObject/ # merged instance masks + ├── image1.png + └── image2.png + +# labelmap.txt +# label : color (RGB) : 'body' parts : actions +background:0,128,0:: +aeroplane:10,10,128:: +bicycle:10,128,0:: +bird:0,108,128:: +boat:108,0,100:: +bottle:18,0,8:: +bus:12,28,0:: +``` + +Mask is a `png` image with 1 or 3 channels where each pixel +has own color which corresponds to a label. +Colors are generated following to Pascal VOC [algorithm](http://host.robots.ox.ac.uk/pascal/VOC/voc2012/htmldoc/devkit_doc.html#sec:voclabelcolormap). +`(0, 0, 0)` is used for background by default. + +- supported shapes: Rectangles, Polygons + +#### Segmentation mask import + +Uploaded file: a zip archive of the following structure: + +``` bash + taskname.zip/ + ├── labelmap.txt # optional, required for non-VOC labels + ├── ImageSets/ + │   └── Segmentation/ + │   └── .txt + ├── SegmentationClass/ + │   ├── image1.png + │   └── image2.png + └── SegmentationObject/ + ├── image1.png + └── image2.png + ``` + +- supported shapes: Polygons + +#### How to create a task from Pascal VOC dataset + +1. Download the Pascal Voc dataset (Can be downloaded from the + [PASCAL VOC website](http://host.robots.ox.ac.uk/pascal/VOC/)) + +1. Create a CVAT task with the following labels: + + ``` bash + aeroplane bicycle bird boat bottle bus car cat chair cow diningtable + dog horse motorbike person pottedplant sheep sofa train tvmonitor + ``` + + You can add `~checkbox=difficult:false ~checkbox=truncated:false` + attributes for each label if you want to use them. + + Select interesting image files (See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task) guide for details) + +1. zip the corresponding annotation files + +1. click `Upload annotation` button, choose `Pascal VOC ZIP 1.1` + + and select the zip file with annotations from previous step. + It may take some time. + +### [YOLO](https://pjreddie.com/darknet/yolo/) + +- [Format specification](https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects) +- supported annotations: Rectangles + +#### YOLO export + +Downloaded file: a zip archive with following structure: + +``` bash +archive.zip/ +├── obj.data +├── obj.names +├── obj__data +│   ├── image1.txt +│   └── image2.txt +└── train.txt # list of subset image paths + +# the only valid subsets are: train, valid +# train.txt and valid.txt: +obj__data/image1.jpg +obj__data/image2.jpg + +# obj.data: +classes = 3 # optional +names = obj.names +train = train.txt +valid = valid.txt # optional +backup = backup/ # optional + +# obj.names: +cat +dog +airplane + +# image_name.txt: +# label_id - id from obj.names +# cx, cy - relative coordinates of the bbox center +# rw, rh - relative size of the bbox +# label_id cx cy rw rh +1 0.3 0.8 0.1 0.3 +2 0.7 0.2 0.3 0.1 +``` + +Each annotation `*.txt` file has a name that corresponds to the name of +the image file (e. g. `frame_000001.txt` is the annotation +for the `frame_000001.jpg` image). +The `*.txt` file structure: each line describes label and bounding box +in the following format `label_id cx cy w h`. +`obj.names` contains the ordered list of label names. + +#### YOLO import + +Uploaded file: a zip archive of the same structure as above +It must be possible to match the CVAT frame (image name) +and annotation file name. There are 2 options: + +1. full match between image name and name of annotation `*.txt` file + (in cases when a task was created from images or archive of images). + +1. match by frame number (if CVAT cannot match by name). File name + should be in the following format `.jpg` . + It should be used when task was created from a video. + +#### How to create a task from YOLO formatted dataset (from VOC for example) + +1. Follow the official [guide](https://pjreddie.com/darknet/yolo/)(see Training YOLO on VOC section) + and prepare the YOLO formatted annotation files. + +1. Zip train images + +``` bash +zip images.zip -j -@ < train.txt +``` + +1. Create a CVAT task with the following labels: + + ``` bash + aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog + horse motorbike person pottedplant sheep sofa train tvmonitor + ``` + + Select images. zip as data. Most likely you should use `share` + functionality because size of images. zip is more than 500Mb. + See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task) + guide for details. + +1. Create `obj.names` with the following content: + + ``` bash + aeroplane + bicycle + bird + boat + bottle + bus + car + cat + chair + cow + diningtable + dog + horse + motorbike + person + pottedplant + sheep + sofa + train + tvmonitor + ``` + +1. Zip all label files together (we need to add only label files that correspond to the train subset) + + ``` bash + cat train.txt | while read p; do echo ${p%/*/*}/labels/${${p##*/}%%.*}.txt; done | zip labels.zip -j -@ obj.names + ``` + +1. Click `Upload annotation` button, choose `YOLO 1.1` and select the zip + + file with labels from the previous step. + +### [MS COCO Object Detection](http://cocodataset.org/#format-data) + +- [Format specification](http://cocodataset.org/#format-data) + +#### COCO dumper description + +Downloaded file: single unpacked `json`. + +- supported annotations: Polygons, Rectangles + +#### COCO loader description + +Uploaded file: single unpacked `*.json` . + +- supported annotations: Polygons, Rectangles (if `segmentation` field is empty) + +#### How to create a task from MS COCO dataset + +1. Download the [MS COCO dataset](http://cocodataset.org/#download). + + For example [2017 Val images](http://images.cocodataset.org/zips/val2017.zip) + and [2017 Train/Val annotations](http://images.cocodataset.org/annotations/annotations_trainval2017.zip). + +1. Create a CVAT task with the following labels: + + ``` bash + person bicycle car motorcycle airplane bus train truck boat "traffic light" "fire hydrant" "stop sign" "parking meter" bench bird cat dog horse sheep cow elephant bear zebra giraffe backpack umbrella handbag tie suitcase frisbee skis snowboard "sports ball" kite "baseball bat" "baseball glove" skateboard surfboard "tennis racket" bottle "wine glass" cup fork knife spoon bowl banana apple sandwich orange broccoli carrot "hot dog" pizza donut cake chair couch "potted plant" bed "dining table" toilet tv laptop mouse remote keyboard "cell phone" microwave oven toaster sink refrigerator book clock vase scissors "teddy bear" "hair drier" toothbrush + ``` + +1. Select val2017.zip as data + (See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task) + guide for details) + +1. Unpack `annotations_trainval2017.zip` + +1. click `Upload annotation` button, + choose `COCO 1.1` and select `instances_val2017.json.json` + annotation file. It can take some time. + +### [TFRecord](https://www.tensorflow.org/tutorials/load_data/tf_records) + +TFRecord is a very flexible format, but we try to correspond the +format that used in +[TF object detection](https://github.com/tensorflow/models/tree/master/research/object_detection) +with minimal modifications. + +Used feature description: + +``` python +image_feature_description = { + 'image/filename': tf.io.FixedLenFeature([], tf.string), + 'image/source_id': tf.io.FixedLenFeature([], tf.string), + 'image/height': tf.io.FixedLenFeature([], tf.int64), + 'image/width': tf.io.FixedLenFeature([], tf.int64), + # Object boxes and classes. + 'image/object/bbox/xmin': tf.io.VarLenFeature(tf.float32), + 'image/object/bbox/xmax': tf.io.VarLenFeature(tf.float32), + 'image/object/bbox/ymin': tf.io.VarLenFeature(tf.float32), + 'image/object/bbox/ymax': tf.io.VarLenFeature(tf.float32), + 'image/object/class/label': tf.io.VarLenFeature(tf.int64), + 'image/object/class/text': tf.io.VarLenFeature(tf.string), +} +``` + +#### TFRecord dumper description + +Downloaded file: a zip archive with following structure: + +``` bash +taskname.zip/ +├── task2.tfrecord +└── label_map.pbtxt +``` + +- supported annotations: Rectangles + +#### TFRecord loader description + +Uploaded file: a zip archive of following structure: + +``` bash +taskname.zip/ +└── task2.tfrecord +``` + +- supported annotations: Rectangles + +#### How to create a task from TFRecord dataset (from VOC2007 for example) + +1. Create `label_map.pbtxt` file with the following content: + +``` js +item { + id: 1 + name: 'aeroplane' +} +item { + id: 2 + name: 'bicycle' +} +item { + id: 3 + name: 'bird' +} +item { + id: 4 + name: 'boat' +} +item { + id: 5 + name: 'bottle' +} +item { + id: 6 + name: 'bus' +} +item { + id: 7 + name: 'car' +} +item { + id: 8 + name: 'cat' +} +item { + id: 9 + name: 'chair' +} +item { + id: 10 + name: 'cow' +} +item { + id: 11 + name: 'diningtable' +} +item { + id: 12 + name: 'dog' +} +item { + id: 13 + name: 'horse' +} +item { + id: 14 + name: 'motorbike' +} +item { + id: 15 + name: 'person' +} +item { + id: 16 + name: 'pottedplant' +} +item { + id: 17 + name: 'sheep' +} +item { + id: 18 + name: 'sofa' +} +item { + id: 19 + name: 'train' +} +item { + id: 20 + name: 'tvmonitor' +} +``` + +1. Use [create_pascal_tf_record.py](https://github.com/tensorflow/models/blob/master/research/object_detection/dataset_tools/create_pascal_tf_record.py) + +to convert VOC2007 dataset to TFRecord format. +As example: + +``` bash +python create_pascal_tf_record.py --data_dir --set train --year VOC2007 --output_path pascal.tfrecord --label_map_path label_map.pbtxt +``` + +1. Zip train images + + ``` bash + cat /VOC2007/ImageSets/Main/train.txt | while read p; do echo /VOC2007/JPEGImages/${p}.jpg ; done | zip images.zip -j -@ + ``` + +1. Create a CVAT task with the following labels: + + ``` bash + aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person pottedplant sheep sofa train tvmonitor + ``` + + Select images. zip as data. + See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task) + guide for details. + +1. Zip `pascal.tfrecord` and `label_map.pbtxt` files together + + ``` bash + zip anno.zip -j + ``` + +1. Click `Upload annotation` button, choose `TFRecord 1.0` and select the zip file + + with labels from the previous step. It may take some time. + +### [MOT sequence](https://arxiv.org/pdf/1906.04567.pdf) + +#### MOT Dumper + +Downloaded file: a zip archive of the following structure: + +``` bash +taskname.zip/ +├── img1/ +| ├── imgage1.jpg +| └── imgage2.jpg +└── gt/ + ├── labels.txt + └── gt.txt + +# labels.txt +cat +dog +person +... + +# gt.txt +# frame_id, track_id, x, y, w, h, "not ignored", class_id, visibility, +1,1,1363,569,103,241,1,1,0.86014 +... + +``` + +- supported annotations: Rectangle shapes and tracks +- supported attributes: `visibility` (number), `ignored` (checkbox) + +#### MOT Loader + +Uploaded file: a zip archive of the structure above or: + +``` bash +taskname.zip/ +├── labels.txt # optional, mandatory for non-official labels +└── gt.txt +``` + +- supported annotations: Rectangle tracks + +### [LabelMe](http://labelme.csail.mit.edu/Release3.0) + +#### LabelMe Dumper + +Downloaded file: a zip archive of the following structure: + +``` bash +taskname.zip/ +├── img1.jpg +└── img1.xml +``` + +- supported annotations: Rectangles, Polygons (with attributes) + +#### LabelMe Loader + +Uploaded file: a zip archive of the following structure: + +``` bash +taskname.zip/ +├── Masks/ +| ├── img1_mask1.png +| └── img1_mask2.png +├── img1.xml +├── img2.xml +└── img3.xml +``` + +- supported annotations: Rectangles, Polygons, Masks (as polygons) diff --git a/cvat/apps/dataset_manager/formats/__init__.py b/cvat/apps/dataset_manager/formats/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/cvat/apps/dataset_manager/formats/coco.py b/cvat/apps/dataset_manager/formats/coco.py index fe323b645db..41d6343a3dc 100644 --- a/cvat/apps/dataset_manager/formats/coco.py +++ b/cvat/apps/dataset_manager/formats/coco.py @@ -2,52 +2,36 @@ # # SPDX-License-Identifier: MIT -format_spec = { - "name": "COCO", - "dumpers": [ - { - "display_name": "{name} {format} {version}", - "format": "JSON", - "version": "1.0", - "handler": "dump" - }, - ], - "loaders": [ - { - "display_name": "{name} {format} {version}", - "format": "JSON", - "version": "1.0", - "handler": "load" - }, - ], -} - -def load(file_object, annotations): - from datumaro.plugins.coco_format.extractor import CocoInstancesExtractor - from cvat.apps.dataset_manager.bindings import import_dm_annotations - - dm_dataset = CocoInstancesExtractor(file_object.name) - import_dm_annotations(dm_dataset, annotations) - -from datumaro.plugins.coco_format.converter import \ - CocoInstancesConverter as _CocoInstancesConverter -class CvatCocoConverter(_CocoInstancesConverter): - NAME = 'cvat_coco' - -def dump(file_object, annotations): - import os.path as osp - import shutil - from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor - from tempfile import TemporaryDirectory - - extractor = CvatAnnotationsExtractor('', annotations) - converter = CvatCocoConverter() +import zipfile +from tempfile import TemporaryDirectory + +from datumaro.components.project import Dataset +from cvat.apps.dataset_manager.bindings import CvatTaskDataExtractor, \ + import_dm_annotations +from cvat.apps.dataset_manager.util import make_zip_archive + +from .registry import dm_env, exporter, importer + + +@exporter(name='COCO', ext='ZIP', version='1.0') +def _export(dst_file, task_data, save_images=False): + extractor = CvatTaskDataExtractor(task_data, include_images=save_images) + extractor = Dataset.from_extractors(extractor) # apply lazy transforms with TemporaryDirectory() as temp_dir: + converter = dm_env.make_converter('coco_instances', + save_images=save_images) converter(extractor, save_dir=temp_dir) - # HACK: file_object should not be used this way, however, - # it is the most efficient way. The correct approach would be to copy - # file contents. - file_object.close() - shutil.move(osp.join(temp_dir, 'annotations', 'instances_default.json'), - file_object.name) \ No newline at end of file + make_zip_archive(temp_dir, dst_file) + +@importer(name='COCO', ext='JSON, ZIP', version='1.0') +def _import(src_file, task_data): + if zipfile.is_zipfile(src_file): + with TemporaryDirectory() as tmp_dir: + zipfile.ZipFile(src_file).extractall(tmp_dir) + + dataset = dm_env.make_importer('coco')(tmp_dir).make_dataset() + import_dm_annotations(dataset, task_data) + else: + dataset = dm_env.make_extractor('coco_instances', src_file.name) + import_dm_annotations(dataset, task_data) \ No newline at end of file diff --git a/cvat/apps/dataset_manager/formats/cvat.py b/cvat/apps/dataset_manager/formats/cvat.py index 4e89f2a437c..e2fa3d80c22 100644 --- a/cvat/apps/dataset_manager/formats/cvat.py +++ b/cvat/apps/dataset_manager/formats/cvat.py @@ -2,31 +2,19 @@ # # SPDX-License-Identifier: MIT -format_spec = { - "name": "CVAT", - "dumpers": [ - { - "display_name": "{name} {format} {version} for videos", - "format": "XML", - "version": "1.1", - "handler": "dump_as_cvat_interpolation" - }, - { - "display_name": "{name} {format} {version} for images", - "format": "XML", - "version": "1.1", - "handler": "dump_as_cvat_annotation" - } - ], - "loaders": [ - { - "display_name": "{name} {format} {version}", - "format": "XML", - "version": "1.1", - "handler": "load", - } - ], -} +import os +import os.path as osp +import zipfile +from collections import OrderedDict +from glob import glob +from tempfile import TemporaryDirectory + +from cvat.apps.dataset_manager.util import make_zip_archive +from cvat.apps.engine.frame_provider import FrameProvider +from datumaro.util.image import save_image + +from .registry import exporter, importer + def pairwise(iterable): a = iter(iterable) @@ -34,7 +22,6 @@ def pairwise(iterable): def create_xml_dumper(file_object): from xml.sax.saxutils import XMLGenerator - from collections import OrderedDict class XmlAnnotationWriter: def __init__(self, file): self.version = "1.1" @@ -184,7 +171,6 @@ def close_root(self): return XmlAnnotationWriter(file_object) def dump_as_cvat_annotation(file_object, annotations): - from collections import OrderedDict dumper = create_xml_dumper(file_object) dumper.open_root() dumper.add_meta(annotations.meta) @@ -298,7 +284,6 @@ def dump_as_cvat_annotation(file_object, annotations): dumper.close_root() def dump_as_cvat_interpolation(file_object, annotations): - from collections import OrderedDict dumper = create_xml_dumper(file_object) dumper.open_root() dumper.add_meta(annotations.meta) @@ -425,8 +410,8 @@ def dump_track(idx, track): dumper.close_root() def load(file_object, annotations): - import xml.etree.ElementTree as et - context = et.iterparse(file_object, events=("start", "end")) + from defusedxml import ElementTree + context = ElementTree.iterparse(file_object, events=("start", "end")) context = iter(context) ev, _ = next(context) @@ -525,3 +510,50 @@ def load(file_object, annotations): annotations.add_tag(annotations.Tag(**tag)) tag = None el.clear() + +def _export(dst_file, task_data, anno_callback, save_images=False): + with TemporaryDirectory() as temp_dir: + with open(osp.join(temp_dir, 'annotations.xml'), 'wb') as f: + anno_callback(f, task_data) + + if save_images: + img_dir = osp.join(temp_dir, 'images') + os.makedirs(img_dir) + frame_provider = FrameProvider(task_data.db_task.data) + frames = frame_provider.get_frames( + frame_provider.Quality.ORIGINAL, + frame_provider.Type.NUMPY_ARRAY) + for frame_id, (frame_data, _) in enumerate(frames): + frame_name = task_data.frame_info[frame_id]['path'] + if '.' in frame_name: + save_image(osp.join(img_dir, frame_name), + frame_data, jpeg_quality=100) + else: + save_image(osp.join(img_dir, frame_name + '.png'), + frame_data) + + make_zip_archive(temp_dir, dst_file) + +@exporter(name='CVAT for video', ext='ZIP', version='1.1') +def _export_video(dst_file, task_data, save_images=False): + _export(dst_file, task_data, + anno_callback=dump_as_cvat_interpolation, save_images=save_images) + +@exporter(name='CVAT for images', ext='ZIP', version='1.1') +def _export_images(dst_file, task_data, save_images=False): + _export(dst_file, task_data, + anno_callback=dump_as_cvat_annotation, save_images=save_images) + +@importer(name='CVAT', ext='XML, ZIP', version='1.1') +def _import(src_file, task_data): + is_zip = zipfile.is_zipfile(src_file) + src_file.seek(0) + if is_zip: + with TemporaryDirectory() as tmp_dir: + zipfile.ZipFile(src_file).extractall(tmp_dir) + + anno_paths = glob(osp.join(tmp_dir, '**', '*.xml'), recursive=True) + for p in anno_paths: + load(p, task_data) + else: + load(src_file, task_data) \ No newline at end of file diff --git a/cvat/apps/dataset_manager/formats/datumaro/__init__.py b/cvat/apps/dataset_manager/formats/datumaro/__init__.py new file mode 100644 index 00000000000..59c423d0716 --- /dev/null +++ b/cvat/apps/dataset_manager/formats/datumaro/__init__.py @@ -0,0 +1,99 @@ +# Copyright (C) 2019 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import json +import os +import os.path as osp +import shutil +from tempfile import TemporaryDirectory + +from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, + import_dm_annotations) +from cvat.apps.dataset_manager.util import make_zip_archive +from cvat.settings.base import BASE_DIR, DATUMARO_PATH +from datumaro.components.project import Project + +from ..registry import dm_env, exporter + + +@exporter(name="Datumaro", ext="ZIP", version="1.0") +class DatumaroProjectExporter: + _REMOTE_IMAGES_EXTRACTOR = 'cvat_rest_api_task_images' + _TEMPLATES_DIR = osp.join(osp.dirname(__file__), 'export_templates') + + @staticmethod + def _save_image_info(save_dir, task_data): + os.makedirs(save_dir, exist_ok=True) + + config = { + 'server_url': task_data._host or 'localhost', + 'task_id': task_data.db_task.id, + } + + images = [] + images_meta = { 'images': images, } + for frame_id, frame in task_data.frame_info.items(): + images.append({ + 'id': frame_id, + 'name': osp.basename(frame['path']), + 'width': frame['width'], + 'height': frame['height'], + }) + + with open(osp.join(save_dir, 'config.json'), 'w') as config_file: + json.dump(config, config_file) + with open(osp.join(save_dir, 'images_meta.json'), 'w') as images_file: + json.dump(images_meta, images_file) + + def _export(self, task_data, save_dir, save_images=False): + dataset = CvatTaskDataExtractor(task_data, include_images=save_images) + converter = dm_env.make_converter('datumaro_project', + save_images=save_images, + config={ 'project_name': task_data.db_task.name, } + ) + converter(dataset, save_dir=save_dir) + + project = Project.load(save_dir) + target_dir = project.config.project_dir + os.makedirs(target_dir, exist_ok=True) + shutil.copyfile( + osp.join(self._TEMPLATES_DIR, 'README.md'), + osp.join(target_dir, 'README.md')) + + if not save_images: + # add remote links to images + source_name = 'task_%s_images' % task_data.db_task.id + project.add_source(source_name, { + 'format': self._REMOTE_IMAGES_EXTRACTOR, + }) + self._save_image_info( + osp.join(save_dir, project.local_source_dir(source_name)), + task_data) + project.save() + + templates_dir = osp.join(self._TEMPLATES_DIR, 'plugins') + target_dir = osp.join(project.config.project_dir, + project.config.env_dir, project.config.plugins_dir) + os.makedirs(target_dir, exist_ok=True) + shutil.copyfile( + osp.join(templates_dir, self._REMOTE_IMAGES_EXTRACTOR + '.py'), + osp.join(target_dir, self._REMOTE_IMAGES_EXTRACTOR + '.py')) + + # Make Datumaro and CVAT CLI modules available to the user + shutil.copytree(DATUMARO_PATH, osp.join(save_dir, 'datumaro'), + ignore=lambda src, names: ['__pycache__'] + [ + n for n in names + if sum([int(n.endswith(ext)) for ext in + ['.pyx', '.pyo', '.pyd', '.pyc']]) + ]) + + cvat_utils_dst_dir = osp.join(save_dir, 'cvat', 'utils') + os.makedirs(cvat_utils_dst_dir) + shutil.copytree(osp.join(BASE_DIR, 'utils', 'cli'), + osp.join(cvat_utils_dst_dir, 'cli')) + + def __call__(self, dst_file, task_data, save_images=False): + with TemporaryDirectory() as temp_dir: + self._export(task_data, save_dir=temp_dir, save_images=save_images) + make_zip_archive(temp_dir, dst_file) diff --git a/cvat/apps/dataset_manager/export_templates/README.md b/cvat/apps/dataset_manager/formats/datumaro/export_templates/README.md similarity index 100% rename from cvat/apps/dataset_manager/export_templates/README.md rename to cvat/apps/dataset_manager/formats/datumaro/export_templates/README.md diff --git a/cvat/apps/dataset_manager/export_templates/plugins/cvat_rest_api_task_images.py b/cvat/apps/dataset_manager/formats/datumaro/export_templates/plugins/cvat_rest_api_task_images.py similarity index 71% rename from cvat/apps/dataset_manager/export_templates/plugins/cvat_rest_api_task_images.py rename to cvat/apps/dataset_manager/formats/datumaro/export_templates/plugins/cvat_rest_api_task_images.py index a4e92f8cd59..b54cce3fbd3 100644 --- a/cvat/apps/dataset_manager/export_templates/plugins/cvat_rest_api_task_images.py +++ b/cvat/apps/dataset_manager/formats/datumaro/export_templates/plugins/cvat_rest_api_task_images.py @@ -1,34 +1,28 @@ -# Copyright (C) 2019-2020 Intel Corporation +# Copyright (C) 2020 Intel Corporation # # SPDX-License-Identifier: MIT -from collections import OrderedDict import getpass import json -import os, os.path as osp -import requests - -from datumaro.components.config import (Config, - SchemaBuilder as _SchemaBuilder, -) -import datumaro.components.extractor as datumaro -from datumaro.util.image import lazy_image, load_image, Image +import os +import os.path as osp +from collections import OrderedDict -from cvat.utils.cli.core import CLI as CVAT_CLI, CVAT_API_V1 +import requests +from cvat.utils.cli.core import CLI as CVAT_CLI +from cvat.utils.cli.core import CVAT_API_V1 +from datumaro.components.config import Config, SchemaBuilder +from datumaro.components.extractor import SourceExtractor, DatasetItem +from datumaro.util.image import Image, lazy_image, load_image -CONFIG_SCHEMA = _SchemaBuilder() \ +CONFIG_SCHEMA = SchemaBuilder() \ .add('task_id', int) \ - .add('server_host', str) \ - .add('server_port', int) \ + .add('server_url', str) \ .build() -DEFAULT_CONFIG = Config({ - 'server_port': 80 -}, schema=CONFIG_SCHEMA, mutable=False) - -class cvat_rest_api_task_images(datumaro.SourceExtractor): +class cvat_rest_api_task_images(SourceExtractor): def _image_local_path(self, item_id): task_id = self._config.task_id return osp.join(self._cache_dir, @@ -53,16 +47,15 @@ def _connect(self): session = None try: - print("Enter credentials for '%s:%s' to read task data:" % \ - (self._config.server_host, self._config.server_port)) + print("Enter credentials for '%s' to read task data:" % \ + (self._config.server_url)) username = input('User: ') password = getpass.getpass() session = requests.Session() session.auth = (username, password) - api = CVAT_API_V1(self._config.server_host, - self._config.server_port) + api = CVAT_API_V1(self._config.server_url) cli = CVAT_CLI(session, api) self._session = session @@ -92,8 +85,7 @@ def __init__(self, url): with open(osp.join(url, 'config.json'), 'r') as config_file: config = json.load(config_file) - config = Config(config, - fallback=DEFAULT_CONFIG, schema=CONFIG_SCHEMA) + config = Config(config, schema=CONFIG_SCHEMA) self._config = config with open(osp.join(url, 'images_meta.json'), 'r') as images_file: @@ -109,7 +101,7 @@ def __init__(self, url): size = (entry['height'], entry['width']) image = Image(data=self._make_image_loader(item_id), path=item_filename, size=size) - item = datumaro.DatasetItem(id=item_id, image=image) + item = DatasetItem(id=item_id, image=image) items.append((item.id, item)) items = sorted(items, key=lambda e: int(e[0])) @@ -125,12 +117,3 @@ def __iter__(self): def __len__(self): return len(self._items) - - # pylint: disable=no-self-use - def subsets(self): - return None - - def get(self, item_id, subset=None, path=None): - if path or subset: - raise KeyError() - return self._items[item_id] diff --git a/cvat/apps/dataset_manager/formats/labelme.py b/cvat/apps/dataset_manager/formats/labelme.py index 8cc0d880cc1..9ea1a76b800 100644 --- a/cvat/apps/dataset_manager/formats/labelme.py +++ b/cvat/apps/dataset_manager/formats/labelme.py @@ -2,67 +2,36 @@ # # SPDX-License-Identifier: MIT -format_spec = { - "name": "LabelMe", - "dumpers": [ - { - "display_name": "{name} {format} {version}", - "format": "ZIP", - "version": "3.0", - "handler": "dump" - } - ], - "loaders": [ - { - "display_name": "{name} {format} {version}", - "format": "ZIP", - "version": "3.0", - "handler": "load", - } - ], -} +from tempfile import TemporaryDirectory +from pyunpack import Archive -from datumaro.components.converter import Converter -class CvatLabelMeConverter(Converter): - def __init__(self, save_images=False): - self._save_images = save_images +from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, + import_dm_annotations) +from cvat.apps.dataset_manager.util import make_zip_archive +from datumaro.components.project import Dataset - def __call__(self, extractor, save_dir): - from datumaro.components.project import Environment, Dataset +from .registry import dm_env, exporter, importer - env = Environment() - id_from_image = env.transforms.get('id_from_image_name') - extractor = extractor.transform(id_from_image) - extractor = Dataset.from_extractors(extractor) # apply lazy transforms - - converter = env.make_converter('label_me', save_images=self._save_images) - converter(extractor, save_dir=save_dir) - -def dump(file_object, annotations): - from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor - from cvat.apps.dataset_manager.util import make_zip_archive - from tempfile import TemporaryDirectory - - extractor = CvatAnnotationsExtractor('', annotations) - converter = CvatLabelMeConverter() +@exporter(name='LabelMe', ext='ZIP', version='3.0') +def _export(dst_file, task_data, save_images=False): + extractor = CvatTaskDataExtractor(task_data, include_images=save_images) + envt = dm_env.transforms + extractor = extractor.transform(envt.get('id_from_image_name')) + extractor = Dataset.from_extractors(extractor) # apply lazy transforms with TemporaryDirectory() as temp_dir: + converter = dm_env.make_converter('label_me', save_images=save_images) converter(extractor, save_dir=temp_dir) - make_zip_archive(temp_dir, file_object) -def load(file_object, annotations): - from pyunpack import Archive - from tempfile import TemporaryDirectory - from datumaro.plugins.labelme_format import LabelMeImporter - from datumaro.components.project import Environment - from cvat.apps.dataset_manager.bindings import import_dm_annotations + make_zip_archive(temp_dir, dst_file) - archive_file = file_object if isinstance(file_object, str) else getattr(file_object, "name") +@importer(name='LabelMe', ext='ZIP', version='3.0') +def _import(src_file, task_data): with TemporaryDirectory() as tmp_dir: - Archive(archive_file).extractall(tmp_dir) + Archive(src_file.name).extractall(tmp_dir) - dm_dataset = LabelMeImporter()(tmp_dir).make_dataset() - masks_to_polygons = Environment().transforms.get('masks_to_polygons') - dm_dataset = dm_dataset.transform(masks_to_polygons) - import_dm_annotations(dm_dataset, annotations) + dataset = dm_env.make_importer('label_me')(tmp_dir).make_dataset() + masks_to_polygons = dm_env.transforms.get('masks_to_polygons') + dataset = dataset.transform(masks_to_polygons) + import_dm_annotations(dataset, task_data) diff --git a/cvat/apps/dataset_manager/formats/mask.py b/cvat/apps/dataset_manager/formats/mask.py index c18553b3265..492fed38aae 100644 --- a/cvat/apps/dataset_manager/formats/mask.py +++ b/cvat/apps/dataset_manager/formats/mask.py @@ -2,75 +2,40 @@ # # SPDX-License-Identifier: MIT -format_spec = { - "name": "MASK", - "dumpers": [ - { - "display_name": "{name} {format} {version}", - "format": "ZIP", - "version": "1.1", - "handler": "dump", - }, - ], - "loaders": [ - { - "display_name": "{name} {format} {version}", - "format": "ZIP", - "version": "1.1", - "handler": "load", - }, - ], -} +from tempfile import TemporaryDirectory -from datumaro.components.converter import Converter -class CvatMaskConverter(Converter): - def __init__(self, save_images=False): - self._save_images = save_images +from pyunpack import Archive - def __call__(self, extractor, save_dir): - from datumaro.components.project import Environment, Dataset +from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, + import_dm_annotations) +from cvat.apps.dataset_manager.util import make_zip_archive +from datumaro.components.project import Dataset - env = Environment() - polygons_to_masks = env.transforms.get('polygons_to_masks') - boxes_to_masks = env.transforms.get('boxes_to_masks') - merge_instance_segments = env.transforms.get('merge_instance_segments') - id_from_image = env.transforms.get('id_from_image_name') +from .registry import dm_env, exporter, importer - extractor = extractor.transform(polygons_to_masks) - extractor = extractor.transform(boxes_to_masks) - extractor = extractor.transform(merge_instance_segments) - extractor = extractor.transform(id_from_image) - extractor = Dataset.from_extractors(extractor) # apply lazy transforms - converter = env.make_converter('voc_segmentation', - apply_colormap=True, label_map='source', - save_images=self._save_images) - converter(extractor, save_dir=save_dir) - -def dump(file_object, annotations): - from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor - from cvat.apps.dataset_manager.util import make_zip_archive - from tempfile import TemporaryDirectory - - extractor = CvatAnnotationsExtractor('', annotations) - converter = CvatMaskConverter() +@exporter(name='Segmentation mask', ext='ZIP', version='1.1') +def _export(dst_file, task_data, save_images=False): + extractor = CvatTaskDataExtractor(task_data, include_images=save_images) + envt = dm_env.transforms + extractor = extractor.transform(envt.get('polygons_to_masks')) + extractor = extractor.transform(envt.get('boxes_to_masks')) + extractor = extractor.transform(envt.get('merge_instance_segments')) + extractor = extractor.transform(envt.get('id_from_image_name')) + extractor = Dataset.from_extractors(extractor) # apply lazy transforms with TemporaryDirectory() as temp_dir: + converter = dm_env.make_converter('voc_segmentation', + apply_colormap=True, label_map='source', save_images=save_images) converter(extractor, save_dir=temp_dir) - make_zip_archive(temp_dir, file_object) -def load(file_object, annotations): - from pyunpack import Archive - from tempfile import TemporaryDirectory - from datumaro.plugins.voc_format.importer import VocImporter - from datumaro.components.project import Environment - from cvat.apps.dataset_manager.bindings import import_dm_annotations + make_zip_archive(temp_dir, dst_file) - archive_file = file_object if isinstance(file_object, str) else getattr(file_object, "name") +@importer(name='Segmentation mask', ext='ZIP', version='1.1') +def _import(src_file, task_data): with TemporaryDirectory() as tmp_dir: - Archive(archive_file).extractall(tmp_dir) + Archive(src_file.name).extractall(tmp_dir) - dm_project = VocImporter()(tmp_dir) - dm_dataset = dm_project.make_dataset() - masks_to_polygons = Environment().transforms.get('masks_to_polygons') - dm_dataset = dm_dataset.transform(masks_to_polygons) - import_dm_annotations(dm_dataset, annotations) + dataset = dm_env.make_importer('voc')(tmp_dir).make_dataset() + masks_to_polygons = dm_env.transforms.get('masks_to_polygons') + dataset = dataset.transform(masks_to_polygons) + import_dm_annotations(dataset, task_data) diff --git a/cvat/apps/dataset_manager/formats/mot.py b/cvat/apps/dataset_manager/formats/mot.py index ced2fccabfe..e8b2ea82a13 100644 --- a/cvat/apps/dataset_manager/formats/mot.py +++ b/cvat/apps/dataset_manager/formats/mot.py @@ -1,60 +1,45 @@ +# Copyright (C) 2019 Intel Corporation +# # SPDX-License-Identifier: MIT -format_spec = { - "name": "MOT", - "dumpers": [ - { - "display_name": "{name} {format} {version}", - "format": "ZIP", - "version": "1.1", - "handler": "dump" - }, - ], - "loaders": [ - { - "display_name": "{name} {format} {version}", - "format": "ZIP", - "version": "1.1", - "handler": "load", - } - ], -} - - -from datumaro.plugins.mot_format import \ - MotSeqGtConverter as _MotConverter -class CvatMotConverter(_MotConverter): - NAME = 'cvat_mot' - -def dump(file_object, annotations): - from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor - from cvat.apps.dataset_manager.util import make_zip_archive - from tempfile import TemporaryDirectory - - extractor = CvatAnnotationsExtractor('', annotations) - converter = CvatMotConverter() + +from tempfile import TemporaryDirectory + +from pyunpack import Archive + +import datumaro.components.extractor as datumaro +from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, + match_frame) +from cvat.apps.dataset_manager.util import make_zip_archive +from datumaro.components.project import Dataset + +from .registry import dm_env, exporter, importer + + +@exporter(name='MOT', ext='ZIP', version='1.1') +def _export(dst_file, task_data, save_images=False): + extractor = CvatTaskDataExtractor(task_data, include_images=save_images) + envt = dm_env.transforms + extractor = extractor.transform(envt.get('id_from_image_name')) + extractor = Dataset.from_extractors(extractor) # apply lazy transforms with TemporaryDirectory() as temp_dir: + converter = dm_env.make_converter('mot_seq_gt', + save_images=save_images) converter(extractor, save_dir=temp_dir) - make_zip_archive(temp_dir, file_object) - -def load(file_object, annotations): - from pyunpack import Archive - from tempfile import TemporaryDirectory - from datumaro.plugins.mot_format import MotSeqImporter - import datumaro.components.extractor as datumaro - from cvat.apps.dataset_manager.bindings import match_frame + make_zip_archive(temp_dir, dst_file) - archive_file = file_object if isinstance(file_object, str) else getattr(file_object, "name") +@importer(name='MOT', ext='ZIP', version='1.1') +def _import(src_file, task_data): with TemporaryDirectory() as tmp_dir: - Archive(archive_file).extractall(tmp_dir) + Archive(src_file.name).extractall(tmp_dir) - tracks = {} + dataset = dm_env.make_importer('mot_seq')(tmp_dir).make_dataset() - dm_dataset = MotSeqImporter()(tmp_dir).make_dataset() - label_cat = dm_dataset.categories()[datumaro.AnnotationType.label] + tracks = {} + label_cat = dataset.categories()[datumaro.AnnotationType.label] - for item in dm_dataset: - frame_id = match_frame(item, annotations) + for item in dataset: + frame_id = match_frame(item, task_data) for ann in item.annotations: if ann.type != datumaro.AnnotationType.bbox: @@ -64,7 +49,7 @@ def load(file_object, annotations): if track_id is None: continue - shape = annotations.TrackedShape( + shape = task_data.TrackedShape( type='rectangle', points=ann.points, occluded=ann.attributes.get('occluded') == True, @@ -77,7 +62,7 @@ def load(file_object, annotations): # build trajectories as lists of shapes in track dict if track_id not in tracks: - tracks[track_id] = annotations.Track( + tracks[track_id] = task_data.Track( label_cat.items[ann.label].name, 0, []) tracks[track_id].shapes.append(shape) @@ -86,4 +71,4 @@ def load(file_object, annotations): track.shapes.sort(key=lambda t: t.frame) # Set outside=True for the last shape in a track to finish the track track.shapes[-1] = track.shapes[-1]._replace(outside=True) - annotations.add_track(track) + task_data.add_track(track) diff --git a/cvat/apps/dataset_manager/formats/pascal_voc.py b/cvat/apps/dataset_manager/formats/pascal_voc.py index a74d14ba3f2..429696baad5 100644 --- a/cvat/apps/dataset_manager/formats/pascal_voc.py +++ b/cvat/apps/dataset_manager/formats/pascal_voc.py @@ -1,46 +1,46 @@ -# Copyright (C) 2018 Intel Corporation +# Copyright (C) 2020 Intel Corporation # # SPDX-License-Identifier: MIT -format_spec = { - "name": "PASCAL VOC", - "dumpers": [ - { - "display_name": "{name} {format} {version}", - "format": "ZIP", - "version": "1.1", - "handler": "dump" - }, - ], - "loaders": [ - { - "display_name": "{name} {format} {version}", - "format": "ZIP", - "version": "1.1", - "handler": "load" - }, - ], -} +import os.path as osp +import shutil +from glob import glob -def load(file_object, annotations): - from glob import glob - import os - import os.path as osp - import shutil - from pyunpack import Archive - from tempfile import TemporaryDirectory - from datumaro.plugins.voc_format.importer import VocImporter - from cvat.apps.dataset_manager.bindings import import_dm_annotations +from tempfile import TemporaryDirectory - archive_file = file_object if isinstance(file_object, str) else getattr(file_object, "name") +from pyunpack import Archive + +from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, + import_dm_annotations) +from cvat.apps.dataset_manager.util import make_zip_archive +from datumaro.components.project import Dataset + +from .registry import dm_env, exporter, importer + + +@exporter(name='PASCAL VOC', ext='ZIP', version='1.1') +def _export(dst_file, task_data, save_images=False): + extractor = CvatTaskDataExtractor(task_data, include_images=save_images) + envt = dm_env.transforms + extractor = extractor.transform(envt.get('id_from_image_name')) + extractor = Dataset.from_extractors(extractor) # apply lazy transforms + with TemporaryDirectory() as temp_dir: + converter = dm_env.make_converter('voc', label_map='source', + save_images=save_images) + converter(extractor, save_dir=temp_dir) + + make_zip_archive(temp_dir, dst_file) + +@importer(name='PASCAL VOC', ext='ZIP', version='1.1') +def _import(src_file, task_data): with TemporaryDirectory() as tmp_dir: - Archive(archive_file).extractall(tmp_dir) + Archive(src_file.name).extractall(tmp_dir) # put label map from the task if not present labelmap_file = osp.join(tmp_dir, 'labelmap.txt') if not osp.isfile(labelmap_file): labels = (label['name'] + ':::' - for _, label in annotations.meta['task']['labels']) + for _, label in task_data.meta['task']['labels']) with open(labelmap_file, 'w') as f: f.write('\n'.join(labels)) @@ -58,34 +58,7 @@ def load(file_object, annotations): for f in anno_files: shutil.move(f, anno_dir) - dm_project = VocImporter()(tmp_dir) - dm_dataset = dm_project.make_dataset() - import_dm_annotations(dm_dataset, annotations) - -from datumaro.components.converter import Converter -class CvatVocConverter(Converter): - def __init__(self, save_images=False): - self._save_images = save_images - - def __call__(self, extractor, save_dir): - from datumaro.components.project import Environment, Dataset - env = Environment() - id_from_image = env.transforms.get('id_from_image_name') - - extractor = extractor.transform(id_from_image) - extractor = Dataset.from_extractors(extractor) # apply lazy transforms - - converter = env.make_converter('voc', label_map='source', - save_images=self._save_images) - converter(extractor, save_dir=save_dir) - -def dump(file_object, annotations): - from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor - from cvat.apps.dataset_manager.util import make_zip_archive - from tempfile import TemporaryDirectory - - extractor = CvatAnnotationsExtractor('', annotations) - converter = CvatVocConverter() - with TemporaryDirectory() as temp_dir: - converter(extractor, save_dir=temp_dir) - make_zip_archive(temp_dir, file_object) \ No newline at end of file + dataset = dm_env.make_importer('voc')(tmp_dir).make_dataset() + masks_to_polygons = dm_env.transforms.get('masks_to_polygons') + dataset = dataset.transform(masks_to_polygons) + import_dm_annotations(dataset, task_data) diff --git a/cvat/apps/dataset_manager/formats/registry.py b/cvat/apps/dataset_manager/formats/registry.py new file mode 100644 index 00000000000..20377dd6703 --- /dev/null +++ b/cvat/apps/dataset_manager/formats/registry.py @@ -0,0 +1,87 @@ + +# Copyright (C) 2020 Intel Corporation +# +# SPDX-License-Identifier: MIT + +from datumaro.components.project import Environment + + +dm_env = Environment() + +class _Format: + NAME = '' + EXT = '' + VERSION = '' + DISPLAY_NAME = '{NAME} {VERSION}' + +class Exporter(_Format): + def __call__(self, dst_file, task_data, **options): + raise NotImplementedError() + +class Importer(_Format): + def __call__(self, src_file, task_data, **options): + raise NotImplementedError() + +def _wrap_format(f_or_cls, klass, name, version, ext, display_name): + import inspect + assert inspect.isclass(f_or_cls) or inspect.isfunction(f_or_cls) + if inspect.isclass(f_or_cls): + assert hasattr(f_or_cls, '__call__') + target = f_or_cls + elif inspect.isfunction(f_or_cls): + class wrapper(klass): + # pylint: disable=arguments-differ + def __call__(self, *args, **kwargs): + f_or_cls(*args, **kwargs) + + wrapper.__name__ = f_or_cls.__name__ + wrapper.__module__ = f_or_cls.__module__ + target = wrapper + + target.NAME = name or klass.NAME or f_or_cls.__name__ + target.VERSION = version or klass.VERSION + target.EXT = ext or klass.EXT + target.DISPLAY_NAME = (display_name or klass.DISPLAY_NAME).format( + NAME=name, VERSION=version, EXT=ext) + assert all([target.NAME, target.VERSION, target.EXT, target.DISPLAY_NAME]) + return target + +EXPORT_FORMATS = {} +def exporter(name, version, ext, display_name=None): + assert name not in EXPORT_FORMATS, "Export format '%s' already registered" % name + def wrap_with_params(f_or_cls): + t = _wrap_format(f_or_cls, Exporter, + name=name, ext=ext, version=version, display_name=display_name) + key = t.DISPLAY_NAME + assert key not in EXPORT_FORMATS, "Export format '%s' already registered" % name + EXPORT_FORMATS[key] = t + return t + return wrap_with_params + +IMPORT_FORMATS = {} +def importer(name, version, ext, display_name=None): + def wrap_with_params(f_or_cls): + t = _wrap_format(f_or_cls, Importer, + name=name, ext=ext, version=version, display_name=display_name) + key = t.DISPLAY_NAME + assert key not in IMPORT_FORMATS, "Import format '%s' already registered" % name + IMPORT_FORMATS[key] = t + return t + return wrap_with_params + +def make_importer(name): + return IMPORT_FORMATS[name]() + +def make_exporter(name): + return EXPORT_FORMATS[name]() + +# pylint: disable=unused-import +import cvat.apps.dataset_manager.formats.coco +import cvat.apps.dataset_manager.formats.cvat +import cvat.apps.dataset_manager.formats.datumaro +import cvat.apps.dataset_manager.formats.labelme +import cvat.apps.dataset_manager.formats.mask +import cvat.apps.dataset_manager.formats.mot +import cvat.apps.dataset_manager.formats.pascal_voc +import cvat.apps.dataset_manager.formats.tfrecord +import cvat.apps.dataset_manager.formats.yolo \ No newline at end of file diff --git a/cvat/apps/dataset_manager/formats/tfrecord.py b/cvat/apps/dataset_manager/formats/tfrecord.py index db6dee6944f..0e4962fa6c4 100644 --- a/cvat/apps/dataset_manager/formats/tfrecord.py +++ b/cvat/apps/dataset_manager/formats/tfrecord.py @@ -2,52 +2,33 @@ # # SPDX-License-Identifier: MIT -format_spec = { - "name": "TFRecord", - "dumpers": [ - { - "display_name": "{name} {format} {version}", - "format": "ZIP", - "version": "1.0", - "handler": "dump" - }, - ], - "loaders": [ - { - "display_name": "{name} {format} {version}", - "format": "ZIP", - "version": "1.0", - "handler": "load" - }, - ], -} - -from datumaro.plugins.tf_detection_api_format.converter import \ - TfDetectionApiConverter as _TfDetectionApiConverter -class CvatTfrecordConverter(_TfDetectionApiConverter): - NAME = 'cvat_tfrecord' - -def dump(file_object, annotations): - from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor - from cvat.apps.dataset_manager.util import make_zip_archive - from tempfile import TemporaryDirectory - - extractor = CvatAnnotationsExtractor('', annotations) - converter = CvatTfrecordConverter() +from tempfile import TemporaryDirectory + +from pyunpack import Archive + +from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, + import_dm_annotations) +from cvat.apps.dataset_manager.util import make_zip_archive +from datumaro.components.project import Dataset + +from .registry import dm_env, exporter, importer + + +@exporter(name='TFRecord', ext='ZIP', version='1.0') +def _export(dst_file, task_data, save_images=False): + extractor = CvatTaskDataExtractor(task_data, include_images=save_images) + extractor = Dataset.from_extractors(extractor) # apply lazy transforms with TemporaryDirectory() as temp_dir: + converter = dm_env.make_converter('tf_detection_api', + save_images=save_images) converter(extractor, save_dir=temp_dir) - make_zip_archive(temp_dir, file_object) -def load(file_object, annotations): - from pyunpack import Archive - from tempfile import TemporaryDirectory - from datumaro.plugins.tf_detection_api_format.importer import TfDetectionApiImporter - from cvat.apps.dataset_manager.bindings import import_dm_annotations + make_zip_archive(temp_dir, dst_file) - archive_file = file_object if isinstance(file_object, str) else getattr(file_object, "name") +@importer(name='TFRecord', ext='ZIP', version='1.0') +def _import(src_file, task_data): with TemporaryDirectory() as tmp_dir: - Archive(archive_file).extractall(tmp_dir) + Archive(src_file.name).extractall(tmp_dir) - dm_project = TfDetectionApiImporter()(tmp_dir) - dm_dataset = dm_project.make_dataset() - import_dm_annotations(dm_dataset, annotations) + dataset = dm_env.make_importer('tf_detection_api')(tmp_dir).make_dataset() + import_dm_annotations(dataset, task_data) diff --git a/cvat/apps/dataset_manager/formats/yolo.py b/cvat/apps/dataset_manager/formats/yolo.py index f21ebe43634..688ff903482 100644 --- a/cvat/apps/dataset_manager/formats/yolo.py +++ b/cvat/apps/dataset_manager/formats/yolo.py @@ -1,39 +1,36 @@ -# Copyright (C) 2018 Intel Corporation +# Copyright (C) 2019 Intel Corporation # # SPDX-License-Identifier: MIT -format_spec = { - "name": "YOLO", - "dumpers": [ - { - "display_name": "{name} {format} {version}", - "format": "ZIP", - "version": "1.1", - "handler": "dump" - }, - ], - "loaders": [ - { - "display_name": "{name} {format} {version}", - "format": "ZIP", - "version": "1.1", - "handler": "load" - }, - ], -} +import os.path as osp +from glob import glob +from tempfile import TemporaryDirectory -def load(file_object, annotations): - from pyunpack import Archive - import os.path as osp - from tempfile import TemporaryDirectory - from glob import glob - from datumaro.components.extractor import DatasetItem - from datumaro.plugins.yolo_format.importer import YoloImporter - from cvat.apps.dataset_manager.bindings import import_dm_annotations, match_frame +from pyunpack import Archive - archive_file = file_object if isinstance(file_object, str) else getattr(file_object, "name") +from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, + import_dm_annotations, match_frame) +from cvat.apps.dataset_manager.util import make_zip_archive +from datumaro.components.extractor import DatasetItem +from datumaro.components.project import Dataset + +from .registry import dm_env, exporter, importer + + +@exporter(name='YOLO', ext='ZIP', version='1.1') +def _export(dst_file, task_data, save_images=False): + extractor = CvatTaskDataExtractor(task_data, include_images=save_images) + extractor = Dataset.from_extractors(extractor) # apply lazy transforms + with TemporaryDirectory() as temp_dir: + converter = dm_env.make_converter('yolo', save_images=save_images) + converter(extractor, save_dir=temp_dir) + + make_zip_archive(temp_dir, dst_file) + +@importer(name='YOLO', ext='ZIP', version='1.1') +def _import(src_file, task_data): with TemporaryDirectory() as tmp_dir: - Archive(archive_file).extractall(tmp_dir) + Archive(src_file.name).extractall(tmp_dir) image_info = {} anno_files = glob(osp.join(tmp_dir, '**', '*.txt'), recursive=True) @@ -41,28 +38,13 @@ def load(file_object, annotations): filename = osp.splitext(osp.basename(filename))[0] frame_info = None try: - frame_id = match_frame(DatasetItem(id=filename), annotations) - frame_info = annotations.frame_info[frame_id] + frame_id = match_frame(DatasetItem(id=filename), task_data) + frame_info = task_data.frame_info[frame_id] except Exception: pass if frame_info is not None: image_info[filename] = (frame_info['height'], frame_info['width']) - dm_project = YoloImporter()(tmp_dir, image_info=image_info) - dm_dataset = dm_project.make_dataset() - import_dm_annotations(dm_dataset, annotations) - -from datumaro.plugins.yolo_format.converter import \ - YoloConverter as _YoloConverter -class CvatYoloConverter(_YoloConverter): - NAME = 'cvat_yolo' - -def dump(file_object, annotations): - from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor - from cvat.apps.dataset_manager.util import make_zip_archive - from tempfile import TemporaryDirectory - extractor = CvatAnnotationsExtractor('', annotations) - converter = CvatYoloConverter() - with TemporaryDirectory() as temp_dir: - converter(extractor, save_dir=temp_dir) - make_zip_archive(temp_dir, file_object) + dataset = dm_env.make_importer('yolo')(tmp_dir, image_info=image_info) \ + .make_dataset() + import_dm_annotations(dataset, task_data) diff --git a/cvat/apps/dataset_manager/serializers.py b/cvat/apps/dataset_manager/serializers.py new file mode 100644 index 00000000000..51cf71ca8da --- /dev/null +++ b/cvat/apps/dataset_manager/serializers.py @@ -0,0 +1,15 @@ +# Copyright (C) 2020 Intel Corporation +# +# SPDX-License-Identifier: MIT + +from rest_framework import serializers + + +class DatasetFormatSerializer(serializers.Serializer): + name = serializers.CharField(max_length=64, source='DISPLAY_NAME') + ext = serializers.CharField(max_length=64, source='EXT') + version = serializers.CharField(max_length=64, source='VERSION') + +class DatasetFormatsSerializer(serializers.Serializer): + importers = DatasetFormatSerializer(many=True) + exporters = DatasetFormatSerializer(many=True) \ No newline at end of file diff --git a/cvat/apps/dataset_manager/task.py b/cvat/apps/dataset_manager/task.py index 688f7792c10..0d9985b724d 100644 --- a/cvat/apps/dataset_manager/task.py +++ b/cvat/apps/dataset_manager/task.py @@ -3,361 +3,739 @@ # # SPDX-License-Identifier: MIT -from datetime import timedelta -import json -import os -import os.path as osp -import shutil -import tempfile +from collections import OrderedDict +from enum import Enum +from django.conf import settings +from django.db import transaction from django.utils import timezone -import django_rq -from cvat.settings.base import DATUMARO_PATH as _DATUMARO_REPO_PATH, \ - BASE_DIR as _CVAT_ROOT_DIR -from cvat.apps.engine.log import slogger -from cvat.apps.engine.models import Task -from cvat.apps.engine.frame_provider import FrameProvider -from .util import current_function_name, make_zip_archive +from cvat.apps.engine import models, serializers +from cvat.apps.engine.plugins import plugin_decorator +from cvat.apps.profiler import silk_profile -from datumaro.components.project import Project, Environment -import datumaro.components.extractor as datumaro -from .bindings import CvatImagesExtractor, CvatTaskExtractor +from .annotation import AnnotationIR, AnnotationManager +from .bindings import TaskData +from .formats.registry import make_exporter, make_importer -_FORMATS_DIR = osp.join(osp.dirname(__file__), 'formats') -_MODULE_NAME = __package__ + '.' + osp.splitext(osp.basename(__file__))[0] -def log_exception(logger=None, exc_info=True): - if logger is None: - logger = slogger - logger.exception("[%s @ %s]: exception occurred" % \ - (_MODULE_NAME, current_function_name(2)), - exc_info=exc_info) +class dotdict(OrderedDict): + """dot.notation access to dictionary attributes""" + __getattr__ = OrderedDict.get + __setattr__ = OrderedDict.__setitem__ + __delattr__ = OrderedDict.__delitem__ + __eq__ = lambda self, other: self.id == other.id + __hash__ = lambda self: self.id -_TASK_IMAGES_EXTRACTOR = '_cvat_task_images' -_TASK_ANNO_EXTRACTOR = '_cvat_task_anno' -_TASK_IMAGES_REMOTE_EXTRACTOR = 'cvat_rest_api_task_images' +class PatchAction(str, Enum): + CREATE = "create" + UPDATE = "update" + DELETE = "delete" -def get_export_cache_dir(db_task): - return osp.join(db_task.get_task_dirname(), 'export_cache') + @classmethod + def values(cls): + return [item.value for item in cls] -EXPORT_FORMAT_DATUMARO_PROJECT = "datumaro_project" + def __str__(self): + return self.value +def bulk_create(db_model, objects, flt_param): + if objects: + if flt_param: + if 'postgresql' in settings.DATABASES["default"]["ENGINE"]: + return db_model.objects.bulk_create(objects) + else: + ids = list(db_model.objects.filter(**flt_param).values_list('id', flat=True)) + db_model.objects.bulk_create(objects) -class TaskProject: - @staticmethod - def _get_datumaro_project_dir(db_task): - return osp.join(db_task.get_task_dirname(), 'datumaro') + return list(db_model.objects.exclude(id__in=ids).filter(**flt_param)) + else: + return db_model.objects.bulk_create(objects) + + return [] + +def _merge_table_rows(rows, keys_for_merge, field_id): + # It is necessary to keep a stable order of original rows + # (e.g. for tracked boxes). Otherwise prev_box.frame can be bigger + # than next_box.frame. + merged_rows = OrderedDict() + + # Group all rows by field_id. In grouped rows replace fields in + # accordance with keys_for_merge structure. + for row in rows: + row_id = row[field_id] + if not row_id in merged_rows: + merged_rows[row_id] = dotdict(row) + for key in keys_for_merge: + merged_rows[row_id][key] = [] + + for key in keys_for_merge: + item = dotdict({v.split('__', 1)[-1]:row[v] for v in keys_for_merge[key]}) + if item.id is not None: + merged_rows[row_id][key].append(item) + + # Remove redundant keys from final objects + redundant_keys = [item for values in keys_for_merge.values() for item in values] + for i in merged_rows: + for j in redundant_keys: + del merged_rows[i][j] + + return list(merged_rows.values()) + +class JobAnnotation: + def __init__(self, pk): + self.db_job = models.Job.objects.select_related('segment__task') \ + .select_for_update().get(id=pk) + + db_segment = self.db_job.segment + self.start_frame = db_segment.start_frame + self.stop_frame = db_segment.stop_frame + self.ir_data = AnnotationIR() + + self.db_labels = {db_label.id:db_label + for db_label in db_segment.task.label_set.all()} + + self.db_attributes = {} + for db_label in self.db_labels.values(): + self.db_attributes[db_label.id] = { + "mutable": OrderedDict(), + "immutable": OrderedDict(), + "all": OrderedDict(), + } + for db_attr in db_label.attributespec_set.all(): + default_value = dotdict([ + ('spec_id', db_attr.id), + ('value', db_attr.default_value), + ]) + if db_attr.mutable: + self.db_attributes[db_label.id]["mutable"][db_attr.id] = default_value + else: + self.db_attributes[db_label.id]["immutable"][db_attr.id] = default_value + + self.db_attributes[db_label.id]["all"][db_attr.id] = default_value + + def reset(self): + self.ir_data.reset() + + def _save_tracks_to_db(self, tracks): + db_tracks = [] + db_track_attrvals = [] + db_shapes = [] + db_shape_attrvals = [] + + for track in tracks: + track_attributes = track.pop("attributes", []) + shapes = track.pop("shapes") + db_track = models.LabeledTrack(job=self.db_job, **track) + if db_track.label_id not in self.db_labels: + raise AttributeError("label_id `{}` is invalid".format(db_track.label_id)) + + for attr in track_attributes: + db_attrval = models.LabeledTrackAttributeVal(**attr) + if db_attrval.spec_id not in self.db_attributes[db_track.label_id]["immutable"]: + raise AttributeError("spec_id `{}` is invalid".format(db_attrval.spec_id)) + db_attrval.track_id = len(db_tracks) + db_track_attrvals.append(db_attrval) + + for shape in shapes: + shape_attributes = shape.pop("attributes", []) + # FIXME: need to clamp points (be sure that all of them inside the image) + # Should we check here or implement a validator? + db_shape = models.TrackedShape(**shape) + db_shape.track_id = len(db_tracks) + + for attr in shape_attributes: + db_attrval = models.TrackedShapeAttributeVal(**attr) + if db_attrval.spec_id not in self.db_attributes[db_track.label_id]["mutable"]: + raise AttributeError("spec_id `{}` is invalid".format(db_attrval.spec_id)) + db_attrval.shape_id = len(db_shapes) + db_shape_attrvals.append(db_attrval) + + db_shapes.append(db_shape) + shape["attributes"] = shape_attributes + + db_tracks.append(db_track) + track["attributes"] = track_attributes + track["shapes"] = shapes + + db_tracks = bulk_create( + db_model=models.LabeledTrack, + objects=db_tracks, + flt_param={"job_id": self.db_job.id} + ) + + for db_attrval in db_track_attrvals: + db_attrval.track_id = db_tracks[db_attrval.track_id].id + bulk_create( + db_model=models.LabeledTrackAttributeVal, + objects=db_track_attrvals, + flt_param={} + ) + + for db_shape in db_shapes: + db_shape.track_id = db_tracks[db_shape.track_id].id + + db_shapes = bulk_create( + db_model=models.TrackedShape, + objects=db_shapes, + flt_param={"track__job_id": self.db_job.id} + ) + + for db_attrval in db_shape_attrvals: + db_attrval.shape_id = db_shapes[db_attrval.shape_id].id + + bulk_create( + db_model=models.TrackedShapeAttributeVal, + objects=db_shape_attrvals, + flt_param={} + ) + + shape_idx = 0 + for track, db_track in zip(tracks, db_tracks): + track["id"] = db_track.id + for shape in track["shapes"]: + shape["id"] = db_shapes[shape_idx].id + shape_idx += 1 + + self.ir_data.tracks = tracks + + def _save_shapes_to_db(self, shapes): + db_shapes = [] + db_attrvals = [] + + for shape in shapes: + attributes = shape.pop("attributes", []) + # FIXME: need to clamp points (be sure that all of them inside the image) + # Should we check here or implement a validator? + db_shape = models.LabeledShape(job=self.db_job, **shape) + if db_shape.label_id not in self.db_labels: + raise AttributeError("label_id `{}` is invalid".format(db_shape.label_id)) + + for attr in attributes: + db_attrval = models.LabeledShapeAttributeVal(**attr) + if db_attrval.spec_id not in self.db_attributes[db_shape.label_id]["all"]: + raise AttributeError("spec_id `{}` is invalid".format(db_attrval.spec_id)) + + db_attrval.shape_id = len(db_shapes) + db_attrvals.append(db_attrval) + + db_shapes.append(db_shape) + shape["attributes"] = attributes + + db_shapes = bulk_create( + db_model=models.LabeledShape, + objects=db_shapes, + flt_param={"job_id": self.db_job.id} + ) + + for db_attrval in db_attrvals: + db_attrval.shape_id = db_shapes[db_attrval.shape_id].id + + bulk_create( + db_model=models.LabeledShapeAttributeVal, + objects=db_attrvals, + flt_param={} + ) + + for shape, db_shape in zip(shapes, db_shapes): + shape["id"] = db_shape.id + + self.ir_data.shapes = shapes + + def _save_tags_to_db(self, tags): + db_tags = [] + db_attrvals = [] + + for tag in tags: + attributes = tag.pop("attributes", []) + db_tag = models.LabeledImage(job=self.db_job, **tag) + if db_tag.label_id not in self.db_labels: + raise AttributeError("label_id `{}` is invalid".format(db_tag.label_id)) + + for attr in attributes: + db_attrval = models.LabeledImageAttributeVal(**attr) + if db_attrval.spec_id not in self.db_attributes[db_tag.label_id]["all"]: + raise AttributeError("spec_id `{}` is invalid".format(db_attrval.spec_id)) + db_attrval.tag_id = len(db_tags) + db_attrvals.append(db_attrval) + + db_tags.append(db_tag) + tag["attributes"] = attributes + + db_tags = bulk_create( + db_model=models.LabeledImage, + objects=db_tags, + flt_param={"job_id": self.db_job.id} + ) + + for db_attrval in db_attrvals: + db_attrval.image_id = db_tags[db_attrval.tag_id].id + + bulk_create( + db_model=models.LabeledImageAttributeVal, + objects=db_attrvals, + flt_param={} + ) + + for tag, db_tag in zip(tags, db_tags): + tag["id"] = db_tag.id + + self.ir_data.tags = tags + + def _commit(self): + db_prev_commit = self.db_job.commits.last() + db_curr_commit = models.JobCommit() + if db_prev_commit: + db_curr_commit.version = db_prev_commit.version + 1 + else: + db_curr_commit.version = 1 + db_curr_commit.job = self.db_job + db_curr_commit.message = "Changes: tags - {}; shapes - {}; tracks - {}".format( + len(self.ir_data.tags), len(self.ir_data.shapes), len(self.ir_data.tracks)) + db_curr_commit.save() + self.ir_data.version = db_curr_commit.version + + def _set_updated_date(self): + db_task = self.db_job.segment.task + db_task.updated_date = timezone.now() + db_task.save() + + def _save_to_db(self, data): + self.reset() + self._save_tags_to_db(data["tags"]) + self._save_shapes_to_db(data["shapes"]) + self._save_tracks_to_db(data["tracks"]) + + return self.ir_data.tags or self.ir_data.shapes or self.ir_data.tracks + + def _create(self, data): + if self._save_to_db(data): + self._set_updated_date() + self.db_job.save() + + def create(self, data): + self._create(data) + self._commit() + + def put(self, data): + self._delete() + self._create(data) + self._commit() + + def update(self, data): + self._delete(data) + self._create(data) + self._commit() + + def _delete(self, data=None): + deleted_shapes = 0 + if data is None: + deleted_shapes += self.db_job.labeledimage_set.all().delete()[0] + deleted_shapes += self.db_job.labeledshape_set.all().delete()[0] + deleted_shapes += self.db_job.labeledtrack_set.all().delete()[0] + else: + labeledimage_ids = [image["id"] for image in data["tags"]] + labeledshape_ids = [shape["id"] for shape in data["shapes"]] + labeledtrack_ids = [track["id"] for track in data["tracks"]] + labeledimage_set = self.db_job.labeledimage_set + labeledimage_set = labeledimage_set.filter(pk__in=labeledimage_ids) + labeledshape_set = self.db_job.labeledshape_set + labeledshape_set = labeledshape_set.filter(pk__in=labeledshape_ids) + labeledtrack_set = self.db_job.labeledtrack_set + labeledtrack_set = labeledtrack_set.filter(pk__in=labeledtrack_ids) + + # It is not important for us that data had some "invalid" objects + # which were skipped (not acutally deleted). The main idea is to + # say that all requested objects are absent in DB after the method. + self.ir_data.tags = data['tags'] + self.ir_data.shapes = data['shapes'] + self.ir_data.tracks = data['tracks'] + + deleted_shapes += labeledimage_set.delete()[0] + deleted_shapes += labeledshape_set.delete()[0] + deleted_shapes += labeledtrack_set.delete()[0] + + if deleted_shapes: + self._set_updated_date() + + def delete(self, data=None): + self._delete(data) + self._commit() @staticmethod - def create(db_task): - task_project = TaskProject(db_task) - task_project._create() - return task_project + def _extend_attributes(attributeval_set, default_attribute_values): + shape_attribute_specs_set = set(attr.spec_id for attr in attributeval_set) + for db_attr in default_attribute_values: + if db_attr.spec_id not in shape_attribute_specs_set: + attributeval_set.append(dotdict([ + ('spec_id', db_attr.spec_id), + ('value', db_attr.value), + ])) + + def _init_tags_from_db(self): + db_tags = self.db_job.labeledimage_set.prefetch_related( + "label", + "labeledimageattributeval_set" + ).values( + 'id', + 'frame', + 'label_id', + 'group', + 'labeledimageattributeval__spec_id', + 'labeledimageattributeval__value', + 'labeledimageattributeval__id', + ).order_by('frame') + + db_tags = _merge_table_rows( + rows=db_tags, + keys_for_merge={ + "labeledimageattributeval_set": [ + 'labeledimageattributeval__spec_id', + 'labeledimageattributeval__value', + 'labeledimageattributeval__id', + ], + }, + field_id='id', + ) + + for db_tag in db_tags: + self._extend_attributes(db_tag.labeledimageattributeval_set, + self.db_attributes[db_tag.label_id]["all"].values()) + + serializer = serializers.LabeledImageSerializer(db_tags, many=True) + self.ir_data.tags = serializer.data + + def _init_shapes_from_db(self): + db_shapes = self.db_job.labeledshape_set.prefetch_related( + "label", + "labeledshapeattributeval_set" + ).values( + 'id', + 'label_id', + 'type', + 'frame', + 'group', + 'occluded', + 'z_order', + 'points', + 'labeledshapeattributeval__spec_id', + 'labeledshapeattributeval__value', + 'labeledshapeattributeval__id', + ).order_by('frame') + + db_shapes = _merge_table_rows( + rows=db_shapes, + keys_for_merge={ + 'labeledshapeattributeval_set': [ + 'labeledshapeattributeval__spec_id', + 'labeledshapeattributeval__value', + 'labeledshapeattributeval__id', + ], + }, + field_id='id', + ) + for db_shape in db_shapes: + self._extend_attributes(db_shape.labeledshapeattributeval_set, + self.db_attributes[db_shape.label_id]["all"].values()) + + serializer = serializers.LabeledShapeSerializer(db_shapes, many=True) + self.ir_data.shapes = serializer.data + + def _init_tracks_from_db(self): + db_tracks = self.db_job.labeledtrack_set.prefetch_related( + "label", + "labeledtrackattributeval_set", + "trackedshape_set__trackedshapeattributeval_set" + ).values( + "id", + "frame", + "label_id", + "group", + "labeledtrackattributeval__spec_id", + "labeledtrackattributeval__value", + "labeledtrackattributeval__id", + "trackedshape__type", + "trackedshape__occluded", + "trackedshape__z_order", + "trackedshape__points", + "trackedshape__id", + "trackedshape__frame", + "trackedshape__outside", + "trackedshape__trackedshapeattributeval__spec_id", + "trackedshape__trackedshapeattributeval__value", + "trackedshape__trackedshapeattributeval__id", + ).order_by('id', 'trackedshape__frame') + + db_tracks = _merge_table_rows( + rows=db_tracks, + keys_for_merge={ + "labeledtrackattributeval_set": [ + "labeledtrackattributeval__spec_id", + "labeledtrackattributeval__value", + "labeledtrackattributeval__id", + ], + "trackedshape_set":[ + "trackedshape__type", + "trackedshape__occluded", + "trackedshape__z_order", + "trackedshape__points", + "trackedshape__id", + "trackedshape__frame", + "trackedshape__outside", + "trackedshape__trackedshapeattributeval__spec_id", + "trackedshape__trackedshapeattributeval__value", + "trackedshape__trackedshapeattributeval__id", + ], + }, + field_id="id", + ) + + for db_track in db_tracks: + db_track["trackedshape_set"] = _merge_table_rows(db_track["trackedshape_set"], { + 'trackedshapeattributeval_set': [ + 'trackedshapeattributeval__value', + 'trackedshapeattributeval__spec_id', + 'trackedshapeattributeval__id', + ] + }, 'id') + + # A result table can consist many equal rows for track/shape attributes + # We need filter unique attributes manually + db_track["labeledtrackattributeval_set"] = list(set(db_track["labeledtrackattributeval_set"])) + self._extend_attributes(db_track.labeledtrackattributeval_set, + self.db_attributes[db_track.label_id]["immutable"].values()) + + default_attribute_values = self.db_attributes[db_track.label_id]["mutable"].values() + for db_shape in db_track["trackedshape_set"]: + db_shape["trackedshapeattributeval_set"] = list( + set(db_shape["trackedshapeattributeval_set"]) + ) + # in case of trackedshapes need to interpolate attriute values and extend it + # by previous shape attribute values (not default values) + self._extend_attributes(db_shape["trackedshapeattributeval_set"], default_attribute_values) + default_attribute_values = db_shape["trackedshapeattributeval_set"] + + + serializer = serializers.LabeledTrackSerializer(db_tracks, many=True) + self.ir_data.tracks = serializer.data + + def _init_version_from_db(self): + db_commit = self.db_job.commits.last() + self.ir_data.version = db_commit.version if db_commit else 0 + + def init_from_db(self): + self._init_tags_from_db() + self._init_shapes_from_db() + self._init_tracks_from_db() + self._init_version_from_db() + + @property + def data(self): + return self.ir_data.data + + def import_annotations(self, src_file, importer): + task_data = TaskData( + annotation_ir=AnnotationIR(), + db_task=self.db_job.segment.task, + create_callback=self.create, + ) + self.delete() + + importer(src_file, task_data) + + self.create(task_data.data.slice(self.start_frame, self.stop_frame).serialize()) + +class TaskAnnotation: + def __init__(self, pk): + self.db_task = models.Task.objects.prefetch_related("data__images").get(id=pk) + + # Postgres doesn't guarantee an order by default without explicit order_by + self.db_jobs = models.Job.objects.select_related("segment").filter(segment__task_id=pk).order_by('id') + self.ir_data = AnnotationIR() + + def reset(self): + self.ir_data.reset() + + def _patch_data(self, data, action): + _data = data if isinstance(data, AnnotationIR) else AnnotationIR(data) + splitted_data = {} + jobs = {} + for db_job in self.db_jobs: + jid = db_job.id + start = db_job.segment.start_frame + stop = db_job.segment.stop_frame + jobs[jid] = { "start": start, "stop": stop } + splitted_data[jid] = _data.slice(start, stop) + + for jid, job_data in splitted_data.items(): + _data = AnnotationIR() + if action is None: + _data.data = put_job_data(jid, job_data) + else: + _data.data = patch_job_data(jid, job_data, action) + if _data.version > self.ir_data.version: + self.ir_data.version = _data.version + self._merge_data(_data, jobs[jid]["start"], self.db_task.overlap) - @staticmethod - def load(db_task): - task_project = TaskProject(db_task) - task_project._load() - task_project._init_dataset() - return task_project + def _merge_data(self, data, start_frame, overlap): + annotation_manager = AnnotationManager(self.ir_data) + annotation_manager.merge(data, start_frame, overlap) - @staticmethod - def from_task(db_task, user): - task_project = TaskProject(db_task) - task_project._import_from_task(user) - return task_project - - def __init__(self, db_task): - self._db_task = db_task - self._project_dir = self._get_datumaro_project_dir(db_task) - self._project = None - self._dataset = None - - def _create(self): - self._project = Project.generate(self._project_dir) - self._project.add_source('task_%s' % self._db_task.id, { - 'format': _TASK_IMAGES_EXTRACTOR, - }) - self._project.env.extractors.register(_TASK_IMAGES_EXTRACTOR, - lambda url: CvatImagesExtractor(url, - FrameProvider(self._db_task.data))) - - self._init_dataset() - self._dataset.define_categories(self._generate_categories()) - - self.save() - - def _load(self): - self._project = Project.load(self._project_dir) - self._project.env.extractors.register(_TASK_IMAGES_EXTRACTOR, - lambda url: CvatImagesExtractor(url, - FrameProvider(self._db_task.data))) - - def _import_from_task(self, user): - self._project = Project.generate(self._project_dir, config={ - 'project_name': self._db_task.name, - 'plugins_dir': _FORMATS_DIR, - }) - - self._project.add_source('task_%s_images' % self._db_task.id, { - 'format': _TASK_IMAGES_EXTRACTOR, - }) - self._project.env.extractors.register(_TASK_IMAGES_EXTRACTOR, - lambda url: CvatImagesExtractor(url, - FrameProvider(self._db_task.data))) - - self._project.add_source('task_%s_anno' % self._db_task.id, { - 'format': _TASK_ANNO_EXTRACTOR, - }) - self._project.env.extractors.register(_TASK_ANNO_EXTRACTOR, - lambda url: CvatTaskExtractor(url, - db_task=self._db_task, user=user)) - - self._init_dataset() - - def _init_dataset(self): - self._dataset = self._project.make_dataset() - - def _generate_categories(self): - categories = {} - label_categories = datumaro.LabelCategories() - - db_labels = self._db_task.label_set.all() - for db_label in db_labels: - db_attributes = db_label.attributespec_set.all() - label_categories.add(db_label.name) - - for db_attr in db_attributes: - label_categories.attributes.add(db_attr.name) - - categories[datumaro.AnnotationType.label] = label_categories - - return categories - - def put_annotations(self, annotations): - raise NotImplementedError() - - def save(self, save_dir=None, save_images=False): - if self._dataset is not None: - self._dataset.save(save_dir=save_dir, save_images=save_images) - else: - self._project.save(save_dir=save_dir) + def put(self, data): + self._patch_data(data, None) - def export(self, dst_format, save_dir, save_images=False, server_url=None): - if self._dataset is None: - self._init_dataset() - if dst_format == EXPORT_FORMAT_DATUMARO_PROJECT: - self._remote_export(save_dir=save_dir, server_url=server_url) - else: - converter = self._dataset.env.make_converter(dst_format, - save_images=save_images) - self._dataset.export_project(converter=converter, save_dir=save_dir) - - def _remote_image_converter(self, save_dir, server_url=None): - os.makedirs(save_dir, exist_ok=True) - - db_task = self._db_task - items = [] - config = { - 'server_host': 'localhost', - 'task_id': db_task.id, - } - if server_url: - if ':' in server_url: - host, port = server_url.rsplit(':', maxsplit=1) - else: - host = server_url - port = None - config['server_host'] = host - if port is not None: - config['server_port'] = int(port) - - images_meta = { - 'images': items, - } - db_video = getattr(self._db_task.data, 'video', None) - if db_video is not None: - for i in range(self._db_task.data.size): - frame_info = { - 'id': i, - 'width': db_video.width, - 'height': db_video.height, - } - items.append(frame_info) + def create(self, data): + self._patch_data(data, PatchAction.CREATE) + + def update(self, data): + self._patch_data(data, PatchAction.UPDATE) + + def delete(self, data=None): + if data: + self._patch_data(data, PatchAction.DELETE) else: - for db_image in self._db_task.data.images.all(): - frame_info = { - 'id': db_image.frame, - 'name': osp.basename(db_image.path), - 'width': db_image.width, - 'height': db_image.height, - } - items.append(frame_info) - - with open(osp.join(save_dir, 'config.json'), 'w') as config_file: - json.dump(config, config_file) - with open(osp.join(save_dir, 'images_meta.json'), 'w') as images_file: - json.dump(images_meta, images_file) - - def _remote_export(self, save_dir, server_url=None): - if self._dataset is None: - self._init_dataset() - - os.makedirs(save_dir, exist_ok=True) - self._dataset.save(save_dir=save_dir, save_images=False, merge=True) - - exported_project = Project.load(save_dir) - source_name = 'task_%s_images' % self._db_task.id - exported_project.add_source(source_name, { - 'format': _TASK_IMAGES_REMOTE_EXTRACTOR, - }) - self._remote_image_converter( - osp.join(save_dir, exported_project.local_source_dir(source_name)), - server_url=server_url) - exported_project.save() - - - templates_dir = osp.join(osp.dirname(__file__), 'export_templates') - target_dir = exported_project.config.project_dir - os.makedirs(target_dir, exist_ok=True) - shutil.copyfile( - osp.join(templates_dir, 'README.md'), - osp.join(target_dir, 'README.md')) - - templates_dir = osp.join(templates_dir, 'plugins') - target_dir = osp.join(target_dir, - exported_project.config.env_dir, - exported_project.config.plugins_dir) - os.makedirs(target_dir, exist_ok=True) - shutil.copyfile( - osp.join(templates_dir, _TASK_IMAGES_REMOTE_EXTRACTOR + '.py'), - osp.join(target_dir, _TASK_IMAGES_REMOTE_EXTRACTOR + '.py')) - - # NOTE: put datumaro component to the archive so that - # it was available to the user - shutil.copytree(_DATUMARO_REPO_PATH, osp.join(save_dir, 'datumaro'), - ignore=lambda src, names: ['__pycache__'] + [ - n for n in names - if sum([int(n.endswith(ext)) for ext in - ['.pyx', '.pyo', '.pyd', '.pyc']]) - ]) - - # include CVAT CLI module also - cvat_utils_dst_dir = osp.join(save_dir, 'cvat', 'utils') - os.makedirs(cvat_utils_dst_dir) - shutil.copytree(osp.join(_CVAT_ROOT_DIR, 'utils', 'cli'), - osp.join(cvat_utils_dst_dir, 'cli')) - - -DEFAULT_FORMAT = EXPORT_FORMAT_DATUMARO_PROJECT -DEFAULT_CACHE_TTL = timedelta(hours=10) -CACHE_TTL = DEFAULT_CACHE_TTL - -def export_project(task_id, user, dst_format=None, server_url=None): - try: - db_task = Task.objects.get(pk=task_id) - - if not dst_format: - dst_format = DEFAULT_FORMAT - - cache_dir = get_export_cache_dir(db_task) - save_dir = osp.join(cache_dir, dst_format) - archive_path = osp.normpath(save_dir) + '.zip' - - task_time = timezone.localtime(db_task.updated_date).timestamp() - if not (osp.exists(archive_path) and \ - task_time <= osp.getmtime(archive_path)): - os.makedirs(cache_dir, exist_ok=True) - with tempfile.TemporaryDirectory( - dir=cache_dir, prefix=dst_format + '_') as temp_dir: - project = TaskProject.from_task(db_task, user) - project.export(dst_format, save_dir=temp_dir, save_images=True, - server_url=server_url) - - os.makedirs(cache_dir, exist_ok=True) - make_zip_archive(temp_dir, archive_path) - - archive_ctime = osp.getctime(archive_path) - scheduler = django_rq.get_scheduler() - cleaning_job = scheduler.enqueue_in(time_delta=CACHE_TTL, - func=clear_export_cache, - task_id=task_id, - file_path=archive_path, file_ctime=archive_ctime) - slogger.task[task_id].info( - "The task '{}' is exported as '{}' " - "and available for downloading for next '{}'. " - "Export cache cleaning job is enqueued, " - "id '{}', start in '{}'".format( - db_task.name, dst_format, CACHE_TTL, - cleaning_job.id, CACHE_TTL)) - - return archive_path - except Exception: - log_exception(slogger.task[task_id]) - raise - -def clear_export_cache(task_id, file_path, file_ctime): - try: - if osp.exists(file_path) and osp.getctime(file_path) == file_ctime: - os.remove(file_path) - slogger.task[task_id].info( - "Export cache file '{}' successfully removed" \ - .format(file_path)) - except Exception: - log_exception(slogger.task[task_id]) - raise - - -EXPORT_FORMATS = [ - { - 'name': 'Datumaro', - 'tag': EXPORT_FORMAT_DATUMARO_PROJECT, - 'is_default': True, - }, - { - 'name': 'PASCAL VOC 2012', - 'tag': 'cvat_voc', - 'is_default': False, - }, - { - 'name': 'MS COCO', - 'tag': 'cvat_coco', - 'is_default': False, - }, - { - 'name': 'YOLO', - 'tag': 'cvat_yolo', - 'is_default': False, - }, - { - 'name': 'TF Detection API', - 'tag': 'cvat_tfrecord', - 'is_default': False, - }, - { - 'name': 'MOT', - 'tag': 'cvat_mot', - 'is_default': False, - }, - { - 'name': 'LabelMe', - 'tag': 'cvat_label_me', - 'is_default': False, - }, -] - -def get_export_formats(): - converters = Environment(config={ - 'plugins_dir': _FORMATS_DIR - }).converters - - available_formats = set(converters.items) - available_formats.add(EXPORT_FORMAT_DATUMARO_PROJECT) - - public_formats = [] - for fmt in EXPORT_FORMATS: - if fmt['tag'] in available_formats: - public_formats.append(fmt) - - return public_formats + for db_job in self.db_jobs: + delete_job_data(db_job.id) + + def init_from_db(self): + self.reset() + + for db_job in self.db_jobs: + annotation = JobAnnotation(db_job.id) + annotation.init_from_db() + if annotation.ir_data.version > self.ir_data.version: + self.ir_data.version = annotation.ir_data.version + db_segment = db_job.segment + start_frame = db_segment.start_frame + overlap = self.db_task.overlap + self._merge_data(annotation.ir_data, start_frame, overlap) + + def export(self, dst_file, exporter, host='', **options): + task_data = TaskData( + annotation_ir=self.ir_data, + db_task=self.db_task, + host=host, + ) + exporter(dst_file, task_data, **options) + + def import_annotations(self, src_file, importer, **options): + task_data = TaskData( + annotation_ir=AnnotationIR(), + db_task=self.db_task, + create_callback=self.create, + ) + self.delete() + + importer(src_file, task_data, **options) + + self.create(task_data.data.serialize()) + + @property + def data(self): + return self.ir_data.data + + +@silk_profile(name="GET job data") +@transaction.atomic +def get_job_data(pk): + annotation = JobAnnotation(pk) + annotation.init_from_db() + + return annotation.data + +@silk_profile(name="POST job data") +@transaction.atomic +def put_job_data(pk, data): + annotation = JobAnnotation(pk) + annotation.put(data) + + return annotation.data + +@silk_profile(name="UPDATE job data") +@plugin_decorator +@transaction.atomic +def patch_job_data(pk, data, action): + annotation = JobAnnotation(pk) + if action == PatchAction.CREATE: + annotation.create(data) + elif action == PatchAction.UPDATE: + annotation.update(data) + elif action == PatchAction.DELETE: + annotation.delete(data) + + return annotation.data + +@silk_profile(name="DELETE job data") +@transaction.atomic +def delete_job_data(pk): + annotation = JobAnnotation(pk) + annotation.delete() + +@silk_profile(name="GET task data") +@transaction.atomic +def get_task_data(pk): + annotation = TaskAnnotation(pk) + annotation.init_from_db() + + return annotation.data + +@silk_profile(name="POST task data") +@transaction.atomic +def put_task_data(pk, data): + annotation = TaskAnnotation(pk) + annotation.put(data) + + return annotation.data + +@silk_profile(name="UPDATE task data") +@transaction.atomic +def patch_task_data(pk, data, action): + annotation = TaskAnnotation(pk) + if action == PatchAction.CREATE: + annotation.create(data) + elif action == PatchAction.UPDATE: + annotation.update(data) + elif action == PatchAction.DELETE: + annotation.delete(data) + + return annotation.data + +@silk_profile(name="DELETE task data") +@transaction.atomic +def delete_task_data(pk): + annotation = TaskAnnotation(pk) + annotation.delete() + +def export_task(task_id, dst_file, format_name, + server_url=None, save_images=False): + # For big tasks dump function may run for a long time and + # we dont need to acquire lock after the task has been initialized from DB. + # But there is the bug with corrupted dump file in case 2 or + # more dump request received at the same time: + # https://github.com/opencv/cvat/issues/217 + with transaction.atomic(): + task = TaskAnnotation(task_id) + task.init_from_db() + + exporter = make_exporter(format_name) + with open(dst_file, 'wb') as f: + task.export(f, exporter, host=server_url, + save_images=save_images) + +@transaction.atomic +def import_task_annotations(task_id, src_file, format_name): + task = TaskAnnotation(task_id) + task.init_from_db() + + importer = make_importer(format_name) + with open(src_file, 'rb') as f: + task.import_annotations(f, importer) + +@transaction.atomic +def import_job_annotations(job_id, src_file, format_name): + job = JobAnnotation(job_id) + job.init_from_db() + + importer = make_importer(format_name) + with open(src_file, 'rb') as f: + job.import_annotations(f, importer) diff --git a/cvat/apps/dataset_manager/_tests.py b/cvat/apps/dataset_manager/tests/_test_formats.py similarity index 80% rename from cvat/apps/dataset_manager/_tests.py rename to cvat/apps/dataset_manager/tests/_test_formats.py index 1a5300756e1..1a001a122cc 100644 --- a/cvat/apps/dataset_manager/_tests.py +++ b/cvat/apps/dataset_manager/tests/_test_formats.py @@ -3,6 +3,7 @@ # # SPDX-License-Identifier: MIT +# FIXME: Git application and package name clash in tests class _GitImportFix: import sys former_path = sys.path[:] @@ -47,19 +48,17 @@ def restore(cls): def _setUpModule(): _GitImportFix.apply() - import cvat.apps.dataset_manager.task as dm - from cvat.apps.engine.models import Task + import cvat.apps.dataset_manager as dm globals()['dm'] = dm - globals()['Task'] = Task import sys sys.path.insert(0, __file__[:__file__.rfind('/dataset_manager/')]) -def tearDownModule(): - _GitImportFix.restore() +# def tearDownModule(): + # _GitImportFix.restore() from io import BytesIO -import os +import os.path as osp import random import tempfile @@ -184,6 +183,24 @@ def _generate_task(self): "type": "polygon", "occluded": False }, + { + "frame": 1, + "label_id": task["labels"][0]["id"], + "group": 1, + "attributes": [], + "points": [100, 300.222, 400, 500, 1, 3], + "type": "points", + "occluded": False + }, + { + "frame": 1, + "label_id": task["labels"][0]["id"], + "group": 1, + "attributes": [], + "points": [2.0, 2.1, 400, 500, 1, 3], + "type": "polyline", + "occluded": False + }, ], "tracks": [ { @@ -269,41 +286,52 @@ def _put_api_v1_task_id_annotations(self, tid, data): return response def _test_export(self, format_name, save_images=False): - self.assertTrue(format_name in [f['tag'] for f in dm.EXPORT_FORMATS]) - task, _ = self._generate_task() - project = dm.TaskProject.from_task( - Task.objects.get(pk=task["id"]), self.user.username) - with tempfile.TemporaryDirectory() as test_dir: - project.export(format_name, test_dir, save_images=save_images) + with tempfile.TemporaryDirectory() as temp_dir: + file_path = osp.join(temp_dir, format_name) + dm.task.export_task(task["id"], file_path, + format_name, save_images=save_images) - self.assertTrue(os.listdir(test_dir)) + with open(file_path, 'rb') as f: + self.assertTrue(len(f.read()) != 0) def test_datumaro(self): - self._test_export(dm.EXPORT_FORMAT_DATUMARO_PROJECT, save_images=False) + self._test_export('Datumaro 1.0', save_images=False) def test_coco(self): - self._test_export('cvat_coco', save_images=True) + self._test_export('COCO 1.0', save_images=True) def test_voc(self): - self._test_export('cvat_voc', save_images=True) + self._test_export('PASCAL VOC 1.1', save_images=True) - def test_tf_detection_api(self): - self._test_export('cvat_tfrecord', save_images=True) + def test_tf_record(self): + self._test_export('TFRecord 1.0', save_images=True) def test_yolo(self): - self._test_export('cvat_yolo', save_images=True) + self._test_export('YOLO 1.1', save_images=True) def test_mot(self): - self._test_export('cvat_mot', save_images=True) + self._test_export('MOT 1.1', save_images=True) def test_labelme(self): - self._test_export('cvat_label_me', save_images=True) + self._test_export('LabelMe 3.0', save_images=True) + + def test_mask(self): + self._test_export('Segmentation mask 1.1', save_images=True) + + def test_cvat_video(self): + self._test_export('CVAT for video 1.1', save_images=True) + + def test_cvat_images(self): + self._test_export('CVAT for images 1.1', save_images=True) + + def test_export_formats_query(self): + formats = dm.views.get_export_formats() + + self.assertEqual(len(formats), 10) - def test_formats_query(self): - formats = dm.get_export_formats() + def test_import_formats_query(self): + formats = dm.views.get_import_formats() - expected = set(f['tag'] for f in dm.EXPORT_FORMATS) - actual = set(f['tag'] for f in formats) - self.assertSetEqual(expected, actual) + self.assertEqual(len(formats), 8) diff --git a/cvat/apps/engine/tests/test_data_manager.py b/cvat/apps/dataset_manager/tests/test_annotation.py similarity index 94% rename from cvat/apps/engine/tests/test_data_manager.py rename to cvat/apps/dataset_manager/tests/test_annotation.py index 968b57525f6..2db3969906a 100644 --- a/cvat/apps/engine/tests/test_data_manager.py +++ b/cvat/apps/dataset_manager/tests/test_annotation.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -from cvat.apps.engine.data_manager import TrackManager +from cvat.apps.dataset_manager.annotation import TrackManager from unittest import TestCase diff --git a/cvat/apps/dataset_manager/views.py b/cvat/apps/dataset_manager/views.py new file mode 100644 index 00000000000..3d77809b1c5 --- /dev/null +++ b/cvat/apps/dataset_manager/views.py @@ -0,0 +1,107 @@ +# Copyright (C) 2019-2020 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import os +import os.path as osp +import tempfile +from datetime import timedelta + +import django_rq +from django.utils import timezone + +import cvat.apps.dataset_manager.task as task +from cvat.apps.engine.log import slogger +from cvat.apps.engine.models import Task +from datumaro.cli.util import make_file_name +from datumaro.util import to_snake_case + +from .formats.registry import EXPORT_FORMATS, IMPORT_FORMATS +from .util import current_function_name + + +_MODULE_NAME = __package__ + '.' + osp.splitext(osp.basename(__file__))[0] +def log_exception(logger=None, exc_info=True): + if logger is None: + logger = slogger + logger.exception("[%s @ %s]: exception occurred" % \ + (_MODULE_NAME, current_function_name(2)), + exc_info=exc_info) + + +def get_export_cache_dir(db_task): + return osp.join(db_task.get_task_dirname(), 'export_cache') + +DEFAULT_CACHE_TTL = timedelta(hours=10) +CACHE_TTL = DEFAULT_CACHE_TTL + + +def export_task(task_id, dst_format, server_url=None, save_images=False): + try: + db_task = Task.objects.get(pk=task_id) + + cache_dir = get_export_cache_dir(db_task) + + exporter = EXPORT_FORMATS[dst_format] + output_base = '%s_%s' % ('dataset' if save_images else 'task', + make_file_name(to_snake_case(dst_format))) + output_path = '%s.%s' % (output_base, exporter.EXT) + output_path = osp.join(cache_dir, output_path) + + task_time = timezone.localtime(db_task.updated_date).timestamp() + if not (osp.exists(output_path) and \ + task_time <= osp.getmtime(output_path)): + os.makedirs(cache_dir, exist_ok=True) + with tempfile.TemporaryDirectory(dir=cache_dir) as temp_dir: + temp_file = osp.join(temp_dir, 'result') + task.export_task(task_id, temp_file, dst_format, + server_url=server_url, save_images=save_images) + os.replace(temp_file, output_path) + + archive_ctime = osp.getctime(output_path) + scheduler = django_rq.get_scheduler() + cleaning_job = scheduler.enqueue_in(time_delta=CACHE_TTL, + func=clear_export_cache, + task_id=task_id, + file_path=output_path, file_ctime=archive_ctime) + slogger.task[task_id].info( + "The task '{}' is exported as '{}' at '{}' " + "and available for downloading for the next {}. " + "Export cache cleaning job is enqueued, id '{}'".format( + db_task.name, dst_format, output_path, CACHE_TTL, + cleaning_job.id)) + + return output_path + except Exception: + log_exception(slogger.task[task_id]) + raise + +def export_task_as_dataset(task_id, dst_format=None, server_url=None): + return export_task(task_id, dst_format, server_url=server_url, save_images=True) + +def export_task_annotations(task_id, dst_format=None, server_url=None): + return export_task(task_id, dst_format, server_url=server_url, save_images=False) + +def clear_export_cache(task_id, file_path, file_ctime): + try: + if osp.exists(file_path) and osp.getctime(file_path) == file_ctime: + os.remove(file_path) + slogger.task[task_id].info( + "Export cache file '{}' successfully removed" \ + .format(file_path)) + except Exception: + log_exception(slogger.task[task_id]) + raise + + +def get_export_formats(): + return list(EXPORT_FORMATS.values()) + +def get_import_formats(): + return list(IMPORT_FORMATS.values()) + +def get_all_formats(): + return { + 'importers': get_import_formats(), + 'exporters': get_export_formats(), + } \ No newline at end of file diff --git a/cvat/apps/documentation/static/documentation/images/image007_DETRAC.jpg b/cvat/apps/documentation/static/documentation/images/image007_DETRAC.jpg index 662b739e284..49c07ab6b9a 100644 Binary files a/cvat/apps/documentation/static/documentation/images/image007_DETRAC.jpg and b/cvat/apps/documentation/static/documentation/images/image007_DETRAC.jpg differ diff --git a/cvat/apps/documentation/static/documentation/images/image013_DETRAC.JPG b/cvat/apps/documentation/static/documentation/images/image013_DETRAC.JPG index 07b43cda2eb..b802ee1c71b 100644 Binary files a/cvat/apps/documentation/static/documentation/images/image013_DETRAC.JPG and b/cvat/apps/documentation/static/documentation/images/image013_DETRAC.JPG differ diff --git a/cvat/apps/documentation/static/documentation/images/image015_DETRAC.jpg b/cvat/apps/documentation/static/documentation/images/image015_DETRAC.jpg new file mode 100644 index 00000000000..b96ab3c1b2a Binary files /dev/null and b/cvat/apps/documentation/static/documentation/images/image015_DETRAC.jpg differ diff --git a/cvat/apps/documentation/static/documentation/images/image138_mapillary_vistas.jpg b/cvat/apps/documentation/static/documentation/images/image138_mapillary_vistas.jpg new file mode 100644 index 00000000000..98f4151c9dd Binary files /dev/null and b/cvat/apps/documentation/static/documentation/images/image138_mapillary_vistas.jpg differ diff --git a/cvat/apps/documentation/static/documentation/images/image139_mapillary_vistas.jpg b/cvat/apps/documentation/static/documentation/images/image139_mapillary_vistas.jpg new file mode 100644 index 00000000000..0d39eb0d429 Binary files /dev/null and b/cvat/apps/documentation/static/documentation/images/image139_mapillary_vistas.jpg differ diff --git a/cvat/apps/documentation/static/documentation/images/image140.jpg b/cvat/apps/documentation/static/documentation/images/image140.jpg new file mode 100644 index 00000000000..d018dd044a9 Binary files /dev/null and b/cvat/apps/documentation/static/documentation/images/image140.jpg differ diff --git a/cvat/apps/documentation/user_guide.md b/cvat/apps/documentation/user_guide.md index b2561de9270..f80274cbd84 100644 --- a/cvat/apps/documentation/user_guide.md +++ b/cvat/apps/documentation/user_guide.md @@ -14,7 +14,7 @@ - [Downloading annotations](#downloading-annotations) - [Task synchronization with a repository](#task-synchronization-with-a-repository) - [Vocabulary](#vocabulary) - - [Workspace — Context menu](#workspace--context-menu) + - [Workspace](#workspace) - [Settings](#settings) - [Bottom Panel](#bottom-panel) - [Side panel](#side-panel) @@ -223,28 +223,24 @@ Go to the [Django administration panel](http://localhost:8080/admin). There you 1. The Dashboard contains elements and each of them relates to a separate task. They are sorted in creation order. Each element contains: task name, preview, progress bar, button ``Open``, and menu ``Actions``. Each button is responsible for a in menu ``Actions`` specific function: - - ``Dump Annotation`` — download an annotation file from a task. Several formats are available: - - [CVAT XML 1.1 for video](/cvat/apps/documentation/xml_format.md#interpolation) + - ``Dump Annotation`` and ``Export as a dataset`` — download annotations or + annotations and images in a specific format. The following formats are available: + - [CVAT for video](/cvat/apps/documentation/xml_format.md#interpolation) is highlighted if a task has the interpolation mode. - - [CVAT XML 1.1 for images](/cvat/apps/documentation/xml_format.md#annotation) - is highlighted if a task has the annotation mode. - - [PASCAL VOC ZIP 1.1](http://host.robots.ox.ac.uk/pascal/VOC/) - - [YOLO ZIP 1.1](https://pjreddie.com/darknet/yolo/) - - [COCO JSON 1.0](http://cocodataset.org/#format-data) - - ``MASK ZIP 1.0`` — archive contains a mask of each frame in the png format and a text file - with the value of each color. - - [TFRecord ZIP 1.0](https://www.tensorflow.org/tutorials/load_data/tf_records) - - [MOT CSV 1.0](https://motchallenge.net/) - - [LabelMe ZIP 3.0 for image](http://labelme.csail.mit.edu/Release3.0/) - - ``Upload annotation`` is possible in same format as ``Dump annotation``, with exception of ``MASK ZIP 1.0`` - format and without choosing whether [CVAT XML 1.1](/cvat/apps/documentation/xml_format.md) - and [LabelMe ZIP 3.0](http://labelme.csail.mit.edu/Release3.0/) - refers to an image or video. - - ``Export as a dataset`` — download a data set from a task. Several formats are available: - - [Datumaro](https://github.com/opencv/cvat/blob/develop/datumaro/docs/design.md) - - [Pascal VOC 2012](http://host.robots.ox.ac.uk/pascal/VOC/) - - [MS COCO](http://cocodataset.org/#format-data) + - [CVAT for images](/cvat/apps/documentation/xml_format.md#annotation) + is highlighted if a task has the annotation mode. + - [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) + - [(VOC) Segmentation mask](http://host.robots.ox.ac.uk/pascal/VOC/) — + archive contains class and instance masks for each frame in the png + format and a text file with the value of each color. - [YOLO](https://pjreddie.com/darknet/yolo/) + - [COCO](http://cocodataset.org/#format-data) + - [TFRecord](https://www.tensorflow.org/tutorials/load_data/tf_records) + - [MOT](https://motchallenge.net/) + - [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0/) + - [Datumaro](https://github.com/opencv/cvat/blob/develop/datumaro/) + - ``Upload annotation`` is available in the same formats as in ``Dump annotation``. + - [CVAT](/cvat/apps/documentation/xml_format.md) accepts both video and image sub-formats. - ``Automatic Annotation`` — automatic annotation with OpenVINO toolkit. Presence depends on how you build CVAT instance. - ``Open bug tracker`` — opens a link to Issue tracker. @@ -383,11 +379,12 @@ The tool consists of: ### Types of shapes (basics) -There are four shapes which you can annotate your images with: +There are five shapes which you can annotate your images with: - ``Rectangle`` or ``Bounding box`` - ``Polygon`` - ``Polyline`` - ``Points`` +- ``Cuboid`` - ``Tag`` And there is how they all look like: @@ -396,7 +393,7 @@ And there is how they all look like: ![](static/documentation/images/image009_mapillary_vistas.jpg "Polyline") ![](static/documentation/images/image010_Affectnet.jpg "Points") -![](static/documentation/images/image135.jpg "Tag") +![](static/documentation/images/image015_DETRAC.jpg "Cuboid") ![](static/documentation/images/image135.jpg "Tag") ``Tag`` - has no shape in the workspace, but is displayed in objects sidebar. @@ -542,22 +539,24 @@ Read more in the section [attribute annotation mode (advanced)](#attribute-annot ![](static/documentation/images/image028.jpg) -1. Choose the format dump of the annotation file. Several formats are available: - - [CVAT XML 1.1 for video](/cvat/apps/documentation/xml_format.md#interpolation) - is highlighted if a task has the interpolation mode - - [CVAT XML 1.1 for images](/cvat/apps/documentation/xml_format.md#annotation) - is highlighted if a task has the annotation mode +1. Choose format dump annotation file. Dump annotation are available in several formats: + - [CVAT for video](/cvat/apps/documentation/xml_format.md#interpolation) + is highlighted if a task has the interpolation mode. + - [CVAT for images](/cvat/apps/documentation/xml_format.md#annotation) + is highlighted if a task has the annotation mode. ![](static/documentation/images/image029.jpg "Example XML format") - - [PASCAL VOC ZIP 1.1](http://host.robots.ox.ac.uk/pascal/VOC/) - - [YOLO ZIP 1.1](https://pjreddie.com/darknet/yolo/) - - [COCO JSON 1.0](http://cocodataset.org/#format-data) - - ``MASK ZIP 1.1`` — archive contains a mask of each frame in the png format and a text file with - the value of each color - - [TFRecord ZIP 1.0](https://www.tensorflow.org/tutorials/load_data/tf_records) - - [MOT ZIP 1.1](https://motchallenge.net/) - - [LabelMe ZIP 3.0 for image](http://labelme.csail.mit.edu/Release3.0/) + - [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) + - [(VOC) Segmentation mask](http://host.robots.ox.ac.uk/pascal/VOC/) — + archive contains class and instance masks for each frame in the png + format and a text file with the value of each color. + - [YOLO](https://pjreddie.com/darknet/yolo/) + - [COCO](http://cocodataset.org/#format-data) + - [TFRecord](https://www.tensorflow.org/tutorials/load_data/tf_records) + - [MOT](https://motchallenge.net/) + - [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0/) + - [Datumaro](https://github.com/opencv/cvat/blob/develop/datumaro/) ### Task synchronization with a repository @@ -609,38 +608,25 @@ Tracks are created in ``Track mode`` - _Automatic_ which is created automatically without a person in the loop --- -### Workspace — Context menu - -Press the ``Right mouse button`` to see the context menu. - -Next options are available clicking inside bounding box: -- ``Copy Object URL`` — copying to the buffer address of an object on the frame -in the task -- ``Change Color`` — change color of active shape -- ``Remove Shape`` — deleting the shape -- ``Switch Occluded`` — attribute is used if an object is occluded by another -object or isn't fully visible on the frame. Use the ``Q`` shortcut to set the -property quickly. -- ``Switch Lock`` — block editing the active shape -- ``Enable Dragging`` — (only for polygons) allows to adjust polygons position -- ``Split`` — allows to split an interpolated track into two separate tracks. This function is the opposite -of the merge function. - -![](static/documentation/images/image089.jpg) -![](static/documentation/images/image090.jpg) -![](static/documentation/images/image103.jpg) +### Workspace -Clicking on the points of poly-shapes, ``Remove`` option is available. +This is the main field in which drawing and editing objects takes place. +In addition the workspace also has the following functions: +- Right-clicking on an object calls up the ``Object card`` - this is an element containing + the necessary controls for changing the label and attributes of the object, as well as the action menu. -![](static/documentation/images/image092.jpg) + ![](static/documentation/images/image138_mapillary_vistas.jpg) -Clicking outside any shapes, you can copy ``Frame URL`` (link to present frame) or ``Job URL`` (link from address bar) +- Right-clicking a point deletes it. -![](static/documentation/images/image091.jpg) + ![](static/documentation/images/image139_mapillary_vistas.jpg) -Applying ``split`` into an interpolated track divides it into two separate tracks. +- ``Z-axis slider`` - Allows you to switch annotation layers hiding the upper layers + (slider is enabled if several z layers are on a frame). + This element has a button for adding a new layer. When pressed, a new layer is added and switched to it. + You can move objects in layers using the ``+`` and ``-`` keys. -![](static/documentation/images/gif010.gif) + ![](static/documentation/images/image140.jpg) --- ### Settings diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py deleted file mode 100644 index ad005bd13ec..00000000000 --- a/cvat/apps/engine/annotation.py +++ /dev/null @@ -1,764 +0,0 @@ -# Copyright (C) 2018 Intel Corporation -# -# SPDX-License-Identifier: MIT - -import os -from enum import Enum -from collections import OrderedDict -from django.utils import timezone - -from django.conf import settings -from django.db import transaction - -from cvat.apps.profiler import silk_profile -from cvat.apps.engine.plugins import plugin_decorator -from cvat.apps.annotation.annotation import AnnotationIR, Annotation -from cvat.apps.engine.utils import execute_python_code, import_modules - -from . import models -from .data_manager import DataManager -from .log import slogger -from . import serializers - -"""dot.notation access to dictionary attributes""" -class dotdict(OrderedDict): - __getattr__ = OrderedDict.get - __setattr__ = OrderedDict.__setitem__ - __delattr__ = OrderedDict.__delitem__ - __eq__ = lambda self, other: self.id == other.id - __hash__ = lambda self: self.id - -class PatchAction(str, Enum): - CREATE = "create" - UPDATE = "update" - DELETE = "delete" - - @classmethod - def values(cls): - return [item.value for item in cls] - - def __str__(self): - return self.value - -@silk_profile(name="GET job data") -@transaction.atomic -def get_job_data(pk, user): - annotation = JobAnnotation(pk, user) - annotation.init_from_db() - - return annotation.data - -@silk_profile(name="POST job data") -@transaction.atomic -def put_job_data(pk, user, data): - annotation = JobAnnotation(pk, user) - annotation.put(data) - - return annotation.data - -@silk_profile(name="UPDATE job data") -@plugin_decorator -@transaction.atomic -def patch_job_data(pk, user, data, action): - annotation = JobAnnotation(pk, user) - if action == PatchAction.CREATE: - annotation.create(data) - elif action == PatchAction.UPDATE: - annotation.update(data) - elif action == PatchAction.DELETE: - annotation.delete(data) - - return annotation.data - -@silk_profile(name="DELETE job data") -@transaction.atomic -def delete_job_data(pk, user): - annotation = JobAnnotation(pk, user) - annotation.delete() - -@silk_profile(name="GET task data") -@transaction.atomic -def get_task_data(pk, user): - annotation = TaskAnnotation(pk, user) - annotation.init_from_db() - - return annotation.data - -@silk_profile(name="POST task data") -@transaction.atomic -def put_task_data(pk, user, data): - annotation = TaskAnnotation(pk, user) - annotation.put(data) - - return annotation.data - -@silk_profile(name="UPDATE task data") -@transaction.atomic -def patch_task_data(pk, user, data, action): - annotation = TaskAnnotation(pk, user) - if action == PatchAction.CREATE: - annotation.create(data) - elif action == PatchAction.UPDATE: - annotation.update(data) - elif action == PatchAction.DELETE: - annotation.delete(data) - - return annotation.data - -@transaction.atomic -def load_task_data(pk, user, filename, loader): - annotation = TaskAnnotation(pk, user) - annotation.upload(filename, loader) - -@transaction.atomic -def load_job_data(pk, user, filename, loader): - annotation = JobAnnotation(pk, user) - annotation.upload(filename, loader) - -@silk_profile(name="DELETE task data") -@transaction.atomic -def delete_task_data(pk, user): - annotation = TaskAnnotation(pk, user) - annotation.delete() - -def dump_task_data(pk, user, filename, dumper, scheme, host): - # For big tasks dump function may run for a long time and - # we dont need to acquire lock after _AnnotationForTask instance - # has been initialized from DB. - # But there is the bug with corrupted dump file in case 2 or more dump request received at the same time. - # https://github.com/opencv/cvat/issues/217 - with transaction.atomic(): - annotation = TaskAnnotation(pk, user) - annotation.init_from_db() - - annotation.dump(filename, dumper, scheme, host) - -def bulk_create(db_model, objects, flt_param): - if objects: - if flt_param: - if 'postgresql' in settings.DATABASES["default"]["ENGINE"]: - return db_model.objects.bulk_create(objects) - else: - ids = list(db_model.objects.filter(**flt_param).values_list('id', flat=True)) - db_model.objects.bulk_create(objects) - - return list(db_model.objects.exclude(id__in=ids).filter(**flt_param)) - else: - return db_model.objects.bulk_create(objects) - - return [] - -def _merge_table_rows(rows, keys_for_merge, field_id): - # It is necessary to keep a stable order of original rows - # (e.g. for tracked boxes). Otherwise prev_box.frame can be bigger - # than next_box.frame. - merged_rows = OrderedDict() - - # Group all rows by field_id. In grouped rows replace fields in - # accordance with keys_for_merge structure. - for row in rows: - row_id = row[field_id] - if not row_id in merged_rows: - merged_rows[row_id] = dotdict(row) - for key in keys_for_merge: - merged_rows[row_id][key] = [] - - for key in keys_for_merge: - item = dotdict({v.split('__', 1)[-1]:row[v] for v in keys_for_merge[key]}) - if item.id is not None: - merged_rows[row_id][key].append(item) - - # Remove redundant keys from final objects - redundant_keys = [item for values in keys_for_merge.values() for item in values] - for i in merged_rows: - for j in redundant_keys: - del merged_rows[i][j] - - return list(merged_rows.values()) - -class JobAnnotation: - def __init__(self, pk, user): - self.user = user - self.db_job = models.Job.objects.select_related('segment__task') \ - .select_for_update().get(id=pk) - - db_segment = self.db_job.segment - self.start_frame = db_segment.start_frame - self.stop_frame = db_segment.stop_frame - self.ir_data = AnnotationIR() - - # pylint: disable=bad-continuation - self.logger = slogger.job[self.db_job.id] - self.db_labels = {db_label.id:db_label - for db_label in db_segment.task.label_set.all()} - - self.db_attributes = {} - for db_label in self.db_labels.values(): - self.db_attributes[db_label.id] = { - "mutable": OrderedDict(), - "immutable": OrderedDict(), - "all": OrderedDict(), - } - for db_attr in db_label.attributespec_set.all(): - default_value = dotdict([ - ('spec_id', db_attr.id), - ('value', db_attr.default_value), - ]) - if db_attr.mutable: - self.db_attributes[db_label.id]["mutable"][db_attr.id] = default_value - else: - self.db_attributes[db_label.id]["immutable"][db_attr.id] = default_value - - self.db_attributes[db_label.id]["all"][db_attr.id] = default_value - - def reset(self): - self.ir_data.reset() - - def _save_tracks_to_db(self, tracks): - db_tracks = [] - db_track_attrvals = [] - db_shapes = [] - db_shape_attrvals = [] - - for track in tracks: - track_attributes = track.pop("attributes", []) - shapes = track.pop("shapes") - db_track = models.LabeledTrack(job=self.db_job, **track) - if db_track.label_id not in self.db_labels: - raise AttributeError("label_id `{}` is invalid".format(db_track.label_id)) - - for attr in track_attributes: - db_attrval = models.LabeledTrackAttributeVal(**attr) - if db_attrval.spec_id not in self.db_attributes[db_track.label_id]["immutable"]: - raise AttributeError("spec_id `{}` is invalid".format(db_attrval.spec_id)) - db_attrval.track_id = len(db_tracks) - db_track_attrvals.append(db_attrval) - - for shape in shapes: - shape_attributes = shape.pop("attributes", []) - # FIXME: need to clamp points (be sure that all of them inside the image) - # Should we check here or implement a validator? - db_shape = models.TrackedShape(**shape) - db_shape.track_id = len(db_tracks) - - for attr in shape_attributes: - db_attrval = models.TrackedShapeAttributeVal(**attr) - if db_attrval.spec_id not in self.db_attributes[db_track.label_id]["mutable"]: - raise AttributeError("spec_id `{}` is invalid".format(db_attrval.spec_id)) - db_attrval.shape_id = len(db_shapes) - db_shape_attrvals.append(db_attrval) - - db_shapes.append(db_shape) - shape["attributes"] = shape_attributes - - db_tracks.append(db_track) - track["attributes"] = track_attributes - track["shapes"] = shapes - - db_tracks = bulk_create( - db_model=models.LabeledTrack, - objects=db_tracks, - flt_param={"job_id": self.db_job.id} - ) - - for db_attrval in db_track_attrvals: - db_attrval.track_id = db_tracks[db_attrval.track_id].id - bulk_create( - db_model=models.LabeledTrackAttributeVal, - objects=db_track_attrvals, - flt_param={} - ) - - for db_shape in db_shapes: - db_shape.track_id = db_tracks[db_shape.track_id].id - - db_shapes = bulk_create( - db_model=models.TrackedShape, - objects=db_shapes, - flt_param={"track__job_id": self.db_job.id} - ) - - for db_attrval in db_shape_attrvals: - db_attrval.shape_id = db_shapes[db_attrval.shape_id].id - - bulk_create( - db_model=models.TrackedShapeAttributeVal, - objects=db_shape_attrvals, - flt_param={} - ) - - shape_idx = 0 - for track, db_track in zip(tracks, db_tracks): - track["id"] = db_track.id - for shape in track["shapes"]: - shape["id"] = db_shapes[shape_idx].id - shape_idx += 1 - - self.ir_data.tracks = tracks - - def _save_shapes_to_db(self, shapes): - db_shapes = [] - db_attrvals = [] - - for shape in shapes: - attributes = shape.pop("attributes", []) - # FIXME: need to clamp points (be sure that all of them inside the image) - # Should we check here or implement a validator? - db_shape = models.LabeledShape(job=self.db_job, **shape) - if db_shape.label_id not in self.db_labels: - raise AttributeError("label_id `{}` is invalid".format(db_shape.label_id)) - - for attr in attributes: - db_attrval = models.LabeledShapeAttributeVal(**attr) - if db_attrval.spec_id not in self.db_attributes[db_shape.label_id]["all"]: - raise AttributeError("spec_id `{}` is invalid".format(db_attrval.spec_id)) - - db_attrval.shape_id = len(db_shapes) - db_attrvals.append(db_attrval) - - db_shapes.append(db_shape) - shape["attributes"] = attributes - - db_shapes = bulk_create( - db_model=models.LabeledShape, - objects=db_shapes, - flt_param={"job_id": self.db_job.id} - ) - - for db_attrval in db_attrvals: - db_attrval.shape_id = db_shapes[db_attrval.shape_id].id - - bulk_create( - db_model=models.LabeledShapeAttributeVal, - objects=db_attrvals, - flt_param={} - ) - - for shape, db_shape in zip(shapes, db_shapes): - shape["id"] = db_shape.id - - self.ir_data.shapes = shapes - - def _save_tags_to_db(self, tags): - db_tags = [] - db_attrvals = [] - - for tag in tags: - attributes = tag.pop("attributes", []) - db_tag = models.LabeledImage(job=self.db_job, **tag) - if db_tag.label_id not in self.db_labels: - raise AttributeError("label_id `{}` is invalid".format(db_tag.label_id)) - - for attr in attributes: - db_attrval = models.LabeledImageAttributeVal(**attr) - if db_attrval.spec_id not in self.db_attributes[db_tag.label_id]["all"]: - raise AttributeError("spec_id `{}` is invalid".format(db_attrval.spec_id)) - db_attrval.tag_id = len(db_tags) - db_attrvals.append(db_attrval) - - db_tags.append(db_tag) - tag["attributes"] = attributes - - db_tags = bulk_create( - db_model=models.LabeledImage, - objects=db_tags, - flt_param={"job_id": self.db_job.id} - ) - - for db_attrval in db_attrvals: - db_attrval.image_id = db_tags[db_attrval.tag_id].id - - bulk_create( - db_model=models.LabeledImageAttributeVal, - objects=db_attrvals, - flt_param={} - ) - - for tag, db_tag in zip(tags, db_tags): - tag["id"] = db_tag.id - - self.ir_data.tags = tags - - def _commit(self): - db_prev_commit = self.db_job.commits.last() - db_curr_commit = models.JobCommit() - if db_prev_commit: - db_curr_commit.version = db_prev_commit.version + 1 - else: - db_curr_commit.version = 1 - db_curr_commit.job = self.db_job - db_curr_commit.message = "Changes: tags - {}; shapes - {}; tracks - {}".format( - len(self.ir_data.tags), len(self.ir_data.shapes), len(self.ir_data.tracks)) - db_curr_commit.save() - self.ir_data.version = db_curr_commit.version - - def _set_updated_date(self): - db_task = self.db_job.segment.task - db_task.updated_date = timezone.now() - db_task.save() - - def _save_to_db(self, data): - self.reset() - self._save_tags_to_db(data["tags"]) - self._save_shapes_to_db(data["shapes"]) - self._save_tracks_to_db(data["tracks"]) - - return self.ir_data.tags or self.ir_data.shapes or self.ir_data.tracks - - def _create(self, data): - if self._save_to_db(data): - self._set_updated_date() - self.db_job.save() - - def create(self, data): - self._create(data) - self._commit() - - def put(self, data): - self._delete() - self._create(data) - self._commit() - - def update(self, data): - self._delete(data) - self._create(data) - self._commit() - - def _delete(self, data=None): - deleted_shapes = 0 - if data is None: - deleted_shapes += self.db_job.labeledimage_set.all().delete()[0] - deleted_shapes += self.db_job.labeledshape_set.all().delete()[0] - deleted_shapes += self.db_job.labeledtrack_set.all().delete()[0] - else: - labeledimage_ids = [image["id"] for image in data["tags"]] - labeledshape_ids = [shape["id"] for shape in data["shapes"]] - labeledtrack_ids = [track["id"] for track in data["tracks"]] - labeledimage_set = self.db_job.labeledimage_set - labeledimage_set = labeledimage_set.filter(pk__in=labeledimage_ids) - labeledshape_set = self.db_job.labeledshape_set - labeledshape_set = labeledshape_set.filter(pk__in=labeledshape_ids) - labeledtrack_set = self.db_job.labeledtrack_set - labeledtrack_set = labeledtrack_set.filter(pk__in=labeledtrack_ids) - - # It is not important for us that data had some "invalid" objects - # which were skipped (not acutally deleted). The main idea is to - # say that all requested objects are absent in DB after the method. - self.ir_data.tags = data['tags'] - self.ir_data.shapes = data['shapes'] - self.ir_data.tracks = data['tracks'] - - deleted_shapes += labeledimage_set.delete()[0] - deleted_shapes += labeledshape_set.delete()[0] - deleted_shapes += labeledtrack_set.delete()[0] - - if deleted_shapes: - self._set_updated_date() - - def delete(self, data=None): - self._delete(data) - self._commit() - - @staticmethod - def _extend_attributes(attributeval_set, default_attribute_values): - shape_attribute_specs_set = set(attr.spec_id for attr in attributeval_set) - for db_attr in default_attribute_values: - if db_attr.spec_id not in shape_attribute_specs_set: - attributeval_set.append(dotdict([ - ('spec_id', db_attr.spec_id), - ('value', db_attr.value), - ])) - - def _init_tags_from_db(self): - db_tags = self.db_job.labeledimage_set.prefetch_related( - "label", - "labeledimageattributeval_set" - ).values( - 'id', - 'frame', - 'label_id', - 'group', - 'labeledimageattributeval__spec_id', - 'labeledimageattributeval__value', - 'labeledimageattributeval__id', - ).order_by('frame') - - db_tags = _merge_table_rows( - rows=db_tags, - keys_for_merge={ - "labeledimageattributeval_set": [ - 'labeledimageattributeval__spec_id', - 'labeledimageattributeval__value', - 'labeledimageattributeval__id', - ], - }, - field_id='id', - ) - - for db_tag in db_tags: - self._extend_attributes(db_tag.labeledimageattributeval_set, - self.db_attributes[db_tag.label_id]["all"].values()) - - serializer = serializers.LabeledImageSerializer(db_tags, many=True) - self.ir_data.tags = serializer.data - - def _init_shapes_from_db(self): - db_shapes = self.db_job.labeledshape_set.prefetch_related( - "label", - "labeledshapeattributeval_set" - ).values( - 'id', - 'label_id', - 'type', - 'frame', - 'group', - 'occluded', - 'z_order', - 'points', - 'labeledshapeattributeval__spec_id', - 'labeledshapeattributeval__value', - 'labeledshapeattributeval__id', - ).order_by('frame') - - db_shapes = _merge_table_rows( - rows=db_shapes, - keys_for_merge={ - 'labeledshapeattributeval_set': [ - 'labeledshapeattributeval__spec_id', - 'labeledshapeattributeval__value', - 'labeledshapeattributeval__id', - ], - }, - field_id='id', - ) - for db_shape in db_shapes: - self._extend_attributes(db_shape.labeledshapeattributeval_set, - self.db_attributes[db_shape.label_id]["all"].values()) - - serializer = serializers.LabeledShapeSerializer(db_shapes, many=True) - self.ir_data.shapes = serializer.data - - def _init_tracks_from_db(self): - db_tracks = self.db_job.labeledtrack_set.prefetch_related( - "label", - "labeledtrackattributeval_set", - "trackedshape_set__trackedshapeattributeval_set" - ).values( - "id", - "frame", - "label_id", - "group", - "labeledtrackattributeval__spec_id", - "labeledtrackattributeval__value", - "labeledtrackattributeval__id", - "trackedshape__type", - "trackedshape__occluded", - "trackedshape__z_order", - "trackedshape__points", - "trackedshape__id", - "trackedshape__frame", - "trackedshape__outside", - "trackedshape__trackedshapeattributeval__spec_id", - "trackedshape__trackedshapeattributeval__value", - "trackedshape__trackedshapeattributeval__id", - ).order_by('id', 'trackedshape__frame') - - db_tracks = _merge_table_rows( - rows=db_tracks, - keys_for_merge={ - "labeledtrackattributeval_set": [ - "labeledtrackattributeval__spec_id", - "labeledtrackattributeval__value", - "labeledtrackattributeval__id", - ], - "trackedshape_set":[ - "trackedshape__type", - "trackedshape__occluded", - "trackedshape__z_order", - "trackedshape__points", - "trackedshape__id", - "trackedshape__frame", - "trackedshape__outside", - "trackedshape__trackedshapeattributeval__spec_id", - "trackedshape__trackedshapeattributeval__value", - "trackedshape__trackedshapeattributeval__id", - ], - }, - field_id="id", - ) - - for db_track in db_tracks: - db_track["trackedshape_set"] = _merge_table_rows(db_track["trackedshape_set"], { - 'trackedshapeattributeval_set': [ - 'trackedshapeattributeval__value', - 'trackedshapeattributeval__spec_id', - 'trackedshapeattributeval__id', - ] - }, 'id') - - # A result table can consist many equal rows for track/shape attributes - # We need filter unique attributes manually - db_track["labeledtrackattributeval_set"] = list(set(db_track["labeledtrackattributeval_set"])) - self._extend_attributes(db_track.labeledtrackattributeval_set, - self.db_attributes[db_track.label_id]["immutable"].values()) - - default_attribute_values = self.db_attributes[db_track.label_id]["mutable"].values() - for db_shape in db_track["trackedshape_set"]: - db_shape["trackedshapeattributeval_set"] = list( - set(db_shape["trackedshapeattributeval_set"]) - ) - # in case of trackedshapes need to interpolate attriute values and extend it - # by previous shape attribute values (not default values) - self._extend_attributes(db_shape["trackedshapeattributeval_set"], default_attribute_values) - default_attribute_values = db_shape["trackedshapeattributeval_set"] - - - serializer = serializers.LabeledTrackSerializer(db_tracks, many=True) - self.ir_data.tracks = serializer.data - - def _init_version_from_db(self): - db_commit = self.db_job.commits.last() - self.ir_data.version = db_commit.version if db_commit else 0 - - def init_from_db(self): - self._init_tags_from_db() - self._init_shapes_from_db() - self._init_tracks_from_db() - self._init_version_from_db() - - @property - def data(self): - return self.ir_data.data - - def upload(self, annotation_file, loader): - annotation_importer = Annotation( - annotation_ir=self.ir_data, - db_task=self.db_job.segment.task, - create_callback=self.create, - ) - self.delete() - db_format = loader.annotation_format - with open(annotation_file, 'rb') as file_object: - source_code = open(os.path.join(settings.BASE_DIR, db_format.handler_file.name)).read() - global_vars = globals() - imports = import_modules(source_code) - global_vars.update(imports) - - execute_python_code(source_code, global_vars) - - global_vars["file_object"] = file_object - global_vars["annotations"] = annotation_importer - - execute_python_code("{}(file_object, annotations)".format(loader.handler), global_vars) - self.create(annotation_importer.data.slice(self.start_frame, self.stop_frame).serialize()) - -class TaskAnnotation: - def __init__(self, pk, user): - self.user = user - self.db_task = models.Task.objects.prefetch_related("data__images").get(id=pk) - - # Postgres doesn't guarantee an order by default without explicit order_by - self.db_jobs = models.Job.objects.select_related("segment").filter(segment__task_id=pk).order_by('id') - self.ir_data = AnnotationIR() - - def reset(self): - self.ir_data.reset() - - def _patch_data(self, data, action): - _data = data if isinstance(data, AnnotationIR) else AnnotationIR(data) - splitted_data = {} - jobs = {} - for db_job in self.db_jobs: - jid = db_job.id - start = db_job.segment.start_frame - stop = db_job.segment.stop_frame - jobs[jid] = { "start": start, "stop": stop } - splitted_data[jid] = _data.slice(start, stop) - - for jid, job_data in splitted_data.items(): - _data = AnnotationIR() - if action is None: - _data.data = put_job_data(jid, self.user, job_data) - else: - _data.data = patch_job_data(jid, self.user, job_data, action) - if _data.version > self.ir_data.version: - self.ir_data.version = _data.version - self._merge_data(_data, jobs[jid]["start"], self.db_task.overlap) - - def _merge_data(self, data, start_frame, overlap): - data_manager = DataManager(self.ir_data) - data_manager.merge(data, start_frame, overlap) - - def put(self, data): - self._patch_data(data, None) - - def create(self, data): - self._patch_data(data, PatchAction.CREATE) - - def update(self, data): - self._patch_data(data, PatchAction.UPDATE) - - def delete(self, data=None): - if data: - self._patch_data(data, PatchAction.DELETE) - else: - for db_job in self.db_jobs: - delete_job_data(db_job.id, self.user) - - def init_from_db(self): - self.reset() - - for db_job in self.db_jobs: - annotation = JobAnnotation(db_job.id, self.user) - annotation.init_from_db() - if annotation.ir_data.version > self.ir_data.version: - self.ir_data.version = annotation.ir_data.version - db_segment = db_job.segment - start_frame = db_segment.start_frame - overlap = self.db_task.overlap - self._merge_data(annotation.ir_data, start_frame, overlap) - - def dump(self, filename, dumper, scheme, host): - anno_exporter = Annotation( - annotation_ir=self.ir_data, - db_task=self.db_task, - scheme=scheme, - host=host, - ) - db_format = dumper.annotation_format - - with open(filename, 'wb') as dump_file: - source_code = open(os.path.join(settings.BASE_DIR, db_format.handler_file.name)).read() - global_vars = globals() - imports = import_modules(source_code) - global_vars.update(imports) - execute_python_code(source_code, global_vars) - global_vars["file_object"] = dump_file - global_vars["annotations"] = anno_exporter - - execute_python_code("{}(file_object, annotations)".format(dumper.handler), global_vars) - - def upload(self, annotation_file, loader): - annotation_importer = Annotation( - annotation_ir=AnnotationIR(), - db_task=self.db_task, - create_callback=self.create, - ) - self.delete() - db_format = loader.annotation_format - with open(annotation_file, 'rb') as file_object: - source_code = open(os.path.join(settings.BASE_DIR, db_format.handler_file.name)).read() - global_vars = globals() - imports = import_modules(source_code) - global_vars.update(imports) - execute_python_code(source_code, global_vars) - - global_vars["file_object"] = file_object - global_vars["annotations"] = annotation_importer - - execute_python_code("{}(file_object, annotations)".format(loader.handler), global_vars) - self.create(annotation_importer.data.serialize()) - - @property - def data(self): - return self.ir_data.data diff --git a/cvat/apps/engine/migrations/0017_db_redesign_20190221.py b/cvat/apps/engine/migrations/0017_db_redesign_20190221.py index 9c54bacd493..60f6b32218d 100644 --- a/cvat/apps/engine/migrations/0017_db_redesign_20190221.py +++ b/cvat/apps/engine/migrations/0017_db_redesign_20190221.py @@ -4,7 +4,7 @@ from django.db import migrations, models import django.db.models.deletion from django.conf import settings -from cvat.apps.engine.annotation import _merge_table_rows +from cvat.apps.dataset_manager.task import _merge_table_rows # some modified functions to transer annotation def _bulk_create(db_model, db_alias, objects, flt_param): diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 5c8cd9c052b..20791520149 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -467,3 +467,6 @@ class LogEventSerializer(serializers.Serializer): message = serializers.CharField(max_length=4096, required=False) payload = serializers.DictField(required=False) is_active = serializers.BooleanField() + +class AnnotationFileSerializer(serializers.Serializer): + annotation_file = serializers.FileField() \ No newline at end of file diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index bc763dfe9af..676e7ce9f4e 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -67,7 +67,8 @@ function blurAllElements() { function uploadAnnotation(jobId, shapeCollectionModel, historyModel, annotationSaverModel, uploadAnnotationButton, format) { - $('#annotationFileSelector').attr('accept', `.${format.format}`); + $('#annotationFileSelector').attr('accept', + format.ext.split(',').map(x => '.' + x.trimStart()).join(', ')); $('#annotationFileSelector').one('change', async (changedFileEvent) => { const file = changedFileEvent.target.files['0']; changedFileEvent.target.value = ''; @@ -76,7 +77,7 @@ function uploadAnnotation(jobId, shapeCollectionModel, historyModel, annotationS const annotationData = new FormData(); annotationData.append('annotation_file', file); try { - await uploadJobAnnotationRequest(jobId, annotationData, format.display_name); + await uploadJobAnnotationRequest(jobId, annotationData, format.name); historyModel.empty(); shapeCollectionModel.empty(); const data = await $.get(`/api/v1/jobs/${jobId}/annotations`); @@ -403,21 +404,19 @@ function setupMenu(job, task, shapeCollectionModel, const loaders = {}; - for (const format of annotationFormats) { - for (const dumper of format.dumpers) { - const item = $(`