diff --git a/cvatjs/.eslintrc.js b/cvatjs/.eslintrc.js index 20e24a7c..ff874af1 100644 --- a/cvatjs/.eslintrc.js +++ b/cvatjs/.eslintrc.js @@ -47,5 +47,6 @@ "security/detect-object-injection": 0, "indent": ["warn", 4], "no-useless-constructor": 0, + "func-names": [0], }, }; diff --git a/cvatjs/src/annotations.js b/cvatjs/src/annotations.js new file mode 100644 index 00000000..a62eef66 --- /dev/null +++ b/cvatjs/src/annotations.js @@ -0,0 +1,573 @@ +/* +* Copyright (C) 2018 Intel Corporation +* SPDX-License-Identifier: MIT +*/ + +/* global + require:false +*/ + +(() => { + const serverProxy = require('./server-proxy'); + const ObjectState = require('./object-state'); + + class Annotation { + constructor(data, clientID, injection) { + this.clientID = clientID; + this.serverID = data.id; + this.labelID = data.label_id; + this.frame = data.frame; + this.attributes = data.attributes.reduce((attributeAccumulator, attr) => { + attributeAccumulator[attr.spec_id] = attr.value; + return attributeAccumulator; + }, {}); + this.taskLabels = injection.labels; + } + } + + class Shape extends Annotation { + constructor(data, clientID, color, injection) { + super(data, clientID, injection); + this.points = data.points; + this.occluded = data.occluded; + this.zOrder = data.z_order; + this.group = data.group; + this.color = color; + this.shape = null; + } + + toJSON() { + return { + occluded: this.occluded, + z_order: this.zOrder, + points: [...this.points], + attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => { + attributeAccumulator.push({ + spec_id: attrId, + value: this.attributes[attrId], + }); + + return attributeAccumulator; + }, []), + id: this.serverID, + frame: this.frame, + label_id: this.labelID, + group: this.group, + }; + } + + get(frame) { + if (frame !== this.frame) { + throw new window.cvat.exceptions.ScriptingError( + 'Got frame is not equal to the frame of the shape', + ); + } + + return { + type: window.cvat.enums.ObjectType.SHAPE, + shape: this.shape, + clientID: this.clientID, + occluded: this.occluded, + zOrder: this.zOrder, + points: [...this.points], + attributes: Object.assign({}, this.attributes), + label: this.taskLabels[this.labelID], + group: this.group, + }; + } + } + + class Track extends Annotation { + constructor(data, clientID, color, injection) { + super(data, clientID, injection); + this.shapes = data.shapes.reduce((shapeAccumulator, value) => { + shapeAccumulator[value.frame] = { + serverID: value.id, + occluded: value.occluded, + zOrder: value.z_order, + points: value.points, + id: value.id, + frame: value.frame, + outside: value.outside, + attributes: value.attributes.reduce((attributeAccumulator, attr) => { + attributeAccumulator[attr.spec_id] = attr.value; + return attributeAccumulator; + }, {}), + }; + + return shapeAccumulator; + }, {}); + + this.group = data.group; + this.attributes = data.attributes.reduce((attributeAccumulator, attr) => { + attributeAccumulator[attr.spec_id] = attr.value; + return attributeAccumulator; + }, {}); + this.color = color; + this.shape = null; + } + + toJSON() { + return { + occluded: this.occluded, + z_order: this.zOrder, + points: [...this.points], + attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => { + attributeAccumulator.push({ + spec_id: attrId, + value: this.attributes[attrId], + }); + + return attributeAccumulator; + }, []), + + id: this.serverID, + frame: this.frame, + label_id: this.labelID, + group: this.group, + shapes: Object.keys(this.shapes).reduce((shapesAccumulator, frame) => { + shapesAccumulator.push({ + type: this.type, + 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) + .reduce((attributeAccumulator, attrId) => { + attributeAccumulator.push({ + spec_id: attrId, + value: this.shapes[frame].attributes[attrId], + }); + + return attributeAccumulator; + }, []), + id: this.shapes[frame].serverID, + frame: +frame, + }); + + return shapesAccumulator; + }, []), + }; + } + + get(targetFrame) { + return Object.assign( + {}, this.interpolatePosition(targetFrame), + { + attributes: this.interpolateAttributes(targetFrame), + label: this.taskLabels[this.labelID], + group: this.group, + type: window.cvat.enums.ObjectType.TRACK, + shape: this.shape, + clientID: this.clientID, + }, + ); + } + + neighborsFrames(targetFrame) { + const frames = Object.keys(this.shapes).map(frame => +frame); + let lDiff = Number.MAX_SAFE_INTEGER; + let rDiff = Number.MAX_SAFE_INTEGER; + + for (const frame of frames) { + const diff = Math.abs(targetFrame - frame); + if (frame <= targetFrame && diff < lDiff) { + lDiff = diff; + } else if (diff < rDiff) { + rDiff = diff; + } + } + + const leftFrame = lDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame - lDiff; + const rightFrame = rDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame + rDiff; + + return { + leftFrame, + rightFrame, + }; + } + + interpolateAttributes(targetFrame) { + const result = {}; + + // First of all copy all unmutable attributes + for (const attrID in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attrID)) { + result[attrID] = this.attributes[attrID]; + } + } + + // Secondly get latest mutable attributes up to target frame + const frames = Object.keys(this.shapes).sort((a, b) => +a - +b); + for (const frame of frames) { + if (frame <= targetFrame) { + const { attributes } = this.shapes[frame]; + + for (const attrID in attributes) { + if (Object.prototype.hasOwnProperty.call(attributes, attrID)) { + result[attrID] = attributes[attrID]; + } + } + } + } + + // Finally fill up remained attributes if they exist + const labelAttributes = this.taskLabels[this.labelID].attributes; + const defValuesByID = labelAttributes.reduce((accumulator, attr) => { + accumulator[attr.id] = attr.defaultValue; + return accumulator; + }, {}); + + for (const attrID of Object.keys(defValuesByID)) { + if (!(attrID in result)) { + result[attrID] = defValuesByID[attrID]; + } + } + + return result; + } + } + + class Tag extends Annotation { + constructor(data, clientID, injection) { + super(data, clientID, injection); + } + + toJSON() { + // TODO: Tags support + return {}; + } + + get(frame) { + if (frame !== this.frame) { + throw new window.cvat.exceptions.ScriptingError( + 'Got frame is not equal to the frame of the shape', + ); + } + } + } + + class RectangleShape extends Shape { + constructor(data, clientID, color, injection) { + super(data, clientID, color, injection); + this.shape = window.cvat.enums.ObjectShape.RECTANGLE; + } + } + + class PolyShape extends Shape { + constructor(data, clientID, color, injection) { + super(data, clientID, color, injection); + } + } + + class PolygonShape extends PolyShape { + constructor(data, clientID, color, injection) { + super(data, clientID, color, injection); + this.shape = 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; + } + } + + class PointsShape extends PolyShape { + constructor(data, clientID, color, injection) { + super(data, clientID, color, injection); + this.shape = 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; + } + + interpolatePosition(targetFrame) { + const { + leftFrame, + rightFrame, + } = this.neighborsFrames(targetFrame); + + const rightPosition = rightFrame ? this.shapes[rightFrame] : null; + const leftPosition = leftFrame ? this.shapes[leftFrame] : null; + + if (leftPosition && leftFrame === targetFrame) { + return { + points: [...leftPosition.points], + occluded: leftPosition.occluded, + outside: leftPosition.outside, + zOrder: leftPosition.zOrder, + }; + } + + if (rightPosition && leftPosition) { + const offset = (targetFrame - leftFrame) / (rightPosition - leftPosition); + const positionOffset = [ + rightPosition.points[0] - leftPosition.points[0], + rightPosition.points[1] - leftPosition.points[1], + rightPosition.points[2] - leftPosition.points[2], + rightPosition.points[3] - leftPosition.points[3], + ]; + + return { // xtl, ytl, xbr, ybr + points: [ + leftPosition.points[0] + positionOffset[0] * offset, + leftPosition.points[1] + positionOffset[1] * offset, + leftPosition.points[2] + positionOffset[2] * offset, + leftPosition.points[3] + positionOffset[3] * offset, + ], + occluded: leftPosition.occluded, + outside: leftPosition.outside, + zOrder: leftPosition.zOrder, + }; + } + + if (rightPosition) { + return { + points: [...rightPosition.points], + occluded: rightPosition.occluded, + outside: true, + zOrder: 0, + }; + } + + if (leftPosition) { + return { + points: [...leftPosition.points], + occluded: leftPosition.occluded, + outside: leftPosition.outside, + zOrder: 0, + }; + } + + throw new window.cvat.exceptions.ScriptingError( + `No one neightbour frame found for the track with client ID: "${this.id}"`, + ); + } + } + + class PolyTrack extends Track { + constructor(data, clientID, color, injection) { + super(data, clientID, color, injection); + } + } + + class PolygonTrack extends PolyTrack { + constructor(data, clientID, color, injection) { + super(data, clientID, color, injection); + this.shape = 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; + } + } + + class PointsTrack extends PolyTrack { + constructor(data, clientID, color, injection) { + super(data, clientID, color, injection); + this.shape = window.cvat.enums.ObjectShape.POINTS; + } + } + + const colors = [ + '#0066FF', '#AF593E', '#01A368', '#FF861F', '#ED0A3F', '#FF3F34', '#76D7EA', + '#8359A3', '#FBE870', '#C5E17A', '#03BB85', '#FFDF00', '#8B8680', '#0A6B0D', + '#8FD8D8', '#A36F40', '#F653A6', '#CA3435', '#FFCBA4', '#FF99CC', '#FA9D5A', + '#FFAE42', '#A78B00', '#788193', '#514E49', '#1164B4', '#F4FA9F', '#FED8B1', + '#C32148', '#01796F', '#E90067', '#FF91A4', '#404E5A', '#6CDAE7', '#FFC1CC', + '#006A93', '#867200', '#E2B631', '#6EEB6E', '#FFC800', '#CC99BA', '#FF007C', + '#BC6CAC', '#DCCCD7', '#EBE1C2', '#A6AAAE', '#B99685', '#0086A7', '#5E4330', + '#C8A2C8', '#708EB3', '#BC8777', '#B2592D', '#497E48', '#6A2963', '#E6335F', + '#00755E', '#B5A895', '#0048ba', '#EED9C4', '#C88A65', '#FF6E4A', '#87421F', + '#B2BEB5', '#926F5B', '#00B9FB', '#6456B7', '#DB5079', '#C62D42', '#FA9C44', + '#DA8A67', '#FD7C6E', '#93CCEA', '#FCF686', '#503E32', '#FF5470', '#9DE093', + '#FF7A00', '#4F69C6', '#A50B5E', '#F0E68C', '#FDFF00', '#F091A9', '#FFFF66', + '#6F9940', '#FC74FD', '#652DC1', '#D6AEDD', '#EE34D2', '#BB3385', '#6B3FA0', + '#33CC99', '#FFDB00', '#87FF2A', '#6EEB6E', '#FFC800', '#CC99BA', '#7A89B8', + '#006A93', '#867200', '#E2B631', '#D9D6CF', + ]; + + + class Collection { + constructor(labels) { + this.labels = labels.reduce((labelAccumulator, label) => { + labelAccumulator[label.id] = label; + return labelAccumulator; + }, {}); + + this.empty(); + } + + import(data) { + this.empty(); + const injection = { + labels: this.labels, + }; + + 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; + } + + for (const tag of data.tags) { + const clientID = ++this.count; + const tagModel = new Tag(tag, clientID, injection); + this.tags[tagModel.frame] = this.tags[tagModel.frame] || []; + this.tags[tagModel.frame].push(tagModel); + this.objects[clientID] = tagModel; + } + + for (const shape of data.shapes) { + const clientID = ++this.count; + const shapeModel = shapeFactory(shape, clientID); + this.shapes[shapeModel.frame] = this.shapes[shapeModel.frame] || []; + this.shapes[shapeModel.frame].push(shapeModel); + this.objects[clientID] = shapeModel; + } + + for (const track of data.tracks) { + const clientID = ++this.count; + const trackModel = trackFactory(track, clientID); + // 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) { + this.tracks.push(trackModel); + this.objects[clientID] = trackModel; + } + } + } + + export() { + const data = { + tracks: Object.values(this.tracks).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()), + }; + + return data; + } + + empty() { + this.shapes = {}; + this.tags = {}; + this.tracks = []; + this.objects = {}; // by id + this.count = 0; + } + + get(frame) { + const { tracks } = this; + const shapes = this.shapes[frame] || []; + const tags = this.tags[frame] || []; + + const states = tracks.map(track => track.get(frame)) + .concat(shapes.map(shape => shape.get(frame))) + .concat(tags.map(tag => tag.get(frame))); + + // filtering here + + const objectStates = []; + for (const state of states) { + const objectState = new ObjectState(state); + objectStates.push(objectState); + } + + return objectStates; + } + } + + const jobCache = {}; + const taskCache = {}; + + 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); + } + + return jobCache[job.id].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); + } + + return taskCache[task.id].get(frame, filter); + } + + module.exports = { + getJobAnnotations, + getTaskAnnotations, + }; +})(); diff --git a/cvatjs/src/api-implementation.js b/cvatjs/src/api-implementation.js index d40e7f80..5ead890a 100644 --- a/cvatjs/src/api-implementation.js +++ b/cvatjs/src/api-implementation.js @@ -14,11 +14,6 @@ const PluginRegistry = require('./plugins'); const serverProxy = require('./server-proxy'); - const { - Task, - Job, - } = require('./session'); - function isBoolean(value) { return typeof (value) === 'boolean'; } @@ -60,28 +55,6 @@ } } - const hidden = require('./hidden'); - function setupEnv(wrappedFunction) { - return async function wrapper(...args) { - try { - if (this instanceof window.cvat.classes.Task) { - hidden.taskID = this.id; - } else if (this instanceof window.cvat.classes.Job) { - hidden.jobID = this.id; - hidden.taskID = this.task.id; - } else { - throw new window.cvat.exceptions.ScriptingError('Bad context for the function'); - } - - const result = await wrappedFunction.call(this, ...args); - return result; - } finally { - delete hidden.taskID; - delete hidden.jobID; - } - }; - } - function implementAPI(cvat) { cvat.plugins.list.implementation = PluginRegistry.list; cvat.plugins.register.implementation = PluginRegistry.register; @@ -194,75 +167,6 @@ return tasks; }; - Task.prototype.save.implementation = setupEnv( - async function saveTaskImplementation(onUpdate) { - // TODO: Add ability to change an owner and an assignee - if (typeof (this.id) !== 'undefined') { - // If the task has been already created, we update it - const taskData = { - name: this.name, - bug_tracker: this.bugTracker, - z_order: this.zOrder, - labels: [...this.labels.map(el => el.toJSON())], - }; - - await serverProxy.tasks.saveTask(this.id, taskData); - return this; - } - - const taskData = { - name: this.name, - labels: this.labels.map(el => el.toJSON()), - image_quality: this.imageQuality, - z_order: Boolean(this.zOrder), - }; - - if (this.bugTracker) { - taskData.bug_tracker = this.bugTracker; - } - if (this.segmentSize) { - taskData.segment_size = this.segmentSize; - } - if (this.overlap) { - taskData.overlap = this.overlap; - } - - const taskFiles = { - client_files: this.clientFiles, - server_files: this.serverFiles, - remote_files: this.remoteFiles, - }; - - const task = await serverProxy.tasks.createTask(taskData, taskFiles, onUpdate); - return new Task(task); - }, - ); - - Task.prototype.delete.implementation = setupEnv( - async function deleteTaskImplementation() { - await serverProxy.tasks.deleteTask(this.id); - }, - ); - - Job.prototype.save.implementation = setupEnv( - async function saveJobImplementation() { - // TODO: Add ability to change an assignee - if (this.id) { - const jobData = { - status: this.status, - }; - - await serverProxy.jobs.saveJob(this.id, jobData); - return this; - } - - throw window.cvat.exceptions.ArgumentError( - 'Can not save job without and id', - ); - }, - ); - - return cvat; } diff --git a/cvatjs/src/api.js b/cvatjs/src/api.js index b730ed68..05337dd2 100644 --- a/cvatjs/src/api.js +++ b/cvatjs/src/api.js @@ -34,134 +34,14 @@ const { Exception, ArgumentError, + DataError, ScriptingError, PluginError, ServerError, } = require('./exceptions'); const pjson = require('../package.json'); - - function buildDublicatedAPI() { - const annotations = { - async upload(file) { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.upload, file); - return result; - }, - - async save() { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.save); - return result; - }, - - async clear() { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.clear); - return result; - }, - - async dump() { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.dump); - return result; - }, - - async statistics() { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.statistics); - return result; - }, - - async put(arrayOfObjects = []) { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.put, arrayOfObjects); - return result; - }, - - async get(frame, filter = {}) { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.get, frame, filter); - return result; - }, - - async search(filter, frameFrom, frameTo) { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.search, filter, frameFrom, frameTo); - return result; - }, - - async select(frame, x, y) { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.select, frame, x, y); - return result; - }, - }; - - const frames = { - async get(frame) { - const result = await PluginRegistry - .apiWrapper.call(this, frames.get, frame); - return result; - }, - }; - - const logs = { - async put(logType, details) { - const result = await PluginRegistry - .apiWrapper.call(this, logs.put, logType, details); - return result; - }, - async save() { - const result = await PluginRegistry - .apiWrapper.call(this, logs.save); - return result; - }, - }; - - const actions = { - async undo(count) { - const result = await PluginRegistry - .apiWrapper.call(this, actions.undo, count); - return result; - }, - async redo(count) { - const result = await PluginRegistry - .apiWrapper.call(this, actions.redo, count); - return result; - }, - async clear() { - const result = await PluginRegistry - .apiWrapper.call(this, actions.clear); - return result; - }, - }; - - const events = { - async subscribe(eventType, callback) { - const result = await PluginRegistry - .apiWrapper.call(this, events.subscribe, eventType, callback); - return result; - }, - async unsubscribe(eventType, callback = null) { - const result = await PluginRegistry - .apiWrapper.call(this, events.unsubscribe, eventType, callback); - return result; - }, - }; - - return { - annotations, - frames, - logs, - actions, - events, - }; - } - - // Two copies of API for Task and for Job - const jobAPI = buildDublicatedAPI(); - const taskAPI = buildDublicatedAPI(); + const clientID = +Date.now().toString().substr(-6); /** * API entrypoint @@ -463,10 +343,22 @@ * @property {integer} preloadFrames the number of subsequent frames which are * loaded in background * @memberof module:API.cvat.config + * @property {integer} taskID this value is displayed in a logs if available + * @memberof module:API.cvat.config + * @property {integer} jobID this value is displayed in a logs if available + * @memberof module:API.cvat.config + * @property {integer} clientID read only auto-generated + * value which is displayed in a logs + * @memberof module:API.cvat.config */ preloadFrames: 300, backendAPI: 'http://localhost:7000/api/v1', proxy: false, + taskID: undefined, + jobID: undefined, + clientID: { + get: () => clientID, + }, }, /** * Namespace contains some library information e.g. api version @@ -509,6 +401,7 @@ exceptions: { Exception, ArgumentError, + DataError, ScriptingError, PluginError, ServerError, @@ -539,104 +432,6 @@ cvat.Job = Object.freeze(cvat.Job); cvat.Task = Object.freeze(cvat.Task); - Object.defineProperties(Job.prototype, Object.freeze({ - annotations: { - value: Object.freeze({ - upload: jobAPI.annotations.upload, - save: jobAPI.annotations.save, - clear: jobAPI.annotations.clear, - dump: jobAPI.annotations.dump, - statistics: jobAPI.annotations, - put: jobAPI.annotations.put, - get: jobAPI.annotations.get, - search: jobAPI.annotations.search, - select: jobAPI.annotations.select, - }), - writable: false, - }, - - frames: { - value: Object.freeze({ - get: jobAPI.frames.get, - }), - writable: false, - }, - - logs: { - value: Object.freeze({ - put: jobAPI.logs.put, - save: jobAPI.logs.save, - }), - writable: false, - }, - - actions: { - value: Object.freeze({ - undo: jobAPI.actions.undo, - redo: jobAPI.actions.redo, - clear: jobAPI.actions.clear, - }), - writable: false, - }, - - events: { - value: Object.freeze({ - subscribe: jobAPI.events.subscribe, - unsubscribe: jobAPI.events.unsubscribe, - }), - writable: false, - }, - })); - - Object.defineProperties(Task.prototype, Object.freeze({ - annotations: { - value: Object.freeze({ - upload: taskAPI.annotations.upload, - save: taskAPI.annotations.save, - clear: taskAPI.annotations.clear, - dump: taskAPI.annotations.dump, - statistics: taskAPI.annotations.statistics, - put: taskAPI.annotations.put, - get: taskAPI.annotations.get, - search: taskAPI.annotations.search, - select: taskAPI.annotations.select, - }), - writable: false, - }, - - frames: { - value: Object.freeze({ - get: taskAPI.frames.get, - }), - writable: false, - }, - - logs: { - value: Object.freeze({ - put: taskAPI.logs.put, - save: taskAPI.logs.save, - }), - writable: false, - }, - - actions: { - value: Object.freeze({ - undo: taskAPI.actions.undo, - redo: taskAPI.actions.redo, - clear: taskAPI.actions.clear, - }), - writable: false, - }, - - events: { - value: Object.freeze({ - subscribe: taskAPI.events.subscribe, - unsubscribe: taskAPI.events.unsubscribe, - }), - writable: false, - }, - })); - const implementAPI = require('./api-implementation'); if (typeof (window) === 'undefined') { // Dummy browser environment @@ -644,7 +439,4 @@ } window.cvat = Object.freeze(implementAPI(cvat)); - - const hidden = require('./hidden'); - hidden.location = cvat.config.backendAPI.slice(0, -7); // TODO: Use JS server instead })(); diff --git a/cvatjs/src/exceptions.js b/cvatjs/src/exceptions.js index 57b59a37..e10c6bae 100644 --- a/cvatjs/src/exceptions.js +++ b/cvatjs/src/exceptions.js @@ -11,8 +11,6 @@ const Platform = require('platform'); const ErrorStackParser = require('error-stack-parser'); - const hidden = require('./hidden'); - /** * Base exception class * @memberof module:API.cvat.exceptions @@ -30,14 +28,14 @@ const system = Platform.os.toString(); const client = `${Platform.name} ${Platform.version}`; const info = ErrorStackParser.parse(this)[0]; - const filename = `${hidden.location}${info.fileName}`; + const filename = `${info.fileName}`; const line = info.lineNumber; const column = info.columnNumber; const { jobID, taskID, clientID, - } = hidden; + } = window.cvat.config; const projID = undefined; // wasn't implemented @@ -192,6 +190,20 @@ } } + /** + * Unexpected problems with data which are not connected with a user input + * @memberof module:API.cvat.exceptions + * @extends module:API.cvat.exceptions.Exception + */ + class DataError extends Exception { + /** + * @param {string} message - Exception message + */ + constructor(message) { + super(message); + } + } + /** * Unexpected situations in code * @memberof module:API.cvat.exceptions @@ -251,6 +263,7 @@ module.exports = { Exception, ArgumentError, + DataError, ScriptingError, PluginError, ServerError, diff --git a/cvatjs/src/frames.js b/cvatjs/src/frames.js index eab8e3cd..e636eef0 100644 --- a/cvatjs/src/frames.js +++ b/cvatjs/src/frames.js @@ -5,10 +5,16 @@ /* global require:false + global:false */ (() => { const PluginRegistry = require('./plugins'); + const serverProxy = require('./server-proxy'); + + // This is the frames storage + const frameDataCache = {}; + const frameCache = {}; /** * Class provides meta information about specific frame and frame itself @@ -53,7 +59,7 @@ /** * Method returns URL encoded image which can be placed in the img tag - * @method image + * @method frame * @returns {string} * @memberof module:API.cvat.classes.FrameData * @instance @@ -61,12 +67,62 @@ * @throws {module:API.cvat.exception.ServerError} * @throws {module:API.cvat.exception.PluginError} */ - async image() { + async frame() { const result = await PluginRegistry - .apiWrapper.call(this, FrameData.prototype.image); + .apiWrapper.call(this, FrameData.prototype.frame); return result; } } - module.exports = FrameData; + FrameData.prototype.frame.implementation = async function () { + if (!(this.number in frameCache[this.tid])) { + const frame = await serverProxy.frames.getFrame(this.tid, this.number); + + if (window.URL.createObjectURL) { // browser env + const url = window.URL.createObjectURL(new Blob([frame])); + frameCache[this.tid][this.number] = url; + } else { + frameCache[this.tid][this.number] = global.Buffer.from(frame, 'binary').toString('base64'); + } + } + + return frameCache[this.tid][this.number]; + }; + + async function getFrame(taskID, mode, frame) { + if (!(taskID in frameDataCache)) { + frameDataCache[taskID] = {}; + frameDataCache[taskID].meta = await serverProxy.frames.getMeta(taskID); + + frameCache[taskID] = {}; + } + + if (!(frame in frameDataCache[taskID])) { + let size = null; + if (mode === 'interpolation') { + [size] = frameDataCache[taskID].meta; + } else if (mode === 'annotation') { + if (frame >= frameDataCache[taskID].meta.length) { + throw new window.cvat.exceptions.ArgumentError( + `Meta information about frame ${frame} can't be received from the server`, + ); + } else { + size = frameDataCache[taskID].meta[frame]; + } + } else { + throw new window.cvat.exceptions.ArgumentError( + `Invalid mode is specified ${mode}`, + ); + } + + frameDataCache[taskID][frame] = new FrameData(size.width, size.height, taskID, frame); + } + + return frameDataCache[taskID][frame]; + } + + module.exports = { + FrameData, + getFrame, + }; })(); diff --git a/cvatjs/src/hidden.js b/cvatjs/src/hidden.js deleted file mode 100644 index 8b2a7a77..00000000 --- a/cvatjs/src/hidden.js +++ /dev/null @@ -1,17 +0,0 @@ -/* -* Copyright (C) 2018 Intel Corporation -* SPDX-License-Identifier: MIT -*/ - -/* Some shared cvat.js data which aren't intended for a user */ -(() => { - const hidden = { - clientID: +Date.now().toString().substr(-6), - projID: undefined, - taskID: undefined, - jobID: undefined, - location: undefined, - }; - - module.exports = hidden; -})(); diff --git a/cvatjs/src/object-state.js b/cvatjs/src/object-state.js index fda9cc78..a44c1c48 100644 --- a/cvatjs/src/object-state.js +++ b/cvatjs/src/object-state.js @@ -16,30 +16,29 @@ */ class ObjectState { /** - * @param {module:API.cvat.enums.ObjectType} type - a type of an object - * @param {module:API.cvat.enums.ObjectShape} shape - - * a type of a shape if an object is a shape of a track - * @param {module:API.cvat.classes.Label} label - a label of an object + * @param {Object} type - an object which contains initialization information + * about points, group, zOrder, outside, occluded, + * attributes, lock, type, label, mode, etc. + * Types of data equal to listed below */ - constructor(type, shape, label) { + constructor(serialized) { const data = { - position: null, + points: null, group: null, zOrder: null, - outside: false, - occluded: false, - attributes: null, - lock: false, - type, - shape, - label, + outside: null, + occluded: null, + lock: null, + attributes: {}, + type: serialized.type, + shape: serialized.shape, }; Object.defineProperties(this, Object.freeze({ type: { /** * @name type - * @type {module:API.cvat.enums.ShapeType} + * @type {module:API.cvat.enums.ObjectType} * @memberof module:API.cvat.classes.ObjectState * @readonly * @instance @@ -49,7 +48,7 @@ shape: { /** * @name shape - * @type {module:API.cvat.enums.ShapeType} + * @type {module:API.cvat.enums.ObjectShape} * @memberof module:API.cvat.classes.ObjectState * @readonly * @instance @@ -67,7 +66,7 @@ get: () => data.label, set: (labelInstance) => { if (!(labelInstance instanceof window.cvat.classes.Label)) { - throw new window.cvat.exceptions.ArgumentException( + throw new window.cvat.exceptions.ArgumentError( `Expected Label instance, but got "${typeof (labelInstance.constructor.name)}"`, ); } @@ -75,7 +74,7 @@ data.label = labelInstance; }, }, - position: { + points: { /** * @typedef {Object} Point * @property {number} x @@ -83,7 +82,7 @@ * @global */ /** - * @name position + * @name points * @type {Point[]} * @memberof module:API.cvat.classes.ObjectState * @instance @@ -95,13 +94,13 @@ for (const point of position) { if (typeof (point) !== 'object' || !('x' in point) || !('y' in point)) { - throw new window.cvat.exceptions.ArgumentException( + throw new window.cvat.exceptions.ArgumentError( `Got invalid point ${point}`, ); } } } else { - throw new window.cvat.exceptions.ArgumentException( + throw new window.cvat.exceptions.ArgumentError( `Got invalid type "${typeof (position.constructor.name)}"`, ); } @@ -120,7 +119,7 @@ get: () => data.group, set: (groupID) => { if (!Number.isInteger(groupID)) { - throw new window.cvat.exceptions.ArgumentException( + throw new window.cvat.exceptions.ArgumentError( `Expected integer, but got ${groupID.constructor.name}`, ); } @@ -139,7 +138,7 @@ get: () => data.zOrder, set: (zOrder) => { if (!Number.isInteger(zOrder)) { - throw new window.cvat.exceptions.ArgumentException( + throw new window.cvat.exceptions.ArgumentError( `Expected integer, but got ${zOrder.constructor.name}`, ); } @@ -157,9 +156,9 @@ */ get: () => data.outside, set: (outside) => { - if (!(typeof (outside) !== 'boolean')) { - throw new window.cvat.exceptions.ArgumentException( - `Expected integer, but got ${outside.constructor.name}`, + if (typeof (outside) !== 'boolean') { + throw new window.cvat.exceptions.ArgumentError( + `Expected boolean, but got ${outside.constructor.name}`, ); } @@ -176,9 +175,9 @@ */ get: () => data.occluded, set: (occluded) => { - if (!(typeof (occluded) !== 'boolean')) { - throw new window.cvat.exceptions.ArgumentException( - `Expected integer, but got ${occluded.constructor.name}`, + if (typeof (occluded) !== 'boolean') { + throw new window.cvat.exceptions.ArgumentError( + `Expected boolean, but got ${occluded.constructor.name}`, ); } @@ -195,9 +194,9 @@ */ get: () => data.lock, set: (lock) => { - if (!(typeof (lock) !== 'boolean')) { - throw new window.cvat.exceptions.ArgumentException( - `Expected integer, but got ${lock.constructor.name}`, + if (typeof (lock) !== 'boolean') { + throw new window.cvat.exceptions.ArgumentError( + `Expected boolean, but got ${lock.constructor.name}`, ); } @@ -217,7 +216,7 @@ get: () => data.attributes, set: (attributes) => { if (typeof (attributes) !== 'object') { - throw new window.cvat.exceptions.ArgumentException( + throw new window.cvat.exceptions.ArgumentError( `Expected object, but got ${attributes.constructor.name}`, ); } @@ -226,7 +225,7 @@ if (Object.prototype.hasOwnProperty.call(attributes, attrId)) { attrId = +attrId; if (!Number.isInteger(attrId)) { - throw new window.cvat.exceptions.ArgumentException( + throw new window.cvat.exceptions.ArgumentError( `Expected integer attribute id, but got ${attrId.constructor.name}`, ); } @@ -236,7 +235,25 @@ } }, }, + })); + + this.label = serialized.label; + this.group = serialized.group; + this.zOrder = serialized.zOrder; + this.outside = serialized.outside; + this.occluded = serialized.occluded; + this.attributes = serialized.attributes; + this.lock = false; + + const points = []; + for (let i = 0; i < serialized.points.length; i += 2) { + points.push({ + x: serialized.points[i], + y: serialized.points[i + 1], + }); + } + this.points = points; } /** diff --git a/cvatjs/src/server-proxy.js b/cvatjs/src/server-proxy.js index ef8283bd..d2384ef6 100644 --- a/cvatjs/src/server-proxy.js +++ b/cvatjs/src/server-proxy.js @@ -167,6 +167,14 @@ } } + // TODO: Perhaps we should redesign the authorization method on the server. + if (authentificationResponse.data.includes('didn\'t match')) { + throw new window.cvat.exceptions.ServerError( + 'The pair login/password is invalid', + 403, + ); + } + setCookie(authentificationResponse); } @@ -370,7 +378,7 @@ } catch (errorData) { const code = errorData.response ? errorData.response.status : errorData.code; throw new window.cvat.exceptions.ServerError( - 'Could not get users from a server', + 'Could not get users from the server', code, ); } @@ -389,7 +397,7 @@ } catch (errorData) { const code = errorData.response ? errorData.response.status : errorData.code; throw new window.cvat.exceptions.ServerError( - 'Could not get users from a server', + 'Could not get users from the server', code, ); } @@ -404,11 +412,12 @@ try { response = await Axios.get(`${backendAPI}/tasks/${tid}/frames/${frame}`, { proxy: window.cvat.config.proxy, + responseType: 'blob', }); } catch (errorData) { const code = errorData.response ? errorData.response.status : errorData.code; throw new window.cvat.exceptions.ServerError( - `Could not get frame ${frame} for a task ${tid} from a server`, + `Could not get frame ${frame} for the task ${tid} from the server`, code, ); } @@ -427,7 +436,45 @@ } catch (errorData) { const code = errorData.response ? errorData.response.status : errorData.code; throw new window.cvat.exceptions.ServerError( - `Could not get frame meta info for a task ${tid} from a server`, + `Could not get frame meta info for the task ${tid} from the server`, + code, + ); + } + + 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) { + const { backendAPI } = window.cvat.config; + + let response = null; + try { + response = await Axios.get(`${backendAPI}/jobs/${jid}/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`, code, ); } @@ -487,6 +534,14 @@ }), writable: false, }, + + annotations: { + value: Object.freeze({ + getTaskAnnotations, + getJobAnnotations, + }), + writable: false, + }, })); } } diff --git a/cvatjs/src/session.js b/cvatjs/src/session.js index 37c25899..09a73515 100644 --- a/cvatjs/src/session.js +++ b/cvatjs/src/session.js @@ -9,6 +9,141 @@ (() => { const PluginRegistry = require('./plugins'); + const serverProxy = require('./server-proxy'); + const { getFrame } = require('./frames'); + const { + getJobAnnotations, + getTaskAnnotations, + } = 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; + }, + + async save() { + const result = await PluginRegistry + .apiWrapper.call(this, annotations.value.save); + return result; + }, + + async clear() { + const result = await PluginRegistry + .apiWrapper.call(this, annotations.value.clear); + return result; + }, + + async dump() { + const result = await PluginRegistry + .apiWrapper.call(this, annotations.value.dump); + return result; + }, + + async statistics() { + const result = await PluginRegistry + .apiWrapper.call(this, annotations.value.statistics); + return result; + }, + + async put(arrayOfObjects = []) { + const result = await PluginRegistry + .apiWrapper.call(this, annotations.value.put, arrayOfObjects); + return result; + }, + + async get(frame, filter = {}) { + const result = await PluginRegistry + .apiWrapper.call(this, annotations.value.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 select(frame, x, y) { + const result = await PluginRegistry + .apiWrapper.call(this, annotations.value.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; + }, + }, + }); + + 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() { + const result = await PluginRegistry + .apiWrapper.call(this, logs.value.save); + return result; + }, + }, + }); + + const actions = Object.freeze({ + value: { + async undo(count) { + const result = await PluginRegistry + .apiWrapper.call(this, actions.value.undo, count); + return result; + }, + async redo(count) { + const result = await PluginRegistry + .apiWrapper.call(this, actions.value.redo, count); + return result; + }, + async clear() { + const result = await PluginRegistry + .apiWrapper.call(this, actions.value.clear); + 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; + }, + async unsubscribe(eventType, callback = null) { + const result = await PluginRegistry + .apiWrapper.call(this, events.value.unsubscribe, eventType, callback); + return result; + }, + }, + }); + + return Object.freeze({ + annotations, + frames, + logs, + actions, + events, + }); + } /** * Base abstract class for Task and Job. It contains common members. @@ -370,6 +505,9 @@ get: () => data.task, }, })); + + this.frames.get.implementation = this.frames.get.implementation.bind(this); + this.annotations.get.implementation = this.annotations.get.implementation.bind(this); } /** @@ -389,6 +527,54 @@ } } + // Fill up the prototype by properties. Class syntax doesn't allow do it + // So, we do it seperately + Object.defineProperties(Job.prototype, buildDublicatedAPI()); + + Job.prototype.save.implementation = async function () { + // TODO: Add ability to change an assignee + if (this.id) { + const jobData = { + status: this.status, + }; + + await serverProxy.jobs.saveJob(this.id, jobData); + return this; + } + + throw window.cvat.exceptions.ArgumentError( + 'Can not save job without and id', + ); + }; + + Job.prototype.frames.get.implementation = async function (frame) { + if (!Number.isInteger(frame) || frame < 0) { + throw new window.cvat.exceptions.ArgumentError( + `Frame must be a positive integer. Got: "${frame}"`, + ); + } + + if (frame < this.startFrame || frame > this.stopFrame) { + throw new window.cvat.exceptions.ArgumentError( + `Frame ${frame} does not exist in the job`, + ); + } + + const frameData = await getFrame(this.task.id, this.task.mode, frame); + return frameData; + }; + + // TODO: Check filter for annotations + Job.prototype.annotations.get.implementation = async function (frame, filter) { + if (frame < this.startFrame || frame > this.stopFrame) { + throw new window.cvat.exceptions.ArgumentError( + `Frame ${frame} does not exist in the job`, + ); + } + + const annotationsData = await getJobAnnotations(this, frame, filter); + return annotationsData; + }; /** * Class representing a task @@ -790,6 +976,9 @@ }, }, })); + + this.frames.get.implementation = this.frames.get.implementation.bind(this); + this.annotations.get.implementation = this.annotations.get.implementation.bind(this); } /** @@ -829,6 +1018,85 @@ } } + // Fill up the prototype by properties. Class syntax doesn't allow do it + // So, we do it seperately + Object.defineProperties(Task.prototype, buildDublicatedAPI()); + + Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) { + // TODO: Add ability to change an owner and an assignee + if (typeof (this.id) !== 'undefined') { + // If the task has been already created, we update it + const taskData = { + name: this.name, + bug_tracker: this.bugTracker, + z_order: this.zOrder, + labels: [...this.labels.map(el => el.toJSON())], + }; + + await serverProxy.tasks.saveTask(this.id, taskData); + return this; + } + + const taskData = { + name: this.name, + labels: this.labels.map(el => el.toJSON()), + image_quality: this.imageQuality, + z_order: Boolean(this.zOrder), + }; + + if (this.bugTracker) { + taskData.bug_tracker = this.bugTracker; + } + if (this.segmentSize) { + taskData.segment_size = this.segmentSize; + } + if (this.overlap) { + taskData.overlap = this.overlap; + } + + const taskFiles = { + client_files: this.clientFiles, + server_files: this.serverFiles, + remote_files: [], // hasn't been supported yet + }; + + const task = await serverProxy.tasks.createTask(taskData, taskFiles, onUpdate); + return new Task(task); + }; + + Task.prototype.delete.implementation = async function () { + serverProxy.tasks.deleteTask(this.id); + }; + + Task.prototype.frames.get.implementation = async function (frame) { + if (frame >= this.size) { + throw new window.cvat.exceptions.ArgumentError( + `Frame ${frame} does not exist in the task`, + ); + } + + const frameData = await getFrame(this.id, this.mode, frame); + return frameData; + }; + + // TODO: Check filter for annotations + Task.prototype.annotations.get.implementation = async function (frame, filter) { + if (!Number.isInteger(frame) || frame < 0) { + throw new window.cvat.exceptions.ArgumentError( + `Frame must be a positive integer. Got: "${frame}"`, + ); + } + + if (frame >= this.size) { + throw new window.cvat.exceptions.ArgumentError( + `Frame ${frame} does not exist in the task`, + ); + } + + const annotationsData = await getTaskAnnotations(this, frame, filter); + return annotationsData; + }; + module.exports = { Job, Task, diff --git a/cvatjs/tests/api/tasks.js b/cvatjs/tests/api/tasks.js index 1abb8606..3f317edb 100644 --- a/cvatjs/tests/api/tasks.js +++ b/cvatjs/tests/api/tasks.js @@ -175,7 +175,7 @@ describe('Feature: delete a task', () => { id: 3, }); - result[0].delete(); + await result[0].delete(); result = await window.cvat.tasks.get({ id: 3, });