From 6fead082dc1c8bb8fb8e558b151d57071d43a35b Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 20 Jan 2025 14:05:24 +0200 Subject: [PATCH 1/2] Fixed issue: object interpolated incorrectly if a frame with the object keyframe is deleted (#8951) --- ...184520_sekachev.bs_fixed_deleted_frames.md | 4 + ...191626_sekachev.bs_fixed_deleted_frames.md | 4 + cvat-core/src/annotations-collection.ts | 64 ++++----- cvat-core/src/annotations-objects.ts | 50 ++++--- cvat-core/src/annotations.ts | 43 +++--- cvat-core/src/frames.ts | 122 +++++++++++------- cvat-core/src/session-implementation.ts | 13 +- cvat-core/tests/mocks/dummy-data.mock.cjs | 24 +++- cvat-core/tests/mocks/server-proxy.mock.cjs | 8 +- 9 files changed, 186 insertions(+), 146 deletions(-) create mode 100644 changelog.d/20250116_184520_sekachev.bs_fixed_deleted_frames.md create mode 100644 changelog.d/20250116_191626_sekachev.bs_fixed_deleted_frames.md diff --git a/changelog.d/20250116_184520_sekachev.bs_fixed_deleted_frames.md b/changelog.d/20250116_184520_sekachev.bs_fixed_deleted_frames.md new file mode 100644 index 000000000000..57c0991b23af --- /dev/null +++ b/changelog.d/20250116_184520_sekachev.bs_fixed_deleted_frames.md @@ -0,0 +1,4 @@ +### Fixed + +- A job cannot be opened if to remove an image with the latest keyframe of a track + () diff --git a/changelog.d/20250116_191626_sekachev.bs_fixed_deleted_frames.md b/changelog.d/20250116_191626_sekachev.bs_fixed_deleted_frames.md new file mode 100644 index 000000000000..bee1fb215061 --- /dev/null +++ b/changelog.d/20250116_191626_sekachev.bs_fixed_deleted_frames.md @@ -0,0 +1,4 @@ +### Fixed + +- A track will be interpolated incorrectly if to delete an image containing the object keyframe + () diff --git a/cvat-core/src/annotations-collection.ts b/cvat-core/src/annotations-collection.ts index 25496dfe69a7..d9c868567393 100644 --- a/cvat-core/src/annotations-collection.ts +++ b/cvat-core/src/annotations-collection.ts @@ -1,13 +1,14 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022-2024 CVAT.ai Corporation +// Copyright (C) 2022-2025 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { shapeFactory, trackFactory, Track, Shape, Tag, - MaskShape, BasicInjection, - SkeletonShape, SkeletonTrack, PolygonShape, CuboidShape, + MaskShape, BasicInjection, SkeletonShape, + SkeletonTrack, PolygonShape, CuboidShape, RectangleShape, PolylineShape, PointsShape, EllipseShape, + InterpolationNotPossibleError, } from './annotations-objects'; import { SerializedCollection, SerializedShape, SerializedTrack } from './server-response-types'; import AnnotationsFilter from './annotations-filter'; @@ -22,7 +23,6 @@ import { HistoryActions, ShapeType, ObjectType, colors, Source, DimensionType, JobType, } from './enums'; import AnnotationHistory from './annotations-history'; -import { Job } from './session'; const validateAttributesList = ( attributes: { spec_id: number, value: string }[], @@ -48,14 +48,9 @@ const labelAttributesAsDict = (label: Label): Record => ( }, {}) ); -export type FrameMeta = Record>> & { - deleted_frames: Record -}; - export default class Collection { public flush: boolean; private stopFrame: number; - private frameMeta: FrameMeta; private labels: Record; private annotationsFilter: AnnotationsFilter; private history: AnnotationHistory; @@ -71,11 +66,10 @@ export default class Collection { history: AnnotationHistory; stopFrame: number; dimension: DimensionType; - frameMeta: Collection['frameMeta']; + framesInfo: BasicInjection['framesInfo']; jobType: JobType; }) { this.stopFrame = data.stopFrame; - this.frameMeta = data.frameMeta; this.labels = data.labels.reduce((labelAccumulator, label) => { labelAccumulator[label.id] = label; @@ -96,15 +90,16 @@ export default class Collection { this.groups = { max: 0, }; // it is an object to we can pass it as an argument by a reference + this.injection = { labels: this.labels, groups: this.groups, - frameMeta: this.frameMeta, + framesInfo: data.framesInfo, history: this.history, dimension: data.dimension, jobType: data.jobType, - nextClientID: () => ++config.globalObjectsCounter, groupColors: {}, + nextClientID: () => ++config.globalObjectsCounter, getMasksOnFrame: (frame: number) => (this.shapes[frame] as MaskShape[]) .filter((object) => object instanceof MaskShape), }; @@ -239,9 +234,13 @@ export default class Collection { } public get(frame: number, allTracks: boolean, filters: object[]): ObjectState[] { + if (this.injection.framesInfo.isFrameDeleted(frame)) { + return []; + } + const { tracks } = this; - const shapes = this.shapes[frame] || []; - const tags = this.tags[frame] || []; + const shapes = this.shapes[frame] ?? []; + const tags = this.tags[frame] ?? []; const objects = [].concat(tracks, shapes, tags); const visible = []; @@ -251,12 +250,17 @@ export default class Collection { continue; } - const stateData = object.get(frame); - if (stateData.outside && !stateData.keyframe && !allTracks && object instanceof Track) { - continue; + try { + const stateData = object.get(frame); + if (stateData.outside && !stateData.keyframe && !allTracks && object instanceof Track) { + continue; + } + visible.push(stateData); + } catch (error: unknown) { + if (!(error instanceof InterpolationNotPossibleError)) { + throw error; + } } - - visible.push(stateData); } const objectStates = []; @@ -771,7 +775,7 @@ export default class Collection { ); } - const { width, height } = this.frameMeta[slicedObject.frame]; + const { width, height } = this.injection.framesInfo[slicedObject.frame]; if (slicedObject instanceof MaskShape) { points1.push(slicedObject.left, slicedObject.top, slicedObject.right, slicedObject.bottom); points2.push(slicedObject.left, slicedObject.top, slicedObject.right, slicedObject.bottom); @@ -923,7 +927,7 @@ export default class Collection { count -= 1; } for (let i = start + 1; lastIsKeyframe ? i < stop : i <= stop; i++) { - if (this.frameMeta.deleted_frames[i]) { + if (this.injection.framesInfo.isFrameDeleted(i)) { count--; } } @@ -936,7 +940,7 @@ export default class Collection { const keyframes = Object.keys(track.shapes) .sort((a, b) => +a - +b) .map((el) => +el) - .filter((frame) => !this.frameMeta.deleted_frames[frame]); + .filter((frame) => !this.injection.framesInfo.isFrameDeleted(frame)); let prevKeyframe = keyframes[0]; let visible = false; @@ -987,19 +991,19 @@ export default class Collection { } const { name: label } = object.label; - if (objectType === 'tag' && !this.frameMeta.deleted_frames[object.frame]) { + if (objectType === 'tag' && !this.injection.framesInfo.isFrameDeleted(object.frame)) { labels[label].tag++; labels[label].manually++; labels[label].total++; } else if (objectType === 'track') { scanTrack(object); - } else if (!this.frameMeta.deleted_frames[object.frame]) { + } else if (!this.injection.framesInfo.isFrameDeleted(object.frame)) { const { shapeType } = object as Shape; labels[label][shapeType].shape++; labels[label].manually++; labels[label].total++; if (shapeType === ShapeType.SKELETON) { - (object as SkeletonShape).elements.forEach((element) => { + (object as unknown as SkeletonShape).elements.forEach((element) => { const combinedName = [label, element.label.name].join(sep); labels[combinedName][element.shapeType].shape++; labels[combinedName].manually++; @@ -1086,7 +1090,7 @@ export default class Collection { outside: state.outside || false, occluded: state.occluded || false, points: state.shapeType === 'mask' ? (() => { - const { width, height } = this.frameMeta[state.frame]; + const { width, height } = this.injection.framesInfo[state.frame]; return cropMask(state.points, width, height); })() : state.points, rotation: state.rotation || 0, @@ -1292,7 +1296,7 @@ export default class Collection { 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)) { - if (!allowDeletedFrames && this.frameMeta[frame].deleted) { + if (!allowDeletedFrames && this.injection.framesInfo.isFrameDeleted(frame)) { continue; } @@ -1359,7 +1363,7 @@ export default class Collection { if (!annotationsFilters) { let frame = frameFrom; while (predicate(frame)) { - if (!allowDeletedFrames && this.frameMeta[frame].deleted) { + if (!allowDeletedFrames && this.injection.framesInfo.isFrameDeleted(frame)) { frame = update(frame); continue; } @@ -1374,7 +1378,7 @@ export default class Collection { const linearSearch = filtersStr.match(/"var":"width"/) || filtersStr.match(/"var":"height"/); for (let frame = frameFrom; predicate(frame); frame = update(frame)) { - if (!allowDeletedFrames && this.frameMeta[frame].deleted) { + if (!allowDeletedFrames && this.injection.framesInfo.isFrameDeleted(frame)) { continue; } diff --git a/cvat-core/src/annotations-objects.ts b/cvat-core/src/annotations-objects.ts index ab7e32de9784..4e85abe3ae03 100644 --- a/cvat-core/src/annotations-objects.ts +++ b/cvat-core/src/annotations-objects.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022-2024 CVAT.ai Corporation +// Copyright (C) 2022-2025 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -58,12 +58,18 @@ function computeNewSource(currentSource: Source): Source { return Source.MANUAL; } +type FrameInfo = { + width: number; + height: number; +}; + export interface BasicInjection { labels: Record; groups: { max: number }; - frameMeta: { - deleted_frames: Record; - }; + framesInfo: Readonly<{ + [index: number]: Readonly; + isFrameDeleted: (frame: number) => boolean; + }>; history: AnnotationHistory; groupColors: Record; parentID?: number; @@ -79,6 +85,8 @@ type AnnotationInjection = BasicInjection & { readOnlyFields?: string[]; }; +export class InterpolationNotPossibleError extends Error {} + class Annotation { public clientID: number; protected taskLabels: Record; @@ -394,7 +402,7 @@ class Annotation { } class Drawn extends Annotation { - protected frameMeta: AnnotationInjection['frameMeta']; + protected framesInfo: AnnotationInjection['framesInfo']; protected descriptions: string[]; public hidden: boolean; protected pinned: boolean; @@ -402,7 +410,7 @@ class Drawn extends Annotation { constructor(data, clientID: number, color: string, injection: AnnotationInjection) { super(data, clientID, color, injection); - this.frameMeta = injection.frameMeta; + this.framesInfo = injection.framesInfo; this.descriptions = data.descriptions || []; this.hidden = false; this.pinned = true; @@ -487,16 +495,10 @@ class Drawn extends Annotation { checkObjectType('points', data.points, null, Array); checkNumberOfPoints(this.shapeType, data.points); // cut points - const { width, height, filename } = this.frameMeta[frame]; + const { width, height } = this.framesInfo[frame]; fittedPoints = this.fitPoints(data.points, data.rotation, width, height); - let check = true; - if (filename && filename.slice(filename.length - 3) === 'pcd') { - check = false; - } - if (check) { - if (!checkShapeArea(this.shapeType, fittedPoints)) { - fittedPoints = []; - } + if (this.dimension === DimensionType.DIMENSION_2D && !checkShapeArea(this.shapeType, fittedPoints)) { + fittedPoints = []; } } @@ -960,7 +962,7 @@ export class Track extends Drawn { let last = Number.MIN_SAFE_INTEGER; for (const frame of frames) { - if (frame in this.frameMeta.deleted_frames) { + if (this.framesInfo.isFrameDeleted(frame)) { continue; } @@ -1414,10 +1416,7 @@ export class Track extends Drawn { }; } - throw new DataError( - 'No one left position or right position was found. ' + - `Interpolation impossible. Client ID: ${this.clientID}`, - ); + throw new InterpolationNotPossibleError(); } } @@ -2216,7 +2215,7 @@ export class MaskShape extends Shape { constructor(data: SerializedShape, clientID: number, color: string, injection: AnnotationInjection) { super(data, clientID, color, injection); const [left, top, right, bottom] = this.points.slice(-4); - const { width, height } = this.frameMeta[this.frame]; + const { width, height } = this.framesInfo[this.frame]; if (left >= width || top >= height || right >= width || bottom >= height) { this.points = cropMask(this.points, width, height); } @@ -2229,7 +2228,7 @@ export class MaskShape extends Shape { protected validateStateBeforeSave(data: ObjectState, updated: ObjectState['updateFlags'], frame?: number): number[] { super.validateStateBeforeSave(data, updated, frame); if (updated.points) { - const { width, height } = this.frameMeta[frame]; + const { width, height } = this.framesInfo[frame]; return cropMask(data.points, width, height); } @@ -2610,7 +2609,7 @@ class PolyTrack extends Track { return Math.sqrt((point1.x - point2.x) ** 2 + (point1.y - point2.y) ** 2); } - function minimizeSegment(baseLength: number, N: number, startInterpolated, stopInterpolated): void { + function minimizeSegment(baseLength: number, N: number, startInterpolated, stopInterpolated): Point2D[] { const threshold = baseLength / (2 * N); const minimized = [interpolatedPoints[startInterpolated]]; let latestPushed = startInterpolated; @@ -3275,10 +3274,7 @@ export class SkeletonTrack extends Track { }; } - throw new DataError( - 'No one left position or right position was found. ' + - `Interpolation impossible. Client ID: ${this.clientID}`, - ); + throw new InterpolationNotPossibleError(); } } diff --git a/cvat-core/src/annotations.ts b/cvat-core/src/annotations.ts index 706cbc853ed2..32dcd4ab18e5 100644 --- a/cvat-core/src/annotations.ts +++ b/cvat-core/src/annotations.ts @@ -1,18 +1,18 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022-2024 CVAT.ai Corporation +// Copyright (C) 2022-2025 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { Storage } from './storage'; import serverProxy from './server-proxy'; -import AnnotationsCollection, { FrameMeta } from './annotations-collection'; +import AnnotationsCollection from './annotations-collection'; import AnnotationsSaver from './annotations-saver'; import AnnotationsHistory from './annotations-history'; import { checkObjectType } from './common'; import Project from './project'; import { Task, Job } from './session'; import { ArgumentError } from './exceptions'; -import { getDeletedFrames } from './frames'; +import { getFramesMeta, getJobFramesMetaSync } from './frames'; import { JobType } from './enums'; const jobCollectionCache = new WeakMap(); @@ -88,22 +88,29 @@ async function getAnnotationsFromServer(session: Job | Task): Promise { const serializedAnnotations = await serverProxy.annotations.getAnnotations(sessionType, session.id); // Get meta information about frames - const startFrame = session instanceof Job ? session.startFrame : 0; - const stopFrame = session instanceof Job ? session.stopFrame : session.size - 1; - const frameMeta: Partial = {}; - for (let i = startFrame; i <= stopFrame; i++) { - frameMeta[i] = await session.frames.get(i); - } - frameMeta.deleted_frames = await getDeletedFrames(sessionType, session.id); + const frameMeta = await getFramesMeta(sessionType, session.id); + const frameNumbers = frameMeta.getSegmentFrameNumbers(session instanceof Job ? session.startFrame : 0); const history = cache.history.has(session) ? cache.history.get(session) : new AnnotationsHistory(); const collection = new AnnotationsCollection({ - labels: session.labels, - history, - stopFrame, - frameMeta: frameMeta as FrameMeta, jobType: session instanceof Job ? session.type : JobType.ANNOTATION, + stopFrame: session instanceof Job ? session.stopFrame : session.size - 1, + labels: session.labels, dimension: session.dimension, + framesInfo: { + isFrameDeleted: session instanceof Job ? + (frame: number) => !!getJobFramesMetaSync(session.id).deletedFrames[frame] : + (frame: number) => !!frameMeta.deletedFrames[frame], + ...frameMeta.frames.reduce((acc, frameInfo, idx) => { + // keep only static information + acc[frameNumbers[idx]] = { + width: frameInfo.width, + height: frameInfo.height, + }; + return acc; + }, {}), + }, + history, }); // eslint-disable-next-line no-unsanitized/method @@ -127,7 +134,12 @@ export function clearCache(session): void { } } -export async function getAnnotations(session, frame, allTracks, filters): Promise> { +export async function getAnnotations( + session: Job | Task, + frame: number, + allTracks: boolean, + filters: object[], +): Promise> { try { return getCollection(session).get(frame, allTracks, filters); } catch (error) { @@ -135,7 +147,6 @@ export async function getAnnotations(session, frame, allTracks, filters): Promis await getAnnotationsFromServer(session); return getCollection(session).get(frame, allTracks, filters); } - throw error; } } diff --git a/cvat-core/src/frames.ts b/cvat-core/src/frames.ts index c611bd5cec0a..2e7f70a1a52b 100644 --- a/cvat-core/src/frames.ts +++ b/cvat-core/src/frames.ts @@ -10,7 +10,7 @@ import { import PluginRegistry from './plugins'; import serverProxy from './server-proxy'; import { SerializedFramesMetaData } from './server-response-types'; -import { Exception, ArgumentError, DataError } from './exceptions'; +import { ArgumentError } from './exceptions'; import { FieldUpdateTrigger } from './common'; import config from './config'; @@ -29,6 +29,7 @@ const frameDataCache: Record | null; activeContextRequest: Promise> | null; + segmentFrameNumbers: number[]; contextCache: Record; timestamp: number; @@ -39,7 +40,39 @@ const frameDataCache: Record = {}; // frame meta data storage by job id -const frameMetaCache: Record> = {}; +const frameMetaCacheSync: Record = {}; +const frameMetaCache: Record> = new Proxy({}, { + set(target, prop, value): boolean { + if (typeof prop === 'string' && value instanceof Promise) { + const result = Reflect.set(target, prop, value); + + // automatically update synced storage each time new promise set + if (result) { + value.then((metaData: FramesMetaData) => { + if (target[prop]) { + frameMetaCacheSync[prop] = metaData; + } + }).catch(() => { + // do nothing + }); + } + + return result; + } + return Reflect.set(target, prop, value); + }, + deleteProperty(target, prop): boolean { + if (typeof prop === 'string') { + const result = Reflect.deleteProperty(target, prop); + if (result) { + delete frameMetaCacheSync[prop]; + } + return result; + } + + return Reflect.deleteProperty(target, prop); + }, +}); enum DeletedFrameState { DELETED = 'deleted', @@ -75,7 +108,7 @@ export class FramesMetaData { const data: typeof initialData = { chunk_size: undefined, deleted_frames: {}, - included_frames: [], + included_frames: null, frame_filter: undefined, frames: [], image_quality: undefined, @@ -146,9 +179,6 @@ export class FramesMetaData { frameFilter: { get: () => data.frame_filter, }, - frames: { - get: () => data.frames, - }, imageQuality: { get: () => data.image_quality, }, @@ -170,7 +200,16 @@ export class FramesMetaData { }), ); - const chunkCount: number = Math.ceil(this.getDataFrameNumbers().length / this.chunkSize); + const frameNumbers = this.getDataFrameNumbers(); + const chunkCount: number = Math.ceil(frameNumbers.length / this.chunkSize); + + let framesInfo = []; + if (initialData.frames.length === 1) { + // it may be a videofile or one image + framesInfo = frameNumbers.map(() => initialData.frames[0]); + } else { + framesInfo = initialData.frames; + } Object.defineProperties( this, @@ -178,6 +217,9 @@ export class FramesMetaData { chunkCount: { get: () => chunkCount, }, + frames: { + get: () => framesInfo, + }, }), ); } @@ -240,7 +282,7 @@ export class FramesMetaData { getDataFrameNumbers(): number[] { if (this.includedFrames) { - return [...this.includedFrames]; + return this.includedFrames.slice(0); } return range(this.startFrame, this.stopFrame + 1, this.frameStep); @@ -368,7 +410,7 @@ Object.defineProperty(FrameData.prototype.data, 'implementation', { async value(this: FrameData, onServerRequest) { const { provider, prefetchAnalyzer, chunkSize, jobStartFrame, - decodeForward, forwardStep, decodedBlocksCacheSize, + decodeForward, forwardStep, decodedBlocksCacheSize, segmentFrameNumbers, } = frameDataCache[this.jobID]; const meta = await frameDataCache[this.jobID].getMeta(); @@ -380,7 +422,6 @@ Object.defineProperty(FrameData.prototype.data, 'implementation', { const requestId = +_.uniqueId(); const requestedDataFrameNumber = meta.getDataFrameNumber(this.number - jobStartFrame); const chunkIndex = meta.getFrameChunkIndex(requestedDataFrameNumber); - const segmentFrameNumbers = meta.getSegmentFrameNumbers(jobStartFrame); const frame = provider.frame(this.number); function findTheNextNotDecodedChunk(currentFrameIndex: number): number | null { @@ -524,7 +565,8 @@ Object.defineProperty(FrameData.prototype.data, 'implementation', { }); prefetchAnalyzer.addRequested(this.number); } - }, () => { + }, + () => { frameDataCache[this.jobID].activeChunkRequest = null; resolveLoadAndDecode(); const decodedFrame = provider.frame(this.number); @@ -538,7 +580,8 @@ Object.defineProperty(FrameData.prototype.data, 'implementation', { } else if (!wasResolved) { reject(this.number); } - }, (error: Error | RequestOutdatedError) => { + }, + (error: Error | RequestOutdatedError) => { frameDataCache[this.jobID].activeChunkRequest = null; resolveLoadAndDecode(); if (error instanceof RequestOutdatedError) { @@ -590,6 +633,14 @@ function mergeMetaData( return Promise.resolve(framesMetaData); } +export function getJobFramesMetaSync(jobID: number): FramesMetaData { + const cached = frameMetaCacheSync[jobID]; + if (!cached) { + throw new Error('Frames meta cache was not initialized for this job'); + } + return cached; +} + export function getFramesMeta(type: 'job' | 'task', id: number, forceReload = false): Promise { if (type === 'task') { // we do not cache task meta currently. So, each new call will results to the server request @@ -642,29 +693,10 @@ function saveJobMeta(meta: FramesMetaData, jobID: number): Promise { - const { mode, jobStartFrame } = frameDataCache[jobID]; - const meta = await frameDataCache[jobID].getMeta(); - let frameMeta = null; - if (mode === 'interpolation' && meta.frames.length === 1) { - // video tasks have 1 frame info, but image tasks will have many infos - [frameMeta] = meta.frames; - } else if (mode === 'annotation' || (mode === 'interpolation' && meta.frames.length > 1)) { - if (frame > meta.stopFrame) { - throw new ArgumentError(`Meta information about frame ${frame} can't be received from the server`); - } - frameMeta = meta.frames[frame - jobStartFrame]; - } else { - throw new DataError(`Invalid mode is specified ${mode}`); - } - - return frameMeta; -} - async function refreshJobCacheIfOutdated(jobID: number): Promise { const cached = frameDataCache[jobID]; if (!cached) { - throw new Error('Frame data cache is abscent'); + throw new Error('Frames meta cache was not initialized for this job'); } const isOutdated = (Date.now() - cached.metaFetchedTimestamp) > config.jobMetaDataReloadPeriod; @@ -820,6 +852,7 @@ export async function getFrame( chunkSize, mode, jobStartFrame, + segmentFrameNumbers: meta.getSegmentFrameNumbers(jobStartFrame), decodeForward: isPlaying, forwardStep: step, provider: new FrameDecoder( @@ -864,7 +897,8 @@ export async function getFrame( // Thus, it is better to only call `refreshJobCacheIfOutdated` from getFrame() await refreshJobCacheIfOutdated(jobID); - const frameMeta = await getFrameMeta(jobID, frame); + const framesMetaData = await frameDataCache[jobID].getMeta(); + const frameMeta = framesMetaData.frames[frame - jobStartFrame]; frameDataCache[jobID].provider.setRenderSize(frameMeta.width, frameMeta.height); frameDataCache[jobID].decodeForward = isPlaying; frameDataCache[jobID].forwardStep = step; @@ -882,20 +916,6 @@ export async function getFrame( }); } -export async function getDeletedFrames(instanceType: 'job' | 'task', id: number): Promise> { - if (instanceType === 'job') { - const meta = await frameDataCache[id].getMeta(); - return meta.deletedFrames; - } - - if (instanceType === 'task') { - const meta = await serverProxy.frames.getMeta('task', id); - return Object.fromEntries(meta.deleted_frames.map((_frame) => [_frame, true])); - } - - throw new Exception(`getDeletedFrames is not implemented for ${instanceType}`); -} - export async function deleteFrame(jobID: number, frame: number): Promise { const meta = await frameMetaCache[jobID]; meta.deletedFrames[frame] = true; @@ -968,9 +988,8 @@ export async function getJobFrameNumbers(jobID: number): Promise { return []; } - const { jobStartFrame } = frameDataCache[jobID]; - const meta = await frameDataCache[jobID].getMeta(); - return meta.getSegmentFrameNumbers(jobStartFrame); + const { segmentFrameNumbers } = frameDataCache[jobID]; + return segmentFrameNumbers.slice(0); } export function clear(jobID: number): void { @@ -983,6 +1002,9 @@ export function clear(jobID: number): void { } delete frameDataCache[jobID]; + } + + if (jobID in frameMetaCache) { delete frameMetaCache[jobID]; } } diff --git a/cvat-core/src/session-implementation.ts b/cvat-core/src/session-implementation.ts index a5c008605749..1642dc9148f2 100644 --- a/cvat-core/src/session-implementation.ts +++ b/cvat-core/src/session-implementation.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022-2024 CVAT.ai Corporation +// Copyright (C) 2022-2025 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -23,7 +23,6 @@ import { findFrame, getContextImage, patchMeta, - getDeletedFrames, decodePreview, } from './frames'; import Issue from './issue'; @@ -352,11 +351,6 @@ export function implementJob(Job: typeof JobClass): typeof JobClass { } const annotationsData = await getAnnotations(this, frame, allTracks, filters); - const deletedFrames = await getDeletedFrames('job', this.id); - if (frame in deletedFrames) { - return []; - } - return annotationsData; }, }); @@ -1041,11 +1035,6 @@ export function implementTask(Task: typeof TaskClass): typeof TaskClass { } const result = await getAnnotations(this, frame, allTracks, filters); - const deletedFrames = await getDeletedFrames('task', this.id); - if (frame in deletedFrames) { - return []; - } - return result; }, }); diff --git a/cvat-core/tests/mocks/dummy-data.mock.cjs b/cvat-core/tests/mocks/dummy-data.mock.cjs index 3653d62a6cd0..232434aa0f5a 100644 --- a/cvat-core/tests/mocks/dummy-data.mock.cjs +++ b/cvat-core/tests/mocks/dummy-data.mock.cjs @@ -1257,8 +1257,6 @@ const tasksDummyData = { url: 'http://localhost:7000/api/jobs?task_id=102', }, image_quality: 50, - start_frame: 0, - stop_frame: 0, frame_filter: '', }, { @@ -1290,8 +1288,6 @@ const tasksDummyData = { url: 'http://localhost:7000/api/jobs?task_id=100', }, image_quality: 50, - start_frame: 0, - stop_frame: 0, frame_filter: '', }, { @@ -1323,8 +1319,6 @@ const tasksDummyData = { url: 'http://localhost:7000/api/jobs?task_id=101', }, image_quality: 50, - start_frame: 0, - stop_frame: 5001, frame_filter: '', }, { @@ -2815,6 +2809,7 @@ const frameMetaDummyData = { stop_frame: 8, frame_filter: '', deleted_frames: [], + included_frames: null, frames: [ { width: 1920, @@ -2862,6 +2857,7 @@ const frameMetaDummyData = { stop_frame: 74, frame_filter: '', deleted_frames: [], + included_frames: null, frames: [ { width: 1920, @@ -2877,6 +2873,7 @@ const frameMetaDummyData = { stop_frame: 4999, frame_filter: '', deleted_frames: [], + included_frames: null, frames: [ { width: 1888, @@ -2892,6 +2889,7 @@ const frameMetaDummyData = { stop_frame: 5001, frame_filter: '', deleted_frames: [], + included_frames: null, frames: [ { width: 1888, @@ -2927,7 +2925,8 @@ const frameMetaDummyData = { name: '730443-under-the-sea-wallpapers-1920x1080-windows-10.jpg', related_files: 0 }], - deleted_frames: [] + deleted_frames: [], + included_frames: null, }, 100: { chunk_size: 36, @@ -2937,6 +2936,7 @@ const frameMetaDummyData = { stop_frame: 8, frame_filter: '', deleted_frames: [7, 8], + included_frames: null, frames: [ { width: 1920, @@ -2999,6 +2999,7 @@ const frameMetaDummyData = { stop_frame: 994, frame_filter: '', deleted_frames: [], + included_frames: null, frames: [ { width: 1888, @@ -3014,6 +3015,7 @@ const frameMetaDummyData = { stop_frame: 1489, frame_filter: '', deleted_frames: [], + included_frames: null, frames: [ { width: 1888, @@ -3044,6 +3046,7 @@ const frameMetaDummyData = { stop_frame: 2479, frame_filter: '', deleted_frames: [], + included_frames: null, frames: [ { width: 1888, @@ -3059,6 +3062,7 @@ const frameMetaDummyData = { stop_frame: 2974, frame_filter: '', deleted_frames: [], + included_frames: null, frames: [ { width: 1888, @@ -3074,6 +3078,7 @@ const frameMetaDummyData = { stop_frame: 3469, frame_filter: '', deleted_frames: [], + included_frames: null, frames: [ { width: 1888, @@ -3089,6 +3094,7 @@ const frameMetaDummyData = { stop_frame: 3964, frame_filter: '', deleted_frames: [], + included_frames: null, frames: [ { width: 1888, @@ -3104,6 +3110,7 @@ const frameMetaDummyData = { stop_frame: 4459, frame_filter: '', deleted_frames: [], + included_frames: null, frames: [ { width: 1888, @@ -3119,6 +3126,7 @@ const frameMetaDummyData = { stop_frame: 4954, frame_filter: '', deleted_frames: [], + included_frames: null, frames: [ { width: 1888, @@ -3134,6 +3142,7 @@ const frameMetaDummyData = { stop_frame: 5001, frame_filter: '', deleted_frames: [], + included_frames: null, frames: [ { width: 1888, @@ -3149,6 +3158,7 @@ const frameMetaDummyData = { stop_frame: 0, frame_filter: '', deleted_frames: [], + included_frames: null, frames: [ { width: 1920, diff --git a/cvat-core/tests/mocks/server-proxy.mock.cjs b/cvat-core/tests/mocks/server-proxy.mock.cjs index d1cbfa9fe1a6..a24c2afcc2e5 100644 --- a/cvat-core/tests/mocks/server-proxy.mock.cjs +++ b/cvat-core/tests/mocks/server-proxy.mock.cjs @@ -379,7 +379,7 @@ class ServerProxy { const task = tasksDummyData.results.find((task) => task.id === id); const jobs = jobsDummyData.results.filter((job) => job.task_id === id); const jobsMeta = jobs.map((job) => frameMetaDummyData[job.id]).flat(); - let framesMeta = jobsMeta.map((jobMeta) => jobMeta.frames); + let framesMeta = jobsMeta.map((jobMeta) => jobMeta.frames).flat(); if (task.mode === 'interpolation') { framesMeta = [framesMeta[0]]; } @@ -388,11 +388,11 @@ class ServerProxy { chunk_size: jobsMeta[0].chunk_size , size: task.size, image_quality: task.image_quality, - start_frame: task.start_frame, - stop_frame: task.stop_frame, + start_frame: Math.min(...jobsMeta.map((meta) => meta.start_frame)), + stop_frame: Math.max(...jobsMeta.map((meta) => meta.stop_frame)), frames: framesMeta, deleted_frames: [], - included_frames: [], + included_frames: null, }; } From 39d4cb0650706375f2342006307f9b58a8817a09 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Mon, 20 Jan 2025 16:50:40 +0300 Subject: [PATCH 2/2] Narrow job dates in task job list (#8967) --- cvat-ui/src/components/job-item/job-item.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/cvat-ui/src/components/job-item/job-item.tsx b/cvat-ui/src/components/job-item/job-item.tsx index 3fe5267f9ef2..2fa239f7184b 100644 --- a/cvat-ui/src/components/job-item/job-item.tsx +++ b/cvat-ui/src/components/job-item/job-item.tsx @@ -29,6 +29,10 @@ import { useSelector } from 'react-redux'; import { CombinedState } from 'reducers'; import JobActionsMenu from './job-actions-menu'; +function formatDate(value: moment.Moment): string { + return value.format('MMM Do YYYY HH:mm'); +} + interface Props { job: Job; task: Task; @@ -141,14 +145,14 @@ function JobItem(props: Props): JSX.Element { - Created on - {`${created.format('MMMM Do YYYY HH:mm')}`} + Created: + {`${formatDate(created)}`} - Last updated - {`${updated.format('MMMM Do YYYY HH:mm')}`} + Updated: + {`${formatDate(updated)}`} @@ -233,7 +237,11 @@ function JobItem(props: Props): JSX.Element { Duration: - {`${moment.duration(now.diff(created)).humanize()}`} + + {`${moment + .duration(now.diff(created)) + .humanize()}`} +