Merge annotations and dataset_manager apps (#1352)

main
zhiltsov-max 6 years ago committed by GitHub
parent 6566e4aa11
commit 5ab549956f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -21,11 +21,12 @@ before_script:
- mkdir -m a=rwx -p ${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}'
# FIXME: Git package and application name conflict in PATH
- 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 && mv ./reports/coverage/lcov.info ${CONTAINER_COVERAGE_DATA_DIR}'
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
- coveralls --merge=coverage.json

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

@ -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 (<https://github.com/opencv/cvat/pull/1493>)
- 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/<id>/annotations`: parameters are `format`, `filename` (now optional), `action` (optional) (https://github.com/opencv/cvat/pull/1352)
- REST API: removed `dataset/formats`, changed format of `annotation/formats` (https://github.com/opencv/cvat/pull/1352)
- Exported annotations are stored for N hours instead of indefinitely (https://github.com/opencv/cvat/pull/1352)
- Formats: CVAT format now accepts ZIP and XML (https://github.com/opencv/cvat/pull/1352)
- Formats: COCO format now accepts ZIP and JSON (https://github.com/opencv/cvat/pull/1352)
- Formats: most of formats renamed, no extension in title (https://github.com/opencv/cvat/pull/1352)
- Formats: definitions are changed, are not stored in DB anymore (https://github.com/opencv/cvat/pull/1352)
- cvat-core: session.annotations.put() now returns 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

@ -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

@ -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 ``<cvat_origin>/api/swagger`` (default: ``localhost:8080/api/swagger``).
available on ``<cvat_origin>/api/swagger``
(default: ``localhost:8080/api/swagger``).
Swagger documentation is visiable on allowed hostes, Update environement variable in docker-compose.yml file with cvat hosted machine IP or domain name. Example - ``ALLOWED_HOSTS: 'localhost, 127.0.0.1'``)

@ -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": {

@ -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,
};

@ -17,7 +17,7 @@
const {
Loader,
Dumper,
} = require('./annotation-format.js');
} = require('./annotation-formats.js');
const {
ScriptingError,
DataError,

@ -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,

@ -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

@ -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,

@ -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;
};

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

@ -6,84 +6,47 @@ const aboutDummyData = {
"version": "0.5.dev20190516142240"
}
const formatsDummyData = [{
"id": 1,
"dumpers": [
const formatsDummyData = {
"exporters": [
{
"display_name": "CVAT XML 1.1 for videos",
"format": "XML",
"name": "CVAT for video 1.1",
"ext": "XML",
"version": "1.1",
"handler": "dump_as_cvat_interpolation"
},
{
"display_name": "CVAT XML 1.1 for images",
"format": "XML",
"name": "CVAT for images 1.1",
"ext": "XML",
"version": "1.1",
"handler": "dump_as_cvat_annotation"
}
],
"loaders": [
{
"display_name": "CVAT XML 1.1",
"format": "XML",
"version": "1.1",
"handler": "load"
}
],
"name": "CVAT",
"created_date": "2019-08-08T12:18:56.571488+03:00",
"updated_date": "2019-08-08T12:18:56.571533+03:00",
"handler_file": "cvat/apps/annotation/cvat.py",
"owner": null
},
{
"id": 2,
"dumpers": [
},
{
"display_name": "PASCAL VOC ZIP 1.0",
"format": "ZIP",
"name": "PASCAL VOC 1.0",
"ext": "ZIP",
"version": "1.0",
"handler": "dump"
}
],
"loaders": [
},
{
"display_name": "PASCAL VOC ZIP 1.0",
"format": "ZIP",
"name": "YOLO 1.0",
"ext": "ZIP",
"version": "1.0",
"handler": "load"
}
},
],
"name": "PASCAL VOC",
"created_date": "2019-08-08T12:18:56.625025+03:00",
"updated_date": "2019-08-08T12:18:56.625071+03:00",
"handler_file": "cvat/apps/annotation/pascal_voc.py",
"owner": null
},
{
"id": 3,
"dumpers": [
"importers": [
{
"name": "CVAT 1.1",
"ext": "XML, ZIP",
"version": "1.1",
},
{
"display_name": "YOLO ZIP 1.0",
"format": "ZIP",
"name": "PASCAL VOC 1.0",
"ext": "ZIP",
"version": "1.0",
"handler": "dump"
}
],
"loaders": [
},
{
"display_name": "YOLO ZIP 1.0",
"format": "ZIP",
"name": "MYFORMAT 1.0",
"ext": "TXT",
"version": "1.0",
"handler": "load"
}
],
"name": "YOLO",
"created_date": "2019-08-08T12:18:56.667534+03:00",
"updated_date": "2019-08-08T12:18:56.667578+03:00",
"handler_file": "cvat/apps/annotation/yolo.py",
"owner": null
}];
};
const usersDummyData = {
"count": 2,

@ -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"

@ -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": {

@ -15,10 +15,9 @@ export enum FormatsActionTypes {
const formatsActions = {
getFormats: () => createAction(FormatsActionTypes.GET_FORMATS),
getFormatsSuccess: (annotationFormats: any[], datasetFormats: any[]) => (
getFormatsSuccess: (annotationFormats: any) => (
createAction(FormatsActionTypes.GET_FORMATS_SUCCESS, {
annotationFormats,
datasetFormats,
})
),
getFormatsFailed: (error: any) => (
@ -32,14 +31,12 @@ export function getFormatsAsync(): ThunkAction {
return async (dispatch): Promise<void> => {
dispatch(formatsActions.getFormats());
let annotationFormats = null;
let datasetFormats = null;
try {
annotationFormats = await cvat.server.formats();
datasetFormats = await cvat.server.datasetFormats();
dispatch(
formatsActions.getFormatsSuccess(annotationFormats, datasetFormats),
formatsActions.getFormatsSuccess(annotationFormats),
);
} catch (error) {
dispatch(formatsActions.getFormatsFailed(error));

@ -169,7 +169,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch(dumpAnnotation(task, dumper));
const url = await task.annotations.dump(task.name, dumper);
const url = await task.annotations.dump(dumper);
const downloadAnchor = (window.document.getElementById('downloadAnchor') as HTMLAnchorElement);
downloadAnchor.href = url;
downloadAnchor.click();
@ -280,7 +280,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
dispatch(exportDataset(task, exporter));
try {
const url = await task.annotations.exportDataset(exporter.tag);
const url = await task.annotations.exportDataset(exporter.name);
const downloadAnchor = (window.document.getElementById('downloadAnchor') as HTMLAnchorElement);
downloadAnchor.href = url;
downloadAnchor.click();

@ -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,
})

@ -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 {

@ -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,
})

@ -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}

@ -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}

@ -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 {

@ -63,8 +63,7 @@ export interface TasksState {
}
export interface FormatsState {
annotationFormats: any[];
datasetFormats: any[];
annotationFormats: any;
fetching: boolean;
initialized: boolean;
}

@ -1,628 +0,0 @@
<!--lint disable list-item-indent-->
<!--lint disable no-duplicate-headings-->
## 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/
│   ├── <image_name1>.xml
│   ├── <image_name2>.xml
│   └── <image_nameN>.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/
├── <image_name1>.xml
├── <image_name2>.xml
├── <image_nameN>.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. `<filename>2008_004457.jpg</filename>`). 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 `<number>.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_<subset>_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_<subset>_data/image1.jpg
obj_<subset>_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 `<number>.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 <path to VOCdevkit> --set train --year VOC2007 --output_path pascal.tfrecord --label_map_path label_map.pbtxt
```
1. Zip train images
```bash
cat <path to VOCdevkit>/VOC2007/ImageSets/Main/train.txt | while read p; do echo <path to VOCdevkit>/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 <path to pascal.tfrecord> <path to label_map.pbtxt>
```
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/
│   └── <any_subset_name>.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, <skipped>
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)

@ -1,4 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
default_app_config = 'cvat.apps.annotation.apps.AnnotationConfig'

@ -1,3 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT

@ -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))

@ -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)

@ -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

@ -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': (),
},
),
]

@ -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',
),
]

@ -1,3 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT

@ -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

@ -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()

@ -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'),
)

@ -1,3 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT

@ -1,3 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT

