From b6cfff36d901ed1ca8194eb2ba6d39c5bd6c2921 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 11 Aug 2023 11:17:26 +0300 Subject: [PATCH] Added cached frames indication (#6586) Depends on #6585 ### Motivation and context Resolved #8 Decoded range is red: image ### How has this been tested? ### Checklist - [x] I submit my changes into the `develop` branch - [x] I have added a description of my changes into the [CHANGELOG](https://github.com/opencv/cvat/blob/develop/CHANGELOG.md) file - [ ] I have updated the documentation accordingly - [ ] I have added tests to cover my changes - [ ] I have linked related issues (see [GitHub docs]( https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)) - [x] I have increased versions of npm packages if it is necessary ([cvat-canvas](https://github.com/opencv/cvat/tree/develop/cvat-canvas#versioning), [cvat-core](https://github.com/opencv/cvat/tree/develop/cvat-core#versioning), [cvat-data](https://github.com/opencv/cvat/tree/develop/cvat-data#versioning) and [cvat-ui](https://github.com/opencv/cvat/tree/develop/cvat-ui#versioning)) ### License - [x] I submit _my code changes_ under the same [MIT License]( https://github.com/opencv/cvat/blob/develop/LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern. --- CHANGELOG.md | 1 + cvat-core/package.json | 2 +- cvat-core/src/frames.ts | 4 +-- cvat-core/src/session-implementation.ts | 23 ++++++------ cvat-core/src/session.ts | 13 +++---- cvat-data/src/ts/cvat-data.ts | 18 ++++------ cvat-ui/package.json | 2 +- cvat-ui/src/actions/annotation-actions.ts | 35 +++++++++++++++++-- .../canvas/views/canvas2d/canvas-wrapper.tsx | 4 +-- .../views/canvas3d/canvas-wrapper3D.tsx | 4 +-- .../components/annotation-page/styles.scss | 32 ++++++++++++++--- .../top-bar/player-navigation.tsx | 15 ++++++++ .../annotation-page/top-bar/top-bar.tsx | 3 ++ .../annotation-page/top-bar/top-bar.tsx | 5 +++ cvat-ui/src/reducers/annotation-reducer.ts | 7 ++++ cvat-ui/src/reducers/index.ts | 1 + 16 files changed, 124 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56720eaa52b6..ba25166343ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 user-provided function on the local machine, and a corresponding CLI command (`auto-annotate`) () +- Cached frames indication on the interface () ### Changed diff --git a/cvat-core/package.json b/cvat-core/package.json index 5556d1877650..d9712f79ea66 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "10.0.1", + "version": "11.0.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", "scripts": { diff --git a/cvat-core/src/frames.ts b/cvat-core/src/frames.ts index 158e35821141..2f5b328f81bf 100644 --- a/cvat-core/src/frames.ts +++ b/cvat-core/src/frames.ts @@ -582,12 +582,12 @@ export async function findFrame( return lastUndeletedFrame; } -export function getRanges(jobID): Array { +export function getCachedChunks(jobID): number[] { if (!(jobID in frameDataCache)) { return []; } - return frameDataCache[jobID].provider.cachedFrames; + return frameDataCache[jobID].provider.cachedChunks(true); } export function clear(jobID: number): void { diff --git a/cvat-core/src/session-implementation.ts b/cvat-core/src/session-implementation.ts index ea57c1881f1c..5a873ae823c3 100644 --- a/cvat-core/src/session-implementation.ts +++ b/cvat-core/src/session-implementation.ts @@ -13,7 +13,7 @@ import { getFrame, deleteFrame, restoreFrame, - getRanges, + getCachedChunks, clear as clearFrames, findFrame, getContextImage, @@ -163,9 +163,9 @@ export function implementJob(Job) { return result; }; - Job.prototype.frames.ranges.implementation = async function () { - const rangesData = await getRanges(this.id); - return rangesData; + Job.prototype.frames.cachedChunks.implementation = async function () { + const cachedChunks = await getCachedChunks(this.id); + return cachedChunks; }; Job.prototype.frames.preview.implementation = async function (this: JobClass): Promise { @@ -570,21 +570,18 @@ export function implementTask(Task) { isPlaying, step, this.dimension, + (chunkNumber, quality) => job.frames.chunk(chunkNumber, quality), ); return result; }; - Task.prototype.frames.ranges.implementation = async function () { - const rangesData = { - decoded: [], - buffered: [], - }; + Task.prototype.frames.cachedChunks.implementation = async function () { + let chunks = []; for (const job of this.jobs) { - const { decoded, buffered } = await getRanges(job.id); - rangesData.decoded.push(decoded); - rangesData.buffered.push(buffered); + const cachedChunks = await getCachedChunks(job.id); + chunks = chunks.concat(cachedChunks); } - return rangesData; + return Array.from(new Set(chunks)); }; Task.prototype.frames.preview.implementation = async function (this: TaskClass): Promise { diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index 4f34ed458ccc..cad8b773ae21 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -210,8 +210,8 @@ function buildDuplicatedAPI(prototype) { prototype.frames.save, ); }, - async ranges() { - const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.ranges); + async cachedChunks() { + const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.cachedChunks); return result; }, async preview() { @@ -329,6 +329,7 @@ export class Job extends Session { public readonly taskId: number; public readonly dimension: DimensionType; public readonly dataChunkType: ChunkType; + public readonly dataChunkSize: number; public readonly bugTracker: string | null; public readonly mode: TaskMode; public readonly labels: Label[]; @@ -369,7 +370,7 @@ export class Job extends Session { delete: CallableFunction; restore: CallableFunction; save: CallableFunction; - ranges: CallableFunction; + cachedChunks: CallableFunction; preview: CallableFunction; contextImage: CallableFunction; search: CallableFunction; @@ -573,7 +574,7 @@ export class Job extends Session { delete: Object.getPrototypeOf(this).frames.delete.bind(this), restore: Object.getPrototypeOf(this).frames.restore.bind(this), save: Object.getPrototypeOf(this).frames.save.bind(this), - ranges: Object.getPrototypeOf(this).frames.ranges.bind(this), + cachedChunks: Object.getPrototypeOf(this).frames.cachedChunks.bind(this), preview: Object.getPrototypeOf(this).frames.preview.bind(this), search: Object.getPrototypeOf(this).frames.search.bind(this), contextImage: Object.getPrototypeOf(this).frames.contextImage.bind(this), @@ -684,7 +685,7 @@ export class Task extends Session { delete: CallableFunction; restore: CallableFunction; save: CallableFunction; - ranges: CallableFunction; + cachedChunks: CallableFunction; preview: CallableFunction; contextImage: CallableFunction; search: CallableFunction; @@ -1101,7 +1102,7 @@ export class Task extends Session { delete: Object.getPrototypeOf(this).frames.delete.bind(this), restore: Object.getPrototypeOf(this).frames.restore.bind(this), save: Object.getPrototypeOf(this).frames.save.bind(this), - ranges: Object.getPrototypeOf(this).frames.ranges.bind(this), + cachedChunks: Object.getPrototypeOf(this).frames.cachedChunks.bind(this), preview: Object.getPrototypeOf(this).frames.preview.bind(this), contextImage: Object.getPrototypeOf(this).frames.contextImage.bind(this), search: Object.getPrototypeOf(this).frames.search.bind(this), diff --git a/cvat-data/src/ts/cvat-data.ts b/cvat-data/src/ts/cvat-data.ts index 00768a043512..8d50fe64083d 100644 --- a/cvat-data/src/ts/cvat-data.ts +++ b/cvat-data/src/ts/cvat-data.ts @@ -327,17 +327,11 @@ export class FrameDecoder { } } - get cachedChunks(): number[] { - return Object.keys(this.decodedChunks).map((chunkNumber: string) => +chunkNumber).sort((a, b) => a - b); - } - - get cachedFrames(): string[] { - const chunks = Object.keys(this.decodedChunks).map((chunkNumber: string) => +chunkNumber).sort((a, b) => a - b); - return chunks.map((chunk) => { - const frames = Object.keys(this.decodedChunks[chunk]).map((frame) => +frame); - const min = Math.min(...frames); - const max = Math.max(...frames); - return `${min}:${max}`; - }); + public cachedChunks(includeInProgress = false): number[] { + const chunkIsBeingDecoded = includeInProgress && this.chunkIsBeingDecoded ? + Math.floor(this.chunkIsBeingDecoded.start / this.chunkSize) : null; + return Object.keys(this.decodedChunks).map((chunkNumber: string) => +chunkNumber).concat( + ...(chunkIsBeingDecoded !== null ? [chunkIsBeingDecoded] : []), + ).sort((a, b) => a - b); } } diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 46e6739ff9c5..90ab58642008 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.54.2", + "version": "1.55.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 8b7a899b9ec3..24c3cc63c175 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -581,10 +581,41 @@ export function switchPlay(playing: boolean): AnyAction { }; } -export function confirmCanvasReady(): AnyAction { +export function confirmCanvasReady(ranges?: string): AnyAction { return { type: AnnotationActionTypes.CONFIRM_CANVAS_READY, - payload: {}, + payload: { ranges }, + }; +} + +export function confirmCanvasReadyAsync(): ThunkAction { + return async (dispatch: ActionCreator, getState: () => CombinedState): Promise => { + try { + const state: CombinedState = getState(); + const { instance: job } = state.annotation.job; + const chunks = await job.frames.cachedChunks() as number[]; + const { startFrame, stopFrame, dataChunkSize } = job; + + const ranges = chunks.map((chunk) => ( + [ + Math.max(startFrame, chunk * dataChunkSize), + Math.min(stopFrame, (chunk + 1) * dataChunkSize - 1), + ] + )).reduce>((acc, val) => { + if (acc.length && acc[acc.length - 1][1] + 1 === val[0]) { + const newMax = val[1]; + acc[acc.length - 1][1] = newMax; + } else { + acc.push(val as [number, number]); + } + return acc; + }, []).map(([start, end]) => `${start}:${end}`).join(';'); + + dispatch(confirmCanvasReady(ranges)); + } catch (error) { + // even if error happens here, do not need to notify the users + dispatch(confirmCanvasReady()); + } }; } diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx index 4493ba014f04..df2d3d63381d 100644 --- a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx @@ -24,7 +24,7 @@ import config from 'config'; import CVATTooltip from 'components/common/cvat-tooltip'; import FrameTags from 'components/annotation-page/tag-annotation-workspace/frame-tags'; import { - confirmCanvasReady, + confirmCanvasReadyAsync, dragCanvas, zoomCanvas, resetCanvas, @@ -259,7 +259,7 @@ function mapStateToProps(state: CombinedState): StateToProps { function mapDispatchToProps(dispatch: any): DispatchToProps { return { onSetupCanvas(): void { - dispatch(confirmCanvasReady()); + dispatch(confirmCanvasReadyAsync()); }, onDragCanvas(enabled: boolean): void { dispatch(dragCanvas(enabled)); diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx index e21a4a138d99..479ad283ede5 100644 --- a/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx @@ -16,7 +16,7 @@ import Spin from 'antd/lib/spin'; import { activateObject, - confirmCanvasReady, + confirmCanvasReadyAsync, createAnnotationsAsync, dragCanvas, editShape, @@ -131,7 +131,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { dispatch(dragCanvas(enabled)); }, onSetupCanvas(): void { - dispatch(confirmCanvasReady()); + dispatch(confirmCanvasReadyAsync()); }, onResetCanvas(): void { dispatch(resetCanvas()); diff --git a/cvat-ui/src/components/annotation-page/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss index 7d75f57d8f7e..9d31599fe3d5 100644 --- a/cvat-ui/src/components/annotation-page/styles.scss +++ b/cvat-ui/src/components/annotation-page/styles.scss @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: MIT -@import '../../base.scss'; +@import '../../base'; .cvat-annotation-page.ant-layout { height: 100%; @@ -126,15 +126,39 @@ } } -.cvat-player-slider { +.cvat-player-slider.ant-slider { width: 350px; margin: 0; + margin-top: $grid-unit-size * -0.5; + + > .ant-slider-handle { + z-index: 100; + margin-top: -3.5px; + } + + > .ant-slider-track { + background: none; + } > .ant-slider-rail { + height: $grid-unit-size; background-color: $player-slider-color; } } +.cvat-player-slider-progress { + width: 350px; + height: $grid-unit-size; + position: absolute; + top: 0; + pointer-events: none; + + > rect { + transition: width 0.5s; + fill: #1890ff; + } +} + .cvat-player-filename-wrapper { max-width: $grid-unit-size * 30; max-height: $grid-unit-size * 3; @@ -221,7 +245,7 @@ .ant-table-thead { > tr > th { - padding: 5px 5px; + padding: $grid-unit-size 0 $grid-unit-size $grid-unit-size * 0.5; } } } @@ -446,7 +470,7 @@ } .group { - background: rgba(216, 233, 250, 0.5); + background: rgba(216, 233, 250, 50%); border: 1px solid #d3e0ec; } } diff --git a/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx b/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx index e95d15cf33d0..931693690be1 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx @@ -21,6 +21,7 @@ interface Props { startFrame: number; stopFrame: number; playing: boolean; + ranges: string; frameNumber: number; frameFilename: string; frameDeleted: boolean; @@ -47,6 +48,7 @@ function PlayerNavigation(props: Props): JSX.Element { deleteFrameShortcut, focusFrameInputShortcut, inputFrameRef, + ranges, onSliderChange, onInputChange, onURLIconClick, @@ -105,6 +107,19 @@ function PlayerNavigation(props: Props): JSX.Element { value={frameNumber || 0} onChange={onSliderChange} /> + {!!ranges && ( + + {ranges.split(';').map((range) => { + const [start, end] = range.split(':').map((num) => +num); + const adjustedStart = Math.max(0, start - 1); + const totalSegments = stopFrame - startFrame; + const segmentWidth = 1000 / totalSegments; + const width = Math.max((end - adjustedStart), 1) * segmentWidth; + const offset = (Math.max((adjustedStart - startFrame), 0) / totalSegments) * 1000; + return (); + })} + + )} diff --git a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx index ee800ce68619..7c88063bf217 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx @@ -69,6 +69,7 @@ interface Props { onRestoreFrame(): void; switchNavigationBlocked(blocked: boolean): void; jobInstance: any; + ranges: string; } export default function AnnotationTopBarComponent(props: Props): JSX.Element { @@ -77,6 +78,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { undoAction, redoAction, playing, + ranges, frameNumber, frameFilename, frameDeleted, @@ -168,6 +170,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { startFrame={startFrame} stopFrame={stopFrame} playing={playing} + ranges={ranges} frameNumber={frameNumber} frameFilename={frameFilename} frameDeleted={frameDeleted} diff --git a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx index f53c5f61cdbe..067d85fe1ad0 100644 --- a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx @@ -65,6 +65,7 @@ interface StateToProps { normalizedKeyMap: Record; canvasInstance: Canvas | Canvas3d; forceExit: boolean; + ranges: string; activeControl: ActiveControl; } @@ -91,6 +92,7 @@ function mapStateToProps(state: CombinedState): StateToProps { annotation: { player: { playing, + ranges, frame: { data: { deleted: frameIsDeleted }, filename: frameFilename, @@ -142,6 +144,7 @@ function mapStateToProps(state: CombinedState): StateToProps { canvasInstance, forceExit, activeControl, + ranges, }; } @@ -638,6 +641,7 @@ class AnnotationTopBarContainer extends React.PureComponent { workspace, canvasIsReady, keyMap, + ranges, normalizedKeyMap, activeControl, searchAnnotations, @@ -766,6 +770,7 @@ class AnnotationTopBarContainer extends React.PureComponent { workspace={workspace} playing={playing} saving={saving} + ranges={ranges} startFrame={startFrame} stopFrame={stopFrame} frameNumber={frameNumber} diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 71524879f8bd..d7d395adb6f1 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -55,6 +55,7 @@ const defaultState: AnnotationState = { job: { openTime: null, labels: [], + groundTruthJobFramesMeta: null, requestedId: null, groundTruthJobId: null, instance: null, @@ -72,6 +73,7 @@ const defaultState: AnnotationState = { delay: 0, changeTime: null, }, + ranges: '', playing: false, frameAngles: [], navigationBlocked: false, @@ -417,8 +419,13 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }; } case AnnotationActionTypes.CONFIRM_CANVAS_READY: { + const { ranges } = action.payload; return { ...state, + player: { + ...state.player, + ranges: ranges || state.player.ranges, + }, canvas: { ...state.canvas, ready: true, diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index f29b7dff8f94..2207d73b807d 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -695,6 +695,7 @@ export interface AnnotationState { delay: number; changeTime: number | null; }; + ranges: string; navigationBlocked: boolean; playing: boolean; frameAngles: number[];