From f83b03f19e9a5eb20db1122f2909c4cbcdc88e15 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 13 Feb 2020 14:20:48 +0300 Subject: [PATCH 01/12] Initial filter function --- cvat-core/src/annotations-collection.js | 63 +++++++++++++++++++++++-- cvat-core/src/annotations.js | 4 +- cvat-core/src/enums.js | 14 ++++++ cvat-core/src/session.js | 17 ++++--- 4 files changed, 87 insertions(+), 11 deletions(-) diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js index 560a79b9371..90e5893f063 100644 --- a/cvat-core/src/annotations-collection.js +++ b/cvat-core/src/annotations-collection.js @@ -39,6 +39,57 @@ } = require('./enums'); const ObjectState = require('./object-state'); + function filterObject(objectState, filter) { + let width = 0; + let height = 0; + const stateAttributes = {}; + + if (typeof (filter.width) === 'number' || typeof (filter.height) === 'number') { + let xtl = Number.MAX_SAFE_INTEGER; + let xbr = Number.MIN_SAFE_INTEGER; + let ytl = Number.MAX_SAFE_INTEGER; + let ybr = Number.MIN_SAFE_INTEGER; + + objectState.points.forEach((coord, idx) => { + if (idx % 2) { // y + ytl = Math.min(ytl, coord); + ybr = Math.max(ybr, coord); + } else { // x + xtl = Math.min(xtl, coord); + xbr = Math.max(xbr, coord); + } + }); + + [width, height] = [xbr - xtl, ybr - ytl]; + } + + if (typeof (filter.attributes) === 'object' && Object.keys(filter.attributes).length) { + const labelAttributes = objectState.label.attributes + .reduce((acc, attr) => { + acc[attr.id] = attr.name; + return acc; + }); + + const objectAttributes = objectState.attributes; + Object.keys(objectAttributes).reduce((acc, key) => { + acc[labelAttributes[key]] = objectAttributes[key]; + return acc; + }, stateAttributes); + } + + return (filter.label ? filter.label === objectState.label.name : true) + && (filter.type ? filter.type === objectState.objectType : true) + && (filter.shape ? filter.shape === objectState.shapeType : true) + && (typeof (filter.occluded) === 'boolean' ? filter.occluded === objectState.occluded : true) + && (typeof (filter.lock) === 'boolean' ? filter.lock === objectState.lock : true) + && (typeof (filter.serverID) === 'number' ? filter.serverID === objectState.serverID : true) + && (typeof (filter.clientID) === 'number' ? filter.clientID === objectState.clientID : true) + && (typeof (filter.width) === 'number' ? filter.width === objectState.width : true) + && (typeof (filter.height) === 'number' ? filter.height === objectState.height : true) + && (typeof (filter.attributes) === 'object' ? Object.keys(filter.attributes) + .every((key) => stateAttributes[key] === filter.attributes[key]) : true); + } + function shapeFactory(shapeData, clientID, injection) { const { type } = shapeData; const color = colors[clientID % colors.length]; @@ -193,7 +244,7 @@ return data; } - get(frame) { + get(frame, allTracks, filter) { const { tracks } = this; const shapes = this.shapes[frame] || []; const tags = this.tags[frame] || []; @@ -204,12 +255,18 @@ const objectStates = []; for (const object of objects) { const stateData = object.get(frame); - if (stateData.outside && !stateData.keyframe) { + if (!allTracks && stateData.outside && !stateData.keyframe) { continue; } const objectState = objectStateFactory.call(object, frame, stateData); - objectStates.push(objectState); + if (typeof (filter) === 'object' && Object.keys(filter).length) { + if (filterObject(objectState, filter)) { + objectStates.push(objectState); + } + } else { + objectStates.push(objectState); + } } return objectStates; diff --git a/cvat-core/src/annotations.js b/cvat-core/src/annotations.js index 516a8280b84..b935c57bfbc 100644 --- a/cvat-core/src/annotations.js +++ b/cvat-core/src/annotations.js @@ -77,12 +77,12 @@ } } - async function getAnnotations(session, frame, filter) { + async function getAnnotations(session, frame, allTracks, filter) { const sessionType = session instanceof Task ? 'task' : 'job'; const cache = getCache(sessionType); if (cache.has(session)) { - return cache.get(session).collection.get(frame, filter); + return cache.get(session).collection.get(frame, allTracks, filter); } await getAnnotationsFromServer(session); diff --git a/cvat-core/src/enums.js b/cvat-core/src/enums.js index df7c5ca4e9f..c4dfdde3d70 100644 --- a/cvat-core/src/enums.js +++ b/cvat-core/src/enums.js @@ -205,6 +205,19 @@ REMOVED_OBJECT: 'Removed object', }); + /* SearchDirection + * @enum {string} + * @name LogType + * @memberof module:API.cvat.enums + * @property {string} FORWARD 'forward' + * @property {string} BACKWARD 'backward' + * @readonly + */ + const SearchDirection = Object.freeze({ + FORWARD: 'forward', + BACKWARD: 'backward', + }); + /** * Array of hex colors * @type {module:API.cvat.classes.Loader[]} values @@ -229,6 +242,7 @@ ObjectShape, LogType, HistoryActions, + SearchDirection, colors, }; })(); diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 2a19364c1c4..28c09b8c66c 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -56,9 +56,10 @@ return result; }, - async get(frame, filter = {}) { + async get(frame, allTracks = false, filter = {}) { const result = await PluginRegistry - .apiWrapper.call(this, prototype.annotations.get, frame, filter); + .apiWrapper.call(this, prototype.annotations.get, + frame, allTracks, filter); return result; }, @@ -280,6 +281,8 @@ * @property {module:API.cvat.enums.ObjectShape} [shape] * @property {boolean} [occluded] a value of occluded property * @property {boolean} [lock] a value of lock property + * @property {number} [serverID] a value of lock property + * @property {number} [clientID] a value of lock property * @property {number} [width] a width of a shape * @property {number} [height] a height of a shape * @property {Object[]} [attributes] dictionary with "name: value" pairs @@ -289,6 +292,8 @@ * Get annotations for a specific frame * @method get * @param {integer} frame get objects from the frame + * @param {boolean} allTracks show all tracks + * even if they are outside and not keyframe * @param {ObjectFilter[]} [filter = []] * get only objects are satisfied to specific filter * @returns {module:API.cvat.classes.ObjectState[]} @@ -1305,14 +1310,14 @@ }; // TODO: Check filter for annotations - Job.prototype.annotations.get.implementation = async function (frame, filter) { + Job.prototype.annotations.get.implementation = async function (frame, allTracks, filter) { if (frame < this.startFrame || frame > this.stopFrame) { throw new ArgumentError( `Frame ${frame} does not exist in the job`, ); } - const annotationsData = await getAnnotations(this, frame, filter); + const annotationsData = await getAnnotations(this, frame, allTracks, filter); return annotationsData; }; @@ -1476,7 +1481,7 @@ }; // TODO: Check filter for annotations - Task.prototype.annotations.get.implementation = async function (frame, filter) { + Task.prototype.annotations.get.implementation = async function (frame, allTracks, filter) { if (!Number.isInteger(frame) || frame < 0) { throw new ArgumentError( `Frame must be a positive integer. Got: "${frame}"`, @@ -1489,7 +1494,7 @@ ); } - const result = await getAnnotations(this, frame, filter); + const result = await getAnnotations(this, frame, allTracks, filter); return result; }; From e5e4f0e34cada38e538d6887bfda78324f10cb3f Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 14 Feb 2020 15:53:51 +0300 Subject: [PATCH 02/12] Updated method for filtering --- cvat-core/package.json | 1 + cvat-core/src/annotations-collection.js | 91 ++++++++----------------- 2 files changed, 29 insertions(+), 63 deletions(-) diff --git a/cvat-core/package.json b/cvat-core/package.json index 8e2088f614c..558f437c1bc 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -38,6 +38,7 @@ "form-data": "^2.5.0", "jest-config": "^24.8.0", "js-cookie": "^2.2.0", + "jsonpath": "^1.0.2", "platform": "^1.3.5", "store": "^2.0.12" } diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js index 90e5893f063..c7e80ce7d1d 100644 --- a/cvat-core/src/annotations-collection.js +++ b/cvat-core/src/annotations-collection.js @@ -1,5 +1,5 @@ /* -* Copyright (C) 2019 Intel Corporation +* Copyright (C) 2019-2020 Intel Corporation * SPDX-License-Identifier: MIT */ @@ -22,6 +22,7 @@ Tag, objectStateFactory, } = require('./annotations-objects'); + const AnnotationsFilter = require('./annotations-filter'); const { checkObjectType } = require('./common'); const Statistics = require('./statistics'); const { Label } = require('./labels'); @@ -39,57 +40,6 @@ } = require('./enums'); const ObjectState = require('./object-state'); - function filterObject(objectState, filter) { - let width = 0; - let height = 0; - const stateAttributes = {}; - - if (typeof (filter.width) === 'number' || typeof (filter.height) === 'number') { - let xtl = Number.MAX_SAFE_INTEGER; - let xbr = Number.MIN_SAFE_INTEGER; - let ytl = Number.MAX_SAFE_INTEGER; - let ybr = Number.MIN_SAFE_INTEGER; - - objectState.points.forEach((coord, idx) => { - if (idx % 2) { // y - ytl = Math.min(ytl, coord); - ybr = Math.max(ybr, coord); - } else { // x - xtl = Math.min(xtl, coord); - xbr = Math.max(xbr, coord); - } - }); - - [width, height] = [xbr - xtl, ybr - ytl]; - } - - if (typeof (filter.attributes) === 'object' && Object.keys(filter.attributes).length) { - const labelAttributes = objectState.label.attributes - .reduce((acc, attr) => { - acc[attr.id] = attr.name; - return acc; - }); - - const objectAttributes = objectState.attributes; - Object.keys(objectAttributes).reduce((acc, key) => { - acc[labelAttributes[key]] = objectAttributes[key]; - return acc; - }, stateAttributes); - } - - return (filter.label ? filter.label === objectState.label.name : true) - && (filter.type ? filter.type === objectState.objectType : true) - && (filter.shape ? filter.shape === objectState.shapeType : true) - && (typeof (filter.occluded) === 'boolean' ? filter.occluded === objectState.occluded : true) - && (typeof (filter.lock) === 'boolean' ? filter.lock === objectState.lock : true) - && (typeof (filter.serverID) === 'number' ? filter.serverID === objectState.serverID : true) - && (typeof (filter.clientID) === 'number' ? filter.clientID === objectState.clientID : true) - && (typeof (filter.width) === 'number' ? filter.width === objectState.width : true) - && (typeof (filter.height) === 'number' ? filter.height === objectState.height : true) - && (typeof (filter.attributes) === 'object' ? Object.keys(filter.attributes) - .every((key) => stateAttributes[key] === filter.attributes[key]) : true); - } - function shapeFactory(shapeData, clientID, injection) { const { type } = shapeData; const color = colors[clientID % colors.length]; @@ -161,6 +111,7 @@ return labelAccumulator; }, {}); + this.annotationsFilter = new AnnotationsFilter(); this.history = data.history; this.shapes = {}; // key is a frame this.tags = {}; // key is a frame @@ -244,30 +195,44 @@ return data; } - get(frame, allTracks, filter) { + get(frame, allTracks, filters) { const { tracks } = this; const shapes = this.shapes[frame] || []; const tags = this.tags[frame] || []; - const objects = tracks.concat(shapes).concat(tags).filter((object) => !object.removed); - // filtering here + const objects = [].concat(tracks, shapes, tags); + const visible = { + models: [], + data: [], + }; - const objectStates = []; for (const object of objects) { + if (object.removed) { + continue; + } + const stateData = object.get(frame); if (!allTracks && stateData.outside && !stateData.keyframe) { continue; } - const objectState = objectStateFactory.call(object, frame, stateData); - if (typeof (filter) === 'object' && Object.keys(filter).length) { - if (filterObject(objectState, filter)) { - objectStates.push(objectState); - } - } else { + visible.models.push(object); + visible.data.push(stateData); + } + + let filtered = []; + if (filters.length) { + filtered = this.annotationsFilter.filter(visible.data, filters); + } + + const objectStates = []; + visible.data.forEach((stateData, idx) => { + if (!filters.length || filtered.includes(stateData.clientID)) { + const model = visible.models[idx]; + const objectState = objectStateFactory.call(model, frame, stateData); objectStates.push(objectState); } - } + }); return objectStates; } From 59fadb43dfd55895b03520c52f260b4dc5051f9c Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 14 Feb 2020 15:54:06 +0300 Subject: [PATCH 03/12] Updated documentation --- cvat-core/src/session.js | 67 +++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 28c09b8c66c..8befcc39170 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -56,10 +56,10 @@ return result; }, - async get(frame, allTracks = false, filter = {}) { + async get(frame, allTracks = false, filters = []) { const result = await PluginRegistry .apiWrapper.call(this, prototype.annotations.get, - frame, allTracks, filter); + frame, allTracks, filters); return result; }, @@ -274,28 +274,33 @@ * @instance * @async */ - /** - * @typedef {Object} ObjectFilter - * @property {string} [label] a name of a label - * @property {module:API.cvat.enums.ObjectType} [type] - * @property {module:API.cvat.enums.ObjectShape} [shape] - * @property {boolean} [occluded] a value of occluded property - * @property {boolean} [lock] a value of lock property - * @property {number} [serverID] a value of lock property - * @property {number} [clientID] a value of lock property - * @property {number} [width] a width of a shape - * @property {number} [height] a height of a shape - * @property {Object[]} [attributes] dictionary with "name: value" pairs - * @global - */ /** * Get annotations for a specific frame + *
Filter supports following operators: + * ==, !=, >, >=, <, <=, ~= and (), |, & for grouping. + *
Filter supports properties: + * width, height, label, serverID, clientID, type, shape, occluded + *
All prop values are case-sensitive. CVAT uses json queries for search. + *
Examples: + *
    + *
  • label=="car" | label==["road sign"]
  • + *
  • width >= height
  • + *
  • attr["Attribute 1"] == attr["Attribute 2"]
  • + *
  • type=="track" & shape="rectangle"
  • + *
  • clientID == 50
  • + *
  • (label=="car" & attr["parked"]==true) + * | (label=="pedestrian" & width > 150)
  • + *
  • (( label==["car \\"mazda\\""]) & + * (attr["sunglass ( help ) es"]==true | + * (width > 150 | height > 150 & (clientID == serverID)))))
  • + *
+ * If you have double quotes in your query string, please escape them using back slash: \" * @method get * @param {integer} frame get objects from the frame * @param {boolean} allTracks show all tracks * even if they are outside and not keyframe - * @param {ObjectFilter[]} [filter = []] - * get only objects are satisfied to specific filter + * @param {string[]} [filters = []] + * get only objects that satisfied to specific filters * @returns {module:API.cvat.classes.ObjectState[]} * @memberof Session.annotations * @throws {module:API.cvat.exceptions.PluginError} @@ -1310,14 +1315,26 @@ }; // TODO: Check filter for annotations - Job.prototype.annotations.get.implementation = async function (frame, allTracks, filter) { + Job.prototype.annotations.get.implementation = async function (frame, allTracks, filters) { + if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) { + throw new ArgumentError( + 'The filters argument must be an array of strings', + ); + } + + if (!Number.isInteger(frame)) { + throw new ArgumentError( + 'The frame argument must be an integer', + ); + } + if (frame < this.startFrame || frame > this.stopFrame) { throw new ArgumentError( `Frame ${frame} does not exist in the job`, ); } - const annotationsData = await getAnnotations(this, frame, allTracks, filter); + const annotationsData = await getAnnotations(this, frame, allTracks, filters); return annotationsData; }; @@ -1481,7 +1498,13 @@ }; // TODO: Check filter for annotations - Task.prototype.annotations.get.implementation = async function (frame, allTracks, filter) { + Task.prototype.annotations.get.implementation = async function (frame, allTracks, filters) { + if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) { + throw new ArgumentError( + 'The filters argument must be an array of strings', + ); + } + if (!Number.isInteger(frame) || frame < 0) { throw new ArgumentError( `Frame must be a positive integer. Got: "${frame}"`, @@ -1494,7 +1517,7 @@ ); } - const result = await getAnnotations(this, frame, allTracks, filter); + const result = await getAnnotations(this, frame, allTracks, filters); return result; }; From 732d648472bc91c13752d4aa949554fc4eeb3932 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 14 Feb 2020 15:54:27 +0300 Subject: [PATCH 04/12] Added annotations filter file --- cvat-core/src/annotations-filter.js | 237 ++++++++++++++++++++++++++++ cvat-core/src/annotations.js | 6 +- 2 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 cvat-core/src/annotations-filter.js diff --git a/cvat-core/src/annotations-filter.js b/cvat-core/src/annotations-filter.js new file mode 100644 index 00000000000..93132af33ac --- /dev/null +++ b/cvat-core/src/annotations-filter.js @@ -0,0 +1,237 @@ +/* +* Copyright (C) 2020 Intel Corporation +* SPDX-License-Identifier: MIT +*/ + +/* global + require:false +*/ + +const jsonpath = require('jsonpath'); +const { AttributeType } = require('./enums'); +const { ArgumentError } = require('./exceptions'); + + +class AnnotationsFilter { + constructor() { + // eslint-disable-next-line security/detect-unsafe-regex + this.operatorRegex = /(==|!=|<=|>=|>|<|~=)(?=(?:[^"]*(["])[^"]*\2)*[^"]*$)/g; + } + + _splitWithOperator(container, expression) { + const operators = ['|', '&']; + const splitted = []; + let nestedCounter = 0; + let isQuotes = false; + let start = -1; + + for (let i = 0; i < expression.length; i++) { + if (expression[i] === '"') { + // all quotes inside other quotes must + // be escaped by a user and changed to ` above + isQuotes = !isQuotes; + } + + // We don't split with operator inside brackets + // It will be done later in recursive call + if (!isQuotes && expression[i] === '(') { + nestedCounter++; + } + if (!isQuotes && expression[i] === ')') { + nestedCounter--; + } + + if (operators.includes(expression[i])) { + if (!nestedCounter) { + const subexpression = expression + .substr(start + 1, i - start - 1).trim(); + splitted.push(subexpression); + splitted.push(expression[i]); + start = i; + } + } + } + + const subexpression = expression + .substr(start + 1).trim(); + splitted.push(subexpression); + + splitted.forEach((internalExpression) => { + if (internalExpression === '|' || internalExpression === '&') { + container.push(internalExpression); + } else { + this._groupByBrackets( + container, + internalExpression, + ); + } + }); + } + + _groupByBrackets(container, expression) { + if (!(expression.startsWith('(') && expression.endsWith(')'))) { + container.push(expression); + } + + let nestedCounter = 0; + let startBracket = null; + let endBracket = null; + let isQuotes = false; + + for (let i = 0; i < expression.length; i++) { + if (expression[i] === '"') { + // all quotes inside other quotes must + // be escaped by a user and changed to ` above + isQuotes = !isQuotes; + } + + if (!isQuotes && expression[i] === '(') { + nestedCounter++; + if (startBracket === null) { + startBracket = i; + } + } + + if (!isQuotes && expression[i] === ')') { + nestedCounter--; + if (!nestedCounter) { + endBracket = i; + + const subcontainer = []; + const subexpression = expression + .substr(startBracket + 1, endBracket - 1 - startBracket); + this._splitWithOperator( + subcontainer, + subexpression, + ); + + container.push(subcontainer); + + startBracket = null; + endBracket = null; + } + } + } + + if (startBracket !== null) { + throw Error(`Extra opening bracket on the position ${startBracket}`); + } + if (endBracket !== null) { + throw Error(`Extra closing bracket on the position ${endBracket}`); + } + } + + _parse(expression) { + const groups = []; + this._splitWithOperator(groups, expression); + } + + _join(groups) { + let expression = ''; + for (const group of groups) { + if (Array.isArray(group)) { + expression += `(${this.join(group)})`; + } else if (typeof (group) === 'string') { + // it can be operator or expression + if (group === '|' || group === '&') { + expression += group; + } else { + let [field, operator, , value] = group.split(this.operatorRegex); + field = `@.${field.trim()}`; + operator = operator.trim(); + value = value.trim(); + if (value === 'width' || value === 'height' || value.startsWith('attr')) { + value = `@.${value}`; + } + expression += [field, operator, value].join(''); + } + } + } + + return expression; + } + + _convertObjects(statesData) { + const objects = statesData.map((state) => { + const labelAttributes = state.label.attributes + .reduce((acc, attr) => { + acc[attr.id] = attr; + return acc; + }, {}); + + let xtl = Number.MAX_SAFE_INTEGER; + let xbr = Number.MIN_SAFE_INTEGER; + let ytl = Number.MAX_SAFE_INTEGER; + let ybr = Number.MIN_SAFE_INTEGER; + + state.points.forEach((coord, idx) => { + if (idx % 2) { // y + ytl = Math.min(ytl, coord); + ybr = Math.max(ybr, coord); + } else { // x + xtl = Math.min(xtl, coord); + xbr = Math.max(xbr, coord); + } + }); + + const [width, height] = [xbr - xtl, ybr - ytl]; + const attributes = {}; + Object.keys(state.attributes).reduce((acc, key) => { + const attr = labelAttributes[key]; + let value = state.attributes[key].replace(/\\"/g, '`'); + if (attr.inputType === AttributeType.NUMBER) { + value = +value; + } else if (attr.inputType === AttributeType.CHECKBOX) { + value = value === 'true'; + } + acc[attr.name] = value; + return acc; + }, attributes); + + return { + width, + height, + attr: attributes, + label: state.label.name.replace(/\\"/g, '`'), + serverID: state.serverID, + clientID: state.clientID, + type: state.objectType, + shape: state.objectShape, + occluded: state.occluded, + }; + }); + + return { + objects, + }; + } + + toJSONQuery(filters) { + try { + const groups = []; + const expression = filters.map((filter) => `(${filter})`).join('|').replace(/\\"/g, '`'); + this._splitWithOperator(groups, expression); + return `$.objects[?(${this._join(groups)})].clientID`; + } catch (error) { + throw new ArgumentError(`Wrong filter expression. ${error.toString()}`); + } + } + + filter(statesData, filters) { + const query = this.toJSONQuery(filters); + try { + const objects = this._convertObjects(statesData); + return jsonpath.query(objects, query); + } catch (error) { + throw new ArgumentError(`Could not apply the filter. ${error.toString()}`); + } + } +} + +module.exports = AnnotationsFilter; + + +/* + // Write unit tests + (( label==["car \\"mazda\\""]) & (attr["sunglass ( help ) es"]==true | (width > 150 | height > 150 & (clientID == serverID))))) +*/ diff --git a/cvat-core/src/annotations.js b/cvat-core/src/annotations.js index b935c57bfbc..f8b9bd99cd5 100644 --- a/cvat-core/src/annotations.js +++ b/cvat-core/src/annotations.js @@ -77,16 +77,16 @@ } } - async function getAnnotations(session, frame, allTracks, filter) { + async function getAnnotations(session, frame, allTracks, filters) { const sessionType = session instanceof Task ? 'task' : 'job'; const cache = getCache(sessionType); if (cache.has(session)) { - return cache.get(session).collection.get(frame, allTracks, filter); + return cache.get(session).collection.get(frame, allTracks, filters); } await getAnnotationsFromServer(session); - return cache.get(session).collection.get(frame, filter); + return cache.get(session).collection.get(frame, allTracks, filters); } async function saveAnnotations(session, onUpdate) { From 49555f215c06eb3203f5bd85eda6d639f3f3d1c0 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 14 Feb 2020 15:56:20 +0300 Subject: [PATCH 05/12] Updated some comments --- cvat-core/src/annotations-filter.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/cvat-core/src/annotations-filter.js b/cvat-core/src/annotations-filter.js index 93132af33ac..d2e2da1d18d 100644 --- a/cvat-core/src/annotations-filter.js +++ b/cvat-core/src/annotations-filter.js @@ -18,6 +18,7 @@ class AnnotationsFilter { this.operatorRegex = /(==|!=|<=|>=|>|<|~=)(?=(?:[^"]*(["])[^"]*\2)*[^"]*$)/g; } + // Method splits expression by operators that are outside of any brackets _splitWithOperator(container, expression) { const operators = ['|', '&']; const splitted = []; @@ -68,6 +69,7 @@ class AnnotationsFilter { }); } + // Method groups bracket containings to nested arrays of container _groupByBrackets(container, expression) { if (!(expression.startsWith('(') && expression.endsWith(')'))) { container.push(expression); @@ -229,9 +231,3 @@ class AnnotationsFilter { } module.exports = AnnotationsFilter; - - -/* - // Write unit tests - (( label==["car \\"mazda\\""]) & (attr["sunglass ( help ) es"]==true | (width > 150 | height > 150 & (clientID == serverID))))) -*/ From 0a55debceb719b198574d260594f69bc61ff6cf1 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 14 Feb 2020 17:58:57 +0300 Subject: [PATCH 06/12] Added filter to UI --- cvat-canvas/src/typescript/canvasView.ts | 13 +- cvat-core/src/annotations-filter.js | 6 +- cvat-core/src/api-implementation.js | 1 - cvat-ui/src/actions/annotation-actions.ts | 114 +++++++++++++++--- .../objects-side-bar/objects-list-header.tsx | 22 +++- .../objects-side-bar/objects-list.tsx | 8 +- .../objects-side-bar/styles.scss | 18 +-- .../objects-side-bar/objects-list.tsx | 74 ++++++------ cvat-ui/src/reducers/annotation-reducer.ts | 26 ++++ cvat-ui/src/reducers/interfaces.ts | 4 + cvat-ui/src/reducers/notifications-reducer.ts | 48 ++++++++ 11 files changed, 256 insertions(+), 78 deletions(-) diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 40b43e42ef1..fdabf19936c 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -830,6 +830,7 @@ export class CanvasViewImpl implements CanvasView, Listener { occluded: state.occluded, hidden: state.hidden, lock: state.lock, + shapeType: state.shapeType, points: [...state.points], attributes: { ...state.attributes }, }; @@ -963,16 +964,16 @@ export class CanvasViewImpl implements CanvasView, Listener { private deactivate(): void { if (this.activeElement.clientID !== null) { const { clientID } = this.activeElement; - const [state] = this.controller.objects - .filter((_state: any): boolean => _state.clientID === clientID); - const shape = this.svgShapes[state.clientID]; + const drawnState = this.drawnStates[clientID]; + const shape = this.svgShapes[clientID]; + shape.removeClass('cvat_canvas_shape_activated'); (shape as any).off('dragstart'); (shape as any).off('dragend'); (shape as any).draggable(false); - if (state.shapeType !== 'points') { + if (drawnState.shapeType !== 'points') { this.selectize(false, shape); } @@ -982,10 +983,10 @@ export class CanvasViewImpl implements CanvasView, Listener { (shape as any).resize(false); // TODO: Hide text only if it is hidden by settings - const text = this.svgTexts[state.clientID]; + const text = this.svgTexts[clientID]; if (text) { text.remove(); - delete this.svgTexts[state.clientID]; + delete this.svgTexts[clientID]; } this.activeElement = { diff --git a/cvat-core/src/annotations-filter.js b/cvat-core/src/annotations-filter.js index d2e2da1d18d..916d5260b03 100644 --- a/cvat-core/src/annotations-filter.js +++ b/cvat-core/src/annotations-filter.js @@ -116,10 +116,10 @@ class AnnotationsFilter { } if (startBracket !== null) { - throw Error(`Extra opening bracket on the position ${startBracket}`); + throw Error('Extra opening bracket found'); } if (endBracket !== null) { - throw Error(`Extra closing bracket on the position ${endBracket}`); + throw Error('Extra closing bracket found'); } } @@ -132,7 +132,7 @@ class AnnotationsFilter { let expression = ''; for (const group of groups) { if (Array.isArray(group)) { - expression += `(${this.join(group)})`; + expression += `(${this._join(group)})`; } else if (typeof (group) === 'string') { // it can be operator or expression if (group === '|' || group === '&') { diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index 80290179276..4b7e2872719 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -9,7 +9,6 @@ require:false */ - (() => { const PluginRegistry = require('./plugins'); const serverProxy = require('./server-proxy'); diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 8594fc7ee55..d1fe89a04db 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -1,4 +1,9 @@ -import { AnyAction, Dispatch, ActionCreator } from 'redux'; +import { + AnyAction, + Dispatch, + ActionCreator, + Store, +} from 'redux'; import { ThunkAction } from 'redux-thunk'; import { @@ -13,6 +18,29 @@ import getCore from 'cvat-core'; import { getCVATStore } from 'cvat-store'; const cvat = getCore(); +let store: null | Store = null; + +function getStore(): Store { + if (store === null) { + store = getCVATStore(); + } + return store; +} + +function receiveAnnotationsParameters(): { filters: string[]; frame: number } { + if (store === null) { + store = getCVATStore(); + } + + const state: CombinedState = getStore().getState(); + const { filters } = state.annotation.annotations; + const frame = state.annotation.player.frame.number; + + return { + filters, + frame, + }; +} export enum AnnotationActionTypes { GET_JOB = 'GET_JOB', @@ -78,16 +106,53 @@ export enum AnnotationActionTypes { UNDO_ACTION_FAILED = 'UNDO_ACTION_FAILED', REDO_ACTION_SUCCESS = 'REDO_ACTION_SUCCESS', REDO_ACTION_FAILED = 'REDO_ACTION_FAILED', + CHANGE_ANNOTATIONS_FILTERS = 'CHANGE_ANNOTATIONS_FILTERS', + FETCH_ANNOTATIONS_SUCCESS = 'FETCH_ANNOTATIONS_SUCCESS', + FETCH_ANNOTATIONS_FAILED = 'FETCH_ANNOTATIONS_FAILED', +} + +export function fetchAnnotationsAsync(sessionInstance: any): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + const { filters, frame } = receiveAnnotationsParameters(); + const states = await sessionInstance.annotations.get(frame, false, filters); + dispatch({ + type: AnnotationActionTypes.FETCH_ANNOTATIONS_SUCCESS, + payload: { + states, + }, + }); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.FETCH_ANNOTATIONS_FAILED, + payload: { + error, + }, + }); + } + }; +} + +export function changeAnnotationsFilters(filters: string[]): AnyAction { + return { + type: AnnotationActionTypes.CHANGE_ANNOTATIONS_FILTERS, + payload: { + filters, + }, + }; } export function undoActionAsync(sessionInstance: any, frame: number): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { + const { filters } = receiveAnnotationsParameters(); + // TODO: use affected IDs as an optimization await sessionInstance.actions.undo(); const history = await sessionInstance.actions.get(); - const states = await sessionInstance.annotations.get(frame); + const states = await sessionInstance.annotations.get(frame, false, filters); dispatch({ type: AnnotationActionTypes.UNDO_ACTION_SUCCESS, @@ -111,10 +176,12 @@ export function redoActionAsync(sessionInstance: any, frame: number): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { + const { filters } = receiveAnnotationsParameters(); + // TODO: use affected IDs as an optimization await sessionInstance.actions.redo(); const history = await sessionInstance.actions.get(); - const states = await sessionInstance.annotations.get(frame); + const states = await sessionInstance.annotations.get(frame, false, filters); dispatch({ type: AnnotationActionTypes.REDO_ACTION_SUCCESS, @@ -174,8 +241,9 @@ export function uploadJobAnnotationsAsync(job: any, loader: any, file: File): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { - const store = getCVATStore(); - const state: CombinedState = store.getState(); + const state: CombinedState = getStore().getState(); + const { filters } = receiveAnnotationsParameters(); + if (state.tasks.activities.loads[job.task.id]) { throw Error('Annotations is being uploaded for the task'); } @@ -208,7 +276,7 @@ ThunkAction, {}, {}, AnyAction> { await job.annotations.clear(true); await job.actions.clear(); const history = await job.actions.get(); - const states = await job.annotations.get(frame); + const states = await job.annotations.get(frame, false, filters); setTimeout(() => { dispatch({ @@ -475,10 +543,9 @@ export function switchPlay(playing: boolean): AnyAction { export function changeFrameAsync(toFrame: number): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { - const store = getCVATStore(); - const state: CombinedState = store.getState(); + const state: CombinedState = getStore().getState(); const { instance: job } = state.annotation.job; - const { number: frame } = state.annotation.player.frame; + const { filters, frame } = receiveAnnotationsParameters(); try { if (toFrame < job.startFrame || toFrame > job.stopFrame) { @@ -505,7 +572,7 @@ ThunkAction, {}, {}, AnyAction> { }); const data = await job.frames.get(toFrame); - const states = await job.annotations.get(toFrame); + const states = await job.annotations.get(toFrame, false, filters); dispatch({ type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS, payload: { @@ -567,8 +634,8 @@ ThunkAction, {}, {}, AnyAction> { }); try { - const store = getCVATStore(); - const state: CombinedState = store.getState(); + const state: CombinedState = getStore().getState(); + const { filters } = receiveAnnotationsParameters(); // First check state if the task is already there let task = state.tasks.current @@ -589,7 +656,7 @@ ThunkAction, {}, {}, AnyAction> { const frameNumber = Math.max(0, job.startFrame); const frameData = await job.frames.get(frameNumber); - const states = await job.annotations.get(frameNumber); + const states = await job.annotations.get(frameNumber, false, filters); const colors = [...cvat.enums.colors]; dispatch({ @@ -713,7 +780,8 @@ export function updateAnnotationsAsync(sessionInstance: any, frame: number, stat ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { - const promises = statesToUpdate.map((state: any): Promise => state.save()); + const promises = statesToUpdate + .map((objectState: any): Promise => objectState.save()); const states = await Promise.all(promises); const history = await sessionInstance.actions.get(); @@ -725,7 +793,8 @@ ThunkAction, {}, {}, AnyAction> { }, }); } catch (error) { - const states = await sessionInstance.annotations.get(frame); + const { filters } = receiveAnnotationsParameters(); + const states = await sessionInstance.annotations.get(frame, false, filters); dispatch({ type: AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED, payload: { @@ -741,8 +810,9 @@ export function createAnnotationsAsync(sessionInstance: any, frame: number, stat ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { + const { filters } = receiveAnnotationsParameters(); await sessionInstance.annotations.put(statesToCreate); - const states = await sessionInstance.annotations.get(frame); + const states = await sessionInstance.annotations.get(frame, false, filters); const history = await sessionInstance.actions.get(); dispatch({ @@ -767,8 +837,9 @@ export function mergeAnnotationsAsync(sessionInstance: any, frame: number, state ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { + const { filters } = receiveAnnotationsParameters(); await sessionInstance.annotations.merge(statesToMerge); - const states = await sessionInstance.annotations.get(frame); + const states = await sessionInstance.annotations.get(frame, false, filters); const history = await sessionInstance.actions.get(); dispatch({ @@ -793,8 +864,9 @@ export function groupAnnotationsAsync(sessionInstance: any, frame: number, state ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { + const { filters } = receiveAnnotationsParameters(); await sessionInstance.annotations.group(statesToGroup); - const states = await sessionInstance.annotations.get(frame); + const states = await sessionInstance.annotations.get(frame, false, filters); const history = await sessionInstance.actions.get(); dispatch({ @@ -818,9 +890,10 @@ ThunkAction, {}, {}, AnyAction> { export function splitAnnotationsAsync(sessionInstance: any, frame: number, stateToSplit: any): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { + const { filters } = receiveAnnotationsParameters(); try { await sessionInstance.annotations.split(stateToSplit, frame); - const states = await sessionInstance.annotations.get(frame); + const states = await sessionInstance.annotations.get(frame, false, filters); const history = await sessionInstance.actions.get(); dispatch({ @@ -849,9 +922,10 @@ export function changeLabelColorAsync( ): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { + const { filters } = receiveAnnotationsParameters(); const updatedLabel = label; updatedLabel.color = color; - const states = await sessionInstance.annotations.get(frameNumber); + const states = await sessionInstance.annotations.get(frameNumber, false, filters); const history = await sessionInstance.actions.get(); dispatch({ diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx index 35b2a59a77e..9706f2eac82 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx @@ -4,11 +4,11 @@ import { Row, Col, Icon, - Input, Select, } from 'antd'; import Text from 'antd/lib/typography/Text'; +import { SelectValue } from 'antd/lib/select'; import { StatesOrdering } from 'reducers/interfaces'; @@ -58,7 +58,9 @@ interface Props { statesLocked: boolean; statesCollapsed: boolean; statesOrdering: StatesOrdering; + annotationsFilters: string[]; changeStatesOrdering(value: StatesOrdering): void; + changeAnnotationsFilters(value: SelectValue): void; lockAllStates(): void; unlockAllStates(): void; collapseAllStates(): void; @@ -69,6 +71,7 @@ interface Props { function ObjectListHeader(props: Props): JSX.Element { const { + annotationsFilters, statesHidden, statesLocked, statesCollapsed, @@ -80,15 +83,26 @@ function ObjectListHeader(props: Props): JSX.Element { expandAllStates, hideAllStates, showAllStates, + changeAnnotationsFilters, } = props; return (
- } +