Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React UI: Objects filtering & search #1155

Merged
merged 14 commits into from
Feb 19, 2020
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