提交 0efd665f 编写于 作者: B Boris Sekachev 提交者: Nikita Manovich

Saving of annotations on the server (#561)

* Annotation saver

* Removed extra code

* Eslint fixes
上级 2b619f1e
......@@ -49,5 +49,6 @@
"no-useless-constructor": 0,
"func-names": [0],
"valid-typeof": [0],
"no-console": [0], // this rule deprecates console.log, console.warn etc. because "it is not good in production code"
},
};
......@@ -47,11 +47,15 @@
return labelAccumulator;
}, {});
this.empty();
this.shapes = {}; // key is frame
this.tags = {}; // key is frame
this.tracks = [];
this.objects = {}; // key is client id
this.count = 0;
this.flush = false;
}
import(data) {
this.empty();
const injection = {
labels: this.labels,
};
......@@ -142,16 +146,21 @@
this.objects[clientID] = trackModel;
}
}
return this;
}
export() {
const data = {
tracks: Object.values(this.tracks).reduce((accumulator, value) => {
tracks: this.tracks.map(track => track.toJSON()),
shapes: Object.values(this.shapes).reduce((accumulator, value) => {
accumulator.push(...value);
return accumulator;
}, []).map(track => track.toJSON()),
shapes: this.shapes.map(shape => shape.toJSON()),
tags: this.shapes.map(tag => tag.toJSON()),
}, []).map(shape => shape.toJSON()),
tags: Object.values(this.tags).reduce((accumulator, value) => {
accumulator.push(...value);
return accumulator;
}, []).map(tag => tag.toJSON()),
};
return data;
......@@ -163,6 +172,8 @@
this.tracks = [];
this.objects = {}; // by id
this.count = 0;
this.flush = true;
}
get(frame) {
......
......@@ -105,6 +105,7 @@
// Method is used to export data to the server
toJSON() {
return {
clientID: this.clientID,
occluded: this.occluded,
z_order: this.zOrder,
points: [...this.points],
......@@ -264,9 +265,11 @@
// Method is used to export data to the server
toJSON() {
return {
occluded: this.occluded,
z_order: this.zOrder,
points: [...this.points],
clientID: this.clientID,
id: this.serverID,
frame: this.frame,
label_id: this.label.id,
group: this.group,
attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => {
attributeAccumulator.push({
spec_id: attrId,
......@@ -275,19 +278,14 @@
return attributeAccumulator;
}, []),
id: this.serverID,
frame: this.frame,
label_id: this.label.id,
group: this.group,
shapes: Object.keys(this.shapes).reduce((shapesAccumulator, frame) => {
shapesAccumulator.push({
type: this.type,
type: this.shape,
occluded: this.shapes[frame].occluded,
z_order: this.shapes[frame].zOrder,
points: [...this.shapes[frame].points],
outside: [...this.shapes[frame].outside],
attributes: Object.keys(...this.shapes[frame].attributes)
outside: this.shapes[frame].outside,
attributes: Object.keys(this.shapes[frame].attributes)
.reduce((attributeAccumulator, attrId) => {
attributeAccumulator.push({
spec_id: attrId,
......@@ -607,6 +605,7 @@
// Method is used to export data to the server
toJSON() {
return {
clientID: this.clientID,
id: this.serverID,
frame: this.frame,
label_id: this.label.id,
......
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => {
const serverProxy = require('./server-proxy');
class AnnotationsSaver {
constructor(version, collection, session) {
this.session = session.constructor.name.toLowerCase();
this.id = session.id;
this.version = version;
this.collection = collection;
this.initialObjects = [];
this.hash = this._getHash();
// We need use data from export instead of initialData
// Otherwise we have differ keys order and JSON comparison code incorrect
const exported = this.collection.export();
for (const shape of exported.shapes) {
this.initialObjects[shape.id] = shape;
}
for (const track of exported.tracks) {
this.initialObjects[track.id] = track;
}
for (const tag of exported.tags) {
this.initialObjects[tag.id] = tag;
}
}
_getHash() {
const exported = this.collection.export();
return JSON.stringify(exported);
}
async _request(data, action) {
const result = await serverProxy.annotations.updateAnnotations(
this.session,
this.id,
data,
action,
);
return result;
}
async _put(data) {
const result = await this._request(data, 'put');
return result;
}
async _create(created) {
const result = await this._request(created, 'create');
return result;
}
async _update(updated) {
const result = await this._request(updated, 'update');
return result;
}
async _delete(deleted) {
const result = await this._request(deleted, 'delete');
return result;
}
_split(exported) {
const splitted = {
created: {
shapes: [],
tracks: [],
tags: [],
},
updated: {
shapes: [],
tracks: [],
tags: [],
},
deleted: {
shapes: [],
tracks: [],
tags: [],
},
};
// Find created and updated objects
for (const type of Object.keys(exported)) {
for (const object of exported[type]) {
if (object.id in this.initialObjects) {
const exportedHash = JSON.stringify(object);
const initialHash = JSON.stringify(this.initialObjects[object.id]);
if (exportedHash !== initialHash) {
splitted.updated[type].push(object);
}
} else if (typeof (object.id) === 'undefined') {
splitted.created[type].push(object);
} else {
throw window.cvat.exceptions.ScriptingError(
`Id of object is defined "${object.id}"`
+ 'but it absents in initial state',
);
}
}
}
// Now find deleted objects
const indexes = exported.tracks.concat(exported.shapes)
.concat(exported.tags).map(object => object.id);
for (const id of Object.keys(this.initialObjects)) {
if (!indexes.includes(+id)) {
const object = this.initialObjects[id];
let type = null;
if ('shapes' in object) {
type = 'tracks';
} else if ('points' in object) {
type = 'shapes';
} else {
type = 'tags';
}
splitted.deleted[type].push(object);
}
}
return splitted;
}
_updateCreatedObjects(saved, indexes) {
const savedLength = saved.tracks.length
+ saved.shapes.length + saved.tags.length;
const indexesLength = indexes.tracks.length
+ indexes.shapes.length + indexes.tags.length;
if (indexesLength !== savedLength) {
throw window.cvat.exception.ScriptingError(
'Number of indexes is differed by number of saved objects'
+ `${indexesLength} vs ${savedLength}`,
);
}
// Updated IDs of created objects
for (const type of Object.keys(indexes)) {
for (let i = 0; i < indexes[type].length; i++) {
const clientID = indexes[type][i];
this.collection.objects[clientID].serverID = saved[type][i].id;
}
}
}
_receiveIndexes(exported) {
// Receive client indexes before saving
const indexes = {
tracks: exported.tracks.map(track => track.clientID),
shapes: exported.shapes.map(shape => shape.clientID),
tags: exported.tags.map(tag => tag.clientID),
};
// Remove them from the request body
exported.tracks.concat(exported.shapes).concat(exported.tags)
.map((value) => {
delete value.clientID;
return value;
});
return indexes;
}
async save(onUpdate) {
if (typeof onUpdate !== 'function') {
onUpdate = (message) => {
console.log(message);
};
}
try {
const exported = this.collection.export();
const { flush } = this.collection;
if (flush) {
onUpdate('New objects are being saved..');
const indexes = this._receiveIndexes(exported);
const savedData = await this._put(Object.assign({}, exported, {
version: this.version,
}));
this.version = savedData.version;
this.collection.flush = false;
onUpdate('Saved objects are being updated in the client');
this._updateCreatedObjects(savedData, indexes);
onUpdate('Initial state is being updated');
for (const object of savedData.shapes
.concat(savedData.tracks).concat(savedData.tags)) {
this.initialObjects[object.id] = object;
}
} else {
const {
created,
updated,
deleted,
} = this._split(exported);
onUpdate('New objects are being saved..');
const indexes = this._receiveIndexes(created);
const createdData = await this._create(Object.assign({}, created, {
version: this.version,
}));
this.version = createdData.version;
onUpdate('Saved objects are being updated in the client');
this._updateCreatedObjects(createdData, indexes);
onUpdate('Initial state is being updated');
for (const object of createdData.shapes
.concat(createdData.tracks).concat(createdData.tags)) {
this.initialObjects[object.id] = object;
}
onUpdate('Changed objects are being saved..');
this._receiveIndexes(updated);
const updatedData = await this._update(Object.assign({}, updated, {
version: this.version,
}));
this.version = createdData.version;
onUpdate('Initial state is being updated');
for (const object of updatedData.shapes
.concat(updatedData.tracks).concat(updatedData.tags)) {
this.initialObjects[object.id] = object;
}
onUpdate('Changed objects are being saved..');
this._receiveIndexes(deleted);
const deletedData = await this._delete(Object.assign({}, deleted, {
version: this.version,
}));
this._version = deletedData.version;
onUpdate('Initial state is being updated');
for (const object of deletedData.shapes
.concat(deletedData.tracks).concat(deletedData.tags)) {
delete this.initialObjects[object.id];
}
}
} catch (error) {
onUpdate(`Can not save annotations: ${error.message}`);
throw error;
}
}
hasUnsavedChanges() {
return this._getHash() !== this._hash;
}
}
module.exports = AnnotationsSaver;
})();
......@@ -10,6 +10,7 @@
(() => {
const serverProxy = require('./server-proxy');
const Collection = require('./annotations-collection');
const AnnotationsSaver = require('./annotations-saver');
const jobCache = {};
const taskCache = {};
......@@ -17,25 +18,53 @@
async function getJobAnnotations(job, frame, filter) {
if (!(job.id in jobCache)) {
const rawAnnotations = await serverProxy.annotations.getJobAnnotations(job.id);
jobCache[job.id] = new Collection(job.task.labels);
jobCache[job.id].import(rawAnnotations);
const collection = new Collection(job.task.labels).import(rawAnnotations);
const saver = new AnnotationsSaver(rawAnnotations.version, collection, job);
jobCache[job.id] = {
collection,
saver,
};
}
return jobCache[job.id].get(frame, filter);
return jobCache[job.id].collection.get(frame, filter);
}
async function getTaskAnnotations(task, frame, filter) {
if (!(task.id in jobCache)) {
const rawAnnotations = await serverProxy.annotations.getTaskAnnotations(task.id);
taskCache[task.id] = new Collection(task.labels);
taskCache[task.id].import(rawAnnotations);
const collection = new Collection(task.labels).import(rawAnnotations);
const saver = new AnnotationsSaver(rawAnnotations.version, collection, task);
taskCache[task.id] = {
collection,
saver,
};
}
return taskCache[task.id].collection.get(frame, filter);
}
async function saveJobAnnotations(job, onUpdate) {
if (job.id in jobCache) {
await jobCache[job.id].saver.save(onUpdate);
}
// If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it
}
async function saveTaskAnnotations(task, onUpdate) {
if (task.id in taskCache) {
await taskCache[task.id].saver.save(onUpdate);
}
return taskCache[task.id].get(frame, filter);
// If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it
}
module.exports = {
getJobAnnotations,
getTaskAnnotations,
saveJobAnnotations,
saveTaskAnnotations,
};
})();
......@@ -498,6 +498,38 @@
return response.data;
}
// Session is 'task' or 'job'
async function updateAnnotations(session, id, data, action) {
const { backendAPI } = window.cvat.config;
let requestFunc = null;
let url = null;
if (action.toUpperCase() === 'PUT') {
requestFunc = Axios.put.bind(Axios);
url = `${backendAPI}/${session}s/${id}/annotations`;
} else {
requestFunc = Axios.patch.bind(Axios);
url = `${backendAPI}/${session}s/${id}/annotations?action=${action}`;
}
let response = null;
try {
response = await requestFunc(url, JSON.stringify(data), {
proxy: window.cvat.config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
const code = errorData.response ? errorData.response.status : errorData.code;
throw new window.cvat.exceptions.ServerError(
`Could not updated annotations for the ${session} ${id} on the server`,
code,
);
}
return response.data;
}
// Set csrftoken header from browser cookies if it exists
// NodeJS env returns 'undefined'
// So in NodeJS we need login after each run
......@@ -556,6 +588,7 @@
value: Object.freeze({
getTaskAnnotations,
getJobAnnotations,
updateAnnotations,
}),
writable: false,
},
......
......@@ -14,6 +14,8 @@
const {
getJobAnnotations,
getTaskAnnotations,
saveJobAnnotations,
saveTaskAnnotations,
} = require('./annotations');
function buildDublicatedAPI() {
......@@ -93,9 +95,9 @@
.apiWrapper.call(this, logs.value.put, logType, details);
return result;
},
async save() {
async save(onUpdate) {
const result = await PluginRegistry
.apiWrapper.call(this, logs.value.save);
.apiWrapper.call(this, logs.value.save, onUpdate);
return result;
},
},
......@@ -169,13 +171,16 @@
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/**
* Save annotation changes on a server
* Save all changes in annotations on a server
* @method save
* @memberof Session.annotations
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
* @instance
* @async
* @param {function} [onUpdate] saving can be long.
* This callback can be used to notify a user about current progress
* Its argument is a text string
*/
/**
* Remove all annotations from a session
......@@ -508,6 +513,7 @@
this.frames.get.implementation = this.frames.get.implementation.bind(this);
this.annotations.get.implementation = this.annotations.get.implementation.bind(this);
this.annotations.save.implementation = this.annotations.save.implementation.bind(this);
}
/**
......@@ -576,6 +582,10 @@
return annotationsData;
};
Job.prototype.annotations.save.implementation = async function (onUpdate) {
await saveJobAnnotations(this, onUpdate);
};
/**
* Class representing a task
* @memberof module:API.cvat.classes
......@@ -979,6 +989,7 @@
this.frames.get.implementation = this.frames.get.implementation.bind(this);
this.annotations.get.implementation = this.annotations.get.implementation.bind(this);
this.annotations.save.implementation = this.annotations.save.implementation.bind(this);
}
/**
......@@ -1097,6 +1108,10 @@
return annotationsData;
};
Task.prototype.annotations.save.implementation = async function (onUpdate) {
await saveTaskAnnotations(this, onUpdate);
};
module.exports = {
Job,
Task,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册