Updated dump/upload in cvat-core (#635)

* Annotation formats from the server
* Fixed dump
* Added API tests
* Dashboard integration
main
Boris Sekachev 7 years ago committed by Nikita Manovich
parent bf61962195
commit 3a6a49625c

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -0,0 +1,235 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
(() => {
/**
* Class representing an annotation loader
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Loader {
constructor(initialData) {
const data = {
display_name: initialData.display_name,
format: initialData.format,
handler: initialData.handler,
version: initialData.version,
};
Object.defineProperties(this, {
name: {
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.display_name,
},
format: {
/**
* @name format
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.format,
},
handler: {
/**
* @name handler
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.handler,
},
version: {
/**
* @name version
* @type {string}
* @memberof module:API.cvat.classes.Loader
* @readonly
* @instance
*/
get: () => data.version,
},
});
}
}
/**
* Class representing an annotation dumper
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Dumper {
constructor(initialData) {
const data = {
display_name: initialData.display_name,
format: initialData.format,
handler: initialData.handler,
version: initialData.version,
};
Object.defineProperties(this, {
name: {
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.display_name,
},
format: {
/**
* @name format
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.format,
},
handler: {
/**
* @name handler
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.handler,
},
version: {
/**
* @name version
* @type {string}
* @memberof module:API.cvat.classes.Dumper
* @readonly
* @instance
*/
get: () => data.version,
},
});
}
}
/**
* Class representing an annotation format
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class AnnotationFormat {
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,
};
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
* @readonly
* @instance
*/
get: () => [...data.loaders],
},
dumpers: {
/**
* @name dumpers
* @type {module:API.cvat.classes.Dumper[]}
* @memberof module:API.cvat.classes.AnnotationFormat
* @readonly
* @instance
*/
get: () => [...data.dumpers],
},
});
}
}
module.exports = {
AnnotationFormat,
Loader,
Dumper,
};
})();

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
@ -13,9 +13,14 @@
const AnnotationsSaver = require('./annotations-saver');
const { checkObjectType } = require('./common');
const { Task } = require('./session');
const {
Loader,
Dumper,
} = require('./annotation-format.js');
const {
ScriptingError,
DataError,
ArgumentError,
} = require('./exceptions');
const jobCache = new WeakMap();
@ -190,15 +195,33 @@
);
}
async function uploadAnnotations(session, file, format) {
async function uploadAnnotations(session, file, loader) {
const sessionType = session instanceof Task ? 'task' : 'job';
await serverProxy.annotations.uploadAnnotations(sessionType, session.id, file, format);
if (!(loader instanceof Loader)) {
throw new ArgumentError(
'A loader must be instance of Loader class',
);
}
await serverProxy.annotations.uploadAnnotations(sessionType, session.id, file, loader.name);
}
async function dumpAnnotations(session, name, format) {
async function dumpAnnotations(session, name, dumper) {
if (!(dumper instanceof Dumper)) {
throw new ArgumentError(
'A dumper must be instance of Dumper class',
);
}
let result = null;
const sessionType = session instanceof Task ? 'task' : 'job';
const result = await serverProxy.annotations
.dumpAnnotations(sessionType, session.id, name, format);
if (sessionType === 'job') {
result = await serverProxy.annotations
.dumpAnnotations(session.task.id, name, dumper.name);
} else {
result = await serverProxy.annotations
.dumpAnnotations(session.id, name, dumper.name);
}
return result;
}

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
@ -27,6 +27,7 @@
} = require('./enums');
const User = require('./user');
const { AnnotationFormat } = require('./annotation-format.js');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
@ -44,6 +45,11 @@
return result;
};
cvat.server.formats.implementation = async () => {
const result = await serverProxy.server.formats();
return result.map(el => new AnnotationFormat(el));
};
cvat.server.login.implementation = async (username, password) => {
await serverProxy.server.login(username, password);
};

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
@ -101,6 +101,20 @@ function build() {
.apiWrapper(cvat.server.share, directory);
return result;
},
/**
* Method returns available annotation formats
* @method formats
* @async
* @memberof module:API.cvat.server
* @returns {module:API.cvat.classes.AnnotationFormat[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async formats() {
const result = await PluginRegistry
.apiWrapper(cvat.server.formats);
return result;
},
/**
* Method allows to login on a server
* @method login
@ -218,7 +232,7 @@ function build() {
* @async
* @memberof module:API.cvat.users
* @param {UserFilter} [filter={}] user filter
* @returns {User[]}
* @returns {module:API.cvat.classes.User[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
@ -90,6 +90,25 @@
}
}
async function formats() {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/server/annotation/formats`, {
proxy: config.proxy,
});
} catch (errorData) {
const code = errorData.response ? errorData.response.status : errorData.code;
throw new ServerError(
'Could not get annotation formats from the server',
code,
);
}
return response.data;
}
async function login(username, password) {
function setCookie(response) {
if (response.headers['set-cookie']) {
@ -532,7 +551,7 @@
async function request() {
try {
const response = await Axios
.post(`${backendAPI}/${session}s/${id}/annotations?upload_format=${format}`, annotationData, {
.put(`${backendAPI}/${session}s/${id}/annotations?format=${format}`, annotationData, {
proxy: config.proxy,
});
if (response.status === 202) {
@ -560,7 +579,7 @@
async function dumpAnnotations(id, name, format) {
const { backendAPI } = config;
const filename = name.replace(/\//g, '_');
let url = `${backendAPI}/tasks/${id}/annotations/${filename}?dump_format=${format}`;
let url = `${backendAPI}/tasks/${id}/annotations/${filename}?format=${format}`;
return new Promise((resolve, reject) => {
async function request() {
@ -603,6 +622,7 @@
value: Object.freeze({
about,
share,
formats,
exception,
login,
logout,

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
@ -19,9 +19,9 @@
Object.defineProperties(prototype, {
annotations: Object.freeze({
value: {
async upload(file, format) {
async upload(file, loader) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.upload, file, format);
.apiWrapper.call(this, prototype.annotations.upload, file, loader);
return result;
},
@ -37,9 +37,9 @@
return result;
},
async dump(name, format) {
async dump(name, dumper) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.dump, name, format);
.apiWrapper.call(this, prototype.annotations.dump, name, dumper);
return result;
},
@ -183,7 +183,8 @@
* @method upload
* @memberof Session.annotations
* @param {File} annotations - a text file with annotations
* @param {string} format - a format of the file
* @param {module:API.cvat.classes.Loader} loader - a loader
* which will be used to upload
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
@ -224,10 +225,12 @@
* @method dump
* @memberof Session.annotations
* @param {string} name - a name of a file with annotations
* @param {string} format - a format of the file
* @param {module:API.cvat.classes.Dumper} dumper - a dumper
* 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}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
* @async
*/
@ -1272,13 +1275,13 @@
return result;
};
Job.prototype.annotations.upload.implementation = async function (file, format) {
const result = await uploadAnnotations(this, file, format);
Job.prototype.annotations.upload.implementation = async function (file, loader) {
const result = await uploadAnnotations(this, file, loader);
return result;
};
Job.prototype.annotations.dump.implementation = async function (name, format) {
const result = await dumpAnnotations(this, name, format);
Job.prototype.annotations.dump.implementation = async function (name, dumper) {
const result = await dumpAnnotations(this, name, dumper);
return result;
};
@ -1418,13 +1421,13 @@
return result;
};
Task.prototype.annotations.upload.implementation = async function (file, format) {
const result = await uploadAnnotations(this, file, format);
Task.prototype.annotations.upload.implementation = async function (file, loader) {
const result = await uploadAnnotations(this, file, loader);
return result;
};
Task.prototype.annotations.dump.implementation = async function (name, format) {
const result = await dumpAnnotations(this, name, format);
Task.prototype.annotations.dump.implementation = async function (name, dumper) {
const result = await dumpAnnotations(this, name, dumper);
return result;
};
})();

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/

@ -17,6 +17,11 @@ jest.mock('../../src/server-proxy', () => {
// Initialize api
window.cvat = require('../../src/api');
const {
AnnotationFormat,
Loader,
Dumper,
} = require('../../src/annotation-format');
// Test cases
describe('Feature: get info about cvat', () => {
@ -49,3 +54,43 @@ describe('Feature: get share storage info', () => {
)).rejects.toThrow(window.cvat.exceptions.ServerError);
});
});
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);
}
});
});
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);
}
}
});
});
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);
}
}
});
});