@ -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.log import slogger
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 run_inference_thread(tid, model_file, weights_file, labels_mapping, attribut
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:

@ -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 django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
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:

@ -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 @@ class ShapeManager(ObjectManager):
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 @@ class TrackManager(ObjectManager):
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 @@ class TrackManager(ObjectManager):
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 @@ class TrackManager(ObjectManager):
points.append(p.x)
points.append(p.y)
shape = copy.copy(shape)
shape = copy(shape)
shape["points"] = points
return shape
@ -323,8 +445,8 @@ class TrackManager(ObjectManager):
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 @@ class TrackManager(ObjectManager):
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 @@ class TrackManager(ObjectManager):
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 @@ class TrackManager(ObjectManager):
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))

@ -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 @@ class CvatAnnotationsExtractor(datumaro.Extractor):
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 @@ class CvatAnnotationsExtractor(datumaro.Extractor):
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()],
))

@ -0,0 +1,753 @@
<!--lint disable list-item-indent-->
<!--lint disable list-item-spacing-->
<!--lint disable emphasis-marker-->
<!--lint disable maximum-line-length-->
<!--lint disable list-item-spacing-->
# 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<a id="how-to-add"></a>
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<a id="formats" />
### CVAT<a id="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/)<a id="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/
│   ├── <image_name1>.jpg
│   ├── <image_name2>.jpg
│   └── <image_nameN>.jpg
├── Annotations/
│   ├── <image_name1>.xml
│   ├── <image_name2>.xml
│   └── <image_nameN>.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/
├── <image_name1>.xml
├── <image_name2>.xml
└── <image_nameN>.xml
```
It must be possible for CVAT to match the frame name and file name
from annotation `.xml` file (the `filename` tag, e. g.
`<filename>2008_004457.jpg</filename>` ).
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 `<number>.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/
│   └── <any_subset_name>.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/)<a id="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_<subset>_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_<subset>_data/image1.jpg
obj_<subset>_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 `<number>.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)<a id="coco" />
- [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)<a id="tfrecord" />
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 <path to VOCdevkit> --set train --year VOC2007 --output_path pascal.tfrecord --label_map_path label_map.pbtxt
```
1. Zip train images
``` bash
cat <path to VOCdevkit>/VOC2007/ImageSets/Main/train.txt | while read p; do echo <path to VOCdevkit>/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 <path to pascal.tfrecord> <path to label_map.pbtxt>
```
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)<a id="mot" />
#### 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, <skipped>
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)<a id="labelme" />
#### 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)

@ -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)
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)

@ -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 create_xml_dumper(file_object):
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_as_cvat_interpolation(file_object, annotations):
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)

@ -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)

@ -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 @@ class cvat_rest_api_task_images(datumaro.SourceExtractor):
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 @@ class cvat_rest_api_task_images(datumaro.SourceExtractor):
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 @@ class cvat_rest_api_task_images(datumaro.SourceExtractor):
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 @@ class cvat_rest_api_task_images(datumaro.SourceExtractor):
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]

@ -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)

@ -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)

@ -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)

@ -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)
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)

@ -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

@ -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)

@ -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)

@ -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)

File diff suppressed because it is too large Load Diff

@ -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 @@ class _GitImportFix:
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 @@ class TaskExportTest(APITestCase):
"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 @@ class TaskExportTest(APITestCase):
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)

@ -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

@ -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(),
}

@ -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)
- [CVAT 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)
- [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.
@ -543,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

@ -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

@ -4,7 +4,7 @@ import cvat.apps.engine.models
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):

@ -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()

