Skip to content

Commit

Permalink
React UI: Objects filtering & search (#1155)
Browse files Browse the repository at this point in the history
* Initial filter function

* Updated method for filtering

* Updated documentation

* Added annotations filter file

* Updated some comments

* Added filter to UI

* Implemented search alorithm

* Removed extra code

* Fixed typos

* Added frame URL

* Object URL

* Removed extra encoding/decoding
  • Loading branch information
bsekachev committed Feb 19, 2020
1 parent 538da9f commit 228b813
Show file tree
Hide file tree
Showing 22 changed files with 861 additions and 116 deletions.
13 changes: 7 additions & 6 deletions cvat-canvas/src/typescript/canvasView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
};
Expand Down Expand Up @@ -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);
}

Expand All @@ -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 = {
Expand Down
1 change: 1 addition & 0 deletions cvat-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
139 changes: 131 additions & 8 deletions cvat-core/src/annotations-collection.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/

Expand All @@ -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');
Expand Down Expand Up @@ -110,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
Expand Down Expand Up @@ -193,25 +195,46 @@
return data;
}

get(frame) {
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 (stateData.outside && !stateData.keyframe) {
if (!allTracks && stateData.outside && !stateData.keyframe) {
continue;
}

const objectState = objectStateFactory.call(object, frame, stateData);
objectStates.push(objectState);
visible.models.push(object);
visible.data.push(stateData);
}

const [, query] = this.annotationsFilter.toJSONQuery(filters);
let filtered = [];
if (filters.length) {
filtered = this.annotationsFilter.filter(visible.data, query);
}

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;
}

Expand Down Expand Up @@ -799,6 +822,106 @@
distance: minimumDistance,
};
}

search(filters, frameFrom, frameTo) {
const [groups, query] = this.annotationsFilter.toJSONQuery(filters);
const sign = Math.sign(frameTo - frameFrom);

const flattenedQuery = groups.flat(Number.MAX_SAFE_INTEGER);
const containsDifficultProperties = flattenedQuery
.some((fragment) => fragment
.match(/^width/) || fragment.match(/^height/));

const deepSearch = (deepSearchFrom, deepSearchTo) => {
// deepSearchFrom is expected to be a frame that doesn't satisfy a filter
// deepSearchTo is expected to be a frame that satifies a filter

let [prev, next] = [deepSearchFrom, deepSearchTo];
// half division method instead of linear search
while (!(Math.abs(prev - next) === 1)) {
const middle = next + Math.floor((prev - next) / 2);
const shapesData = this.tracks.map((track) => track.get(middle));
const filtered = this.annotationsFilter.filter(shapesData, query);
if (filtered.length) {
next = middle;
} else {
prev = middle;
}
}

return next;
};

const keyframesMemory = {};
const predicate = sign > 0
? (frame) => frame <= frameTo
: (frame) => frame >= frameTo;
const update = sign > 0
? (frame) => frame + 1
: (frame) => frame - 1;
for (let frame = frameFrom; predicate(frame); frame = update(frame)) {
// First prepare all data for the frame
// Consider all shapes, tags, and tracks that have keyframe here
// In particular consider first and last frame as keyframes for all frames
const statesData = [].concat(
(frame in this.shapes ? this.shapes[frame] : [])
.map((shape) => shape.get(frame)),
(frame in this.tags ? this.tags[frame] : [])
.map((tag) => tag.get(frame)),
);
const tracks = Object.values(this.tracks)
.filter((track) => (
frame in track.shapes
|| frame === frameFrom
|| frame === frameTo
));
statesData.push(...tracks.map((track) => track.get(frame)));

// Nothing to filtering, go to the next iteration
if (!statesData.length) {
continue;
}

// Filtering
const filtered = this.annotationsFilter.filter(statesData, query);

// Now we are checking whether we need deep search or not
// Deep search is needed in some difficult cases
// For example when filter contains fields which
// can be changed between keyframes (like: height and width of a shape)
// It's expected, that a track doesn't satisfy a filter on the previous keyframe
// At the same time it sutisfies the filter on the next keyframe
let withDeepSearch = false;
if (containsDifficultProperties) {
for (const track of tracks) {
const trackIsSatisfy = filtered.includes(track.clientID);
if (!trackIsSatisfy) {
keyframesMemory[track.clientID] = [
filtered.includes(track.clientID),
frame,
];
} else if (keyframesMemory[track.clientID]
&& keyframesMemory[track.clientID][0] === false) {
withDeepSearch = true;
}
}
}

if (withDeepSearch) {
const reducer = sign > 0 ? Math.min : Math.max;
const deepSearchFrom = reducer(
...Object.values(keyframesMemory).map((value) => value[1]),
);
return deepSearch(deepSearchFrom, frame);
}

if (filtered.length) {
return frame;
}
}

return null;
}
}

module.exports = Collection;
Expand Down
Loading

0 comments on commit 228b813

Please sign in to comment.