From 345d448c95136d249e00185e5998257b90bcd47a Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 5 Jul 2019 12:15:24 +0300 Subject: [PATCH 01/10] Fixed logout host --- cvatjs/src/server-proxy.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cvatjs/src/server-proxy.js b/cvatjs/src/server-proxy.js index 65380a033506..df983621291a 100644 --- a/cvatjs/src/server-proxy.js +++ b/cvatjs/src/server-proxy.js @@ -179,10 +179,10 @@ } async function logout() { - const { backendAPI } = window.cvat.config; + const host = window.cvat.config.backendAPI.slice(0, -7); try { - await Axios.get(`${backendAPI}/auth/logout`, { + await Axios.get(`${host}/auth/logout`, { proxy: window.cvat.config.proxy, }); } catch (errorData) { From e8e3754b5eecefaa6ea9c272b9ec8d29ca0fda52 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 5 Jul 2019 14:06:54 +0300 Subject: [PATCH 02/10] Shape editing --- cvatjs/src/annotations.js | 196 ++++++++++++++++++++++++++++++++++++- cvatjs/src/object-state.js | 155 ++++++++++++----------------- 2 files changed, 255 insertions(+), 96 deletions(-) diff --git a/cvatjs/src/annotations.js b/cvatjs/src/annotations.js index a62eef668760..4e72b35e7bd0 100644 --- a/cvatjs/src/annotations.js +++ b/cvatjs/src/annotations.js @@ -17,11 +17,23 @@ this.serverID = data.id; this.labelID = data.label_id; this.frame = data.frame; + this.removed = false; + this.lock = false; this.attributes = data.attributes.reduce((attributeAccumulator, attr) => { attributeAccumulator[attr.spec_id] = attr.value; return attributeAccumulator; }, {}); this.taskLabels = injection.labels; + this.appendDefaultAttributes(); + } + + appendDefaultAttributes() { + const labelAttributes = this.taskLabels[this.labelID].attributes; + for (const attribute of labelAttributes) { + if (!(attribute.id in this.attributes)) { + this.attributes[attribute.id] = attribute.defaultValue; + } + } } } @@ -36,6 +48,7 @@ this.shape = null; } + // Method used to export data to the server toJSON() { return { occluded: this.occluded, @@ -56,6 +69,7 @@ }; } + // Method used for ObjectStates creating get(frame) { if (frame !== this.frame) { throw new window.cvat.exceptions.ScriptingError( @@ -68,6 +82,7 @@ shape: this.shape, clientID: this.clientID, occluded: this.occluded, + lock: this.lock, zOrder: this.zOrder, points: [...this.points], attributes: Object.assign({}, this.attributes), @@ -75,6 +90,144 @@ group: this.group, }; } + + save(frame, data) { + if (frame !== this.frame) { + throw new window.cvat.exceptions.ScriptingError( + 'Got frame is not equal to the frame of the shape', + ); + } + + const savedLabelID = this.labelID; + const savedAttributes = this.attributes; + const savedPoints = this.points; + const savedOccluded = this.occluded; + const savedZOrder = this.zOrder; + const savedGroup = this.group; + const savedColor = this.color; + + try { + if (this.labelID !== data.labelID) { + if (!(data.label instanceof window.cvat.classes.Label)) { + throw new window.cvat.exceptions.ArgumentError( + `Expected Label instance, but got "${typeof (data.label.constructor.name)}"`, + ); + } + + this.labelID = data.label.id; + this.attributes = {}; + this.appendDefaultAttributes(); + } + + if (JSON.stringify(this.attributes) !== JSON.stringify(data.attributes)) { + const labelAttributes = this.taskLabels[this.labelID] + .attributes.map(attr => `${attr.id}`); + + for (const attrID of Object.keys(data.attributes)) { + if (labelAttributes.includes(attrID)) { + this.attributes[attrID] = data.attributes[attrID]; + } + } + } + + if (JSON.stringify(this.points) !== JSON.stringify(data.points)) { + const points = []; + if (!Array.isArray(data.points)) { + throw new window.cvat.exceptions.ArgumentError( + `Got invalid points type "${typeof (data.points.constructor.name)}". Array is expected`, + ); + } + + for (const coordinate of data.points) { + if (typeof (coordinate) !== 'number') { + throw new window.cvat.exceptions.ArgumentError( + `Got invalid point coordinate: "${coordinate}"`, + ); + } + + points.push(coordinate); + } + + // truncate in frame size if it is need + this.points = points; + } + + if (this.occluded !== data.occluded) { + if (typeof (data.occluded) !== 'boolean') { + throw new window.cvat.exceptions.ArgumentError( + `Expected boolean, but got ${data.occluded.constructor.name}`, + ); + } + + this.occluded = data.occluded; + } + + if (this.zOrder !== data.zOrder) { + if (!Number.isInteger(data.zOrder)) { + throw new window.cvat.exceptions.ArgumentError( + `Expected integer, but got ${data.zOrder.constructor.name}`, + ); + } + + this.zOrder = data.zOrder; + } + + if (this.group !== data.group) { + if (!Number.isInteger(data.group)) { + throw new window.cvat.exceptions.ArgumentError( + `Expected integer, but got ${data.group.constructor.name}`, + ); + } + + this.group = data.group; + } + + if (this.color !== data.color) { + if (typeof (data.color) !== 'string') { + throw new window.cvat.exceptions.ArgumentError( + `Expected color represented by a string, but got ${data.color.constructor.name}`, + ); + } + + if (/^#[0-9A-F]{6}$/i.test(data.color)) { + throw new window.cvat.exceptions.ArgumentError( + `Got invalid color value: "${data.color}"`, + ); + } + + this.color = data.color; + } + + data.label = this.taskLabels[this.labelID]; + data.points = [...this.points]; + data.occluded = this.occluded; + data.zOrder = this.zOrder; + data.group = this.group; + data.color = this.color; + data.lock = this.lock; + + if (this.removed) { + return null; + } + + return data; + } catch (exception) { + // reverse all changes if any error + this.labelID = savedLabelID; + this.attributes = savedAttributes; + this.points = savedPoints; + this.occluded = savedOccluded; + this.zOrder = savedZOrder; + this.group = savedGroup; + this.color = savedColor; + + throw exception; + } + } + + delete() { + this.removed = true; + } } class Track extends Annotation { @@ -107,6 +260,7 @@ this.shape = null; } + // Method used to export data to the server toJSON() { return { occluded: this.occluded, @@ -150,6 +304,7 @@ }; } + // Method used for ObjectStates creating get(targetFrame) { return Object.assign( {}, this.interpolatePosition(targetFrame), @@ -160,6 +315,7 @@ type: window.cvat.enums.ObjectType.TRACK, shape: this.shape, clientID: this.clientID, + lock: this.lock, }, ); } @@ -226,6 +382,20 @@ return result; } + + save(frame, data) { + + + if (this.removed) { + return null; + } + + return data; + } + + delete() { + this.removed = true; + } } class Tag extends Annotation { @@ -233,17 +403,29 @@ super(data, clientID, injection); } + // Method used to export data to the server toJSON() { // TODO: Tags support return {}; } + // Method used for ObjectStates creating get(frame) { if (frame !== this.frame) { throw new window.cvat.exceptions.ScriptingError( 'Got frame is not equal to the frame of the shape', ); } + + // TODO: Tags support + } + + save(frame, objectState) { + + } + + delete() { + this.removed = true; } } @@ -527,15 +709,21 @@ 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))); + const objects = tracks.concat(shapes).concat(tags).filter(object => !object.removed); + const states = objects.map(object => object.get(frame)); // filtering here const objectStates = []; - for (const state of states) { + for (let i = 0; i < objects.length; i++) { + const state = states[i]; + const object = objects[i]; const objectState = new ObjectState(state); + + // Rewrite default implementations of save/delete + objectState.save.implementation = object.save.bind(object, frame, objectState); + objectState.delete.implementation = object.delete.bind(object); + objectStates.push(objectState); } diff --git a/cvatjs/src/object-state.js b/cvatjs/src/object-state.js index a44c1c48a7cd..28337b66c443 100644 --- a/cvatjs/src/object-state.js +++ b/cvatjs/src/object-state.js @@ -16,10 +16,14 @@ */ class ObjectState { /** - * @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 + * @param {Object} serialized - is an dictionary which contains + * initial information about an ObjectState; + * Necessary fields: type, shape; + * Necessary fields for ObjectStates which haven't been saved in a collection yet: + * jobID, frame; + * Optional fields: points, group, zOrder, outside, occluded, + * attributes, lock, label, mode, color; + * These fields can be set later via setters */ constructor(serialized) { const data = { @@ -29,12 +33,35 @@ outside: null, occluded: null, lock: null, + color: null, attributes: {}, + jobID: serialized.jobID, + frame: serialized.frame, type: serialized.type, shape: serialized.shape, }; Object.defineProperties(this, Object.freeze({ + jobID: { + /** + * @name jobID + * @type {integer} + * @memberof module:API.cvat.classes.ObjectState + * @readonly + * @instance + */ + get: () => data.jobID, + }, + frame: { + /** + * @name frame + * @type {integer} + * @memberof module:API.cvat.classes.ObjectState + * @readonly + * @instance + */ + get: () => data.frame, + }, type: { /** * @name type @@ -61,50 +88,34 @@ * @type {module:API.cvat.classes.Label} * @memberof module:API.cvat.classes.ObjectState * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} */ get: () => data.label, set: (labelInstance) => { - if (!(labelInstance instanceof window.cvat.classes.Label)) { - throw new window.cvat.exceptions.ArgumentError( - `Expected Label instance, but got "${typeof (labelInstance.constructor.name)}"`, - ); - } - data.label = labelInstance; }, }, - points: { + color: { /** - * @typedef {Object} Point - * @property {number} x - * @property {number} y - * @global + * @name color + * @type {string} + * @memberof module:API.cvat.classes.ObjectState + * @instance */ + get: () => data.color, + set: (color) => { + data.color = color; + }, + }, + points: { + /** * @name points - * @type {Point[]} + * @type {number[]} * @memberof module:API.cvat.classes.ObjectState * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} */ get: () => data.position, set: (position) => { - if (Array.isArray(position)) { - for (const point of position) { - if (typeof (point) !== 'object' - || !('x' in point) || !('y' in point)) { - throw new window.cvat.exceptions.ArgumentError( - `Got invalid point ${point}`, - ); - } - } - } else { - throw new window.cvat.exceptions.ArgumentError( - `Got invalid type "${typeof (position.constructor.name)}"`, - ); - } - data.position = position; }, }, @@ -114,16 +125,9 @@ * @type {integer} * @memberof module:API.cvat.classes.ObjectState * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} */ get: () => data.group, set: (groupID) => { - if (!Number.isInteger(groupID)) { - throw new window.cvat.exceptions.ArgumentError( - `Expected integer, but got ${groupID.constructor.name}`, - ); - } - data.group = groupID; }, }, @@ -133,16 +137,9 @@ * @type {integer} * @memberof module:API.cvat.classes.ObjectState * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} */ get: () => data.zOrder, set: (zOrder) => { - if (!Number.isInteger(zOrder)) { - throw new window.cvat.exceptions.ArgumentError( - `Expected integer, but got ${zOrder.constructor.name}`, - ); - } - data.zOrder = zOrder; }, }, @@ -152,16 +149,9 @@ * @type {boolean} * @memberof module:API.cvat.classes.ObjectState * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} */ get: () => data.outside, set: (outside) => { - if (typeof (outside) !== 'boolean') { - throw new window.cvat.exceptions.ArgumentError( - `Expected boolean, but got ${outside.constructor.name}`, - ); - } - data.outside = outside; }, }, @@ -171,16 +161,9 @@ * @type {boolean} * @memberof module:API.cvat.classes.ObjectState * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} */ get: () => data.occluded, set: (occluded) => { - if (typeof (occluded) !== 'boolean') { - throw new window.cvat.exceptions.ArgumentError( - `Expected boolean, but got ${occluded.constructor.name}`, - ); - } - data.occluded = occluded; }, }, @@ -190,16 +173,9 @@ * @type {boolean} * @memberof module:API.cvat.classes.ObjectState * @instance - * @throws {module:API.cvat.exceptions.ArgumentError} */ get: () => data.lock, set: (lock) => { - if (typeof (lock) !== 'boolean') { - throw new window.cvat.exceptions.ArgumentError( - `Expected boolean, but got ${lock.constructor.name}`, - ); - } - data.lock = lock; }, }, @@ -210,8 +186,8 @@ * @name attributes * @type {Object} * @memberof module:API.cvat.classes.ObjectState - * @instance * @throws {module:API.cvat.exceptions.ArgumentError} + * @instance */ get: () => data.attributes, set: (attributes) => { @@ -221,21 +197,11 @@ ); } - for (let attrId in attributes) { - if (Object.prototype.hasOwnProperty.call(attributes, attrId)) { - attrId = +attrId; - if (!Number.isInteger(attrId)) { - throw new window.cvat.exceptions.ArgumentError( - `Expected integer attribute id, but got ${attrId.constructor.name}`, - ); - } - - data.attributes[attrId] = attributes[attrId]; - } + for (const attrID of Object.keys(attributes)) { + data.attributes[attrID] = attributes[attrID]; } }, }, - })); this.label = serialized.label; @@ -244,26 +210,21 @@ this.outside = serialized.outside; this.occluded = serialized.occluded; this.attributes = serialized.attributes; + this.points = serialized.points; + this.color = serialized.color; 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; } /** - * Method saves object state in a collection + * Method saves/updates an object state in a collection * @method save * @memberof module:API.cvat.classes.ObjectState * @readonly * @instance * @async * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + * @returns {module:API.cvat.classes.ObjectState} */ async save() { const result = await PluginRegistry @@ -272,7 +233,7 @@ } /** - * Method deletes object from a collection + * Method deletes an object from a collection * @method delete * @memberof module:API.cvat.classes.ObjectState * @readonly @@ -287,6 +248,16 @@ } } + // Default implementation saves element in collection + ObjectState.prototype.save.implementation = async function () { + + }; + + // Default implementation do nothing + ObjectState.prototype.delete.implementation = function () { + + }; + module.exports = ObjectState; })(); From 04ffc9fa212a8c078049ae1ea44ae306bbfeecdb Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 5 Jul 2019 16:37:03 +0300 Subject: [PATCH 03/10] Track editing --- cvatjs/src/annotations.js | 216 ++++++++++++++++++++++++++++++++++--- cvatjs/src/object-state.js | 16 ++- 2 files changed, 214 insertions(+), 18 deletions(-) diff --git a/cvatjs/src/annotations.js b/cvatjs/src/annotations.js index 7c177fcbdbaa..87a9bf1c06b0 100644 --- a/cvatjs/src/annotations.js +++ b/cvatjs/src/annotations.js @@ -98,8 +98,8 @@ ); } + let savedAttributes = null; const savedLabelID = this.labelID; - const savedAttributes = this.attributes; const savedPoints = this.points; const savedOccluded = this.occluded; const savedZOrder = this.zOrder; @@ -114,12 +114,15 @@ ); } + savedAttributes = JSON.stringify(this.attributes); this.labelID = data.label.id; this.attributes = {}; this.appendDefaultAttributes(); } if (JSON.stringify(this.attributes) !== JSON.stringify(data.attributes)) { + savedAttributes = savedAttributes || JSON.stringify(this.attributes); + const labelAttributes = this.taskLabels[this.labelID] .attributes.map(attr => `${attr.id}`); @@ -214,13 +217,18 @@ } catch (exception) { // reverse all changes if any error this.labelID = savedLabelID; - this.attributes = savedAttributes; this.points = savedPoints; this.occluded = savedOccluded; this.zOrder = savedZOrder; this.group = savedGroup; this.color = savedColor; + // If savedAttributes === null, attributes haven't been changed + if (savedAttributes) { + this.attributes = JSON.parse(savedAttributes); + } + + throw exception; } } @@ -366,25 +374,194 @@ } } } + } - // 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; - }, {}); + save(frame, data) { + let savedAttributes = null; + let savedShapeArray = null; + let savedOccluded; + let savedZOrder; + let savedPoints; + let savedOutside; + + const savedLabelID = this.shapes; + const savedGroup = this.group; + const savedColor = this.color; + const savedKeyFrame = frame in this.shapes; - for (const attrID of Object.keys(defValuesByID)) { - if (!(attrID in result)) { - result[attrID] = defValuesByID[attrID]; + try { + if (this.labelID !== data.labelID) { + if (!(data.label instanceof window.cvat.classes.Label)) { + throw new window.cvat.exceptions.ArgumentError( + `Expected Label instance, but got "${typeof (data.label.constructor.name)}"`, + ); + } + + this.labelID = data.label.id; + + // deep copy before removing all attributes + savedAttributes = JSON.stringify(this.attributes); + savedShapeArray = JSON.stringify(this.shapes); + this.attributes = {}; + for (const shape of this.shapes) { + shape.attributes = {}; + } + + this.appendDefaultAttributes(); } - } - return result; - } + if (JSON.stringify(this.attributes) !== JSON.stringify(data.attributes)) { + savedAttributes = savedAttributes || JSON.stringify(this.attributes); - save(frame, data) { + const labelAttributes = this.taskLabels[this.labelID] + .attributes.map(attr => `${attr.id}`); + + for (const attrID of Object.keys(data.attributes)) { + // separate on mutable and unmutable + if (labelAttributes.includes(attrID)) { + this.attributes[attrID] = data.attributes[attrID]; + } + } + } + + if (this.group !== data.group) { + if (!Number.isInteger(data.group)) { + throw new window.cvat.exceptions.ArgumentError( + `Expected integer, but got ${data.group.constructor.name}`, + ); + } + + this.group = data.group; + } + + if (this.color !== data.color) { + if (typeof (data.color) !== 'string') { + throw new window.cvat.exceptions.ArgumentError( + `Expected color represented by a string, but got ${data.color.constructor.name}`, + ); + } + + if (/^#[0-9A-F]{6}$/i.test(data.color)) { + throw new window.cvat.exceptions.ArgumentError( + `Got invalid color value: "${data.color}"`, + ); + } + + this.color = data.color; + } + + if (!data.keyframe && savedKeyFrame) { + savedShapeArray = savedShapeArray || JSON.stringify(this.shapes); + delete this.shapes[frame]; + } else { + if (!savedKeyFrame) { + this.shapes[frame] = {}; + } + + const shape = this.shapes[frame]; + savedOccluded = shape.occluded; + savedOutside = shape.outside; + savedZOrder = shape.zOrder; + savedPoints = shape.points; + + if (JSON.stringify(shape.points) !== JSON.stringify(data.points)) { + const points = []; + if (!Array.isArray(data.points)) { + throw new window.cvat.exceptions.ArgumentError( + `Got invalid points type "${typeof (data.points.constructor.name)}". Array is expected`, + ); + } + + for (const coordinate of data.points) { + if (typeof (coordinate) !== 'number') { + throw new window.cvat.exceptions.ArgumentError( + `Got invalid point coordinate: "${coordinate}"`, + ); + } + + points.push(coordinate); + } + + // truncate in frame size if it is need + shape.points = points; + } + + if (shape.occluded !== data.occluded) { + if (typeof (data.occluded) !== 'boolean') { + throw new window.cvat.exceptions.ArgumentError( + `Expected boolean, but got ${data.occluded.constructor.name}`, + ); + } + + shape.occluded = data.occluded; + } + + if (shape.outside !== data.outside) { + if (typeof (data.outside) !== 'boolean') { + throw new window.cvat.exceptions.ArgumentError( + `Expected boolean, but got ${data.outside.constructor.name}`, + ); + } + + shape.outside = data.outside; + } + + if (shape.zOrder !== data.zOrder) { + if (!Number.isInteger(data.zOrder)) { + throw new window.cvat.exceptions.ArgumentError( + `Expected integer, but got ${data.zOrder.constructor.name}`, + ); + } + shape.zOrder = data.zOrder; + } + + data.outside = shape.outside; + data.points = [...shape.points]; + data.occluded = shape.occluded; + data.zOrder = shape.zOrder; + } + } catch (exception) { + this.labelID = savedLabelID; + this.color = savedColor; + this.group = savedGroup; + + // If savedAttributes === null, attributes haven't been changed + if (savedAttributes) { + this.attributes = JSON.parse(savedAttributes); + } + + // If savedShapeArray === null, mutable attributes haven't been changed + // or a shape has been removed + if (savedShapeArray) { + this.shapes = JSON.parse(savedShapeArray); + } + + if (!savedKeyFrame) { + delete this.shapes[frame]; + } else { + if (savedOccluded !== null) { + this.shapes[frame].occluded = savedOccluded; + } + + if (savedOutside !== null) { + this.shapes[frame].outside = savedOutside; + } + + if (savedZOrder !== null) { + this.shapes[frame].zOrder = savedZOrder; + } + + if (savedPoints !== null) { + this.shapes[frame].points = savedPoints; + } + } + } + + data.label = this.taskLabels[this.labelID]; + data.group = this.group; + data.color = this.color; + data.lock = this.lock; if (this.removed) { return null; @@ -412,15 +589,18 @@ occluded: leftPosition.occluded, outside: leftPosition.outside, zOrder: leftPosition.zOrder, + keyframe: true, }; } if (rightPosition && leftPosition) { - return this.interpolatePosition( + return Object.assign({}, this.interpolatePosition( leftPosition, rightPosition, targetFrame, - ); + ), { + keyframe: false, + }); } if (rightPosition) { @@ -429,6 +609,7 @@ occluded: rightPosition.occluded, outside: true, zOrder: 0, + keyframe: false, }; } @@ -438,6 +619,7 @@ occluded: leftPosition.occluded, outside: leftPosition.outside, zOrder: 0, + keyframe: false, }; } diff --git a/cvatjs/src/object-state.js b/cvatjs/src/object-state.js index 28337b66c443..eb3dbd77719f 100644 --- a/cvatjs/src/object-state.js +++ b/cvatjs/src/object-state.js @@ -22,7 +22,7 @@ * Necessary fields for ObjectStates which haven't been saved in a collection yet: * jobID, frame; * Optional fields: points, group, zOrder, outside, occluded, - * attributes, lock, label, mode, color; + * attributes, lock, label, mode, color, keyframe * These fields can be set later via setters */ constructor(serialized) { @@ -34,6 +34,7 @@ occluded: null, lock: null, color: null, + keyframe: null, attributes: {}, jobID: serialized.jobID, frame: serialized.frame, @@ -155,6 +156,18 @@ data.outside = outside; }, }, + keyframe: { + /** + * @name keyframe + * @type {boolean} + * @memberof module:API.cvat.classes.ObjectState + * @instance + */ + get: () => data.keyframe, + set: (keyframe) => { + data.keyframe = keyframe; + }, + }, occluded: { /** * @name occluded @@ -208,6 +221,7 @@ this.group = serialized.group; this.zOrder = serialized.zOrder; this.outside = serialized.outside; + this.keyframe = serialized.keyframe; this.occluded = serialized.occluded; this.attributes = serialized.attributes; this.points = serialized.points; From c7522ccd3b604831b5fb15466f08694aeb22d1f8 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 9 Jul 2019 13:07:45 +0300 Subject: [PATCH 04/10] Debugged version of saving --- cvatjs/.eslintrc.js | 1 + cvatjs/src/annotations.js | 672 +++++++++++++++---------------- cvatjs/src/api-implementation.js | 2 +- cvatjs/src/api.js | 12 + cvatjs/src/object-state.js | 84 +++- 5 files changed, 400 insertions(+), 371 deletions(-) diff --git a/cvatjs/.eslintrc.js b/cvatjs/.eslintrc.js index ff874af13a7f..6989c97e628a 100644 --- a/cvatjs/.eslintrc.js +++ b/cvatjs/.eslintrc.js @@ -48,5 +48,6 @@ "indent": ["warn", 4], "no-useless-constructor": 0, "func-names": [0], + "valid-typeof": [0], }, }; diff --git a/cvatjs/src/annotations.js b/cvatjs/src/annotations.js index 87a9bf1c06b0..63cb78e3a79e 100644 --- a/cvatjs/src/annotations.js +++ b/cvatjs/src/annotations.js @@ -11,30 +11,82 @@ const serverProxy = require('./server-proxy'); const ObjectState = require('./object-state'); + 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); + + 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; this.clientID = clientID; this.serverID = data.id; - this.labelID = data.label_id; + this.label = this.taskLabels[data.label_id]; this.frame = data.frame; this.removed = false; this.lock = false; + this.cache = {}; this.attributes = data.attributes.reduce((attributeAccumulator, attr) => { attributeAccumulator[attr.spec_id] = attr.value; return attributeAccumulator; }, {}); - this.taskLabels = injection.labels; - this.appendDefaultAttributes(); + this.appendDefaultAttributes(this.label); } - appendDefaultAttributes() { - const labelAttributes = this.taskLabels[this.labelID].attributes; + appendDefaultAttributes(label) { + const labelAttributes = label.attributes; for (const attribute of labelAttributes) { if (!(attribute.id in this.attributes)) { this.attributes[attribute.id] = attribute.defaultValue; } } } + + delete() { + this.removed = true; + } } class Shape extends Annotation { @@ -64,7 +116,7 @@ }, []), id: this.serverID, frame: this.frame, - label_id: this.labelID, + label_id: this.label.id, group: this.group, }; } @@ -77,18 +129,25 @@ ); } - return { - type: window.cvat.enums.ObjectType.SHAPE, - shape: this.shape, - clientID: this.clientID, - occluded: this.occluded, - lock: this.lock, - zOrder: this.zOrder, - points: [...this.points], - attributes: Object.assign({}, this.attributes), - label: this.taskLabels[this.labelID], - group: this.group, - }; + if (!(frame in this.cache)) { + const interpolation = { + type: window.cvat.enums.ObjectType.SHAPE, + shape: this.shape, + clientID: this.clientID, + occluded: this.occluded, + lock: this.lock, + zOrder: this.zOrder, + points: [...this.points], + attributes: Object.assign({}, this.attributes), + label: this.label, + group: this.group, + color: this.color, + }; + + this.cache[frame] = interpolation; + } + + return this.cache[frame]; } save(frame, data) { @@ -98,143 +157,83 @@ ); } - let savedAttributes = null; - const savedLabelID = this.labelID; - const savedPoints = this.points; - const savedOccluded = this.occluded; - const savedZOrder = this.zOrder; - const savedGroup = this.group; - const savedColor = this.color; - - try { - if (this.labelID !== data.labelID) { - if (!(data.label instanceof window.cvat.classes.Label)) { - throw new window.cvat.exceptions.ArgumentError( - `Expected Label instance, but got "${typeof (data.label.constructor.name)}"`, - ); - } - - savedAttributes = JSON.stringify(this.attributes); - this.labelID = data.label.id; - this.attributes = {}; - this.appendDefaultAttributes(); - } - - if (JSON.stringify(this.attributes) !== JSON.stringify(data.attributes)) { - savedAttributes = savedAttributes || JSON.stringify(this.attributes); - - const labelAttributes = this.taskLabels[this.labelID] - .attributes.map(attr => `${attr.id}`); - - for (const attrID of Object.keys(data.attributes)) { - if (labelAttributes.includes(attrID)) { - this.attributes[attrID] = data.attributes[attrID]; - } - } - } - - if (JSON.stringify(this.points) !== JSON.stringify(data.points)) { - const points = []; - if (!Array.isArray(data.points)) { - throw new window.cvat.exceptions.ArgumentError( - `Got invalid points type "${typeof (data.points.constructor.name)}". Array is expected`, - ); - } - - for (const coordinate of data.points) { - if (typeof (coordinate) !== 'number') { - throw new window.cvat.exceptions.ArgumentError( - `Got invalid point coordinate: "${coordinate}"`, - ); - } + if (this.lock && data.lock) { + return objectStateFactory.call(this, frame, this.get(frame)); + } - points.push(coordinate); - } + // All changes are done in this temporary object + const copy = Object.assign(this.get(frame)); + copy.attributes = Object.assign(copy.attributes); + copy.points = [...copy.points]; - // truncate in frame size if it is need - this.points = points; - } + const updated = data.updateFlags; - if (this.occluded !== data.occluded) { - if (typeof (data.occluded) !== 'boolean') { - throw new window.cvat.exceptions.ArgumentError( - `Expected boolean, but got ${data.occluded.constructor.name}`, - ); - } + if (updated.label) { + checkObjectType('label', data.label, null, window.cvat.classes.Label); + copy.label = data.label; + copy.attributes = {}; + this.appendDefaultAttributes.call(copy, copy.label); + } - this.occluded = data.occluded; - } + if (updated.attributes) { + const labelAttributes = this.label + .attributes.map(attr => `${attr.id}`); - if (this.zOrder !== data.zOrder) { - if (!Number.isInteger(data.zOrder)) { - throw new window.cvat.exceptions.ArgumentError( - `Expected integer, but got ${data.zOrder.constructor.name}`, - ); + for (const attrID of Object.keys(data.attributes)) { + if (labelAttributes.includes(attrID)) { + copy.attributes[attrID] = data.attributes[attrID]; } - - this.zOrder = data.zOrder; } + } - if (this.group !== data.group) { - if (!Number.isInteger(data.group)) { - throw new window.cvat.exceptions.ArgumentError( - `Expected integer, but got ${data.group.constructor.name}`, - ); - } - - this.group = data.group; + if (updated.points) { + checkObjectType('points', data.points, null, Array); + copy.points = []; + for (const coordinate of data.points) { + checkObjectType('coordinate', coordinate, 'number', null); + copy.points.push(coordinate); } + } - if (this.color !== data.color) { - if (typeof (data.color) !== 'string') { - throw new window.cvat.exceptions.ArgumentError( - `Expected color represented by a string, but got ${data.color.constructor.name}`, - ); - } - - if (/^#[0-9A-F]{6}$/i.test(data.color)) { - throw new window.cvat.exceptions.ArgumentError( - `Got invalid color value: "${data.color}"`, - ); - } + if (updated.occluded) { + checkObjectType('occluded', data.occluded, 'boolean', null); + copy.occluded = data.occluded; + } - this.color = data.color; - } + if (updated.group) { + checkObjectType('group', data.group, 'integer', null); + copy.group = data.group; + } - data.label = this.taskLabels[this.labelID]; - data.points = [...this.points]; - data.occluded = this.occluded; - data.zOrder = this.zOrder; - data.group = this.group; - data.color = this.color; - data.lock = this.lock; + if (updated.zOrder) { + checkObjectType('zOrder', data.zOrder, 'integer', null); + copy.zOrder = data.zOrder; + } - if (this.removed) { - return null; - } + if (updated.lock) { + checkObjectType('lock', data.lock, 'boolean', null); + copy.lock = data.lock; + } - return data; - } catch (exception) { - // reverse all changes if any error - this.labelID = savedLabelID; - this.points = savedPoints; - this.occluded = savedOccluded; - this.zOrder = savedZOrder; - this.group = savedGroup; - this.color = savedColor; - - // If savedAttributes === null, attributes haven't been changed - if (savedAttributes) { - this.attributes = JSON.parse(savedAttributes); + if (updated.color) { + checkObjectType('color', data.color, 'string', null); + if (/^#[0-9A-F]{6}$/i.test(data.color)) { + throw new window.cvat.exceptions.ArgumentError( + `Got invalid color value: "${data.color}"`, + ); } + copy.color = data.color; + } - throw exception; + // Reset flags and commit all changes + updated.reset(); + for (const prop of Object.keys(copy)) { + this[prop] = copy[prop]; + this.cache[frame][prop] = copy[prop]; } - } - delete() { - this.removed = true; + return objectStateFactory.call(this, frame, this.get(frame)); } } @@ -247,7 +246,6 @@ 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) => { @@ -285,7 +283,7 @@ id: this.serverID, frame: this.frame, - label_id: this.labelID, + label_id: this.label.id, group: this.group, shapes: Object.keys(this.shapes).reduce((shapesAccumulator, frame) => { shapesAccumulator.push({ @@ -313,19 +311,26 @@ } // Method used for ObjectStates creating - get(targetFrame) { - return Object.assign( - {}, this.getPosition(targetFrame), - { - attributes: this.getAttributes(targetFrame), - label: this.taskLabels[this.labelID], - group: this.group, - type: window.cvat.enums.ObjectType.TRACK, - shape: this.shape, - clientID: this.clientID, - lock: this.lock, - }, - ); + get(frame) { + if (!(frame in this.cache)) { + const interpolation = Object.assign( + {}, this.getPosition(frame), + { + attributes: this.getAttributes(frame), + label: this.label, + group: this.group, + type: window.cvat.enums.ObjectType.TRACK, + shape: this.shape, + clientID: this.clientID, + lock: this.lock, + color: this.color, + }, + ); + + this.cache[frame] = interpolation; + } + + return JSON.parse(JSON.stringify(this.cache[frame])); } neighborsFrames(targetFrame) { @@ -374,204 +379,174 @@ } } } + + return result; } save(frame, data) { - let savedAttributes = null; - let savedShapeArray = null; - let savedOccluded; - let savedZOrder; - let savedPoints; - let savedOutside; - - const savedLabelID = this.shapes; - const savedGroup = this.group; - const savedColor = this.color; - const savedKeyFrame = frame in this.shapes; - - try { - if (this.labelID !== data.labelID) { - if (!(data.label instanceof window.cvat.classes.Label)) { - throw new window.cvat.exceptions.ArgumentError( - `Expected Label instance, but got "${typeof (data.label.constructor.name)}"`, - ); - } - - this.labelID = data.label.id; + if (this.lock || data.lock) { + this.lock = data.lock; + return objectStateFactory.call(this, frame, this.get(frame)); + } - // deep copy before removing all attributes - savedAttributes = JSON.stringify(this.attributes); - savedShapeArray = JSON.stringify(this.shapes); - this.attributes = {}; - for (const shape of this.shapes) { - shape.attributes = {}; - } + // All changes are done in this temporary object + const copy = Object.assign(this.get(frame)); + copy.attributes = Object.assign(copy.attributes); + copy.points = [...copy.points]; - this.appendDefaultAttributes(); - } + const updated = data.updateFlags; + let positionUpdated = false; - if (JSON.stringify(this.attributes) !== JSON.stringify(data.attributes)) { - savedAttributes = savedAttributes || JSON.stringify(this.attributes); + if (updated.label) { + checkObjectType('label', data.label, null, window.cvat.classes.Label); + copy.label = data.label; + copy.attributes = {}; - const labelAttributes = this.taskLabels[this.labelID] - .attributes.map(attr => `${attr.id}`); + // Shape attributes will be removed later after all checks + this.appendDefaultAttributes.call(copy, copy.label); + } - for (const attrID of Object.keys(data.attributes)) { - // separate on mutable and unmutable - if (labelAttributes.includes(attrID)) { + if (updated.attributes) { + const labelAttributes = this.label.attributes + .reduce((accumulator, value) => { + accumulator[value.id] = value; + return accumulator; + }, {}); + + for (const attrID of Object.keys(data.attributes)) { + if (attrID in labelAttributes) { + copy.attributes[attrID] = data.attributes[attrID]; + if (!labelAttributes[attrID].mutable) { this.attributes[attrID] = data.attributes[attrID]; + } else { + // Mutable attributes will be updated later + positionUpdated = true; } } } + } - if (this.group !== data.group) { - if (!Number.isInteger(data.group)) { - throw new window.cvat.exceptions.ArgumentError( - `Expected integer, but got ${data.group.constructor.name}`, - ); - } - - this.group = data.group; + if (updated.points) { + checkObjectType('points', data.points, null, Array); + copy.points = []; + for (const coordinate of data.points) { + checkObjectType('coordinate', coordinate, 'number', null); + copy.points.push(coordinate); } + positionUpdated = true; + } - if (this.color !== data.color) { - if (typeof (data.color) !== 'string') { - throw new window.cvat.exceptions.ArgumentError( - `Expected color represented by a string, but got ${data.color.constructor.name}`, - ); - } - - if (/^#[0-9A-F]{6}$/i.test(data.color)) { - throw new window.cvat.exceptions.ArgumentError( - `Got invalid color value: "${data.color}"`, - ); - } + if (updated.occluded) { + checkObjectType('occluded', data.occluded, 'boolean', null); + copy.occluded = data.occluded; + positionUpdated = true; + } - this.color = data.color; - } + if (updated.outside) { + checkObjectType('outside', data.outside, 'boolean', null); + copy.outside = data.outside; + positionUpdated = true; + } - if (!data.keyframe && savedKeyFrame) { - savedShapeArray = savedShapeArray || JSON.stringify(this.shapes); - delete this.shapes[frame]; - } else { - if (!savedKeyFrame) { - this.shapes[frame] = {}; - } + if (updated.group) { + checkObjectType('group', data.group, 'integer', null); + copy.group = data.group; + } - const shape = this.shapes[frame]; - savedOccluded = shape.occluded; - savedOutside = shape.outside; - savedZOrder = shape.zOrder; - savedPoints = shape.points; - - if (JSON.stringify(shape.points) !== JSON.stringify(data.points)) { - const points = []; - if (!Array.isArray(data.points)) { - throw new window.cvat.exceptions.ArgumentError( - `Got invalid points type "${typeof (data.points.constructor.name)}". Array is expected`, - ); - } + if (updated.zOrder) { + checkObjectType('zOrder', data.zOrder, 'integer', null); + copy.zOrder = data.zOrder; + positionUpdated = true; + } - for (const coordinate of data.points) { - if (typeof (coordinate) !== 'number') { - throw new window.cvat.exceptions.ArgumentError( - `Got invalid point coordinate: "${coordinate}"`, - ); - } + if (updated.lock) { + checkObjectType('lock', data.lock, 'boolean', null); + copy.lock = data.lock; + } - points.push(coordinate); - } + if (updated.color) { + checkObjectType('color', data.color, 'string', null); + if (/^#[0-9A-F]{6}$/i.test(data.color)) { + throw new window.cvat.exceptions.ArgumentError( + `Got invalid color value: "${data.color}"`, + ); + } - // truncate in frame size if it is need - shape.points = points; - } + copy.color = data.color; + } - if (shape.occluded !== data.occluded) { - if (typeof (data.occluded) !== 'boolean') { - throw new window.cvat.exceptions.ArgumentError( - `Expected boolean, but got ${data.occluded.constructor.name}`, - ); - } + // Commit all changes + for (const prop of Object.keys(copy)) { + if (Object.hasOwnProperty.call(this, prop)) { + this[prop] = copy[prop]; + } - shape.occluded = data.occluded; - } + this.cache[frame][prop] = copy[prop]; + } - if (shape.outside !== data.outside) { - if (typeof (data.outside) !== 'boolean') { - throw new window.cvat.exceptions.ArgumentError( - `Expected boolean, but got ${data.outside.constructor.name}`, - ); - } + if (updated.label) { + for (const shape of this.shapes) { + shape.attributes = {}; + } + } - shape.outside = data.outside; + // Remove keyframe + if (updated.keyframe && !data.keyframe) { + // Remove all cache after this keyframe because it have just become outdated + for (const cacheFrame in this.cache) { + if (+cacheFrame > frame) { + delete this.cache[frame]; } + } - if (shape.zOrder !== data.zOrder) { - if (!Number.isInteger(data.zOrder)) { - throw new window.cvat.exceptions.ArgumentError( - `Expected integer, but got ${data.zOrder.constructor.name}`, - ); - } + this.cache[frame].keyframe = false; + delete this.shapes[frame]; + updated.reset(); - shape.zOrder = data.zOrder; - } - - data.outside = shape.outside; - data.points = [...shape.points]; - data.occluded = shape.occluded; - data.zOrder = shape.zOrder; - } - } catch (exception) { - this.labelID = savedLabelID; - this.color = savedColor; - this.group = savedGroup; - - // If savedAttributes === null, attributes haven't been changed - if (savedAttributes) { - this.attributes = JSON.parse(savedAttributes); - } + return objectStateFactory.call(this, frame, this.get(frame)); + } - // If savedShapeArray === null, mutable attributes haven't been changed - // or a shape has been removed - if (savedShapeArray) { - this.shapes = JSON.parse(savedShapeArray); + // Add/update keyframe + if (positionUpdated || (updated.keyframe && data.keyframe)) { + // Remove all cache after this keyframe because it have just become outdated + for (const cacheFrame in this.cache) { + if (+cacheFrame > frame) { + delete this.cache[frame]; + } } - if (!savedKeyFrame) { - delete this.shapes[frame]; - } else { - if (savedOccluded !== null) { - this.shapes[frame].occluded = savedOccluded; - } + this.cache[frame].keyframe = true; + data.keyframe = true; - if (savedOutside !== null) { - this.shapes[frame].outside = savedOutside; - } + this.shapes[frame] = { + frame, + zOrder: copy.zOrder, + points: copy.points, + outside: copy.outside, + occluded: copy.occluded, + attributes: {}, + }; - if (savedZOrder !== null) { - this.shapes[frame].zOrder = savedZOrder; - } + if (updated.attributes) { + const labelAttributes = this.label.attributes + .reduce((accumulator, value) => { + accumulator[value.id] = value; + return accumulator; + }, {}); - if (savedPoints !== null) { - this.shapes[frame].points = savedPoints; + // Unmutable attributes were updated above + for (const attrID of Object.keys(data.attributes)) { + if (attrID in labelAttributes && labelAttributes[attrID].mutable) { + this.shapes[frame].attributes[attrID] = data.attributes[attrID]; + this.shapes[frame].attributes[attrID] = data.attributes[attrID]; + } } } } - data.label = this.taskLabels[this.labelID]; - data.group = this.group; - data.color = this.color; - data.lock = this.lock; + updated.reset(); - if (this.removed) { - return null; - } - - return data; - } - - delete() { - this.removed = true; + return objectStateFactory.call(this, frame, this.get(frame)); } getPosition(targetFrame) { @@ -1170,22 +1145,22 @@ 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}"`, - ); + 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; @@ -1200,22 +1175,22 @@ 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}"`, - ); + 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; @@ -1280,20 +1255,11 @@ const tags = this.tags[frame] || []; const objects = tracks.concat(shapes).concat(tags).filter(object => !object.removed); - const states = objects.map(object => object.get(frame)); - // filtering here const objectStates = []; - for (let i = 0; i < objects.length; i++) { - const state = states[i]; - const object = objects[i]; - const objectState = new ObjectState(state); - - // Rewrite default implementations of save/delete - objectState.save.implementation = object.save.bind(object, frame, objectState); - objectState.delete.implementation = object.delete.bind(object); - + for (const object of objects) { + const objectState = objectStateFactory.call(object, frame, object.get(frame)); objectStates.push(objectState); } diff --git a/cvatjs/src/api-implementation.js b/cvatjs/src/api-implementation.js index 0a4d4961ff25..e2d7505e8bc8 100644 --- a/cvatjs/src/api-implementation.js +++ b/cvatjs/src/api-implementation.js @@ -116,7 +116,7 @@ if ('taskID' in filter) { task = await serverProxy.tasks.getTasks(`id=${filter.taskID}`); } else { - const [job] = await serverProxy.jobs.getJob(filter.jobID); + const job = await serverProxy.jobs.getJob(filter.jobID); task = await serverProxy.tasks.getTasks(`id=${job.task_id}`); } diff --git a/cvatjs/src/api.js b/cvatjs/src/api.js index 4f88de8cea7a..c2a1bed65dc8 100644 --- a/cvatjs/src/api.js +++ b/cvatjs/src/api.js @@ -453,3 +453,15 @@ window.cvat = Object.freeze(implementAPI(cvat)); })(); + +async function tmp() { + await window.cvat.server.login('admin', 'nimda760'); + const task = (await window.cvat.tasks.get({ id: 2 }))[0]; + const job = (await window.cvat.jobs.get({ jobID: 2 }))[0]; + const annotations = await task.annotations.get(1); + annotations[0].points = [1,2,3,4,5,6]; + await annotations[0].save(); + let a = 5; +} + +tmp(); diff --git a/cvatjs/src/object-state.js b/cvatjs/src/object-state.js index eb3dbd77719f..22557a0bec11 100644 --- a/cvatjs/src/object-state.js +++ b/cvatjs/src/object-state.js @@ -27,22 +27,50 @@ */ constructor(serialized) { const data = { + label: null, + attributes: {}, + points: null, - group: null, - zOrder: null, outside: null, occluded: null, + keyframe: null, + + group: null, + zOrder: null, lock: null, color: null, - keyframe: null, - attributes: {}, + jobID: serialized.jobID, frame: serialized.frame, type: serialized.type, shape: serialized.shape, + updateFlags: {}, }; + // Shows whether any properties updated since last reset() or interpolation + Object.defineProperty(data.updateFlags, 'reset', { + value: function reset() { + this.label = false; + this.attributes = false; + + this.points = false; + this.outside = false; + this.occluded = false; + this.keyframe = false; + + this.group = false; + this.zOrder = false; + this.lock = false; + this.color = false; + }, + writable: false, + }); + Object.defineProperties(this, Object.freeze({ + // Internal property. We don't need document it. + updateFlags: { + get: () => data.updateFlags, + }, jobID: { /** * @name jobID @@ -92,6 +120,7 @@ */ get: () => data.label, set: (labelInstance) => { + data.updateFlags.label = true; data.label = labelInstance; }, }, @@ -104,20 +133,21 @@ */ get: () => data.color, set: (color) => { + data.updateFlags.color = true; data.color = color; }, }, points: { - /** * @name points * @type {number[]} * @memberof module:API.cvat.classes.ObjectState * @instance */ - get: () => data.position, - set: (position) => { - data.position = position; + get: () => data.points, + set: (points) => { + data.updateFlags.points = true; + data.points = [...points]; }, }, group: { @@ -128,8 +158,9 @@ * @instance */ get: () => data.group, - set: (groupID) => { - data.group = groupID; + set: (group) => { + data.updateFlags.group = true; + data.group = group; }, }, zOrder: { @@ -141,6 +172,7 @@ */ get: () => data.zOrder, set: (zOrder) => { + data.updateFlags.zOrder = true; data.zOrder = zOrder; }, }, @@ -153,6 +185,7 @@ */ get: () => data.outside, set: (outside) => { + data.updateFlags.outside = true; data.outside = outside; }, }, @@ -165,6 +198,7 @@ */ get: () => data.keyframe, set: (keyframe) => { + data.updateFlags.keyframe = true; data.keyframe = keyframe; }, }, @@ -177,6 +211,7 @@ */ get: () => data.occluded, set: (occluded) => { + data.updateFlags.occluded = true; data.occluded = occluded; }, }, @@ -189,6 +224,7 @@ */ get: () => data.lock, set: (lock) => { + data.updateFlags.lock = true; data.lock = lock; }, }, @@ -205,12 +241,19 @@ get: () => data.attributes, set: (attributes) => { if (typeof (attributes) !== 'object') { + if (typeof (attributes) === 'undefined') { + throw new window.cvat.exceptions.ArgumentError( + 'Expected attributes are object, but got undefined', + ); + } + throw new window.cvat.exceptions.ArgumentError( - `Expected object, but got ${attributes.constructor.name}`, + `Expected attributes are object, but got ${attributes.constructor.name}`, ); } for (const attrID of Object.keys(attributes)) { + data.updateFlags.attributes = true; data.attributes[attrID] = attributes[attrID]; } }, @@ -226,7 +269,9 @@ this.attributes = serialized.attributes; this.points = serialized.points; this.color = serialized.color; - this.lock = false; + this.lock = serialized.lock; + + data.updateFlags.reset(); } /** @@ -252,24 +297,29 @@ * @memberof module:API.cvat.classes.ObjectState * @readonly * @instance + * @param {boolean} [force=false] * @async * @throws {module:API.cvat.exceptions.PluginError} */ - async delete() { + async delete(force = false) { const result = await PluginRegistry - .apiWrapper.call(this, ObjectState.prototype.delete); + .apiWrapper.call(this, ObjectState.prototype.delete, force); return result; } } // Default implementation saves element in collection ObjectState.prototype.save.implementation = async function () { - + if (this.updateInCollection) { + this.updateInCollection(); + } else { + // add new object into collection + } }; // Default implementation do nothing - ObjectState.prototype.delete.implementation = function () { - + ObjectState.prototype.delete.implementation = function (force) { + force = true; }; From 49646c0a6157d17926b8af29ff022011e2fc69db Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 9 Jul 2019 13:09:12 +0300 Subject: [PATCH 05/10] Removed extra code --- cvatjs/src/api.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/cvatjs/src/api.js b/cvatjs/src/api.js index c2a1bed65dc8..4f88de8cea7a 100644 --- a/cvatjs/src/api.js +++ b/cvatjs/src/api.js @@ -453,15 +453,3 @@ window.cvat = Object.freeze(implementAPI(cvat)); })(); - -async function tmp() { - await window.cvat.server.login('admin', 'nimda760'); - const task = (await window.cvat.tasks.get({ id: 2 }))[0]; - const job = (await window.cvat.jobs.get({ jobID: 2 }))[0]; - const annotations = await task.annotations.get(1); - annotations[0].points = [1,2,3,4,5,6]; - await annotations[0].save(); - let a = 5; -} - -tmp(); From 40560c3f1c4d977578bfab87e9f475f167b7f9c7 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 9 Jul 2019 13:10:59 +0300 Subject: [PATCH 06/10] Fixed indents --- cvatjs/src/annotations.js | 68 +++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/cvatjs/src/annotations.js b/cvatjs/src/annotations.js index 63cb78e3a79e..ed2b28ded94f 100644 --- a/cvatjs/src/annotations.js +++ b/cvatjs/src/annotations.js @@ -574,8 +574,8 @@ rightPosition, targetFrame, ), { - keyframe: false, - }); + keyframe: false, + }); } if (rightPosition) { @@ -1145,22 +1145,22 @@ 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}"`, - ); + 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; @@ -1175,22 +1175,22 @@ 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}"`, - ); + 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; From c9a2e233ad23a7bc579faa53241722aaf4a0d528 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 9 Jul 2019 13:25:22 +0300 Subject: [PATCH 07/10] Delete function --- cvatjs/src/annotations.js | 12 ++++++------ cvatjs/src/object-state.js | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/cvatjs/src/annotations.js b/cvatjs/src/annotations.js index ed2b28ded94f..011206d0ccfe 100644 --- a/cvatjs/src/annotations.js +++ b/cvatjs/src/annotations.js @@ -84,8 +84,12 @@ } } - delete() { - this.removed = true; + delete(force) { + if (!this.lock || force) { + this.removed = true; + } + + return true; } } @@ -629,10 +633,6 @@ save(frame, objectState) { } - - delete() { - this.removed = true; - } } class RectangleShape extends Shape { diff --git a/cvatjs/src/object-state.js b/cvatjs/src/object-state.js index 22557a0bec11..c3a92888ba31 100644 --- a/cvatjs/src/object-state.js +++ b/cvatjs/src/object-state.js @@ -283,7 +283,7 @@ * @async * @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.ArgumentError} - * @returns {module:API.cvat.classes.ObjectState} + * @returns {module:API.cvat.classes.ObjectState} updated state of an object */ async save() { const result = await PluginRegistry @@ -297,8 +297,9 @@ * @memberof module:API.cvat.classes.ObjectState * @readonly * @instance - * @param {boolean} [force=false] + * @param {boolean} [force=false] delete object even if it is locked * @async + * @returns {boolean} wheter object was deleted * @throws {module:API.cvat.exceptions.PluginError} */ async delete(force = false) { @@ -311,15 +312,19 @@ // Default implementation saves element in collection ObjectState.prototype.save.implementation = async function () { if (this.updateInCollection) { - this.updateInCollection(); - } else { - // add new object into collection + return this.updateInCollection(); } + + return this; }; // Default implementation do nothing - ObjectState.prototype.delete.implementation = function (force) { - force = true; + ObjectState.prototype.delete.implementation = async function (force) { + if (this.deleteFromCollection) { + return this.deleteFromCollection(force); + } + + return false; }; From ccbe5de9115dd2e285029e0204dd820f1f821dfd Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 9 Jul 2019 14:10:27 +0300 Subject: [PATCH 08/10] Save for tags --- cvatjs/src/annotations.js | 147 +++++++++++++++++++++++++++----------- cvatjs/src/api.js | 12 ++++ 2 files changed, 116 insertions(+), 43 deletions(-) diff --git a/cvatjs/src/annotations.js b/cvatjs/src/annotations.js index 011206d0ccfe..0fdf5ee32cef 100644 --- a/cvatjs/src/annotations.js +++ b/cvatjs/src/annotations.js @@ -63,11 +63,11 @@ this.taskLabels = injection.labels; this.clientID = clientID; this.serverID = data.id; + this.group = data.group; this.label = this.taskLabels[data.label_id]; this.frame = data.frame; this.removed = false; this.lock = false; - this.cache = {}; this.attributes = data.attributes.reduce((attributeAccumulator, attr) => { attributeAccumulator[attr.spec_id] = attr.value; return attributeAccumulator; @@ -99,12 +99,11 @@ this.points = data.points; this.occluded = data.occluded; this.zOrder = data.z_order; - this.group = data.group; this.color = color; this.shape = null; } - // Method used to export data to the server + // Method is used to export data to the server toJSON() { return { occluded: this.occluded, @@ -125,7 +124,7 @@ }; } - // Method used for ObjectStates creating + // Method is used to construct ObjectState objects get(frame) { if (frame !== this.frame) { throw new window.cvat.exceptions.ScriptingError( @@ -133,25 +132,19 @@ ); } - if (!(frame in this.cache)) { - const interpolation = { - type: window.cvat.enums.ObjectType.SHAPE, - shape: this.shape, - clientID: this.clientID, - occluded: this.occluded, - lock: this.lock, - zOrder: this.zOrder, - points: [...this.points], - attributes: Object.assign({}, this.attributes), - label: this.label, - group: this.group, - color: this.color, - }; - - this.cache[frame] = interpolation; - } - - return this.cache[frame]; + return { + type: window.cvat.enums.ObjectType.SHAPE, + shape: this.shape, + clientID: this.clientID, + occluded: this.occluded, + lock: this.lock, + zOrder: this.zOrder, + points: [...this.points], + attributes: Object.assign({}, this.attributes), + label: this.label, + group: this.group, + color: this.color, + }; } save(frame, data) { @@ -166,10 +159,7 @@ } // All changes are done in this temporary object - const copy = Object.assign(this.get(frame)); - copy.attributes = Object.assign(copy.attributes); - copy.points = [...copy.points]; - + const copy = this.get(frame); const updated = data.updateFlags; if (updated.label) { @@ -180,7 +170,7 @@ } if (updated.attributes) { - const labelAttributes = this.label + const labelAttributes = copy.label .attributes.map(attr => `${attr.id}`); for (const attrID of Object.keys(data.attributes)) { @@ -233,8 +223,9 @@ // Reset flags and commit all changes updated.reset(); for (const prop of Object.keys(copy)) { - this[prop] = copy[prop]; - this.cache[frame][prop] = copy[prop]; + if (prop in this) { + this[prop] = copy[prop]; + } } return objectStateFactory.call(this, frame, this.get(frame)); @@ -261,16 +252,17 @@ return shapeAccumulator; }, {}); - this.group = data.group; this.attributes = data.attributes.reduce((attributeAccumulator, attr) => { attributeAccumulator[attr.spec_id] = attr.value; return attributeAccumulator; }, {}); + + this.cache = {}; this.color = color; this.shape = null; } - // Method used to export data to the server + // Method is used to export data to the server toJSON() { return { occluded: this.occluded, @@ -314,7 +306,7 @@ }; } - // Method used for ObjectStates creating + // Method is used to construct ObjectState objects get(frame) { if (!(frame in this.cache)) { const interpolation = Object.assign( @@ -411,7 +403,7 @@ } if (updated.attributes) { - const labelAttributes = this.label.attributes + const labelAttributes = copy.label.attributes .reduce((accumulator, value) => { accumulator[value.id] = value; return accumulator; @@ -481,7 +473,7 @@ // Commit all changes for (const prop of Object.keys(copy)) { - if (Object.hasOwnProperty.call(this, prop)) { + if (prop in this) { this[prop] = copy[prop]; } @@ -578,8 +570,8 @@ rightPosition, targetFrame, ), { - keyframe: false, - }); + keyframe: false, + }); } if (rightPosition) { @@ -613,13 +605,25 @@ super(data, clientID, injection); } - // Method used to export data to the server + // Method is used to export data to the server toJSON() { - // TODO: Tags support - return {}; + return { + 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, + value: this.attributes[attrId], + }); + + return attributeAccumulator; + }, []), + }; } - // Method used for ObjectStates creating + // Method is used to construct ObjectState objects get(frame) { if (frame !== this.frame) { throw new window.cvat.exceptions.ScriptingError( @@ -627,11 +631,68 @@ ); } - // TODO: Tags support + return { + type: window.cvat.enums.ObjectType.TAG, + clientID: this.clientID, + lock: this.lock, + attributes: Object.assign({}, this.attributes), + label: this.label, + group: this.group, + }; } - save(frame, objectState) { + save(frame, data) { + if (frame !== this.frame) { + throw new window.cvat.exceptions.ScriptingError( + 'Got frame is not equal to the frame of the shape', + ); + } + + if (this.lock && data.lock) { + return objectStateFactory.call(this, frame, this.get(frame)); + } + + // All changes are done in this temporary object + const copy = this.get(frame); + const updated = data.updateFlags; + if (updated.label) { + checkObjectType('label', data.label, null, window.cvat.classes.Label); + copy.label = data.label; + copy.attributes = {}; + this.appendDefaultAttributes.call(copy, copy.label); + } + + if (updated.attributes) { + const labelAttributes = copy.label + .attributes.map(attr => `${attr.id}`); + + for (const attrID of Object.keys(data.attributes)) { + if (labelAttributes.includes(attrID)) { + copy.attributes[attrID] = data.attributes[attrID]; + } + } + } + + if (updated.group) { + checkObjectType('group', data.group, 'integer', null); + copy.group = data.group; + } + + if (updated.lock) { + checkObjectType('lock', data.lock, 'boolean', null); + copy.lock = data.lock; + } + + // Reset flags and commit all changes + updated.reset(); + for (const prop of Object.keys(copy)) { + if (prop in this) { + this[prop] = copy[prop]; + } + } + + return objectStateFactory.call(this, frame, this.get(frame)); } } diff --git a/cvatjs/src/api.js b/cvatjs/src/api.js index 4f88de8cea7a..9c57bc0db7f6 100644 --- a/cvatjs/src/api.js +++ b/cvatjs/src/api.js @@ -453,3 +453,15 @@ window.cvat = Object.freeze(implementAPI(cvat)); })(); + +async function tmp() { + await window.cvat.server.login('admin', 'nimda760'); + const task = (await window.cvat.tasks.get({ id: 2 }))[0]; + const job = (await window.cvat.jobs.get({ jobID: 2 }))[0]; + const annotations = await task.annotations.get(1); + annotations[0].points = [1,2,3,4,5,6]; + await annotations[0].delete(); + let a = 5; +} + +tmp(); From bf880e06bbca7ac98d5e08afba2887f005b8ea37 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 9 Jul 2019 14:11:00 +0300 Subject: [PATCH 09/10] Removed extra code --- cvatjs/src/api.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/cvatjs/src/api.js b/cvatjs/src/api.js index 9c57bc0db7f6..4f88de8cea7a 100644 --- a/cvatjs/src/api.js +++ b/cvatjs/src/api.js @@ -453,15 +453,3 @@ window.cvat = Object.freeze(implementAPI(cvat)); })(); - -async function tmp() { - await window.cvat.server.login('admin', 'nimda760'); - const task = (await window.cvat.tasks.get({ id: 2 }))[0]; - const job = (await window.cvat.jobs.get({ jobID: 2 }))[0]; - const annotations = await task.annotations.get(1); - annotations[0].points = [1,2,3,4,5,6]; - await annotations[0].delete(); - let a = 5; -} - -tmp(); From 54919931bf6f7aa2d53071162eca133d6538eec0 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 9 Jul 2019 14:18:05 +0300 Subject: [PATCH 10/10] Removed extra field from ObjectState --- cvatjs/src/object-state.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/cvatjs/src/object-state.js b/cvatjs/src/object-state.js index c3a92888ba31..44acce924425 100644 --- a/cvatjs/src/object-state.js +++ b/cvatjs/src/object-state.js @@ -18,9 +18,8 @@ /** * @param {Object} serialized - is an dictionary which contains * initial information about an ObjectState; - * Necessary fields: type, shape; - * Necessary fields for ObjectStates which haven't been saved in a collection yet: - * jobID, frame; + * 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 * These fields can be set later via setters @@ -40,7 +39,6 @@ lock: null, color: null, - jobID: serialized.jobID, frame: serialized.frame, type: serialized.type, shape: serialized.shape, @@ -71,16 +69,6 @@ updateFlags: { get: () => data.updateFlags, }, - jobID: { - /** - * @name jobID - * @type {integer} - * @memberof module:API.cvat.classes.ObjectState - * @readonly - * @instance - */ - get: () => data.jobID, - }, frame: { /** * @name frame