diff --git a/CHANGELOG.md b/CHANGELOG.md index 3632f99b..38d0d5b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Issues disappear when rescale a browser () - Auth token key is not returned when registering without email verification () - Error in create project from backup for standard 3D annotation () +- Annotations search does not work correctly in some corner cases (when use complex properties with width, height) () ### Security - Updated ELK to 6.8.23 which uses log4j 2.17.1 () diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json index 11a96727..64961417 100644 --- a/cvat-core/package-lock.json +++ b/cvat-core/package-lock.json @@ -1,12 +1,12 @@ { "name": "cvat-core", - "version": "4.1.1", + "version": "4.1.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cvat-core", - "version": "4.1.1", + "version": "4.1.2", "license": "MIT", "dependencies": { "axios": "^0.21.4", diff --git a/cvat-core/package.json b/cvat-core/package.json index c6985e15..b03134e5 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "4.1.1", + "version": "4.1.2", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "babel.config.js", "scripts": { diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js index e4097adb..31892dc1 100644 --- a/cvat-core/src/annotations-collection.js +++ b/cvat-core/src/annotations-collection.js @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2021 Intel Corporation +// Copyright (C) 2019-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -917,35 +917,14 @@ search(filters, frameFrom, frameTo) { const sign = Math.sign(frameTo - frameFrom); const filtersStr = JSON.stringify(filters); - const containsDifficultProperties = filtersStr.match(/"var":"width"/) || filtersStr.match(/"var":"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 satisfies 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, filters); - if (filtered.length) { - next = middle; - } else { - prev = middle; - } - } + const linearSearch = filtersStr.match(/"var":"width"/) || filtersStr.match(/"var":"height"/); - 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 not outside tracks that have keyframe here - // In particular consider first and last frame as keyframes for all frames + // In particular consider first and last frame as keyframes for all tracks const statesData = [].concat( (frame in this.shapes ? this.shapes[frame] : []) .filter((shape) => !shape.removed) @@ -955,7 +934,9 @@ .map((tag) => tag.get(frame)), ); const tracks = Object.values(this.tracks) - .filter((track) => frame in track.shapes || frame === frameFrom || frame === frameTo) + .filter((track) => ( + frame in track.shapes || frame === frameFrom || + frame === frameTo || linearSearch)) .filter((track) => !track.removed); statesData.push(...tracks.map((track) => track.get(frame)).filter((state) => !state.outside)); @@ -966,31 +947,6 @@ // Filtering const filtered = this.annotationsFilter.filter(statesData, filters); - - // 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; } diff --git a/cvat-core/tests/api/annotations.js b/cvat-core/tests/api/annotations.js index ab0d716e..1fd5888e 100644 --- a/cvat-core/tests/api/annotations.js +++ b/cvat-core/tests/api/annotations.js @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -831,3 +831,34 @@ describe('Feature: select object', () => { expect(task.annotations.select(annotations, '5', '10')).rejects.toThrow(window.cvat.exceptions.ArgumentError); }); }); + +describe('Feature: search frame', () => { + test('applying different filters', async () => { + const job = (await window.cvat.jobs.get({ jobID: 102 }))[0]; + await job.annotations.clear(true); + let frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"tag"]}]}]'), 495, 994); + expect(frame).toBe(500); + frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"tag"]},{"==":[{"var":"label"},"bicycle"]}]}]'), 495, 994); + expect(frame).toBe(500); + frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"track"]},{"==":[{"var":"label"},"bicycle"]}]}]'), 495, 994); + expect(frame).toBe(null); + + frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"shape"]},{"==":[{"var":"shape"},"rectangle"]}]}]'), 495, 994); + expect(frame).toBe(510); + frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"shape"]},{"==":[{"var":"shape"},"rectangle"]}]}]'), 511, 994); + expect(frame).toBe(null); + frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"shape"]},{"==":[{"var":"shape"},"polygon"]}]}]'), 511, 994); + expect(frame).toBe(520); + frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"attr.motorcycle.model"},"some text for test"]}]}]'), 495, 994); + expect(frame).toBe(520); + frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"attr.motorcycle.model"},"some text for test"]},{"==":[{"var":"shape"},"ellipse"]}]}]'), 495, 994); + expect(frame).toBe(null); + + frame = await job.annotations.search(JSON.parse('[{"and":[{"<=":[450,{"var":"width"},550]}]}]'), 540, 994); + expect(frame).toBe(563); + frame = await job.annotations.search(JSON.parse('[{"and":[{"<=":[450,{"var":"width"},550]}]}]'), 588, 994); + expect(frame).toBe(null); + frame = await job.annotations.search(JSON.parse('[{"and":[{">=":[{"var":"width"},500]},{"<=":[{"var":"height"},300]}]}]'), 540, 994); + expect(frame).toBe(575); + }); +}); diff --git a/cvat-core/tests/mocks/dummy-data.mock.js b/cvat-core/tests/mocks/dummy-data.mock.js index f4fdd789..753b0aff 100644 --- a/cvat-core/tests/mocks/dummy-data.mock.js +++ b/cvat-core/tests/mocks/dummy-data.mock.js @@ -1455,6 +1455,74 @@ const taskAnnotationsDummyData = { }, ], }, + 102: { + version: 21, + tags: [{ + id: 1, + frame: 500, + label_id: 22, + group: 0, + attributes: [{ + spec_id: 13, + value: 'woman', + }, { + spec_id: 14, + value: 'false', + }], + }], + shapes: [{ + type: 'rectangle', + occluded: false, + z_order: 1, + points: [557.7890625, 276.2216796875, 907.1888732910156, 695.5014038085938], + id: 2, + frame: 510, + label_id: 21, + group: 0, + attributes: [], + }, { + type: 'polygon', + occluded: false, + z_order: 2, + points: [0, 0, 500, 500, 1000, 0], + id: 3, + frame: 520, + label_id: 23, + group: 0, + attributes: [{ spec_id: 15, value: 'some text for test' }], + }], + tracks: [ + { + id: 4, + frame: 550, + label_id: 24, + group: 0, + shapes: [ + { + type: 'rectangle', + occluded: true, + z_order: 2, + points: [100, 100, 500, 500], + id: 1, + frame: 550, + outside: false, + attributes: [], + }, + { + type: 'rectangle', + occluded: false, + z_order: 2, + points: [100, 100, 700, 300], + id: 3, + frame: 600, + outside: false, + attributes: [], + }, + ], + attributes: [], + }, + ], + }, 101: { version: 21, tags: [],