@ -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 = $(`<option>${dumper.display_name}</li>`);
for (const dumper of annotationFormats.exporters) {
const item = $(`<option>${dumper.name}</li>`);
if (!isDefaultFormat(dumper.display_name, window.cvat.job.mode)) {
item.addClass('regular');
}
item.appendTo(downloadButton);
if (!isDefaultFormat(dumper.name, window.cvat.job.mode)) {
item.addClass('regular');
}
for (const loader of format.loaders) {
loaders[loader.display_name] = loader;
$(`<option class="regular">${loader.display_name}</li>`).appendTo(uploadButton);
}
item.appendTo(downloadButton);
}
for (const loader of annotationFormats.importers) {
loaders[loader.name] = loader;
$(`<option class="regular">${loader.name}</li>`).appendTo(uploadButton);
}
downloadButton.on('change', async (e) => {
@ -425,7 +424,7 @@ function setupMenu(job, task, shapeCollectionModel,
downloadButton.prop('value', 'Dump Annotation');
try {
downloadButton.prop('disabled', true);
await dumpAnnotationRequest(task.id, task.name, dumper);
await dumpAnnotationRequest(task.id, dumper);
} catch (error) {
showMessage(error.message);
} finally {

@ -130,14 +130,12 @@ function showOverlay(message) {
return overlayWindow[0];
}
async function dumpAnnotationRequest(tid, taskName, format) {
async function dumpAnnotationRequest(tid, format) {
// URL Router on the server doesn't work correctly with slashes.
// So, we have to replace them on the client side
taskName = taskName.replace(/\//g, '_');
const name = encodeURIComponent(`${tid}_${taskName}`);
return new Promise((resolve, reject) => {
const url = `/api/v1/tasks/${tid}/annotations/${name}`;
let queryString = `format=${format}`;
const url = `/api/v1/tasks/${tid}/annotations`;
let queryString = `format=${encodeURIComponent(format)}`;
async function request() {
$.get(`${url}?${queryString}`)
@ -224,6 +222,6 @@ $(document).ready(() => {
});
function isDefaultFormat(dumperName, taskMode) {
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');
}

File diff suppressed because one or more lines are too long

@ -1,28 +1,86 @@
# Copyright (C) 2018 Intel Corporation
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
# FIXME: Git application and package name clash in tests
class _GitImportFix:
import sys
former_path = sys.path[:]
@classmethod
def apply(cls):
# HACK: fix application and module name clash
# 'git' app is found earlier than a library in the path.
# The clash is introduced by unittest discover
import sys
print('apply')
apps_dir = __file__[:__file__.rfind('/engine/')]
assert 'apps' in apps_dir
try:
sys.path.remove(apps_dir)
except ValueError:
pass
for name in list(sys.modules):
if name.startswith('git.') or name == 'git':
m = sys.modules.pop(name, None)
del m
import git
assert apps_dir not in git.__file__
@classmethod
def restore(cls):
import sys
print('restore')
for name in list(sys.modules):
if name.startswith('git.') or name == 'git':
m = sys.modules.pop(name)
del m
sys.path.insert(0, __file__[:__file__.rfind('/engine/')])
import importlib
importlib.invalidate_caches()
def _setUpModule():
_GitImportFix.apply()
import sys
sys.path.insert(0, __file__[:__file__.rfind('/engine/')])
# def tearDownModule():
# _GitImportFix.restore()
import io
import os
import shutil
from PIL import Image
from io import BytesIO
from enum import Enum
import os.path as osp
import random
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.conf import settings
from django.contrib.auth.models import User, Group
from cvat.apps.engine.models import (Task, Segment, Job, StatusChoice,
AttributeType, Project, Data)
from unittest import mock
import io
import shutil
import tempfile
import xml.etree.ElementTree as ET
from collections import defaultdict
import zipfile
from pycocotools import coco as coco_loader
import tempfile
from collections import defaultdict
from enum import Enum
from glob import glob
from io import BytesIO
from unittest import mock
import av
import numpy as np
from django.conf import settings
from django.contrib.auth.models import Group, User
from PIL import Image
from pycocotools import coco as coco_loader
from rest_framework import status
from rest_framework.test import APIClient, APITestCase
from cvat.apps.engine.models import (AttributeType, Data, Job, Project,
Segment, StatusChoice, Task)
_setUpModule()
def create_db_users(cls):
(group_admin, _) = Group.objects.get_or_create(name="admin")
@ -2448,7 +2506,7 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
def _dump_api_v1_tasks_id_annotations(self, pk, user, query_params=""):
with ForceLogin(user, self.client):
response = self.client.get(
"/api/v1/tasks/{0}/annotations/my_task_{0}?{1}".format(pk, query_params))
"/api/v1/tasks/{0}/annotations{1}".format(pk, query_params))
return response
@ -2470,7 +2528,7 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
return response
def _get_annotation_formats(self, user):
def _get_formats(self, user):
with ForceLogin(user, self.client):
response = self.client.get(
path="/api/v1/server/annotation/formats"
@ -3036,65 +3094,85 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
"shapes": [],
"tracks": [],
}
if annotation_format == "CVAT XML 1.1 for videos":
if annotation_format == "CVAT for video 1.1":
annotations["tracks"] = rectangle_tracks_with_attrs + rectangle_tracks_wo_attrs
elif annotation_format == "CVAT XML 1.1 for images":
elif annotation_format == "CVAT for images 1.1":
annotations["shapes"] = rectangle_shapes_with_attrs + rectangle_shapes_wo_attrs \
+ polygon_shapes_wo_attrs + polygon_shapes_with_attrs
annotations["tags"] = tags_with_attrs + tags_wo_attrs
elif annotation_format == "PASCAL VOC ZIP 1.1":
elif annotation_format == "PASCAL VOC 1.1":
annotations["shapes"] = rectangle_shapes_wo_attrs
annotations["tags"] = tags_wo_attrs
elif annotation_format == "YOLO ZIP 1.1" or \
annotation_format == "TFRecord ZIP 1.0":
elif annotation_format == "YOLO 1.1" or \
annotation_format == "TFRecord 1.0":
annotations["shapes"] = rectangle_shapes_wo_attrs
elif annotation_format == "COCO JSON 1.0":
elif annotation_format == "COCO 1.0":
annotations["shapes"] = polygon_shapes_wo_attrs
elif annotation_format == "MASK ZIP 1.1":
elif annotation_format == "Segmentation mask 1.1":
annotations["shapes"] = rectangle_shapes_wo_attrs + polygon_shapes_wo_attrs
annotations["tracks"] = rectangle_tracks_wo_attrs
elif annotation_format == "MOT ZIP 1.1":
elif annotation_format == "MOT 1.1":
annotations["tracks"] = rectangle_tracks_wo_attrs
elif annotation_format == "LabelMe ZIP 3.0":
elif annotation_format == "LabelMe 3.0":
annotations["shapes"] = rectangle_shapes_with_attrs + \
rectangle_shapes_wo_attrs + \
polygon_shapes_wo_attrs + \
polygon_shapes_with_attrs
elif annotation_format == "Datumaro 1.0":
annotations["shapes"] = rectangle_shapes_with_attrs + \
rectangle_shapes_wo_attrs + \
polygon_shapes_wo_attrs + \
polygon_shapes_with_attrs
annotations["tags"] = tags_with_attrs + tags_wo_attrs
else:
raise Exception("Unknown format {}".format(annotation_format))
return annotations
response = self._get_annotation_formats(annotator)
response = self._get_formats(annotator)
self.assertEqual(response.status_code, HTTP_200_OK)
if annotator is not None:
supported_formats = response.data
data = response.data
else:
supported_formats = [{
"name": "CVAT",
"dumpers": [{
"display_name": "CVAT XML 1.1 for images"
}],
"loaders": [{
"display_name": "CVAT XML 1.1"
}]
}]
self.assertTrue(isinstance(supported_formats, list) and supported_formats)
for annotation_format in supported_formats:
for dumper in annotation_format["dumpers"]:
data = self._get_formats(owner).data
import_formats = data['importers']
export_formats = data['exporters']
self.assertTrue(isinstance(import_formats, list) and import_formats)
self.assertTrue(isinstance(export_formats, list) and export_formats)
import_formats = { v['name'] for v in import_formats }
export_formats = { v['name'] for v in export_formats }
formats = { exp: exp if exp in import_formats else None
for exp in export_formats }
if 'CVAT 1.1' in import_formats:
if 'CVAT for video 1.1' in export_formats:
formats['CVAT for video 1.1'] = 'CVAT 1.1'
if 'CVAT for images 1.1' in export_formats:
formats['CVAT for images 1.1'] = 'CVAT 1.1'
if import_formats ^ export_formats:
# NOTE: this may not be an error, so we should not fail
print("The following import formats have no pair:",
import_formats - export_formats)
print("The following export formats have no pair:",
export_formats - import_formats)
for export_format, import_format in formats.items():
with self.subTest(export_format=export_format,
import_format=import_format):
# 1. create task
task, jobs = self._create_task(owner, assignee)
# 2. add annotation
data = _get_initial_annotation(dumper["display_name"])
data = _get_initial_annotation(export_format)
response = self._put_api_v1_tasks_id_annotations(task["id"], annotator, data)
data["version"] += 1
@ -3103,49 +3181,60 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
# 3. download annotation
response = self._dump_api_v1_tasks_id_annotations(task["id"], annotator,
"format={}".format(dumper["display_name"]))
"?format={}".format(export_format))
self.assertEqual(response.status_code, HTTP_202_ACCEPTED)
response = self._dump_api_v1_tasks_id_annotations(task["id"], annotator,
"format={}".format(dumper["display_name"]))
"?format={}".format(export_format))
self.assertEqual(response.status_code, HTTP_201_CREATED)
response = self._dump_api_v1_tasks_id_annotations(task["id"], annotator,
"action=download&format={}".format(dumper["display_name"]))
"?format={}&action=download".format(export_format))
self.assertEqual(response.status_code, HTTP_200_OK)
# 4. check downloaded data
if response.status_code == status.HTTP_200_OK:
if annotator is not None:
self.assertTrue(response.streaming)
content = io.BytesIO(b"".join(response.streaming_content))
self._check_dump_content(content, task, jobs, data, annotation_format["name"])
self._check_dump_content(content, task, jobs, data, export_format)
content.seek(0)
else:
content = io.BytesIO()
# 5. remove annotation form the task
response = self._delete_api_v1_tasks_id_annotations(task["id"], annotator)
data["version"] += 1
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
# 5. remove annotation form the task
response = self._delete_api_v1_tasks_id_annotations(task["id"], annotator)
data["version"] += 1
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
# 6. upload annotation and check annotation
uploaded_data = {
"annotation_file": content,
}
# 6. upload annotation
if not import_format:
continue
for loader in annotation_format["loaders"]:
if loader["display_name"] == "MASK ZIP 1.1":
continue # can't really predict the result and check
response = self._upload_api_v1_tasks_id_annotations(task["id"], annotator, uploaded_data, "format={}".format(loader["display_name"]))
self.assertEqual(response.status_code, HTTP_202_ACCEPTED)
uploaded_data = {
"annotation_file": content,
}
response = self._upload_api_v1_tasks_id_annotations(
task["id"], annotator, uploaded_data,
"format={}".format(import_format))
self.assertEqual(response.status_code, HTTP_202_ACCEPTED)
response = self._upload_api_v1_tasks_id_annotations(task["id"], annotator, {}, "format={}".format(loader["display_name"]))
self.assertEqual(response.status_code, HTTP_201_CREATED)
response = self._upload_api_v1_tasks_id_annotations(
task["id"], annotator, {},
"format={}".format(import_format))
self.assertEqual(response.status_code, HTTP_201_CREATED)
# 7. check annotation
if import_format == "Segmentation mask 1.1":
continue # can't really predict the result to check
response = self._get_api_v1_tasks_id_annotations(task["id"], annotator)
self.assertEqual(response.status_code, HTTP_200_OK)
response = self._get_api_v1_tasks_id_annotations(task["id"], annotator)
self.assertEqual(response.status_code, HTTP_200_OK)
data["version"] += 2 # upload is delete + put
self._check_response(response, data)
if annotator is None:
continue
data["version"] += 2 # upload is delete + put
self._check_response(response, data)
def _check_dump_content(self, content, task, jobs, data, annotation_format_name):
def _check_dump_content(self, content, task, jobs, data, format_name):
def etree_to_dict(t):
d = {t.tag: {} if t.attrib else None}
children = list(t)
@ -3164,26 +3253,33 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
d[t.tag] = text
return d
if annotation_format_name == "CVAT":
xmldump = ET.fromstring(content.read())
self.assertEqual(xmldump.tag, "annotations")
tags = xmldump.findall("./meta")
self.assertEqual(len(tags), 1)
meta = etree_to_dict(tags[0])["meta"]
self.assertEqual(meta["task"]["name"], task["name"])
elif annotation_format_name == "PASCAL VOC":
if format_name in {"CVAT for video 1.1", "CVAT for images 1.1"}:
with tempfile.TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(content).extractall(tmp_dir)
xmls = glob(osp.join(tmp_dir, '**', '*.xml'), recursive=True)
self.assertTrue(xmls)
for xml in xmls:
xmlroot = ET.parse(xml).getroot()
self.assertEqual(xmlroot.tag, "annotations")
tags = xmlroot.findall("./meta")
self.assertEqual(len(tags), 1)
meta = etree_to_dict(tags[0])["meta"]
self.assertEqual(meta["task"]["name"], task["name"])
elif format_name == "PASCAL VOC 1.1":
self.assertTrue(zipfile.is_zipfile(content))
elif annotation_format_name == "YOLO":
elif format_name == "YOLO 1.1":
self.assertTrue(zipfile.is_zipfile(content))
elif annotation_format_name == "COCO":
with tempfile.NamedTemporaryFile() as tmp_file:
tmp_file.write(content.read())
tmp_file.flush()
coco = coco_loader.COCO(tmp_file.name)
self.assertTrue(coco.getAnnIds())
elif annotation_format_name == "TFRecord":
elif format_name == "COCO 1.0":
with tempfile.TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(content).extractall(tmp_dir)
jsons = glob(osp.join(tmp_dir, '**', '*.json'), recursive=True)
self.assertTrue(jsons)
for json in jsons:
coco = coco_loader.COCO(json)
self.assertTrue(coco.getAnnIds())
elif format_name == "TFRecord 1.0":
self.assertTrue(zipfile.is_zipfile(content))
elif annotation_format_name == "MASK":
elif format_name == "Segmentation mask 1.1":
self.assertTrue(zipfile.is_zipfile(content))
@ -3234,31 +3330,22 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
]
}"""
response = self._get_annotation_formats(user)
self.assertEqual(response.status_code, status.HTTP_200_OK)
supported_formats = response.data
self.assertTrue(isinstance(supported_formats, list) and supported_formats)
coco_format = None
for f in response.data:
if f["name"] == "COCO":
coco_format = f
break
self.assertTrue(coco_format)
loader = coco_format["loaders"][0]
task, _ = self._create_task(user, user)
content = io.BytesIO(generate_coco_anno())
content.seek(0)
format_name = "COCO 1.0"
uploaded_data = {
"annotation_file": content,
}
response = self._upload_api_v1_tasks_id_annotations(task["id"], user, uploaded_data, "format={}".format(loader["display_name"]))
response = self._upload_api_v1_tasks_id_annotations(
task["id"], user, uploaded_data,
"format={}".format(format_name))
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
response = self._upload_api_v1_tasks_id_annotations(task["id"], user, {}, "format={}".format(loader["display_name"]))
response = self._upload_api_v1_tasks_id_annotations(
task["id"], user, {}, "format={}".format(format_name))
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self._get_api_v1_tasks_id_annotations(task["id"], user)

@ -4,57 +4,51 @@
import os
import os.path as osp
import re
import traceback
import shutil
import traceback
from datetime import datetime
from tempfile import mkstemp
from django.views.generic import RedirectView
import django_rq
from django.conf import settings
from django.contrib.auth.models import User
from django.db import IntegrityError
from django.http import HttpResponse, HttpResponseNotFound
from django.shortcuts import render
from django.conf import settings
from sendfile import sendfile
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer
from rest_framework import status
from rest_framework import viewsets
from rest_framework import serializers
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.generic import RedirectView
from django_filters import rest_framework as filters
from django_filters.rest_framework import DjangoFilterBackend
from drf_yasg import openapi
from drf_yasg.inspectors import CoreAPICompatInspector, NotHandled
from drf_yasg.utils import swagger_auto_schema
from rest_framework import mixins, serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework import mixins
from rest_framework.exceptions import APIException
from django_filters import rest_framework as filters
import django_rq
from django.db import IntegrityError
from django.utils import timezone
from rest_framework.permissions import SAFE_METHODS, IsAuthenticated
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from sendfile import sendfile
from . import annotation, task, models
from cvat.settings.base import JS_3RDPARTY, CSS_3RDPARTY
from cvat.apps.authentication.decorators import login_required
from .log import slogger, clogger
from cvat.apps.engine.models import StatusChoice, Task, Job, Plugin
from cvat.apps.engine.serializers import (TaskSerializer, UserSerializer,
ExceptionSerializer, AboutSerializer, JobSerializer, DataMetaSerializer,
RqStatusSerializer, DataSerializer, LabeledDataSerializer,
PluginSerializer, FileInfoSerializer, LogEventSerializer,
ProjectSerializer, BasicUserSerializer)
from cvat.apps.annotation.serializers import AnnotationFileSerializer, AnnotationFormatSerializer
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
import cvat.apps.dataset_manager as dm
import cvat.apps.dataset_manager.views # pylint: disable=unused-import
from cvat.apps.authentication import auth
from rest_framework.permissions import SAFE_METHODS
from cvat.apps.annotation.models import AnnotationDumper, AnnotationLoader
from cvat.apps.annotation.format import get_annotation_formats
from cvat.apps.authentication.decorators import login_required
from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer
from cvat.apps.engine.frame_provider import FrameProvider
import cvat.apps.dataset_manager.task as DatumaroTask
from cvat.apps.engine.models import Job, Plugin, StatusChoice, Task
from cvat.apps.engine.serializers import (
AboutSerializer, AnnotationFileSerializer, BasicUserSerializer,
DataMetaSerializer, DataSerializer, ExceptionSerializer,
FileInfoSerializer, JobSerializer, LabeledDataSerializer,
LogEventSerializer, PluginSerializer, ProjectSerializer,
RqStatusSerializer, TaskSerializer, UserSerializer)
from cvat.settings.base import CSS_3RDPARTY, JS_3RDPARTY
from . import models, task
from .log import clogger, slogger
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from django.utils.decorators import method_decorator
from drf_yasg.inspectors import NotHandled, CoreAPICompatInspector
from django_filters.rest_framework import DjangoFilterBackend
# drf-yasg component doesn't handle correctly URL_FORMAT_OVERRIDE and
# send requests with ?format=openapi suffix instead of ?scheme=openapi.
@ -207,19 +201,12 @@ class ServerViewSet(viewsets.ViewSet):
status=status.HTTP_400_BAD_REQUEST)
@staticmethod
@swagger_auto_schema(method='get', operation_summary='Method provides the list of available annotations formats supported by the server',
responses={'200': AnnotationFormatSerializer(many=True)})
@swagger_auto_schema(method='get', operation_summary='Method provides the list of supported annotations formats',
responses={'200': DatasetFormatsSerializer()})
@action(detail=False, methods=['GET'], url_path='annotation/formats')
def annotation_formats(request):
data = get_annotation_formats()
return Response(data)
@staticmethod
@action(detail=False, methods=['GET'], url_path='dataset/formats')
def dataset_formats(request):
data = DatumaroTask.get_export_formats()
data = JSONRenderer().render(data)
return Response(data)
data = dm.views.get_all_formats()
return Response(DatasetFormatsSerializer(data).data)
class ProjectFilter(filters.FilterSet):
name = filters.CharFilter(field_name="name", lookup_expr="icontains")
@ -470,8 +457,35 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
slogger.task[pk].error(msg, exc_info=True)
return Response(data=msg + '\n' + str(e), status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema(method='get', operation_summary='Method returns annotations for a specific task')
@swagger_auto_schema(method='put', operation_summary='Method performs an update of all annotations in a specific task')
@swagger_auto_schema(method='get', operation_summary='Method allows to download task annotations',
manual_parameters=[
openapi.Parameter('format', openapi.IN_QUERY,
description="Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats",
type=openapi.TYPE_STRING, required=False),
openapi.Parameter('filename', openapi.IN_QUERY,
description="Desired output file name",
type=openapi.TYPE_STRING, required=False),
openapi.Parameter('action', in_=openapi.IN_QUERY,
description='Used to start downloading process after annotation file had been created',
type=openapi.TYPE_STRING, required=False, enum=['download'])
],
responses={
'202': openapi.Response(description='Dump of annotations has been started'),
'201': openapi.Response(description='Annotations file is ready to download'),
'200': openapi.Response(description='Download of file started')
}
)
@swagger_auto_schema(method='put', operation_summary='Method allows to upload task annotations',
manual_parameters=[
openapi.Parameter('format', openapi.IN_QUERY,
description="Input format name\nYou can get the list of supported formats at:\n/server/annotation/formats",
type=openapi.TYPE_STRING, required=False),
],
responses={
'202': openapi.Response(description='Uploading has been started'),
'201': openapi.Response(description='Uploading has finished'),
}
)
@swagger_auto_schema(method='patch', operation_summary='Method performs a partial update of annotations in a specific task',
manual_parameters=[openapi.Parameter('action', in_=openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING,
enum=['create', 'update', 'delete'])])
@ -479,116 +493,54 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
@action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'],
serializer_class=LabeledDataSerializer)
def annotations(self, request, pk):
self.get_object() # force to call check_object_permissions
db_task = self.get_object() # force to call check_object_permissions
if request.method == 'GET':
data = annotation.get_task_data(pk, request.user)
serializer = LabeledDataSerializer(data=data)
if serializer.is_valid(raise_exception=True):
return Response(serializer.data)
format_name = request.query_params.get('format')
if format_name:
return _export_annotations(db_task=db_task,
rq_id="/api/v1/tasks/{}/annotations/{}".format(pk, format_name),
request=request,
action=request.query_params.get("action", "").lower(),
callback=dm.views.export_task_annotations,
format_name=format_name,
filename=request.query_params.get("filename", "").lower(),
)
else:
data = dm.task.get_task_data(pk)
serializer = LabeledDataSerializer(data=data)
if serializer.is_valid(raise_exception=True):
return Response(serializer.data)
elif request.method == 'PUT':
if request.query_params.get("format", ""):
return load_data_proxy(
format_name = request.query_params.get('format')
if format_name:
return _import_annotations(
request=request,
rq_id="{}@/api/v1/tasks/{}/annotations/upload".format(request.user, pk),
rq_func=annotation.load_task_data,
rq_func=dm.task.import_task_annotations,
pk=pk,
format_name=format_name,
)
else:
serializer = LabeledDataSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
data = annotation.put_task_data(pk, request.user, serializer.data)
data = dm.task.put_task_data(pk, serializer.data)
return Response(data)
elif request.method == 'DELETE':
annotation.delete_task_data(pk, request.user)
dm.task.delete_task_data(pk)
return Response(status=status.HTTP_204_NO_CONTENT)
elif request.method == 'PATCH':
action = self.request.query_params.get("action", None)
if action not in annotation.PatchAction.values():
if action not in dm.task.PatchAction.values():
raise serializers.ValidationError(
"Please specify a correct 'action' for the request")
serializer = LabeledDataSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
try:
data = annotation.patch_task_data(pk, request.user, serializer.data, action)
data = dm.task.patch_task_data(pk, serializer.data, action)
except (AttributeError, IntegrityError) as e:
return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST)
return Response(data)
@swagger_auto_schema(method='get', operation_summary='Method allows to download annotations as a file',
manual_parameters=[openapi.Parameter('filename', openapi.IN_PATH, description="A name of a file with annotations",
type=openapi.TYPE_STRING, required=True),
openapi.Parameter('format', openapi.IN_QUERY, description="A name of a dumper\nYou can get annotation dumpers from this API:\n/server/annotation/formats",
type=openapi.TYPE_STRING, required=True),
openapi.Parameter('action', in_=openapi.IN_QUERY, description='Used to start downloading process after annotation file had been created',
required=False, enum=['download'], type=openapi.TYPE_STRING)],
responses={'202': openapi.Response(description='Dump of annotations has been started'),
'201': openapi.Response(description='Annotations file is ready to download'),
'200': openapi.Response(description='Download of file started')})
@action(detail=True, methods=['GET'], serializer_class=None,
url_path='annotations/(?P<filename>[^/]+)')
def dump(self, request, pk, filename):
"""
Dump of annotations in common case is a long process which cannot be performed within one request.
First request starts dumping process. When the file is ready (code 201) you can get it with query parameter action=download.
"""
filename = re.sub(r'[\\/*?:"<>|]', '_', filename)
username = request.user.username
db_task = self.get_object() # call check_object_permissions as well
timestamp = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
action = request.query_params.get("action")
if action not in [None, "download"]:
raise serializers.ValidationError(
"Please specify a correct 'action' for the request")
dump_format = request.query_params.get("format", "")
try:
db_dumper = AnnotationDumper.objects.get(display_name=dump_format)
except ObjectDoesNotExist:
raise serializers.ValidationError(
"Please specify a correct 'format' parameter for the request")
file_path = os.path.join(db_task.get_task_artifacts_dirname(),
"{}.{}.{}.{}".format(filename, username, timestamp, db_dumper.format.lower()))
queue = django_rq.get_queue("default")
rq_id = "{}@/api/v1/tasks/{}/annotations/{}/{}".format(username, pk, dump_format, filename)
rq_job = queue.fetch_job(rq_id)
if rq_job:
if rq_job.is_finished:
if not rq_job.meta.get("download"):
if action == "download":
rq_job.meta[action] = True
rq_job.save_meta()
return sendfile(request, rq_job.meta["file_path"], attachment=True,
attachment_filename="{}.{}".format(filename, db_dumper.format.lower()))
else:
return Response(status=status.HTTP_201_CREATED)
else: # Remove the old dump file
try:
os.remove(rq_job.meta["file_path"])
except OSError:
pass
finally:
rq_job.delete()
elif rq_job.is_failed:
exc_info = str(rq_job.exc_info)
rq_job.delete()
return Response(data=exc_info, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
else:
return Response(status=status.HTTP_202_ACCEPTED)
rq_job = queue.enqueue_call(
func=annotation.dump_task_data,
args=(pk, request.user, file_path, db_dumper,
request.scheme, request.get_host()),
job_id=rq_id,
)
rq_job.meta["file_path"] = file_path
rq_job.save_meta()
return Response(status=status.HTTP_202_ACCEPTED)
@swagger_auto_schema(method='get', operation_summary='When task is being created the method returns information about a status of the creation process')
@action(detail=True, methods=['GET'], serializer_class=RqStatusSerializer)
def status(self, request, pk):
@ -644,75 +596,36 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
return Response(serializer.data)
@swagger_auto_schema(method='get', operation_summary='Export task as a dataset in a specific format',
manual_parameters=[openapi.Parameter('action', in_=openapi.IN_QUERY,
required=False, type=openapi.TYPE_STRING, enum=['download']),
openapi.Parameter('format', in_=openapi.IN_QUERY, required=False, type=openapi.TYPE_STRING)],
responses={'202': openapi.Response(description='Dump of annotations has been started'),
'201': openapi.Response(description='Annotations file is ready to download'),
'200': openapi.Response(description='Download of file started')})
manual_parameters=[
openapi.Parameter('format', openapi.IN_QUERY,
description="Desired output format name\nYou can get the list of supported formats at:\n/server/annotation/formats",
type=openapi.TYPE_STRING, required=True),
openapi.Parameter('filename', openapi.IN_QUERY,
description="Desired output file name",
type=openapi.TYPE_STRING, required=False),
openapi.Parameter('action', in_=openapi.IN_QUERY,
description='Used to start downloading process after annotation file had been created',
type=openapi.TYPE_STRING, required=False, enum=['download'])
],
responses={'202': openapi.Response(description='Exporting has been started'),
'201': openapi.Response(description='Output file is ready for downloading'),
'200': openapi.Response(description='Download of file started')
}
)
@action(detail=True, methods=['GET'], serializer_class=None,
url_path='dataset')
def dataset_export(self, request, pk):
db_task = self.get_object()
action = request.query_params.get("action", "")
action = action.lower()
if action not in ["", "download"]:
raise serializers.ValidationError(
"Unexpected parameter 'action' specified for the request")
dst_format = request.query_params.get("format", "")
if not dst_format:
dst_format = DatumaroTask.DEFAULT_FORMAT
dst_format = dst_format.lower()
if dst_format not in [f['tag']
for f in DatumaroTask.get_export_formats()]:
raise serializers.ValidationError(
"Unexpected parameter 'format' specified for the request")
rq_id = "/api/v1/tasks/{}/dataset/{}".format(pk, dst_format)
queue = django_rq.get_queue("default")
rq_job = queue.fetch_job(rq_id)
if rq_job:
last_task_update_time = timezone.localtime(db_task.updated_date)
request_time = rq_job.meta.get('request_time', None)
if request_time is None or request_time < last_task_update_time:
rq_job.cancel()
rq_job.delete()
else:
if rq_job.is_finished:
file_path = rq_job.return_value
if action == "download" and osp.exists(file_path):
rq_job.delete()
timestamp = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
filename = "task_{}-{}-{}.zip".format(
db_task.name, timestamp, dst_format)
return sendfile(request, file_path, attachment=True,
attachment_filename=filename.lower())
else:
if osp.exists(file_path):
return Response(status=status.HTTP_201_CREATED)
elif rq_job.is_failed:
exc_info = str(rq_job.exc_info)
rq_job.delete()
return Response(exc_info,
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
else:
return Response(status=status.HTTP_202_ACCEPTED)
try:
server_address = request.get_host()
except Exception:
server_address = None
ttl = DatumaroTask.CACHE_TTL.total_seconds()
queue.enqueue_call(func=DatumaroTask.export_project,
args=(pk, request.user, dst_format, server_address), job_id=rq_id,
meta={ 'request_time': timezone.localtime() },
result_ttl=ttl, failure_ttl=ttl)
return Response(status=status.HTTP_202_ACCEPTED)
db_task = self.get_object() # force to call check_object_permissions
format_name = request.query_params.get("format", "")
return _export_annotations(db_task=db_task,
rq_id="/api/v1/tasks/{}/dataset/{}".format(pk, format_name),
request=request,
action=request.query_params.get("action", "").lower(),
callback=dm.views.export_task_as_dataset,
format_name=format_name,
filename=request.query_params.get("filename", "").lower(),
)
@method_decorator(name='retrieve', decorator=swagger_auto_schema(operation_summary='Method returns details of a job'))
@method_decorator(name='update', decorator=swagger_auto_schema(operation_summary='Method updates a job by id'))
@ -748,37 +661,38 @@ class JobViewSet(viewsets.GenericViewSet,
def annotations(self, request, pk):
self.get_object() # force to call check_object_permissions
if request.method == 'GET':
data = annotation.get_job_data(pk, request.user)
data = dm.task.get_job_data(pk)
return Response(data)
elif request.method == 'PUT':
if request.query_params.get("format", ""):
return load_data_proxy(
format_name = request.query_params.get("format", "")
if format_name:
return _import_annotations(
request=request,
rq_id="{}@/api/v1/jobs/{}/annotations/upload".format(request.user, pk),
rq_func=annotation.load_job_data,
rq_func=dm.task.import_job_annotations,
pk=pk,
format_name=format_name
)
else:
serializer = LabeledDataSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
try:
data = annotation.put_job_data(pk, request.user, serializer.data)
data = dm.task.put_job_data(pk, serializer.data)
except (AttributeError, IntegrityError) as e:
return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST)
return Response(data)
elif request.method == 'DELETE':
annotation.delete_job_data(pk, request.user)
dm.task.delete_job_data(pk)
return Response(status=status.HTTP_204_NO_CONTENT)
elif request.method == 'PATCH':
action = self.request.query_params.get("action", None)
if action not in annotation.PatchAction.values():
if action not in dm.task.PatchAction.values():
raise serializers.ValidationError(
"Please specify a correct 'action' for the request")
serializer = LabeledDataSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
try:
data = annotation.patch_job_data(pk, request.user,
serializer.data, action)
data = dm.task.patch_job_data(pk, serializer.data, action)
except (AttributeError, IntegrityError) as e:
return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST)
return Response(data)
@ -874,19 +788,17 @@ def rq_handler(job, exc_type, exc_value, tb):
# '201': openapi.Response(description='Annotations have been uploaded')},
# tags=['tasks'])
# @api_view(['PUT'])
def load_data_proxy(request, rq_id, rq_func, pk):
def _import_annotations(request, rq_id, rq_func, pk, format_name):
queue = django_rq.get_queue("default")
rq_job = queue.fetch_job(rq_id)
upload_format = request.query_params.get("format", "")
if not rq_job:
serializer = AnnotationFileSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
try:
db_parser = AnnotationLoader.objects.get(pk=upload_format)
except ObjectDoesNotExist:
if format_name not in \
[f.DISPLAY_NAME for f in dm.views.get_import_formats()]:
raise serializers.ValidationError(
"Please specify a correct 'format' parameter for the upload request")
"Unknown input format '{}'".format(format_name))
anno_file = serializer.validated_data['annotation_file']
fd, filename = mkstemp(prefix='cvat_{}'.format(pk))
@ -895,7 +807,7 @@ def load_data_proxy(request, rq_id, rq_func, pk):
f.write(chunk)
rq_job = queue.enqueue_call(
func=rq_func,
args=(pk, request.user, filename, db_parser),
args=(pk, filename, format_name),
job_id=rq_id
)
rq_job.meta['tmp_file'] = filename
@ -915,3 +827,60 @@ def load_data_proxy(request, rq_id, rq_func, pk):
return Response(data=exc_info, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(status=status.HTTP_202_ACCEPTED)
def _export_annotations(db_task, rq_id, request, format_name, action, callback, filename):
if action not in {"", "download"}:
raise serializers.ValidationError(
"Unexpected action specified for the request")
if format_name not in [f.DISPLAY_NAME for f in dm.views.get_export_formats()]:
raise serializers.ValidationError(
"Unknown format specified for the request")
queue = django_rq.get_queue("default")
rq_job = queue.fetch_job(rq_id)
if rq_job:
last_task_update_time = timezone.localtime(db_task.updated_date)
request_time = rq_job.meta.get('request_time', None)
if request_time is None or request_time < last_task_update_time:
rq_job.cancel()
rq_job.delete()
else:
if rq_job.is_finished:
file_path = rq_job.return_value
if action == "download" and osp.exists(file_path):
rq_job.delete()
timestamp = datetime.strftime(last_task_update_time,
"%Y_%m_%d_%H_%M_%S")
filename = filename or \
"task_{}-{}-{}{}".format(
db_task.name, timestamp,
format_name, osp.splitext(file_path)[1])
return sendfile(request, file_path, attachment=True,
attachment_filename=filename.lower())
else:
if osp.exists(file_path):
return Response(status=status.HTTP_201_CREATED)
elif rq_job.is_failed:
exc_info = str(rq_job.exc_info)
rq_job.delete()
return Response(exc_info,
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
else:
return Response(status=status.HTTP_202_ACCEPTED)
try:
if request.scheme:
server_address = request.scheme + '://'
server_address += request.get_host()
except Exception:
server_address = None
ttl = dm.views.CACHE_TTL.total_seconds()
queue.enqueue_call(func=callback,
args=(db_task.id, format_name, server_address), job_id=rq_id,
meta={ 'request_time': timezone.localtime() },
result_ttl=ttl, failure_ttl=ttl)
return Response(status=status.HTTP_202_ACCEPTED)

@ -2,28 +2,27 @@
#
# SPDX-License-Identifier: MIT
from django.db import transaction
from django.utils import timezone
from cvat.apps.engine.log import slogger
from cvat.apps.engine.models import Task, Job, User
from cvat.apps.engine.annotation import dump_task_data
from cvat.apps.engine.plugins import add_plugin
from cvat.apps.git.models import GitStatusChoice
from cvat.apps.annotation.models import AnnotationDumper
from cvat.apps.git.models import GitData
from collections import OrderedDict
import subprocess
import django_rq
import datetime
import shutil
import json
import math
import git
import os
import re
import shutil
import subprocess
from glob import glob
from tempfile import TemporaryDirectory
import django_rq
import git
from django.db import transaction
from django.utils import timezone
from pyunpack import Archive
from cvat.apps.dataset_manager.task import export_task
from cvat.apps.engine.log import slogger
from cvat.apps.engine.models import Job, Task, User
from cvat.apps.engine.plugins import add_plugin
from cvat.apps.git.models import GitData, GitStatusChoice
def _have_no_access_exception(ex):
@ -267,25 +266,30 @@ class Git:
# Dump an annotation
timestamp = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
display_name = "CVAT XML 1.1"
display_name += " for images" if self._task_mode == "annotation" else " for videos"
cvat_dumper = AnnotationDumper.objects.get(display_name=display_name)
if self._task_mode == "annotation":
format_name = "CVAT for images 1.1"
else:
format_name = "CVAT for video 1.1"
dump_name = os.path.join(db_task.get_task_dirname(),
"git_annotation_{}.xml".format(timestamp))
dump_task_data(
pk=self._tid,
user=user,
filename=dump_name,
dumper=cvat_dumper,
scheme=scheme,
host=host,
"git_annotation_{}.zip".format(timestamp))
export_task(
task_id=self._tid,
dst_file=dump_name,
format_name=format_name,
server_url=scheme + host,
save_images=False,
)
ext = os.path.splitext(self._path)[1]
if ext == '.zip':
subprocess.run(args=['7z', 'a', self._annotation_file, dump_name])
shutil.move(dump_name, self._annotation_file)
elif ext == '.xml':
shutil.copyfile(dump_name, self._annotation_file)
with TemporaryDirectory() as tmp_dir:
# TODO: remove extra packing-unpacking
Archive(src_path).extractall(tmp_dir)
anno_paths = glob(osp.join(tmp_dir, '**', '*.xml'),
recursive=True)
shutil.move(anno_paths[0], self._annotation_file)
else:
raise Exception("Got unknown annotation file type")
@ -455,7 +459,7 @@ def update_states():
slogger.glob("Exception occured during a status updating for db_git with tid: {}".format(db_git.task_id))
@transaction.atomic
def _onsave(jid, user, data, action):
def _onsave(jid, data, action):
db_task = Job.objects.select_related('segment__task').get(pk = jid).segment.task
try:
db_git = GitData.objects.select_for_update().get(pk = db_task.id)
@ -493,18 +497,18 @@ def _onsave(jid, user, data, action):
except GitData.DoesNotExist:
pass
def _ondump(tid, user, data_format, scheme, host, plugin_meta_data):
db_task = Task.objects.get(pk = tid)
try:
db_git = GitData.objects.get(pk = db_task)
plugin_meta_data['git'] = OrderedDict({
"url": db_git.url,
"path": db_git.path,
})
except GitData.DoesNotExist:
pass
add_plugin("patch_job_data", _onsave, "after", exc_ok = False)
# TODO: Append git repository into dump file
# def _ondump(task_id, dst_file, format_name,
# server_url=None, save_images=False, plugin_meta_data):
# db_task = Task.objects.get(pk = tid)
# try:
# db_git = GitData.objects.get(pk = db_task)
# plugin_meta_data['git'] = OrderedDict({
# "url": db_git.url,
# "path": db_git.path,
# })
# except GitData.DoesNotExist:
# pass
# add_plugin("_dump", _ondump, "before", exc_ok = False)

@ -1,5 +1,5 @@
# Copyright (C) 2018-2019 Intel Corporation
# Copyright (C) 2018-2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
@ -7,9 +7,9 @@ from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
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
@ -203,7 +203,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('tf annotation for task {} done'.format(tid))
except Exception as ex:
try:

@ -93,12 +93,11 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'cvat.apps.engine',
'cvat.apps.authentication',
'cvat.apps.documentation',
'cvat.apps.git',
'cvat.apps.dataset_manager',
'cvat.apps.annotation',
'cvat.apps.engine',
'cvat.apps.git',
'django_rq',
'compressor',
'cacheops',

@ -443,7 +443,6 @@ def extract_command(args):
if not args.filter:
raise CliException("Expected a filter expression ('-e' argument)")
os.makedirs(dst_dir, exist_ok=False)
dataset.extract_project(save_dir=dst_dir, filter_expr=args.filter,
**filter_args)

@ -6,6 +6,7 @@
import argparse
from datumaro.cli.util import MultilineFormatter
from datumaro.util import to_snake_case
class CliPlugin:
@ -41,16 +42,3 @@ def remove_plugin_type(s):
for t in {'transform', 'extractor', 'converter', 'launcher', 'importer'}:
s = s.replace('_' + t, '')
return s
def to_snake_case(s):
if not s:
return ''
name = [s[0].lower()]
for char in s[1:]:
if char.isalpha() and char.isupper():
name.append('_')
name.append(char.lower())
else:
name.append(char)
return ''.join(name)

@ -3,6 +3,7 @@
#
# SPDX-License-Identifier: MIT
import logging as log
from lxml import etree as ET # NOTE: lxml has proper XPath implementation
from datumaro.components.extractor import (Transform,
Annotation, AnnotationType,
@ -210,7 +211,11 @@ class DatasetItemEncoder:
def XPathDatasetFilter(extractor, xpath=None):
if xpath is None:
return extractor
xpath = ET.XPath(xpath)
try:
xpath = ET.XPath(xpath)
except Exception:
log.error("Failed to create XPath from expression '%s'", xpath)
raise
f = lambda item: bool(xpath(
DatasetItemEncoder.encode(item, extractor.categories())))
return extractor.select(f)
@ -220,7 +225,11 @@ class XPathAnnotationsFilter(Transform):
super().__init__(extractor)
if xpath is not None:
xpath = ET.XPath(xpath)
try:
xpath = ET.XPath(xpath)
except Exception:
log.error("Failed to create XPath from expression '%s'", xpath)
raise
self._filter = xpath
self._remove_empty = remove_empty

@ -749,9 +749,7 @@ class SourceExtractor(Extractor):
self._subset = subset
def subsets(self):
if self._subset:
return [self._subset]
return None
return [self._subset]
def get_subset(self, name):
if name != self._subset:

@ -498,7 +498,7 @@ class ProjectDataset(Dataset):
if not categories[cat_type] == source_cat:
raise NotImplementedError(
"Merging different categories is not implemented yet")
if own_source is not None and len(own_source) != 0:
if own_source is not None and (not categories or len(own_source) != 0):
categories.update(own_source.categories())
self._categories = categories

@ -363,7 +363,7 @@ class _Converter:
class CvatConverter(Converter, CliPlugin):
@classmethod
def build_cmdline_parser(cls, **kwargs):
parser = super().__init__(**kwargs)
parser = super().build_cmdline_parser(**kwargs)
parser.add_argument('--save-images', action='store_true',
help="Save images (default: %(default)s)")
return parser

@ -286,3 +286,27 @@ class DatumaroConverter(Converter, CliPlugin):
def __call__(self, extractor, save_dir):
converter = _Converter(extractor, save_dir, **self._options)
converter.convert()
class DatumaroProjectConverter(Converter):
@classmethod
def build_cmdline_parser(cls, **kwargs):
parser = super().build_cmdline_parser(**kwargs)
parser.add_argument('--save-images', action='store_true',
help="Save images (default: %(default)s)")
return parser
def __init__(self, config=None, save_images=False):
self._config = config
self._save_images = save_images
def __call__(self, extractor, save_dir):
os.makedirs(save_dir, exist_ok=True)
from datumaro.components.project import Project
project = Project.generate(save_dir, config=self._config)
converter = project.env.make_converter('datumaro',
save_images=self._save_images)
converter(extractor, save_dir=osp.join(
project.config.project_dir, project.config.dataset_dir))

@ -42,4 +42,21 @@ def cast(value, type_conv, default=None):
try:
return type_conv(value)
except Exception:
return default
return default
def to_snake_case(s):
if not s:
return ''
name = [s[0].lower()]
for idx, char in enumerate(s[1:]):
idx = idx + 1
if char.isalpha() and char.isupper():
prev_char = s[idx - 1]
if not (prev_char.isalpha() and prev_char.isupper()):
# avoid "HTML" -> "h_t_m_l"
name.append('_')
name.append(char.lower())
else:
name.append(char)
return ''.join(name)

@ -45,20 +45,30 @@ def load_image(path):
assert image.shape[2] in {3, 4}
return image
def save_image(path, image, params=None):
def save_image(path, image, **kwargs):
if not kwargs:
kwargs = {}
if _IMAGE_BACKEND == _IMAGE_BACKENDS.cv2:
import cv2
params = []
ext = path[-4:]
if ext.upper() == '.JPG':
params = [ int(cv2.IMWRITE_JPEG_QUALITY), 75 ]
params = [
int(cv2.IMWRITE_JPEG_QUALITY), kwargs.get('jpeg_quality', 75)
]
image = image.astype(np.uint8)
cv2.imwrite(path, image, params=params)
elif _IMAGE_BACKEND == _IMAGE_BACKENDS.PIL:
from PIL import Image
if not params:
params = {}
params = {}
params['quality'] = kwargs.get('jpeg_quality')
if kwargs.get('jpeg_quality') == 100:
params['subsampling'] = 0
image = image.astype(np.uint8)
if len(image.shape) == 3 and image.shape[2] in {3, 4}:
@ -68,15 +78,22 @@ def save_image(path, image, params=None):
else:
raise NotImplementedError()
def encode_image(image, ext, params=None):
def encode_image(image, ext, **kwargs):
if not kwargs:
kwargs = {}
if _IMAGE_BACKEND == _IMAGE_BACKENDS.cv2:
import cv2
params = []
if not ext.startswith('.'):
ext = '.' + ext
if ext.upper() == '.JPG':
params = [ int(cv2.IMWRITE_JPEG_QUALITY), 75 ]
params = [
int(cv2.IMWRITE_JPEG_QUALITY), kwargs.get('jpeg_quality', 75)
]
image = image.astype(np.uint8)
success, result = cv2.imencode(ext, image, params=params)
@ -89,8 +106,10 @@ def encode_image(image, ext, params=None):
if ext.startswith('.'):
ext = ext[1:]
if not params:
params = {}
params = {}
params['quality'] = kwargs.get('jpeg_quality')
if kwargs.get('jpeg_quality') == 100:
params['subsampling'] = 0
image = image.astype(np.uint8)
if len(image.shape) == 3 and image.shape[2] in {3, 4}:
@ -176,9 +195,9 @@ class Image:
path = ''
self._path = path
assert data is not None or path, "Image can not be empty"
if data is None and path:
if osp.isfile(path):
assert data is not None or path or loader, "Image can not be empty"
if data is None and (path or loader):
if osp.isfile(path) or loader:
data = lazy_image(path, loader=loader, cache=cache)
self._data = data

@ -26,7 +26,7 @@ class ImageOperationsTest(TestCase):
path = osp.join(test_dir, 'img.png') # lossless
image_module._IMAGE_BACKEND = save_backend
image_module.save_image(path, src_image)
image_module.save_image(path, src_image, jpeg_quality=100)
image_module._IMAGE_BACKEND = load_backend
dst_image = image_module.load_image(path)
@ -43,7 +43,8 @@ class ImageOperationsTest(TestCase):
src_image = np.random.randint(0, 255 + 1, (2, 4, c))
image_module._IMAGE_BACKEND = save_backend
buffer = image_module.encode_image(src_image, '.png') # lossless
buffer = image_module.encode_image(src_image, '.png',
jpeg_quality=100) # lossless
image_module._IMAGE_BACKEND = load_backend
dst_image = image_module.decode_image(buffer)

@ -55,8 +55,7 @@ class ImageTest(TestCase):
self.assertEqual((2, 4), image_lazy.size)
self.assertEqual((5, 6), image_eager.size)
@staticmethod
def test_ctors():
def test_ctors(self):
with TestDir() as test_dir:
path = osp.join(test_dir, 'path.png')
image = np.ones([2, 4, 3])
@ -69,11 +68,14 @@ class ImageTest(TestCase):
{ 'data': image, 'path': path, 'loader': load_image, 'size': (2, 4) },
{ 'path': path },
{ 'path': path, 'loader': load_image },
{ 'path': 'somepath', 'loader': lambda p: image },
{ 'loader': lambda p: image },
{ 'path': path, 'size': (2, 4) },
]:
img = Image(**args)
# pylint: disable=pointless-statement
if img.has_data:
img.data
img.size
# pylint: enable=pointless-statement
with self.subTest(**args):
img = Image(**args)
# pylint: disable=pointless-statement
if img.has_data:
img.data
img.size
# pylint: enable=pointless-statement

@ -41,4 +41,4 @@ optional arguments:
- Delete some tasks
`cli.py delete 100 101 102`
- Dump annotations
`cli.py dump --format "CVAT XML 1.1 for images" 103 output.xml`
`cli.py dump --format "CVAT for images 1.1" 103 output.xml`

@ -29,7 +29,7 @@ def main():
config_log(args.loglevel)
with requests.Session() as session:
session.auth = args.auth
api = CVAT_API_V1(args.server_host, args.server_port)
api = CVAT_API_V1('%s:%s' % (args.server_host, args.server_port))
cli = CLI(session, api)
try:
actions[args.action](cli, **args.__dict__)

@ -1,4 +1,7 @@
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
import json
import logging
import os
@ -145,8 +148,8 @@ class CLI():
class CVAT_API_V1():
""" Build parameterized API URLs """
def __init__(self, host, port):
self.base = 'http://{}:{}/api/v1/'.format(host, port)
def __init__(self, host):
self.base = 'http://{}/api/v1/'.format(host)
@property
def tasks(self):
@ -169,5 +172,5 @@ class CVAT_API_V1():
.format(fileformat)
def tasks_id_annotations_filename(self, task_id, name, fileformat):
return self.tasks_id(task_id) + '/annotations/{}?format={}' \
.format(name, fileformat)
return self.tasks_id(task_id) + '/annotations?format={}&filename={}' \
.format(fileformat, name)

@ -212,7 +212,7 @@ dump_parser.add_argument(
'--format',
dest='fileformat',
type=str,
default='CVAT XML 1.1 for images',
default='CVAT for images 1.1',
help='annotation format (default: %(default)s)'
)
@ -238,6 +238,6 @@ upload_parser.add_argument(
'--format',
dest='fileformat',
type=str,
default='CVAT XML 1.1',
default='CVAT 1.1',
help='annotation format (default: %(default)s)'
)

@ -1,25 +1,29 @@
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
import logging
import io
import logging
import os
import sys
import unittest
from django.conf import settings
from PIL import Image
from requests.auth import HTTPBasicAuth
from utils.cli.core import CLI, CVAT_API_V1, ResourceType
from rest_framework.test import APITestCase, RequestsClient
from cvat.apps.engine.tests.test_rest_api import create_db_users
from cvat.apps.engine.tests.test_rest_api import generate_image_file
from PIL import Image
from cvat.apps.engine.tests._test_rest_api import (create_db_users,
generate_image_file)
from utils.cli.core import CLI, CVAT_API_V1, ResourceType
class TestCLI(APITestCase):
class TestCLI(APITestCase):
@unittest.mock.patch('sys.stdout', new_callable=io.StringIO)
def setUp(self, mock_stdout):
self.client = RequestsClient()
self.client.auth = HTTPBasicAuth('admin', 'admin')
self.api = CVAT_API_V1('testserver', '')
self.api = CVAT_API_V1('testserver')
self.cli = CLI(self.client, self.api)
self.taskname = 'test_task'
self.cli.tasks_create(self.taskname,
@ -61,7 +65,7 @@ class TestCLI(APITestCase):
def test_tasks_dump(self):
path = os.path.join(settings.SHARE_ROOT, 'test_cli.xml')
self.cli.tasks_dump(1, 'CVAT XML 1.1 for images', path)
self.cli.tasks_dump(1, 'CVAT for images 1.1', path)
self.assertTrue(os.path.exists(path))
os.remove(path)
@ -132,6 +136,6 @@ class TestCLI(APITestCase):
path = os.path.join(settings.SHARE_ROOT, 'test_cli.json')
with open(path, "wb") as coco:
coco.write(content)
self.cli.tasks_upload(1, 'COCO JSON 1.0', path)
self.cli.tasks_upload(1, 'COCO 1.0', path)
self.assertRegex(self.mock_stdout.getvalue(), '.*{}.*'.format("annotation file"))
os.remove(path)
Loading…
Cancel
Save