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

CVAT.js implemented API methods and bug fixes (#564)

* Increase/decrease ZOrder

* ZOrder up/down

* Some crutial bugs fixed

* hasUnsavedChanges and merge

* Added API description

* Fixed bugs

* Merge was tested

* New file

* Fixed unit tests

* Fixed small bug which reproduced only after build
上级 3ae8a720
......@@ -17,9 +17,12 @@
PolygonTrack,
PolylineTrack,
PointsTrack,
Track,
Shape,
Tag,
objectStateFactory,
} = require('./annotations-objects');
const { checkObjectType } = require('./common');
const colors = [
'#0066FF', '#AF593E', '#01A368', '#FF861F', '#ED0A3F', '#FF3F34', '#76D7EA',
......@@ -39,6 +42,65 @@
'#006A93', '#867200', '#E2B631', '#D9D6CF',
];
function shapeFactory(shapeData, clientID, injection) {
const { type } = shapeData;
const color = colors[clientID % colors.length];
let shapeModel = null;
switch (type) {
case 'rectangle':
shapeModel = new RectangleShape(shapeData, clientID, color, injection);
break;
case 'polygon':
shapeModel = new PolygonShape(shapeData, clientID, color, injection);
break;
case 'polyline':
shapeModel = new PolylineShape(shapeData, clientID, color, injection);
break;
case 'points':
shapeModel = new PointsShape(shapeData, clientID, color, injection);
break;
default:
throw new window.cvat.exceptions.DataError(
`An unexpected type of shape "${type}"`,
);
}
return shapeModel;
}
function trackFactory(trackData, clientID, injection) {
if (trackData.shapes.length) {
const { type } = trackData.shapes[0];
const color = colors[clientID % colors.length];
let trackModel = null;
switch (type) {
case 'rectangle':
trackModel = new RectangleTrack(trackData, clientID, color, injection);
break;
case 'polygon':
trackModel = new PolygonTrack(trackData, clientID, color, injection);
break;
case 'polyline':
trackModel = new PolylineTrack(trackData, clientID, color, injection);
break;
case 'points':
trackModel = new PointsTrack(trackData, clientID, color, injection);
break;
default:
throw new window.cvat.exceptions.DataError(
`An unexpected type of track "${type}"`,
);
}
return trackModel;
}
console.warn('The track without any shapes had been found. It was ignored.');
return null;
}
class Collection {
constructor(labels) {
......@@ -47,82 +109,23 @@
return labelAccumulator;
}, {});
this.shapes = {}; // key is frame
this.tags = {}; // key is frame
this.shapes = {}; // key is a frame
this.tags = {}; // key is a frame
this.tracks = [];
this.objects = {}; // key is client id
this.objects = {}; // key is a client id
this.count = 0;
this.flush = false;
}
import(data) {
const injection = {
this.collectionZ = {}; // key is a frame
this.injection = {
labels: this.labels,
collectionZ: this.collectionZ,
};
}
function shapeFactory(shapeData, clientID) {
const { type } = shapeData;
const color = colors[clientID % colors.length];
let shapeModel = null;
switch (type) {
case 'rectangle':
shapeModel = new RectangleShape(shapeData, clientID, color, injection);
break;
case 'polygon':
shapeModel = new PolygonShape(shapeData, clientID, color, injection);
break;
case 'polyline':
shapeModel = new PolylineShape(shapeData, clientID, color, injection);
break;
case 'points':
shapeModel = new PointsShape(shapeData, clientID, color, injection);
break;
default:
throw new window.cvat.exceptions.DataError(
`An unexpected type of shape "${type}"`,
);
}
return shapeModel;
}
function trackFactory(trackData, clientID) {
if (trackData.shapes.length) {
const { type } = trackData.shapes[0];
const color = colors[clientID % colors.length];
let trackModel = null;
switch (type) {
case 'rectangle':
trackModel = new RectangleTrack(trackData, clientID, color, injection);
break;
case 'polygon':
trackModel = new PolygonTrack(trackData, clientID, color, injection);
break;
case 'polyline':
trackModel = new PolylineTrack(trackData, clientID, color, injection);
break;
case 'points':
trackModel = new PointsTrack(trackData, clientID, color, injection);
break;
default:
throw new window.cvat.exceptions.DataError(
`An unexpected type of track "${type}"`,
);
}
return trackModel;
}
console.warn('The track without any shapes had been found. It was ignored.');
return null;
}
import(data) {
for (const tag of data.tags) {
const clientID = ++this.count;
const tagModel = new Tag(tag, clientID, injection);
const tagModel = new Tag(tag, clientID, this.injection);
this.tags[tagModel.frame] = this.tags[tagModel.frame] || [];
this.tags[tagModel.frame].push(tagModel);
this.objects[clientID] = tagModel;
......@@ -130,7 +133,7 @@
for (const shape of data.shapes) {
const clientID = ++this.count;
const shapeModel = shapeFactory(shape, clientID);
const shapeModel = shapeFactory(shape, clientID, this.injection);
this.shapes[shapeModel.frame] = this.shapes[shapeModel.frame] || [];
this.shapes[shapeModel.frame].push(shapeModel);
this.objects[clientID] = shapeModel;
......@@ -138,7 +141,7 @@
for (const track of data.tracks) {
const clientID = ++this.count;
const trackModel = trackFactory(track, clientID);
const trackModel = trackFactory(track, clientID, this.injection);
// The function can return null if track doesn't have any shapes.
// In this case a corresponded message will be sent to the console
if (trackModel) {
......@@ -152,15 +155,19 @@
export() {
const data = {
tracks: this.tracks.map(track => track.toJSON()),
shapes: Object.values(this.shapes).reduce((accumulator, value) => {
accumulator.push(...value);
return accumulator;
}, []).map(shape => shape.toJSON()),
tracks: this.tracks.filter(track => !track.removed)
.map(track => track.toJSON()),
shapes: Object.values(this.shapes)
.reduce((accumulator, value) => {
accumulator.push(...value);
return accumulator;
}, []).filter(shape => !shape.removed)
.map(shape => shape.toJSON()),
tags: Object.values(this.tags).reduce((accumulator, value) => {
accumulator.push(...value);
return accumulator;
}, []).map(tag => tag.toJSON()),
}, []).filter(tag => !tag.removed)
.map(tag => tag.toJSON()),
};
return data;
......@@ -186,12 +193,202 @@
const objectStates = [];
for (const object of objects) {
const objectState = objectStateFactory.call(object, frame, object.get(frame));
const stateData = object.get(frame);
if (stateData.outside && !stateData.keyframe) {
continue;
}
const objectState = objectStateFactory.call(object, frame, stateData);
objectStates.push(objectState);
}
return objectStates;
}
merge(objectStates) {
checkObjectType('merged shapes', objectStates, null, Array);
if (!objectStates.length) return;
const objectsForMerge = objectStates.map((state) => {
checkObjectType('object state', state, null, window.cvat.classes.ObjectState);
const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') {
throw new window.cvat.exceptions.ArgumentError(
'The object has not been saved yet. Call ObjectState.save() before you can merge it',
);
}
return object;
});
const keyframes = {}; // frame: position
const { label, shapeType } = objectStates[0];
const labelAttributes = label.attributes.reduce((accumulator, attribute) => {
accumulator[attribute.id] = attribute;
return accumulator;
}, {});
for (let i = 0; i < objectsForMerge.length; i++) {
// For each state get corresponding object
const object = objectsForMerge[i];
const state = objectStates[i];
if (state.label.id !== label.id) {
throw new window.cvat.exceptions.ArgumentError(
`All shape labels are expected to be ${label.name}, but got ${state.label.name}`,
);
}
if (state.shapeType !== shapeType) {
throw new window.cvat.exceptions.ArgumentError(
`All shapes are expected to be ${shapeType}, but got ${state.shapeType}`,
);
}
// If this object is shape, get it position and save as a keyframe
if (object instanceof Shape) {
// Frame already saved and it is not outside
if (object.frame in keyframes && !keyframes[object.frame].outside) {
throw new window.cvat.exceptions.ArgumentError(
'Expected only one visible shape per frame',
);
}
keyframes[object.frame] = {
type: shapeType,
frame: object.frame,
points: [...object.points],
occluded: object.occluded,
zOrder: object.zOrder,
outside: false,
attributes: Object.keys(object.attributes).reduce((accumulator, attrID) => {
// We save only mutable attributes inside a keyframe
if (attrID in labelAttributes && labelAttributes[attrID].mutable) {
accumulator.push({
spec_id: +attrID,
value: object.attributes[attrID],
});
}
return accumulator;
}, []),
};
// Push outside shape after each annotation shape
// Any not outside shape rewrites it
if (!((object.frame + 1) in keyframes)) {
keyframes[object.frame + 1] = JSON
.parse(JSON.stringify(keyframes[object.frame]));
keyframes[object.frame + 1].outside = true;
keyframes[object.frame + 1].frame++;
}
} else if (object instanceof Track) {
// If this object is track, iterate through all its
// keyframes and push copies to new keyframes
const attributes = {}; // id:value
for (const keyframe of Object.keys(object.shapes)) {
const shape = object.shapes[keyframe];
// Frame already saved and it is not outside
if (keyframe in keyframes && !keyframes[keyframe].outside) {
// This shape is outside and non-outside shape already exists
if (shape.outside) {
continue;
}
throw new window.cvat.exceptions.ArgumentError(
'Expected only one visible shape per frame',
);
}
// We do not save an attribute if it has the same value
// We save only updates
let updatedAttributes = false;
for (const attrID in shape.attributes) {
if (!(attrID in attributes)
|| attributes[attrID] !== shape.attributes[attrID]) {
updatedAttributes = true;
attributes[attrID] = shape.attributes[attrID];
}
}
keyframes[keyframe] = {
type: shapeType,
frame: +keyframe,
points: [...shape.points],
occluded: shape.occluded,
outside: shape.outside,
zOrder: shape.zOrder,
attributes: updatedAttributes ? Object.keys(attributes)
.reduce((accumulator, attrID) => {
accumulator.push({
spec_id: +attrID,
value: attributes[attrID],
});
return accumulator;
}, []) : [],
};
}
} else {
throw new window.cvat.exceptions.ArgumentError(
`Trying to merge unknown object type: ${object.constructor.name}. `
+ 'Only shapes and tracks are expected.',
);
}
}
let firstNonOutside = false;
for (const frame of Object.keys(keyframes).sort((a, b) => +a - +b)) {
// Remove all outside frames at the begin
firstNonOutside = firstNonOutside || keyframes[frame].outside;
if (!firstNonOutside && keyframes[frame].outside) {
delete keyframes[frame];
} else {
break;
}
}
const clientID = ++this.count;
const track = {
frame: Math.min.apply(null, Object.keys(keyframes).map(frame => +frame)),
shapes: Object.values(keyframes),
group: 0,
label_id: label.id,
attributes: Object.keys(objectStates[0].attributes)
.reduce((accumulator, attrID) => {
if (!labelAttributes[attrID].mutable) {
accumulator.push({
spec_id: +attrID,
value: objectStates[0].attributes[attrID],
});
}
return accumulator;
}, []),
};
const trackModel = trackFactory(track, clientID, this.injection);
if (trackModel) {
this.tracks.push(trackModel);
this.objects[clientID] = trackModel;
}
// Remove other shapes
for (const object of objectsForMerge) {
object.removed = true;
}
}
split(objectState) {
checkObjectType('object state', objectState, window.cvat.classes.ObjectState, null);
// TODO: split
}
group(array) {
checkObjectType('merged shapes', array, Array, null);
for (const shape of array) {
checkObjectType('object state', shape, window.cvat.classes.ObjectState, null);
}
// TODO:
}
}
module.exports = Collection;
......
......@@ -9,54 +9,22 @@
(() => {
const ObjectState = require('./object-state');
const { checkObjectType } = require('./common');
// Called with the Annotation context
function objectStateFactory(frame, data) {
const objectState = new ObjectState(data);
// Rewrite default implementations of save/delete
objectState.updateInCollection = this.save.bind(this, frame, objectState);
objectState.deleteFromCollection = this.delete.bind(this);
objectState.hidden = {
save: this.save.bind(this, frame, objectState),
delete: this.delete.bind(this),
up: this.up.bind(this, frame, objectState),
down: this.down.bind(this, frame, objectState),
};
return objectState;
}
function checkObjectType(name, value, type, instance) {
if (type) {
if (typeof (value) !== type) {
// specific case for integers which aren't native type in JS
if (type === 'integer' && Number.isInteger(value)) {
return;
}
if (value !== undefined) {
throw new window.cvat.exceptions.ArgumentError(
`Got ${typeof (value)} value for ${name}. `
+ `Expected ${type}`,
);
}
throw new window.cvat.exceptions.ArgumentError(
`Got undefined value for ${name}. `
+ `Expected ${type}`,
);
}
} else if (instance) {
if (!(value instanceof instance)) {
if (value !== undefined) {
throw new window.cvat.exceptions.ArgumentError(
`Got ${value.constructor.name} value for ${name}. `
+ `Expected instance of ${instance.name}`,
);
}
throw new window.cvat.exceptions.ArgumentError(
`Got undefined value for ${name}. `
+ `Expected instance of ${instance.name}`,
);
}
}
}
class Annotation {
constructor(data, clientID, injection) {
this.taskLabels = injection.labels;
......@@ -92,19 +60,73 @@
}
}
class Shape extends Annotation {
class Drawn extends Annotation {
constructor(data, clientID, color, injection) {
super(data, clientID, injection);
this.collectionZ = injection.collectionZ;
const z = this._getZ(this.frame);
z.max = Math.max(z.max, this.zOrder || 0);
z.min = Math.min(z.min, this.zOrder || 0);
this.color = color;
this.shapeType = null;
}
_getZ(frame) {
this.collectionZ[frame] = this.collectionZ[frame] || {
max: 0,
min: 0,
};
return this.collectionZ[frame];
}
save() {
throw window.cvat.exceptions.ScriptingError(
'Is not implemented',
);
}
get() {
throw window.cvat.exceptions.ScriptingError(
'Is not implemented',
);
}
toJSON() {
throw window.cvat.exceptions.ScriptingError(
'Is not implemented',
);
}
// Increase ZOrder within frame
up(frame, objectState) {
const z = this._getZ(frame);
z.max++;
objectState.zOrder = z.max;
}
// Decrease ZOrder within frame
down(frame, objectState) {
const z = this._getZ(frame);
z.min--;
objectState.zOrder = z.min;
}
}
class Shape extends Drawn {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.points = data.points;
this.occluded = data.occluded;
this.zOrder = data.z_order;
this.color = color;
this.shape = null;
}
// Method is used to export data to the server
toJSON() {
return {
type: this.shapeType,
clientID: this.clientID,
occluded: this.occluded,
z_order: this.zOrder,
......@@ -133,9 +155,10 @@
}
return {
type: window.cvat.enums.ObjectType.SHAPE,
shape: this.shape,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: this.shapeType,
clientID: this.clientID,
serverID: this.serverID,
occluded: this.occluded,
lock: this.lock,
zOrder: this.zOrder,
......@@ -232,16 +255,15 @@
}
}
class Track extends Annotation {
class Track extends Drawn {
constructor(data, clientID, color, injection) {
super(data, clientID, injection);
super(data, clientID, color, injection);
this.shapes = data.shapes.reduce((shapeAccumulator, value) => {
shapeAccumulator[value.frame] = {
serverID: value.id,
occluded: value.occluded,
zOrder: value.z_order,
points: value.points,
frame: value.frame,
outside: value.outside,
attributes: value.attributes.reduce((attributeAccumulator, attr) => {
attributeAccumulator[attr.spec_id] = attr.value;
......@@ -249,17 +271,14 @@
}, {}),
};
return shapeAccumulator;
}, {});
const z = this._getZ(value.frame);
z.max = Math.max(z.max, value.z_order);
z.min = Math.min(z.min, value.z_order);
this.attributes = data.attributes.reduce((attributeAccumulator, attr) => {
attributeAccumulator[attr.spec_id] = attr.value;
return attributeAccumulator;
return shapeAccumulator;
}, {});
this.cache = {};
this.color = color;
this.shape = null;
}
// Method is used to export data to the server
......@@ -280,7 +299,7 @@
}, []),
shapes: Object.keys(this.shapes).reduce((shapesAccumulator, frame) => {
shapesAccumulator.push({
type: this.shape,
type: this.shapeType,
occluded: this.shapes[frame].occluded,
z_order: this.shapes[frame].zOrder,
points: [...this.shapes[frame].points],
......@@ -312,9 +331,10 @@
attributes: this.getAttributes(frame),
label: this.label,
group: this.group,
type: window.cvat.enums.ObjectType.TRACK,
shape: this.shape,
objectType: window.cvat.enums.ObjectType.TRACK,
shapeType: this.shapeType,
clientID: this.clientID,
serverID: this.serverID,
lock: this.lock,
color: this.color,
},
......@@ -630,8 +650,9 @@
}
return {
type: window.cvat.enums.ObjectType.TAG,
objectType: window.cvat.enums.ObjectType.TAG,
clientID: this.clientID,
serverID: this.serverID,
lock: this.lock,
attributes: Object.assign({}, this.attributes),
label: this.label,
......@@ -697,7 +718,7 @@
class RectangleShape extends Shape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shape = window.cvat.enums.ObjectShape.RECTANGLE;
this.shapeType = window.cvat.enums.ObjectShape.RECTANGLE;
}
}
......@@ -710,28 +731,28 @@
class PolygonShape extends PolyShape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shape = window.cvat.enums.ObjectShape.POLYGON;
this.shapeType = window.cvat.enums.ObjectShape.POLYGON;
}
}
class PolylineShape extends PolyShape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shape = window.cvat.enums.ObjectShape.POLYLINE;
this.shapeType = window.cvat.enums.ObjectShape.POLYLINE;
}
}
class PointsShape extends PolyShape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shape = window.cvat.enums.ObjectShape.POINTS;
this.shapeType = window.cvat.enums.ObjectShape.POINTS;
}
}
class RectangleTrack extends Track {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shape = window.cvat.enums.ObjectShape.RECTANGLE;
this.shapeType = window.cvat.enums.ObjectShape.RECTANGLE;
}
interpolatePosition(leftPosition, rightPosition, targetFrame) {
......@@ -1142,25 +1163,21 @@
class PolygonTrack extends PolyTrack {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shape = window.cvat.enums.ObjectShape.POLYGON;
this.shapeType = window.cvat.enums.ObjectShape.POLYGON;
}
}
class PolylineTrack extends PolyTrack {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shape = window.cvat.enums.ObjectShape.POLYLINE;
}
appendMapping() {
// TODO after checking how it works with polygons
this.shapeType = window.cvat.enums.ObjectShape.POLYLINE;
}
}
class PointsTrack extends PolyTrack {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shape = window.cvat.enums.ObjectShape.POINTS;
this.shapeType = window.cvat.enums.ObjectShape.POINTS;
}
}
......@@ -1173,6 +1190,8 @@
PolygonTrack,
PolylineTrack,
PointsTrack,
Track,
Shape,
Tag,
objectStateFactory,
};
......
......@@ -12,7 +12,7 @@
class AnnotationsSaver {
constructor(version, collection, session) {
this.session = session.constructor.name.toLowerCase();
this.sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job';
this.id = session.id;
this.version = version;
this.collection = collection;
......@@ -42,7 +42,7 @@
async _request(data, action) {
const result = await serverProxy.annotations.updateAnnotations(
this.session,
this.sessionType,
this.id,
data,
action,
......@@ -249,6 +249,9 @@
delete this.initialObjects[object.id];
}
}
this.hash = this._getHash();
onUpdate('Saving is done');
} catch (error) {
onUpdate(`Can not save annotations: ${error.message}`);
throw error;
......@@ -256,7 +259,7 @@
}
hasUnsavedChanges() {
return this._getHash() !== this._hash;
return this._getHash() !== this.hash;
}
}
......
......@@ -15,56 +15,107 @@
const jobCache = {};
const taskCache = {};
async function getJobAnnotations(job, frame, filter) {
if (!(job.id in jobCache)) {
const rawAnnotations = await serverProxy.annotations.getJobAnnotations(job.id);
const collection = new Collection(job.task.labels).import(rawAnnotations);
const saver = new AnnotationsSaver(rawAnnotations.version, collection, job);
function getCache(sessionType) {
if (sessionType === 'task') {
return taskCache;
}
jobCache[job.id] = {
collection,
saver,
};
if (sessionType === 'job') {
return jobCache;
}
return jobCache[job.id].collection.get(frame, filter);
throw new window.cvat.exceptions.ScriptingError(
`Unknown session type was received ${sessionType}`,
);
}
async function getTaskAnnotations(task, frame, filter) {
if (!(task.id in jobCache)) {
const rawAnnotations = await serverProxy.annotations.getTaskAnnotations(task.id);
const collection = new Collection(task.labels).import(rawAnnotations);
const saver = new AnnotationsSaver(rawAnnotations.version, collection, task);
async function getAnnotations(session, frame, filter) {
const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (!(session.id in cache)) {
const rawAnnotations = await serverProxy.annotations
.getAnnotations(sessionType, session.id);
const collection = new Collection(session.labels || session.task.labels)
.import(rawAnnotations);
const saver = new AnnotationsSaver(rawAnnotations.version, collection, session);
taskCache[task.id] = {
cache[session.id] = {
collection,
saver,
};
}
return taskCache[task.id].collection.get(frame, filter);
return cache[session.id].collection.get(frame, filter);
}
async function saveJobAnnotations(job, onUpdate) {
if (job.id in jobCache) {
await jobCache[job.id].saver.save(onUpdate);
async function saveAnnotations(session, onUpdate) {
const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (session.id in cache) {
await cache[session.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);
function mergeAnnotations(session, objectStates) {
const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (session.id in cache) {
return cache[session.id].collection.merge(objectStates);
}
// If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it
throw window.cvat.exceptions.DataError(
'Collection has not been initialized yet. Call annotations.get() before',
);
}
function splitAnnotations(session, objectState, frame) {
const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (session.id in cache) {
return cache[session.id].collection.split(objectState, frame);
}
throw window.cvat.exceptions.DataError(
'Collection has not been initialized yet. Call annotations.get() before',
);
}
function groupAnnotations(session, objectStates) {
const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (session.id in cache) {
return cache[session.id].collection.group(objectStates);
}
throw window.cvat.exceptions.DataError(
'Collection has not been initialized yet. Call annotations.get() before',
);
}
function hasUnsavedChanges(session) {
const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (session.id in cache) {
return cache[session.id].saver.hasUnsavedChanges();
}
return false;
}
module.exports = {
getJobAnnotations,
getTaskAnnotations,
saveJobAnnotations,
saveTaskAnnotations,
getAnnotations,
saveAnnotations,
hasUnsavedChanges,
mergeAnnotations,
splitAnnotations,
groupAnnotations,
};
})();
......@@ -13,47 +13,13 @@
(() => {
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
function isBoolean(value) {
return typeof (value) === 'boolean';
}
function isInteger(value) {
return typeof (value) === 'number' && Number.isInteger(value);
}
function isEnum(value) {
// Called with specific Enum context
for (const key in this) {
if (Object.prototype.hasOwnProperty.call(this, key)) {
if (this[key] === value) {
return true;
}
}
}
return false;
}
function isString(value) {
return typeof (value) === 'string';
}
function checkFilter(filter, fields) {
for (const prop in filter) {
if (Object.prototype.hasOwnProperty.call(filter, prop)) {
if (!(prop in fields)) {
throw new window.cvat.exceptions.ArgumentError(
`Unsupported filter property has been recieved: "${prop}"`,
);
} else if (!fields[prop](filter[prop])) {
throw new window.cvat.exceptions.ArgumentError(
`Received filter property ${prop} is not satisfied for checker`,
);
}
}
}
}
const {
isBoolean,
isInteger,
isEnum,
isString,
checkFilter,
} = require('./common');
function implementAPI(cvat) {
cvat.plugins.list.implementation = PluginRegistry.list;
......@@ -112,17 +78,19 @@
);
}
let task = null;
let tasks = null;
if ('taskID' in filter) {
task = await serverProxy.tasks.getTasks(`id=${filter.taskID}`);
tasks = await serverProxy.tasks.getTasks(`id=${filter.taskID}`);
} else {
const job = await serverProxy.jobs.getJob(filter.jobID);
task = await serverProxy.tasks.getTasks(`id=${job.task_id}`);
if (typeof (job.task_id) !== 'undefined') {
tasks = await serverProxy.tasks.getTasks(`id=${job.task_id}`);
}
}
// If task was found by its id, then create task instance and get Job instance from it
if (task.length) {
task = new window.cvat.classes.Task(task[0]);
if (tasks !== null && tasks.length) {
const task = new window.cvat.classes.Task(tasks[0]);
return filter.jobID ? task.jobs.filter(job => job.id === filter.jobID) : task.jobs;
}
......
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/
(() => {
function isBoolean(value) {
return typeof (value) === 'boolean';
}
function isInteger(value) {
return typeof (value) === 'number' && Number.isInteger(value);
}
// Called with specific Enum context
function isEnum(value) {
for (const key in this) {
if (Object.prototype.hasOwnProperty.call(this, key)) {
if (this[key] === value) {
return true;
}
}
}
return false;
}
function isString(value) {
return typeof (value) === 'string';
}
function checkFilter(filter, fields) {
for (const prop in filter) {
if (Object.prototype.hasOwnProperty.call(filter, prop)) {
if (!(prop in fields)) {
throw new window.cvat.exceptions.ArgumentError(
`Unsupported filter property has been recieved: "${prop}"`,
);
} else if (!fields[prop](filter[prop])) {
throw new window.cvat.exceptions.ArgumentError(
`Received filter property ${prop} is not satisfied for checker`,
);
}
}
}
}
function checkObjectType(name, value, type, instance) {
if (type) {
if (typeof (value) !== type) {
// specific case for integers which aren't native type in JS
if (type === 'integer' && Number.isInteger(value)) {
return;
}
throw new window.cvat.exceptions.ArgumentError(
`Got "${name}" value of type: "${typeof (value)}". `
+ `Expected "${type}"`,
);
}
} else if (instance) {
if (!(value instanceof instance)) {
if (value !== undefined) {
throw new window.cvat.exceptions.ArgumentError(
`Got ${value.constructor.name} value for ${name}. `
+ `Expected instance of ${instance.name}`,
);
}
throw new window.cvat.exceptions.ArgumentError(
`Got undefined value for ${name}. `
+ `Expected instance of ${instance.name}`,
);
}
}
}
module.exports = {
isBoolean,
isInteger,
isEnum,
isString,
checkFilter,
checkObjectType,
};
})();
......@@ -21,7 +21,7 @@
* Necessary fields: type, shape
* Necessary fields for objects which haven't been added to collection yet: frame
* Optional fields: points, group, zOrder, outside, occluded,
* attributes, lock, label, mode, color, keyframe
* attributes, lock, label, mode, color, keyframe, clientID, serverID
* These fields can be set later via setters
*/
constructor(serialized) {
......@@ -39,9 +39,12 @@
lock: null,
color: null,
clientID: serialized.clientID,
serverID: serialized.serverID,
frame: serialized.frame,
type: serialized.type,
shape: serialized.shape,
objectType: serialized.objectType,
shapeType: serialized.shapeType,
updateFlags: {},
};
......@@ -79,25 +82,45 @@
*/
get: () => data.frame,
},
type: {
objectType: {
/**
* @name type
* @name objectType
* @type {module:API.cvat.enums.ObjectType}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.type,
get: () => data.objectType,
},
shape: {
shapeType: {
/**
* @name shape
* @name shapeType
* @type {module:API.cvat.enums.ObjectShape}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.shape,
get: () => data.shapeType,
},
clientID: {
/**
* @name clientID
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.clientID,
},
serverID: {
/**
* @name serverID
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.serverID,
},
label: {
/**
......@@ -287,7 +310,7 @@
* @instance
* @param {boolean} [force=false] delete object even if it is locked
* @async
* @returns {boolean} wheter object was deleted
* @returns {boolean} true if object has been deleted
* @throws {module:API.cvat.exceptions.PluginError}
*/
async delete(force = false) {
......@@ -295,12 +318,42 @@
.apiWrapper.call(this, ObjectState.prototype.delete, force);
return result;
}
/**
* Set the highest ZOrder within a frame
* @method up
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
*/
async up() {
const result = await PluginRegistry
.apiWrapper.call(this, ObjectState.prototype.up);
return result;
}
/**
* Set the lowest ZOrder within a frame
* @method down
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
*/
async down() {
const result = await PluginRegistry
.apiWrapper.call(this, ObjectState.prototype.down);
return result;
}
}
// Default implementation saves element in collection
ObjectState.prototype.save.implementation = async function () {
if (this.updateInCollection) {
return this.updateInCollection();
if (this.hidden && this.hidden.save) {
return this.hidden.save();
}
return this;
......@@ -308,8 +361,24 @@
// Default implementation do nothing
ObjectState.prototype.delete.implementation = async function (force) {
if (this.deleteFromCollection) {
return this.deleteFromCollection(force);
if (this.hidden && this.hidden.delete) {
return this.hidden.delete(force);
}
return false;
};
ObjectState.prototype.up.implementation = async function () {
if (this.hidden && this.hidden.up) {
return this.hidden.up();
}
return false;
};
ObjectState.prototype.down.implementation = async function () {
if (this.hidden && this.hidden.down) {
return this.hidden.down();
}
return false;
......
......@@ -460,37 +460,19 @@
return response.data;
}
async function getTaskAnnotations(tid) {
const { backendAPI } = window.cvat.config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/tasks/${tid}/annotations`, {
proxy: window.cvat.config.proxy,
});
} catch (errorData) {
const code = errorData.response ? errorData.response.status : errorData.code;
throw new window.cvat.exceptions.ServerError(
`Could not get annotations for the task ${tid} from the server`,
code,
);
}
return response.data;
}
async function getJobAnnotations(jid) {
// Session is 'task' or 'job'
async function getAnnotations(session, id) {
const { backendAPI } = window.cvat.config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/jobs/${jid}/annotations`, {
response = await Axios.get(`${backendAPI}/${session}s/${id}/annotations`, {
proxy: window.cvat.config.proxy,
});
} catch (errorData) {
const code = errorData.response ? errorData.response.status : errorData.code;
throw new window.cvat.exceptions.ServerError(
`Could not get annotations for the job ${jid} from the server`,
`Could not get annotations for the ${session} ${id} from the server`,
code,
);
}
......@@ -586,9 +568,8 @@
annotations: {
value: Object.freeze({
getTaskAnnotations,
getJobAnnotations,
updateAnnotations,
getAnnotations,
}),
writable: false,
},
......
......@@ -12,138 +12,159 @@
const serverProxy = require('./server-proxy');
const { getFrame } = require('./frames');
const {
getJobAnnotations,
getTaskAnnotations,
saveJobAnnotations,
saveTaskAnnotations,
getAnnotations,
saveAnnotations,
hasUnsavedChanges,
mergeAnnotations,
splitAnnotations,
groupAnnotations,
} = require('./annotations');
function buildDublicatedAPI() {
const annotations = Object.freeze({
value: {
async upload(file) {
const result = await PluginRegistry
.apiWrapper.call(this, annotations.value.upload, file);
return result;
},
function buildDublicatedAPI(prototype) {
Object.defineProperties(prototype, {
annotations: Object.freeze({
value: {
async upload(file) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.upload, file);
return result;
},
async save() {
const result = await PluginRegistry
.apiWrapper.call(this, annotations.value.save);
return result;
},
async save() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.save);
return result;
},
async clear() {
const result = await PluginRegistry
.apiWrapper.call(this, annotations.value.clear);
return result;
},
async clear() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.clear);
return result;
},
async dump() {
const result = await PluginRegistry
.apiWrapper.call(this, annotations.value.dump);
return result;
},
async dump() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.dump);
return result;
},
async statistics() {
const result = await PluginRegistry
.apiWrapper.call(this, annotations.value.statistics);
return result;
},
async statistics() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.statistics);
return result;
},
async put(arrayOfObjects = []) {
const result = await PluginRegistry
.apiWrapper.call(this, annotations.value.put, arrayOfObjects);
return result;
},
async put(arrayOfObjects = []) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.put, arrayOfObjects);
return result;
},
async get(frame, filter = {}) {
const result = await PluginRegistry
.apiWrapper.call(this, annotations.value.get, frame, filter);
return result;
},
async get(frame, filter = {}) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.get, frame, filter);
return result;
},
async search(filter, frameFrom, frameTo) {
const result = await PluginRegistry
.apiWrapper.call(this, annotations.value.search,
filter, frameFrom, frameTo);
return result;
},
async search(filter, frameFrom, frameTo) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.search,
filter, frameFrom, frameTo);
return result;
},
async select(frame, x, y) {
const result = await PluginRegistry
.apiWrapper.call(this, annotations.value.select, frame, x, y);
return result;
},
},
});
async select(frame, x, y) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.select, frame, x, y);
return result;
},
const frames = Object.freeze({
value: {
async get(frame) {
const result = await PluginRegistry
.apiWrapper.call(this, frames.value.get, frame);
return result;
},
},
});
async hasUnsavedChanges() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.hasUnsavedChanges);
return result;
},
const logs = Object.freeze({
value: {
async put(logType, details) {
const result = await PluginRegistry
.apiWrapper.call(this, logs.value.put, logType, details);
return result;
},
async save(onUpdate) {
const result = await PluginRegistry
.apiWrapper.call(this, logs.value.save, onUpdate);
return result;
},
},
});
async merge(objectStates) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.merge, objectStates);
return result;
},
const actions = Object.freeze({
value: {
async undo(count) {
const result = await PluginRegistry
.apiWrapper.call(this, actions.value.undo, count);
return result;
async split(objectState, frame) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.split, objectState, frame);
return result;
},
async group(objectStates) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.group, objectStates);
return result;
},
},
async redo(count) {
const result = await PluginRegistry
.apiWrapper.call(this, actions.value.redo, count);
return result;
writable: true,
}),
frames: Object.freeze({
value: {
async get(frame) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.frames.get, frame);
return result;
},
},
async clear() {
const result = await PluginRegistry
.apiWrapper.call(this, actions.value.clear);
return result;
writable: true,
}),
logs: Object.freeze({
value: {
async put(logType, details) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.logs.put, logType, details);
return result;
},
async save(onUpdate) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.logs.save, onUpdate);
return result;
},
},
},
});
const events = Object.freeze({
value: {
async subscribe(eventType, callback) {
const result = await PluginRegistry
.apiWrapper.call(this, events.value.subscribe, eventType, callback);
return result;
writable: true,
}),
actions: Object.freeze({
value: {
async undo(count) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.actions.undo, count);
return result;
},
async redo(count) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.actions.redo, count);
return result;
},
async clear() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.actions.clear);
return result;
},
},
async unsubscribe(eventType, callback = null) {
const result = await PluginRegistry
.apiWrapper.call(this, events.value.unsubscribe, eventType, callback);
return result;
writable: true,
}),
events: Object.freeze({
value: {
async subscribe(evType, callback) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.events.subscribe, evType, callback);
return result;
},
async unsubscribe(evType, callback = null) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.events.unsubscribe, evType, callback);
return result;
},
},
},
});
return Object.freeze({
annotations,
frames,
logs,
actions,
events,
writable: true,
}),
});
}
......@@ -268,12 +289,60 @@
* @param {float} x horizontal coordinate
* @param {float} y vertical coordinate
* @returns {(integer|null)}
* identifier of a selected object or null if no one of objects is on position
* an ID of a selected object or null if no one of objects is on position
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
* @async
*/
/**
* Method unites several shapes and tracks into the one
* All shapes must be the same (rectangle, polygon, etc)
* All labels must be the same
* After successful merge you need to update object states on a frame
* @method merge
* @memberof Session.annotations
* @param {module:API.cvat.classes.ObjectState[]} objectStates
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
* @async
*/
/**
* Method splits a track into two parts
* (start frame: previous frame), (frame, last frame)
* After successful split you need to update object states on a frame
* @method split
* @memberof Session.annotations
* @param {module:API.cvat.classes.ObjectState} objectState
* @param {integer} frame
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
* @instance
* @async
*/
/**
* Method creates a new group and put all passed objects into it
* After successful split you need to update object states on a frame
* @method group
* @memberof Session.annotations
* @param {module:API.cvat.classes.ObjectState[]} objectStates
* @returns {integer} an ID of created group
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
* @instance
* @async
*/
/**
* Indicate if there are any changes in
* annotations which haven't been saved on a server
* @method hasUnsavedChanges
* @memberof Session.annotations
* @returns {boolean}
* @throws {module:API.cvat.exceptions.PluginError}
* @instance
* @async
*/
/**
......@@ -511,9 +580,22 @@
},
}));
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);
// When we call a function, for example: task.annotations.get()
// In the method get we lose the task context
// So, we need return it
this.annotations = {
get: Object.getPrototypeOf(this).annotations.get.bind(this),
save: Object.getPrototypeOf(this).annotations.save.bind(this),
merge: Object.getPrototypeOf(this).annotations.merge.bind(this),
split: Object.getPrototypeOf(this).annotations.split.bind(this),
group: Object.getPrototypeOf(this).annotations.group.bind(this),
hasUnsavedChanges: Object.getPrototypeOf(this)
.annotations.hasUnsavedChanges.bind(this),
};
this.frames = {
get: Object.getPrototypeOf(this).frames.get.bind(this),
};
}
/**
......@@ -535,7 +617,7 @@
// Fill up the prototype by properties. Class syntax doesn't allow do it
// So, we do it seperately
Object.defineProperties(Job.prototype, buildDublicatedAPI());
buildDublicatedAPI(Job.prototype);
Job.prototype.save.implementation = async function () {
// TODO: Add ability to change an assignee
......@@ -578,12 +660,28 @@
);
}
const annotationsData = await getJobAnnotations(this, frame, filter);
const annotationsData = await getAnnotations(this, frame, filter);
return annotationsData;
};
Job.prototype.annotations.save.implementation = async function (onUpdate) {
await saveJobAnnotations(this, onUpdate);
await saveAnnotations(this, onUpdate);
};
Job.prototype.annotations.merge.implementation = async function (objectStates) {
await mergeAnnotations(this, objectStates);
};
Job.prototype.annotations.split.implementation = async function (objectState, frame) {
await splitAnnotations(this, objectState, frame);
};
Job.prototype.annotations.group.implementation = async function (objectStates) {
await groupAnnotations(this, objectStates);
};
Job.prototype.annotations.hasUnsavedChanges.implementation = async function () {
return hasUnsavedChanges(this);
};
/**
......@@ -987,9 +1085,22 @@
},
}));
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);
// When we call a function, for example: task.annotations.get()
// In the method get we lose the task context
// So, we need return it
this.annotations = {
get: Object.getPrototypeOf(this).annotations.get.bind(this),
save: Object.getPrototypeOf(this).annotations.save.bind(this),
merge: Object.getPrototypeOf(this).annotations.merge.bind(this),
split: Object.getPrototypeOf(this).annotations.split.bind(this),
group: Object.getPrototypeOf(this).annotations.group.bind(this),
hasUnsavedChanges: Object.getPrototypeOf(this)
.annotations.hasUnsavedChanges.bind(this),
};
this.frames = {
get: Object.getPrototypeOf(this).frames.get.bind(this),
};
}
/**
......@@ -1031,7 +1142,7 @@
// Fill up the prototype by properties. Class syntax doesn't allow do it
// So, we do it seperately
Object.defineProperties(Task.prototype, buildDublicatedAPI());
buildDublicatedAPI(Task.prototype);
Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) {
// TODO: Add ability to change an owner and an assignee
......@@ -1104,12 +1215,28 @@
);
}
const annotationsData = await getTaskAnnotations(this, frame, filter);
const annotationsData = await getAnnotations(this, frame, filter);
return annotationsData;
};
Task.prototype.annotations.save.implementation = async function (onUpdate) {
await saveTaskAnnotations(this, onUpdate);
await saveAnnotations(this, onUpdate);
};
Task.prototype.annotations.merge.implementation = async function (objectStates) {
await mergeAnnotations(this, objectStates);
};
Task.prototype.annotations.split.implementation = async function (objectState, frame) {
await splitAnnotations(this, objectState, frame);
};
Task.prototype.annotations.group.implementation = async function (objectStates) {
await groupAnnotations(this, objectStates);
};
Task.prototype.annotations.hasUnsavedChanges.implementation = async function () {
return hasUnsavedChanges(this);
};
module.exports = {
......
......@@ -54,6 +54,10 @@ class ServerProxy {
return null;
}
async function logout() {
return null;
}
async function getTasks(filter = '') {
function QueryStringToJSON(query) {
const pairs = [...new URLSearchParams(query).entries()];
......@@ -134,7 +138,7 @@ class ServerProxy {
}
async function getJob(jobID) {
return tasksDummyData.results.reduce((acc, task) => {
const jobs = tasksDummyData.results.reduce((acc, task) => {
for (const segment of task.segments) {
for (const job of segment.jobs) {
const copy = JSON.parse(JSON.stringify(job));
......@@ -148,6 +152,10 @@ class ServerProxy {
return acc;
}, []).filter(job => job.id === jobID);
return jobs[0] || {
detail: 'Not found.',
};
}
async function saveJob(id, jobData) {
......@@ -185,6 +193,14 @@ class ServerProxy {
return null;
}
async function getAnnotations() {
return null;
}
async function updateAnnotations() {
return null;
}
Object.defineProperties(this, Object.freeze({
server: {
value: Object.freeze({
......@@ -192,6 +208,7 @@ class ServerProxy {
share,
exception,
login,
logout,
}),
writable: false,
},
......@@ -229,6 +246,14 @@ class ServerProxy {
}),
writable: false,
},
annotations: {
value: Object.freeze({
updateAnnotations,
getAnnotations,
}),
writable: false,
},
}));
}
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册