@ -6,6 +6,85 @@ const aboutDummyData = {
"version": "0.5.dev20190516142240"
}
const formatsDummyData = [{
"id": 1,
"dumpers": [
{
"display_name": "CVAT XML 1.1 for videos",
"format": "XML",
"version": "1.1",
"handler": "dump_as_cvat_interpolation"
},
{
"display_name": "CVAT XML 1.1 for images",
"format": "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",
"version": "1.0",
"handler": "dump"
}
],
"loaders": [
{
"display_name": "PASCAL VOC ZIP 1.0",
"format": "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": [
{
"display_name": "YOLO ZIP 1.0",
"format": "ZIP",
"version": "1.0",
"handler": "dump"
}
],
"loaders": [
{
"display_name": "YOLO ZIP 1.0",
"format": "ZIP",
"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,
"next": null,
@ -2445,4 +2524,5 @@ module.exports = {
taskAnnotationsDummyData,
jobAnnotationsDummyData,
frameMetaDummyData,
formatsDummyData,
}

@ -12,6 +12,7 @@
const {
tasksDummyData,
aboutDummyData,
formatsDummyData,
shareDummyData,
usersDummyData,
taskAnnotationsDummyData,
@ -48,6 +49,10 @@ class ServerProxy {
return JSON.parse(JSON.stringify(position));
}
async function formats() {
return JSON.parse(JSON.stringify(formatsDummyData));
}
async function exception() {
return null;
}
@ -239,6 +244,7 @@ class ServerProxy {
value: Object.freeze({
about,
share,
formats,
exception,
login,
logout,

@ -6,8 +6,6 @@
/* global
userConfirm:false
dumpAnnotationRequest:false
uploadTaskAnnotationRequest:false
LabelsInfo:false
showMessage:false
showOverlay:false
@ -85,10 +83,8 @@ class TaskView {
if (file) {
button.text('Uploading..');
button.prop('disabled', true);
const annotationData = new FormData();
annotationData.append('annotation_file', file);
try {
await uploadTaskAnnotationRequest(this._task.id, annotationData, format);
await this._task.annotations.upload(file, format);
} catch (error) {
showMessage(error.message);
} finally {
@ -102,7 +98,12 @@ class TaskView {
async _dump(button, format) {
button.disabled = true;
try {
await dumpAnnotationRequest(this._task.id, this._task.name, format);
const url = await this._task.annotations.dump(this._task.name, format);
const a = document.createElement('a');
a.href = `${url}`;
document.body.appendChild(a);
a.click();
a.remove();
} catch (error) {
showMessage(error.message);
} finally {
@ -139,22 +140,22 @@ class TaskView {
for (const format of this._annotationFormats) {
for (const dumper of format.dumpers) {
const listItem = $(`<li>${dumper.display_name}</li>`).on('click', () => {
const listItem = $(`<li>${dumper.name}</li>`).on('click', () => {
dropdownDownloadMenu.addClass('hidden');
this._dump(downloadButton[0], dumper.display_name);
this._dump(downloadButton[0], dumper);
});
if (isDefaultFormat(dumper.display_name, this._task.mode)) {
if (isDefaultFormat(dumper.name, this._task.mode)) {
listItem.addClass('bold');
}
dropdownDownloadMenu.append(listItem);
}
for (const loader of format.loaders) {
dropdownUploadMenu.append($(`<li>${loader.display_name}</li>`).on('click', () => {
dropdownUploadMenu.append($(`<li>${loader.name}</li>`).on('click', () => {
dropdownUploadMenu.addClass('hidden');
userConfirm('The current annotation will be lost. Are you sure?', () => {
this._upload(uploadButton, loader.display_name);
this._upload(uploadButton, loader);
});
}));
}
@ -728,10 +729,10 @@ window.addEventListener('DOMContentLoaded', () => {
// TODO: Use REST API in order to get meta
$.get('/dashboard/meta'),
$.get(`/api/v1/tasks${window.location.search}`),
$.get('/api/v1/server/annotation/formats'),
window.cvat.server.formats(),
).then((metaData, taskData, annotationFormats) => {
try {
new DashboardView(metaData[0], taskData[0], annotationFormats[0]);
new DashboardView(metaData[0], taskData[0], annotationFormats);
} catch (exception) {
$('#content').empty();
const message = `Can not build CVAT dashboard. Exception: ${exception}.`;

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save