From 33aa9696de0e8be1ea506f1b3f18940f34401328 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 12 Feb 2024 11:44:12 +0200 Subject: [PATCH 01/43] open mode by query parameters --- cvat-ui/src/actions/annotation-actions.ts | 9 +++-- .../annotation-page/annotation-page.tsx | 6 ++-- .../views/canvas2d/canvas-context-menu.tsx | 2 +- .../canvas/views/canvas2d/canvas-wrapper.tsx | 14 ++++---- .../single-object-workspace.tsx | 33 +++++++++++++++++++ .../objects-side-bar/issues-list.tsx | 2 +- .../objects-side-bar/objects-list-header.tsx | 2 +- .../annotation-page/annotation-page.tsx | 9 ++++- .../annotation-page/top-bar/top-bar.tsx | 2 +- cvat-ui/src/reducers/annotation-reducer.ts | 22 ++++++++----- cvat-ui/src/reducers/index.ts | 11 ++++--- cvat-ui/src/utils/filter-annotations.ts | 2 +- 12 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 cvat-ui/src/components/annotation-page/single-object-workspace/single-object-workspace.tsx diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index ac3be64331b7..458a4c5353fa 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -866,13 +866,16 @@ export function closeJob(): ThunkAction { } export function getJobAsync({ - taskID, jobID, initialFrame, initialFilters, initialOpenGuide, + taskID, jobID, initialFrame, initialFilters, queryParameters, }: { taskID: number; jobID: number; initialFrame: number | null; initialFilters: object[]; - initialOpenGuide: boolean; + queryParameters: { + initialOpenGuide?: boolean; + initialWorkspace?: Workspace; + } }): ThunkAction { return async (dispatch: ActionCreator, getState): Promise => { try { @@ -960,7 +963,7 @@ export function getJobAsync({ payload: { openTime, job, - initialOpenGuide, + queryParameters, groundTruthInstance: gtJob || null, groundTruthJobFramesMeta, issues, diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index 670fd3ce996b..fa170f2dd0db 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -146,9 +146,9 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { {workspace === Workspace.STANDARD3D && } {workspace === Workspace.STANDARD && } - {workspace === Workspace.ATTRIBUTE_ANNOTATION && } - {workspace === Workspace.TAG_ANNOTATION && } - {workspace === Workspace.REVIEW_WORKSPACE && } + {workspace === Workspace.ATTRIBUTES && } + {workspace === Workspace.TAGS && } + {workspace === Workspace.REVIEW && } diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-context-menu.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-context-menu.tsx index 4c57555d330f..0f4011964825 100644 --- a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-context-menu.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-context-menu.tsx @@ -130,7 +130,7 @@ export default function CanvasContextMenu(props: Props): JSX.Element | null { } const copyObject = state?.isGroundTruth ? state : null; - if (workspace === Workspace.REVIEW_WORKSPACE) { + if (workspace === Workspace.REVIEW) { const conflict = frameConflicts .find((qualityConflict: QualityConflict) => qualityConflict.annotationConflicts.some( (annotationConflict: AnnotationConflict) => ( 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 1e2cdea614a5..034bcb929454 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 @@ -372,7 +372,7 @@ class CanvasWrapperComponent extends React.PureComponent { wrapper.appendChild(canvasInstance.html()); canvasInstance.configure({ - forceDisableEditing: workspace === Workspace.REVIEW_WORKSPACE, + forceDisableEditing: workspace === Workspace.REVIEW, undefinedAttrValue: config.UNDEFINED_ATTRIBUTE_VALUE, displayAllText: showObjectsTextAlways, autoborders: automaticBordering, @@ -548,11 +548,11 @@ class CanvasWrapperComponent extends React.PureComponent { } if (prevProps.workspace !== workspace) { - if (workspace === Workspace.REVIEW_WORKSPACE) { + if (workspace === Workspace.REVIEW) { canvasInstance.configure({ forceDisableEditing: true, }); - } else if (prevProps.workspace === Workspace.REVIEW_WORKSPACE) { + } else if (prevProps.workspace === Workspace.REVIEW) { canvasInstance.configure({ forceDisableEditing: false, }); @@ -730,7 +730,7 @@ class CanvasWrapperComponent extends React.PureComponent { const { workspace, activatedStateID, onActivateObject } = this.props; if ((e.target as HTMLElement).tagName === 'svg' && e.button !== 2) { - if (activatedStateID !== null && workspace !== Workspace.ATTRIBUTE_ANNOTATION) { + if (activatedStateID !== null && workspace !== Workspace.ATTRIBUTES) { onActivateObject(null, null); } } @@ -796,7 +796,7 @@ class CanvasWrapperComponent extends React.PureComponent { jobInstance, activatedStateID, activatedElementID, workspace, onActivateObject, } = this.props; - if (![Workspace.STANDARD, Workspace.REVIEW_WORKSPACE].includes(workspace)) { + if (![Workspace.STANDARD, Workspace.REVIEW].includes(workspace)) { return; } @@ -904,7 +904,7 @@ class CanvasWrapperComponent extends React.PureComponent { if (activatedStateID !== null) { const [activatedState] = annotations.filter((state: any): boolean => state.clientID === activatedStateID); - if (workspace === Workspace.ATTRIBUTE_ANNOTATION) { + if (workspace === Workspace.ATTRIBUTES) { if (activatedState.objectType !== ObjectType.TAG) { canvasInstance.focus(activatedStateID, aamZoomMargin); } else { @@ -914,7 +914,7 @@ class CanvasWrapperComponent extends React.PureComponent { if (activatedState && activatedState.objectType !== ObjectType.TAG) { canvasInstance.activate(activatedStateID, activatedAttributeID); } - } else if (workspace === Workspace.ATTRIBUTE_ANNOTATION) { + } else if (workspace === Workspace.ATTRIBUTES) { canvasInstance.fit(); } } diff --git a/cvat-ui/src/components/annotation-page/single-object-workspace/single-object-workspace.tsx b/cvat-ui/src/components/annotation-page/single-object-workspace/single-object-workspace.tsx new file mode 100644 index 000000000000..d9ba2fb6a384 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/single-object-workspace/single-object-workspace.tsx @@ -0,0 +1,33 @@ +// Copyright (C) 2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React from 'react'; +import Layout from 'antd/lib/layout'; + +import CanvasLayout from 'components/annotation-page/canvas/grid-layout/canvas-layout'; +import ControlsSideBarContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar'; +import CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas-context-menu'; +import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list'; +import ObjectSideBarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; +import CanvasPointContextMenuComponent from 'components/annotation-page/canvas/views/canvas2d/canvas-point-context-menu'; +import IssueAggregatorComponent from 'components/annotation-page/review/issues-aggregator'; +import RemoveConfirmComponent from 'components/annotation-page/standard-workspace/remove-confirm'; +import PropagateConfirmComponent from 'components/annotation-page/standard-workspace/propagate-confirm'; + +export default function SingleObjectWorkspace(): JSX.Element { + return ( + +
hello
+ {/* + + } /> + + + + + */} +
+ ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/issues-list.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/issues-list.tsx index 135568b82afa..aec8f0375e37 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/issues-list.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/issues-list.tsx @@ -115,7 +115,7 @@ export default function LabelsListComponent(): JSX.Element { { - workspace === Workspace.REVIEW_WORKSPACE ? ( + workspace === Workspace.REVIEW ? ( {!readonly && } - { workspace === Workspace.REVIEW_WORKSPACE && ( + { workspace === Workspace.REVIEW && ( )} diff --git a/cvat-ui/src/containers/annotation-page/annotation-page.tsx b/cvat-ui/src/containers/annotation-page/annotation-page.tsx index 6adf53b63169..575ada42989e 100644 --- a/cvat-ui/src/containers/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/containers/annotation-page/annotation-page.tsx @@ -64,6 +64,10 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps { const searchParams = new URLSearchParams(window.location.search); const initialFilters: object[] = []; const initialOpenGuide = searchParams.has('openGuide'); + const initialWorkspace = Object.entries(Workspace).find(([key]) => ( + key === searchParams.get('openWorkspace')?.toUpperCase() + )); + const parsedFrame = +(searchParams.get('frame') || 'NaN'); const initialFrame = Number.isInteger(parsedFrame) && parsedFrame >= 0 ? parsedFrame : null; @@ -88,7 +92,10 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps { jobID, initialFrame, initialFilters, - initialOpenGuide, + queryParameters: { + initialOpenGuide, + ...(initialWorkspace ? { initialWorkspace: initialWorkspace[1] } : {}), + }, })); }, saveLogs(): void { 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 9944b90f3ff3..13f83be6ac6c 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 @@ -109,7 +109,7 @@ function mapStateToProps(state: CombinedState): StateToProps { history, filters: annotationFilters, }, - job: { instance: jobInstance, initialOpenGuide }, + job: { instance: jobInstance, queryParameters: { initialOpenGuide } }, canvas: { ready: canvasIsReady, instance: canvasInstance, activeControl }, workspace, }, diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 7579d50b8a9b..5ee763d2c733 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -57,7 +57,7 @@ const defaultState: AnnotationState = { requestedId: null, groundTruthJobFramesMeta: null, groundTruthInstance: null, - initialOpenGuide: false, + queryParameters: {}, instance: null, attributes: {}, fetching: false, @@ -154,19 +154,23 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { frameData: data, minZ, maxZ, - initialOpenGuide, + queryParameters, groundTruthInstance, groundTruthJobFramesMeta, } = action.payload; - const isReview = job.stage === JobStage.VALIDATION; - let workspaceSelected = Workspace.STANDARD; - const defaultLabel = job.labels.length ? job.labels[0] : null; + const isReview = job.stage === JobStage.VALIDATION; + let workspaceSelected = null; let activeShapeType = defaultLabel && defaultLabel.type !== 'any' ? defaultLabel.type : ShapeType.RECTANGLE; - if (job.dimension === DimensionType.DIMENSION_3D) { + if (job.dimension === DimensionType.DIMENSION_2D) { + if (queryParameters.initialWorkspace !== Workspace.STANDARD3D) { + workspaceSelected = queryParameters.initialWorkspace; + } + workspaceSelected = workspaceSelected || (isReview ? Workspace.REVIEW : Workspace.STANDARD); + } else { workspaceSelected = Workspace.STANDARD3D; activeShapeType = ShapeType.CUBOID; } @@ -188,9 +192,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { acc[label.id] = label.attributes; return acc; }, {}), - initialOpenGuide, groundTruthInstance, groundTruthJobFramesMeta, + queryParameters: { + initialOpenGuide: queryParameters.initialOpenGuide, + }, }, annotations: { ...state.annotations, @@ -225,7 +231,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }, colors, workspace: isReview && job.dimension === DimensionType.DIMENSION_2D ? - Workspace.REVIEW_WORKSPACE : workspaceSelected, + Workspace.REVIEW : workspaceSelected, }; } case AnnotationActionTypes.GET_JOB_FAILED: { diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index 8cf2b2884545..d85387ad503b 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -694,7 +694,9 @@ export interface AnnotationState { labels: Label[]; requestedId: number | null; instance: Job | null | undefined; - initialOpenGuide: boolean; + queryParameters: { + initialOpenGuide?: boolean; + }; groundTruthJobFramesMeta: FramesMetaData | null; groundTruthInstance: Job | null; attributes: Record; @@ -773,9 +775,10 @@ export interface AnnotationState { export enum Workspace { STANDARD3D = 'Standard 3D', STANDARD = 'Standard', - ATTRIBUTE_ANNOTATION = 'Attribute annotation', - TAG_ANNOTATION = 'Tag annotation', - REVIEW_WORKSPACE = 'Review', + ATTRIBUTES = 'Attribute annotation', + SINGLE_OBJECT = 'Single object', + TAGS = 'Tag annotation', + REVIEW = 'Review', } export enum GridColor { diff --git a/cvat-ui/src/utils/filter-annotations.ts b/cvat-ui/src/utils/filter-annotations.ts index afdfc42df5e8..0e167e7abbee 100644 --- a/cvat-ui/src/utils/filter-annotations.ts +++ b/cvat-ui/src/utils/filter-annotations.ts @@ -32,7 +32,7 @@ export function filterAnnotations(annotations: ObjectState[], params: FilterAnno } // GT tracks are shown only on GT frames - if (workspace === Workspace.REVIEW_WORKSPACE && groundTruthJobFramesMeta && frame) { + if (workspace === Workspace.REVIEW && groundTruthJobFramesMeta && frame) { if (state.objectType === ObjectType.TRACK && state.isGroundTruth) { return groundTruthJobFramesMeta.includedFrames.includes(frame); } From 6492e1bf022b1b1f798625f4e9333fb4c06bb3f4 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 12 Feb 2024 14:26:40 +0200 Subject: [PATCH 02/43] Draft solution --- .../annotation-page/annotation-page.tsx | 2 + .../single-object-workspace.tsx | 33 -- .../single-shape-sidebar.tsx | 288 ++++++++++++++++++ .../single-shape-workspace.tsx | 19 ++ .../single-shape-workspace/styles.scss | 5 + .../tag-annotation-workspace/styles.scss | 2 +- .../tag-annotation-sidebar.tsx | 2 +- cvat-ui/src/reducers/annotation-reducer.ts | 2 +- cvat-ui/src/reducers/index.ts | 2 +- 9 files changed, 318 insertions(+), 37 deletions(-) delete mode 100644 cvat-ui/src/components/annotation-page/single-object-workspace/single-object-workspace.tsx create mode 100644 cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx create mode 100644 cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-workspace.tsx create mode 100644 cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index fa170f2dd0db..46ef41ad5c2f 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -13,6 +13,7 @@ import Button from 'antd/lib/button'; import './styles.scss'; import AttributeAnnotationWorkspace from 'components/annotation-page/attribute-annotation-workspace/attribute-annotation-workspace'; +import SingleShapeWorkspace from 'components/annotation-page/single-shape-workspace/single-shape-workspace'; import ReviewAnnotationsWorkspace from 'components/annotation-page/review-workspace/review-workspace'; import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace'; import StandardWorkspace3DComponent from 'components/annotation-page/standard3D-workspace/standard3D-workspace'; @@ -146,6 +147,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { {workspace === Workspace.STANDARD3D && } {workspace === Workspace.STANDARD && } + {workspace === Workspace.SINGLE_SHAPE && } {workspace === Workspace.ATTRIBUTES && } {workspace === Workspace.TAGS && } {workspace === Workspace.REVIEW && } diff --git a/cvat-ui/src/components/annotation-page/single-object-workspace/single-object-workspace.tsx b/cvat-ui/src/components/annotation-page/single-object-workspace/single-object-workspace.tsx deleted file mode 100644 index d9ba2fb6a384..000000000000 --- a/cvat-ui/src/components/annotation-page/single-object-workspace/single-object-workspace.tsx +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2024 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -import './styles.scss'; -import React from 'react'; -import Layout from 'antd/lib/layout'; - -import CanvasLayout from 'components/annotation-page/canvas/grid-layout/canvas-layout'; -import ControlsSideBarContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar'; -import CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas-context-menu'; -import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list'; -import ObjectSideBarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; -import CanvasPointContextMenuComponent from 'components/annotation-page/canvas/views/canvas2d/canvas-point-context-menu'; -import IssueAggregatorComponent from 'components/annotation-page/review/issues-aggregator'; -import RemoveConfirmComponent from 'components/annotation-page/standard-workspace/remove-confirm'; -import PropagateConfirmComponent from 'components/annotation-page/standard-workspace/propagate-confirm'; - -export default function SingleObjectWorkspace(): JSX.Element { - return ( - -
hello
- {/* - - } /> - - - - - */} -
- ); -} diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx new file mode 100644 index 000000000000..b27a0ef7bf72 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -0,0 +1,288 @@ +// Copyright (C) 2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useEffect, useReducer, useState } from 'react'; +import Layout, { SiderProps } from 'antd/lib/layout'; +import { Row, Col } from 'antd/lib/grid'; +import Text from 'antd/lib/typography/Text'; +import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; +import { getCVATStore } from 'cvat-store'; +import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; +import { CombinedState, ObjectType, ShapeType, Workspace } from 'reducers'; +import { useDispatch } from 'react-redux'; +import { changeFrameAsync, changeWorkspace, createAnnotationsAsync, saveAnnotationsAsync } from 'actions/annotation-actions'; +import { Job, Label, LabelType, getCore } from 'cvat-core-wrapper'; +import { useSelector } from 'react-redux'; +import { Checkbox, Select } from 'antd'; +import { ActionUnion, createAction } from 'utils/redux'; +import LabelSelector from 'components/label-selector/label-selector'; + +enum Mode { + CONFIGURATION = 'configuration', + ANNOTATION = 'annotation', +} + +const cvat = getCore(); + +const getState = (): CombinedState => getCVATStore().getState(); + +enum ReducerActionType { + SWITCH_SIDEBAR_COLLAPSED = 'SWITCH_SIDEBAR_COLLAPSED', + SWITCH_AUTO_NEXT_FRAME = 'SWITCH_AUTO_NEXT_FRAME', + SWITCH_AUTOSAVE_ON_FINISH = 'SWITCH_AUTOSAVE_ON_FINISH', + UPDATE_ACTIVE_LABEL = 'UPDATE_ACTIVE_LABEL', +} + +export const reducerActions = { + switchSidebarCollapsed: () => ( + createAction(ReducerActionType.SWITCH_SIDEBAR_COLLAPSED) + ), + switchAutoNextFrame: () => ( + createAction(ReducerActionType.SWITCH_AUTO_NEXT_FRAME) + ), + switchAutoSaveOnFinish: () => ( + createAction(ReducerActionType.SWITCH_AUTOSAVE_ON_FINISH) + ), + setLabel: (label: Label, type?: LabelType) => ( + createAction(ReducerActionType.UPDATE_ACTIVE_LABEL, { + label, + labelType: type || label.type, + }) + ), +}; + +interface State { + sidebarCollabased: boolean; + autoNextFrame: boolean; + saveOnFinish: boolean; + shapeType: ShapeType; + label: Label | null; + labelType: LabelType; +} + +const reducer = (state: State, action: ActionUnion): State => { + if (action.type === ReducerActionType.SWITCH_SIDEBAR_COLLAPSED) { + return { + ...state, + sidebarCollabased: !state.sidebarCollabased, + }; + } + + if (action.type === ReducerActionType.SWITCH_AUTO_NEXT_FRAME) { + return { + ...state, + autoNextFrame: !state.autoNextFrame, + }; + } + + if (action.type === ReducerActionType.SWITCH_AUTOSAVE_ON_FINISH) { + return { + ...state, + saveOnFinish: !state.saveOnFinish, + }; + } + + if (action.type === ReducerActionType.UPDATE_ACTIVE_LABEL) { + return { + ...state, + label: action.payload.label, + labelType: action.payload.label.type, + }; + } + + return state; +}; + +function cancelCurrentCanvasOp(canvas: Canvas): void { + if (canvas.mode() !== CanvasMode.IDLE) { + canvas.cancel(); + } +} + +function SingleShapeSidebar(): JSX.Element { + const appDispatch = useDispatch(); + const isCanvasReady = useSelector((_state: CombinedState) => _state.annotation.canvas.ready); + const jobInstance = useSelector((_state: CombinedState) => _state.annotation.job.instance); + + const [state, dispatch] = useReducer(reducer, { + sidebarCollabased: false, + autoNextFrame: true, + saveOnFinish: true, + shapeType: ShapeType.RECTANGLE, + label: (jobInstance as Job).labels[0] || null, + labelType: (jobInstance as Job).labels[0]?.type || LabelType.ANY, + }); + + // const [mode, setMode] = useState(Mode.ANNOTATION); + + const siderProps: SiderProps = { + className: 'cvat-single-shape-annotation-sidebar', + theme: 'light', + width: 300, + collapsedWidth: 0, + reverseArrow: true, + collapsible: true, + trigger: null, + collapsed: state.sidebarCollabased, + }; + + // todo: select label if more than one + // todo: shape type if label is any + + // todo: check deleted frame + // todo: skip button + + // todo: next frame delay??????// + // todo: simpler navigation + // todo: do not annotate if label is "any"? + + const runDrawing = (): void => { + const canvas = getState().annotation.canvas.instance; + canvas?.draw({ + enabled: true, + shapeType: ShapeType.POINTS, + numberOfPoints: 1, + crosshair: true, + }); + }; + + useEffect(() => { + cancelCurrentCanvasOp( + getState().annotation.canvas.instance as Canvas, + ); + + if (isCanvasReady && state.label && state.labelType !== LabelType.ANY) { + runDrawing(); + } + }, [isCanvasReady, state.label, state.labelType]); + + useEffect(() => { + const canvas = getState().annotation.canvas.instance as Canvas; + const onDrawDone = (): void => { + const { + annotation: { + player: { + frame: { + number: frame, + }, + }, + }, + } = getState(); + + const { stopFrame } = jobInstance as Job; + if (frame < stopFrame) { + if (state.autoNextFrame) { + appDispatch(changeFrameAsync(frame + 1)); + } + } else { + appDispatch(changeWorkspace(Workspace.STANDARD)); + if (state.saveOnFinish) { + appDispatch(saveAnnotationsAsync()); + } + } + }; + + cancelCurrentCanvasOp(canvas); + (canvas as Canvas).html().addEventListener('canvas.drawn', onDrawDone); + + return (() => { + cancelCurrentCanvasOp(canvas); + (canvas as Canvas).html().removeEventListener('canvas.drawn', onDrawDone); + }); + }, []); + + return ( + + {/* eslint-disable-next-line */} + { + dispatch(reducerActions.switchSidebarCollapsed()); + }} + > + {state.sidebarCollabased ? : } + + + + Select label: + + + + + dispatch(reducerActions.setLabel(label))} + /> + + + { state.label && state.labelType === 'any' ? ( + <> + + + Select label type: + + + + + + + + + ) : null } + + + { + dispatch(reducerActions.switchAutoNextFrame()); + }} + > + Automatically go to the next frame + + + + + + { + dispatch(reducerActions.switchAutoSaveOnFinish()); + }} + > + Save automatically after finish + + + + { state.label !== null ? ( + + + Please, click + { `${(state.label as Label).name}` } + on the image + + + ) : ( + + + There are not any labels to annotate + + + )} + + ); +} + +export default React.memo(SingleShapeSidebar); diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-workspace.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-workspace.tsx new file mode 100644 index 000000000000..8f5efe603db5 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-workspace.tsx @@ -0,0 +1,19 @@ +// Copyright (C) 2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React from 'react'; +import Layout from 'antd/lib/layout'; + +import CanvasLayout from 'components/annotation-page/canvas/grid-layout/canvas-layout'; +import SingleShapeSidebar from './single-shape-sidebar/single-shape-sidebar'; + +export default function SingleShapeWorkspace(): JSX.Element { + return ( + + + + + ); +} diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss new file mode 100644 index 000000000000..386d3dff88c6 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss @@ -0,0 +1,5 @@ +// Copyright (C) 2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +@import 'base'; diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss index 713749ecde07..6436d7ac29ea 100644 --- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss @@ -47,7 +47,7 @@ } } -.labels-tag-annotation-sidebar-not-found-wrapper { +.cvat-tag-annotation-sidebar-empty { margin-top: $grid-unit-size * 4; } diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx index 1f820686654a..a3cb4bb366da 100644 --- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx @@ -240,7 +240,7 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen > {sidebarCollapsed ? : } - + Can't place tag on this frame. diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 5ee763d2c733..ab340183f498 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -169,7 +169,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { if (queryParameters.initialWorkspace !== Workspace.STANDARD3D) { workspaceSelected = queryParameters.initialWorkspace; } - workspaceSelected = workspaceSelected || (isReview ? Workspace.REVIEW : Workspace.STANDARD); + workspaceSelected = workspaceSelected || (isReview ? Workspace.REVIEW : Workspace.SINGLE_SHAPE); } else { workspaceSelected = Workspace.STANDARD3D; activeShapeType = ShapeType.CUBOID; diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index d85387ad503b..8c39ba53d148 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -776,7 +776,7 @@ export enum Workspace { STANDARD3D = 'Standard 3D', STANDARD = 'Standard', ATTRIBUTES = 'Attribute annotation', - SINGLE_OBJECT = 'Single object', + SINGLE_SHAPE = 'Single shape', TAGS = 'Tag annotation', REVIEW = 'Review', } From dcd20ba9aa4d281922a54ef59bf6bbbb8766a823 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 13 Feb 2024 14:33:50 +0200 Subject: [PATCH 03/43] Updated implementation --- .../single-shape-sidebar.tsx | 147 ++++++++++++------ .../single-shape-workspace/styles.scss | 33 ++++ 2 files changed, 131 insertions(+), 49 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index b27a0ef7bf72..0a324debcf7d 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -2,36 +2,33 @@ // // SPDX-License-Identifier: MIT -import React, { useEffect, useReducer, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import React, { useEffect, useReducer } from 'react'; import Layout, { SiderProps } from 'antd/lib/layout'; import { Row, Col } from 'antd/lib/grid'; import Text from 'antd/lib/typography/Text'; +import Checkbox from 'antd/lib/checkbox'; +import InputNumber from 'antd/lib/input-number'; +import Select from 'antd/lib/select'; +import Paragraph from 'antd/lib/typography/Paragraph'; + import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; import { getCVATStore } from 'cvat-store'; import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; -import { CombinedState, ObjectType, ShapeType, Workspace } from 'reducers'; -import { useDispatch } from 'react-redux'; -import { changeFrameAsync, changeWorkspace, createAnnotationsAsync, saveAnnotationsAsync } from 'actions/annotation-actions'; -import { Job, Label, LabelType, getCore } from 'cvat-core-wrapper'; -import { useSelector } from 'react-redux'; -import { Checkbox, Select } from 'antd'; +import { CombinedState, Workspace } from 'reducers'; +import { changeFrameAsync, changeWorkspace, saveAnnotationsAsync } from 'actions/annotation-actions'; +import { Job, Label, LabelType } from 'cvat-core-wrapper'; import { ActionUnion, createAction } from 'utils/redux'; import LabelSelector from 'components/label-selector/label-selector'; -enum Mode { - CONFIGURATION = 'configuration', - ANNOTATION = 'annotation', -} - -const cvat = getCore(); - const getState = (): CombinedState => getCVATStore().getState(); enum ReducerActionType { SWITCH_SIDEBAR_COLLAPSED = 'SWITCH_SIDEBAR_COLLAPSED', SWITCH_AUTO_NEXT_FRAME = 'SWITCH_AUTO_NEXT_FRAME', SWITCH_AUTOSAVE_ON_FINISH = 'SWITCH_AUTOSAVE_ON_FINISH', - UPDATE_ACTIVE_LABEL = 'UPDATE_ACTIVE_LABEL', + SET_ACTIVE_LABEL = 'SET_ACTIVE_LABEL', + SET_POINTS_COUNT = 'SET_POINTS_COUNT', } export const reducerActions = { @@ -44,24 +41,41 @@ export const reducerActions = { switchAutoSaveOnFinish: () => ( createAction(ReducerActionType.SWITCH_AUTOSAVE_ON_FINISH) ), - setLabel: (label: Label, type?: LabelType) => ( - createAction(ReducerActionType.UPDATE_ACTIVE_LABEL, { + setActiveLabel: (label: Label, type?: LabelType) => ( + createAction(ReducerActionType.SET_ACTIVE_LABEL, { label, labelType: type || label.type, }) ), + setPointsCount: (pointsCount: number) => ( + createAction(ReducerActionType.SET_POINTS_COUNT, { + pointsCount, + }) + ), }; interface State { sidebarCollabased: boolean; autoNextFrame: boolean; saveOnFinish: boolean; - shapeType: ShapeType; + pointsCount: number; + labels: Label[]; label: Label | null; labelType: LabelType; } const reducer = (state: State, action: ActionUnion): State => { + const getMinimalPoints = (labelType: LabelType): number => { + let minimalPoints = 3; + if (labelType === LabelType.POLYLINE) { + minimalPoints = 2; + } else if (labelType === LabelType.POINTS) { + minimalPoints = 1; + } + + return minimalPoints; + }; + if (action.type === ReducerActionType.SWITCH_SIDEBAR_COLLAPSED) { return { ...state, @@ -83,11 +97,19 @@ const reducer = (state: State, action: ActionUnion): Stat }; } - if (action.type === ReducerActionType.UPDATE_ACTIVE_LABEL) { + if (action.type === ReducerActionType.SET_ACTIVE_LABEL) { return { ...state, label: action.payload.label, - labelType: action.payload.label.type, + labelType: action.payload.labelType, + pointsCount: Math.max(state.pointsCount, getMinimalPoints(action.payload.labelType)), + }; + } + + if (action.type === ReducerActionType.SET_POINTS_COUNT) { + return { + ...state, + pointsCount: Math.max(action.payload.pointsCount, getMinimalPoints(state.labelType)), }; } @@ -109,12 +131,18 @@ function SingleShapeSidebar(): JSX.Element { sidebarCollabased: false, autoNextFrame: true, saveOnFinish: true, - shapeType: ShapeType.RECTANGLE, + pointsCount: 1, + labels: (jobInstance as Job).labels.filter((label) => label.type !== LabelType.TAG), label: (jobInstance as Job).labels[0] || null, labelType: (jobInstance as Job).labels[0]?.type || LabelType.ANY, }); - // const [mode, setMode] = useState(Mode.ANNOTATION); + let message = ''; + if (state.labelType === LabelType.POINTS) { + message = `${state.pointsCount === 1 ? 'one point' : `${state.pointsCount} points`}`; + } else { + message = `${state.labelType === LabelType.ELLIPSE ? 'an ellipse' : `a ${state.labelType}`}`; + } const siderProps: SiderProps = { className: 'cvat-single-shape-annotation-sidebar', @@ -127,22 +155,12 @@ function SingleShapeSidebar(): JSX.Element { collapsed: state.sidebarCollabased, }; - // todo: select label if more than one - // todo: shape type if label is any - - // todo: check deleted frame - // todo: skip button - - // todo: next frame delay??????// - // todo: simpler navigation - // todo: do not annotate if label is "any"? - const runDrawing = (): void => { const canvas = getState().annotation.canvas.instance; canvas?.draw({ enabled: true, - shapeType: ShapeType.POINTS, - numberOfPoints: 1, + shapeType: state.labelType, + numberOfPoints: state.pointsCount, crosshair: true, }); }; @@ -155,7 +173,7 @@ function SingleShapeSidebar(): JSX.Element { if (isCanvasReady && state.label && state.labelType !== LabelType.ANY) { runDrawing(); } - }, [isCanvasReady, state.label, state.labelType]); + }, [isCanvasReady, state.label, state.labelType, state.pointsCount]); useEffect(() => { const canvas = getState().annotation.canvas.instance as Canvas; @@ -207,41 +225,69 @@ function SingleShapeSidebar(): JSX.Element { - Select label: + Label selector dispatch(reducerActions.setLabel(label))} + onChange={(label) => dispatch(reducerActions.setActiveLabel(label))} /> - { state.label && state.labelType === 'any' ? ( + { state.label && state.label.type === 'any' ? ( <> - Select label type: + Label type selector {LabelType.RECTANGLE} + + + + + + + ) : null } + { state.label && [LabelType.POLYGON, LabelType.POLYLINE, LabelType.POINTS].includes(state.labelType) ? ( + <> + + + Number of points + + + + + { + if (value !== null) { + dispatch(reducerActions.setPointsCount(value)); + } + }} + /> + + + + ) : null } - Save automatically after finish + Automatically save after the latest frame { state.label !== null ? ( - Please, click - { `${(state.label as Label).name}` } - on the image + + Annotate + {` ${(state.label as Label).name} `} + on the image, using + {` ${message} `} + ) : ( diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss index 386d3dff88c6..8e8acb5f6dc1 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss @@ -3,3 +3,36 @@ // SPDX-License-Identifier: MIT @import 'base'; + +.cvat-single-shape-annotation-sidebar { + padding: 8px; + + .cvat-single-shape-annotation-sidebar-label-select, + .cvat-single-shape-annotation-sidebar-label-type-selector { + .ant-select { + width: 160px; + } + } + + .cvat-single-shape-annotation-sidebar-points-count-input { + .ant-input-number { + width: 160px; + } + } + + .cvat-single-shape-annotation-sidebar-label-type, + .cvat-single-shape-annotation-sidebar-points-count, + .cvat-single-shape-annotation-sidebar-label-select, + .cvat-single-shape-annotation-sidebar-points-count-input, + .cvat-single-shape-annotation-sidebar-auto-next-frame-checkbox, + .cvat-single-shape-annotation-sidebar-auto-save-checkbox { + margin-top: 8px; + } + + .cvat-single-shape-annotation-sidebar-hint { + row-gap: 0; + text-align: center; + font-size: large; + padding-top: 32px; + } +} From f70e1fddd3abd51bfb6e915d569bc6234c9b4250 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 13 Feb 2024 14:55:56 +0200 Subject: [PATCH 04/43] Navigate only empty --- .../single-shape-sidebar.tsx | 27 ++++++ .../single-shape-workspace/styles.scss | 3 + .../annotation-page/top-bar/top-bar.tsx | 90 ++++++++++--------- 3 files changed, 76 insertions(+), 44 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 0a324debcf7d..b1fb2796e10d 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -27,6 +27,7 @@ enum ReducerActionType { SWITCH_SIDEBAR_COLLAPSED = 'SWITCH_SIDEBAR_COLLAPSED', SWITCH_AUTO_NEXT_FRAME = 'SWITCH_AUTO_NEXT_FRAME', SWITCH_AUTOSAVE_ON_FINISH = 'SWITCH_AUTOSAVE_ON_FINISH', + SWITCH_NAVIGATE_EMPTY_ONLY = 'SWITCH_NAVIGATE_EMPTY_ONLY', SET_ACTIVE_LABEL = 'SET_ACTIVE_LABEL', SET_POINTS_COUNT = 'SET_POINTS_COUNT', } @@ -41,6 +42,9 @@ export const reducerActions = { switchAutoSaveOnFinish: () => ( createAction(ReducerActionType.SWITCH_AUTOSAVE_ON_FINISH) ), + switchNavigateEmptyOnly: () => ( + createAction(ReducerActionType.SWITCH_NAVIGATE_EMPTY_ONLY) + ), setActiveLabel: (label: Label, type?: LabelType) => ( createAction(ReducerActionType.SET_ACTIVE_LABEL, { label, @@ -58,6 +62,7 @@ interface State { sidebarCollabased: boolean; autoNextFrame: boolean; saveOnFinish: boolean; + navigateOnlyEmpty: boolean; pointsCount: number; labels: Label[]; label: Label | null; @@ -90,6 +95,13 @@ const reducer = (state: State, action: ActionUnion): Stat }; } + if (action.type === ReducerActionType.SWITCH_NAVIGATE_EMPTY_ONLY) { + return { + ...state, + navigateOnlyEmpty: !state.navigateOnlyEmpty, + }; + } + if (action.type === ReducerActionType.SWITCH_AUTOSAVE_ON_FINISH) { return { ...state, @@ -131,6 +143,7 @@ function SingleShapeSidebar(): JSX.Element { sidebarCollabased: false, autoNextFrame: true, saveOnFinish: true, + navigateOnlyEmpty: true, pointsCount: 1, labels: (jobInstance as Job).labels.filter((label) => label.type !== LabelType.TAG), label: (jobInstance as Job).labels[0] || null, @@ -312,15 +325,29 @@ function SingleShapeSidebar(): JSX.Element { + + + { + dispatch(reducerActions.switchNavigateEmptyOnly()); + }} + > + Navigate only empty frames + + + { state.label !== null ? ( +
Annotate {` ${(state.label as Label).name} `} on the image, using {` ${message} `} +
) : ( diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss index 8e8acb5f6dc1..dcaaae3f421a 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss @@ -25,6 +25,7 @@ .cvat-single-shape-annotation-sidebar-label-select, .cvat-single-shape-annotation-sidebar-points-count-input, .cvat-single-shape-annotation-sidebar-auto-next-frame-checkbox, + .cvat-single-shape-annotation-sidebar-navigate-empty-checkbox, .cvat-single-shape-annotation-sidebar-auto-save-checkbox { margin-top: 8px; } @@ -34,5 +35,7 @@ text-align: center; font-size: large; padding-top: 32px; + bottom: 0; + position: absolute; } } 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 97952cbbfc8a..0ba1ebefc2ba 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 @@ -142,51 +142,53 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { }), ); - playerItems.push([( - - ), 0]); + if (workspace !== Workspace.SINGLE_SHAPE) { + playerItems.push([( + + ), 0]); - playerItems.push([( - - ), 10]); + playerItems.push([( + + ), 10]); + } return ( From 2f135399a520cb370cdef855b498980b357e8377 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 13 Feb 2024 16:16:47 +0200 Subject: [PATCH 05/43] Optionally hidden message --- .../single-shape-sidebar/single-shape-sidebar.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index b1fb2796e10d..1a150ff4f06b 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -337,7 +337,7 @@ function SingleShapeSidebar(): JSX.Element { - { state.label !== null ? ( + { state.label !== null && state.labelType !== LabelType.ANY && (
@@ -350,12 +350,6 @@ function SingleShapeSidebar(): JSX.Element {
- ) : ( - - - There are not any labels to annotate - - )} ); From e5ef56c6fc412d3e59398b308552a9bb9f126a05 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 15 Feb 2024 09:15:52 +0200 Subject: [PATCH 06/43] Refactored existing components --- .../annotation-page/top-bar/left-group.tsx | 24 ++ .../top-bar/player-buttons.tsx | 51 +++- .../top-bar/player-navigation.tsx | 26 +- .../annotation-page/top-bar/top-bar.tsx | 18 +- .../annotation-page/top-bar/top-bar.tsx | 281 +++++++----------- cvat-ui/src/reducers/annotation-reducer.ts | 4 +- cvat-ui/src/reducers/index.ts | 2 +- 7 files changed, 219 insertions(+), 187 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx index f1f2105fdf26..5a5838c7d067 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx @@ -16,6 +16,7 @@ import { MainMenuIcon, UndoIcon, RedoIcon } from 'icons'; import { ActiveControl, ToolsBlockerState } from 'reducers'; import CVATTooltip from 'components/common/cvat-tooltip'; import customizableComponents from 'components/customizable-components'; +import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; interface Props { saving: boolean; @@ -27,6 +28,7 @@ interface Props { switchToolsBlockerShortcut: string; toolsBlockerState: ToolsBlockerState; activeControl: ActiveControl; + keyMap: KeyMap; onSaveAnnotation(): void; onUndoClick(): void; onRedoClick(): void; @@ -37,6 +39,7 @@ interface Props { function LeftGroup(props: Props): JSX.Element { const { saving, + keyMap, undoAction, redoAction, undoShortcut, @@ -66,8 +69,29 @@ function LeftGroup(props: Props): JSX.Element { const shouldEnableToolsBlockerOnClick = [ActiveControl.OPENCV_TOOLS].includes(activeControl); const SaveButtonComponent = customizableComponents.SAVE_ANNOTATION_BUTTON; + const subKeyMap = { + UNDO: keyMap.UNDO, + REDO: keyMap.REDO, + }; + + const handlers = { + UNDO: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + if (undoAction) { + onUndoClick(); + } + }, + REDO: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + if (redoAction) { + onRedoClick(); + } + }, + }; + return ( <> + CVAT is saving your annotations, please wait diff --git a/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx b/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx index a3bbc7201e29..a94a7a0977a2 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2020-2024 Intel Corporation // // SPDX-License-Identifier: MIT @@ -8,7 +8,7 @@ import Icon from '@ant-design/icons'; import Popover from 'antd/lib/popover'; import CVATTooltip from 'components/common/cvat-tooltip'; - +import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import { FirstIcon, BackJumpIcon, @@ -33,6 +33,7 @@ interface Props { backwardShortcut: string; prevButtonType: string; nextButtonType: string; + keyMap: KeyMap; onSwitchPlay(): void; onPrevFrame(): void; onNextFrame(): void; @@ -40,6 +41,7 @@ interface Props { onBackward(): void; onFirstFrame(): void; onLastFrame(): void; + onSearchAnnotations(direction: 'forward' | 'backward'): void; setPrevButton(type: 'regular' | 'filtered' | 'empty'): void; setNextButton(type: 'regular' | 'filtered' | 'empty'): void; } @@ -54,6 +56,7 @@ function PlayerButtons(props: Props): JSX.Element { backwardShortcut, prevButtonType, nextButtonType, + keyMap, onSwitchPlay, onPrevFrame, onNextFrame, @@ -61,10 +64,53 @@ function PlayerButtons(props: Props): JSX.Element { onBackward, onFirstFrame, onLastFrame, + onSearchAnnotations, setPrevButton, setNextButton, } = props; + const subKeyMap = { + NEXT_FRAME: keyMap.NEXT_FRAME, + PREV_FRAME: keyMap.PREV_FRAME, + FORWARD_FRAME: keyMap.FORWARD_FRAME, + BACKWARD_FRAME: keyMap.BACKWARD_FRAME, + SEARCH_FORWARD: keyMap.SEARCH_FORWARD, + SEARCH_BACKWARD: keyMap.SEARCH_BACKWARD, + PLAY_PAUSE: keyMap.PLAY_PAUSE, + FOCUS_INPUT_FRAME: keyMap.FOCUS_INPUT_FRAME, + }; + + const handlers = { + NEXT_FRAME: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + onNextFrame(); + }, + PREV_FRAME: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + onPrevFrame(); + }, + FORWARD_FRAME: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + onForward(); + }, + BACKWARD_FRAME: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + onBackward(); + }, + SEARCH_FORWARD: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + onSearchAnnotations('forward'); + }, + SEARCH_BACKWARD: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + onSearchAnnotations('backward'); + }, + PLAY_PAUSE: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + onSwitchPlay(); + }, + }; + const prevRegularText = 'Go back'; const prevFilteredText = 'Go back with a filter'; const prevEmptyText = 'Go back to an empty frame'; @@ -104,6 +150,7 @@ function PlayerButtons(props: Props): JSX.Element { return ( + 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 4a05b4444a1f..dbc254bb6d07 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 @@ -1,10 +1,9 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import React, { useState, useEffect, useCallback } from 'react'; - import { Row, Col } from 'antd/lib/grid'; import Icon, { LinkOutlined, DeleteOutlined } from '@ant-design/icons'; import Slider from 'antd/lib/slider'; @@ -16,6 +15,7 @@ import Modal from 'antd/lib/modal'; import { RestoreIcon } from 'icons'; import CVATTooltip from 'components/common/cvat-tooltip'; import { clamp } from 'utils/math'; +import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; interface Props { startFrame: number; @@ -29,6 +29,7 @@ interface Props { deleteFrameShortcut: string; focusFrameInputShortcut: string; inputFrameRef: React.RefObject; + keyMap: KeyMap; onSliderChange(value: number): void; onInputChange(value: number): void; onURLIconClick(): void; @@ -49,6 +50,7 @@ function PlayerNavigation(props: Props): JSX.Element { focusFrameInputShortcut, inputFrameRef, ranges, + keyMap, onSliderChange, onInputChange, onURLIconClick, @@ -85,6 +87,25 @@ function PlayerNavigation(props: Props): JSX.Element { }); } }, [playing, frameNumber]); + + const subKeyMap = { + DELETE_FRAME: keyMap.DELETE_FRAME, + FOCUS_INPUT_FRAME: keyMap.FOCUS_INPUT_FRAME, + }; + + const handlers = { + DELETE_FRAME: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + onDeleteFrame(); + }, + FOCUS_INPUT_FRAME: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + if (inputFrameRef.current) { + inputFrameRef.current.focus(); + } + }, + }; + const deleteFrameIcon = !frameDeleted ? ( @@ -97,6 +118,7 @@ function PlayerNavigation(props: Props): JSX.Element { 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 0ba1ebefc2ba..05ee3cb49b10 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 @@ -10,7 +10,9 @@ import { Col, Row } from 'antd/lib/grid'; import { ActiveControl, CombinedState, ToolsBlockerState, Workspace, } from 'reducers'; +import { Job } from 'cvat-core-wrapper'; import { usePlugins } from 'utils/hooks'; +import { KeyMap } from 'utils/mousetrap-react'; import LeftGroup from './left-group'; import PlayerButtons from './player-buttons'; import PlayerNavigation from './player-navigation'; @@ -46,6 +48,9 @@ interface Props { deleteFrameAvailable: boolean; annotationFilters: object[]; initialOpenGuide: boolean; + keyMap: KeyMap; + jobInstance: Job; + ranges: string; changeWorkspace(workspace: Workspace): void; showStatistics(): void; showFilters(): void; @@ -57,6 +62,7 @@ interface Props { onBackward(): void; onFirstFrame(): void; onLastFrame(): void; + onSearchAnnotations(direction: 'forward' | 'backward'): void; setPrevButtonType(type: 'regular' | 'filtered' | 'empty'): void; setNextButtonType(type: 'regular' | 'filtered' | 'empty'): void; onSliderChange(value: number): void; @@ -69,8 +75,6 @@ interface Props { onDeleteFrame(): void; onRestoreFrame(): void; switchNavigationBlocked(blocked: boolean): void; - jobInstance: any; - ranges: string; } export default function AnnotationTopBarComponent(props: Props): JSX.Element { @@ -104,6 +108,9 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { toolsBlockerState, annotationFilters, initialOpenGuide, + deleteFrameAvailable, + jobInstance, + keyMap, showStatistics, showFilters, changeWorkspace, @@ -115,6 +122,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { onBackward, onFirstFrame, onLastFrame, + onSearchAnnotations, setPrevButtonType, setNextButtonType, onSliderChange, @@ -125,10 +133,8 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { onFinishDraw, onSwitchToolsBlockerState, onDeleteFrame, - deleteFrameAvailable, onRestoreFrame, switchNavigationBlocked, - jobInstance, } = props; const playerPlugins = usePlugins( @@ -154,6 +160,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { backwardShortcut={backwardShortcut} prevButtonType={prevButtonType} nextButtonType={nextButtonType} + keyMap={keyMap} onPrevFrame={onPrevFrame} onNextFrame={onNextFrame} onForward={onForward} @@ -161,6 +168,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { onFirstFrame={onFirstFrame} onLastFrame={onLastFrame} onSwitchPlay={onSwitchPlay} + onSearchAnnotations={onSearchAnnotations} setPrevButton={setPrevButtonType} setNextButton={setNextButtonType} /> @@ -179,6 +187,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { deleteFrameShortcut={deleteFrameShortcut} focusFrameInputShortcut={focusFrameInputShortcut} inputFrameRef={inputFrameRef} + keyMap={keyMap} onSliderChange={onSliderChange} onInputChange={onInputChange} onURLIconClick={onURLIconClick} @@ -207,6 +216,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { onRedoClick={onRedoClick} onFinishDraw={onFinishDraw} onSwitchToolsBlockerState={onSwitchToolsBlockerState} + keyMap={keyMap} /> 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 13f83be6ac6c..85a4b9c025f7 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 @@ -39,7 +39,7 @@ import { ToolsBlockerState, } from 'reducers'; import isAbleToChangeFrame from 'utils/is-able-to-change-frame'; -import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; +import { KeyMap } from 'utils/mousetrap-react'; import { switchToolsBlockerState } from 'actions/settings-actions'; import { writeLatestFrame } from 'utils/remember-latest-frame'; @@ -273,17 +273,17 @@ class AnnotationTopBarContainer extends React.PureComponent { } private undo = (): void => { - const { undo } = this.props; + const { undo, canvasIsReady, undoAction } = this.props; - if (isAbleToChangeFrame()) { + if (isAbleToChangeFrame() && canvasIsReady && undoAction) { undo(); } }; private redo = (): void => { - const { redo } = this.props; + const { redo, canvasIsReady, redoAction } = this.props; - if (isAbleToChangeFrame()) { + if (isAbleToChangeFrame() && canvasIsReady && redoAction) { redo(); } }; @@ -312,12 +312,13 @@ class AnnotationTopBarContainer extends React.PureComponent { private onFirstFrame = async (): Promise => { const { - frameNumber, jobInstance, playing, onSwitchPlay, showDeletedFrames, + frameNumber, jobInstance, playing, + onSwitchPlay, showDeletedFrames, canvasIsReady, } = this.props; const newFrame = await jobInstance.frames.search({ notDeleted: !showDeletedFrames }, jobInstance.startFrame, frameNumber); - if (newFrame !== frameNumber && newFrame !== null) { + if (newFrame !== frameNumber && newFrame !== null && canvasIsReady) { if (playing) { onSwitchPlay(false); } @@ -327,7 +328,8 @@ class AnnotationTopBarContainer extends React.PureComponent { private onBackward = async (): Promise => { const { - frameNumber, frameStep, jobInstance, playing, onSwitchPlay, showDeletedFrames, + frameNumber, frameStep, jobInstance, playing, + onSwitchPlay, showDeletedFrames, canvasIsReady, } = this.props; const newFrame = await jobInstance.frames.search( @@ -336,7 +338,7 @@ class AnnotationTopBarContainer extends React.PureComponent { jobInstance.startFrame, ); - if (newFrame !== frameNumber && newFrame !== null) { + if (newFrame !== frameNumber && newFrame !== null && canvasIsReady) { if (playing) { onSwitchPlay(false); } @@ -347,7 +349,8 @@ class AnnotationTopBarContainer extends React.PureComponent { private onPrevFrame = async (): Promise => { const { prevButtonType } = this.state; const { - frameNumber, jobInstance, playing, onSwitchPlay, showDeletedFrames, + frameNumber, jobInstance, playing, searchAnnotations, + onSwitchPlay, showDeletedFrames, canvasIsReady, } = this.props; const { startFrame } = jobInstance; @@ -358,7 +361,7 @@ class AnnotationTopBarContainer extends React.PureComponent { jobInstance.startFrame, ); - if (newFrame !== frameNumber && newFrame !== null) { + if (newFrame !== frameNumber && newFrame !== null && canvasIsReady && isAbleToChangeFrame()) { if (playing) { onSwitchPlay(false); } @@ -366,7 +369,7 @@ class AnnotationTopBarContainer extends React.PureComponent { if (prevButtonType === 'regular') { this.changeFrame(newFrame); } else if (prevButtonType === 'filtered') { - this.searchAnnotations(newFrame, startFrame); + searchAnnotations(jobInstance, newFrame, startFrame); } else { this.searchEmptyFrame(newFrame, startFrame); } @@ -376,7 +379,8 @@ class AnnotationTopBarContainer extends React.PureComponent { private onNextFrame = async (): Promise => { const { nextButtonType } = this.state; const { - frameNumber, jobInstance, playing, onSwitchPlay, showDeletedFrames, + frameNumber, jobInstance, playing, searchAnnotations, + onSwitchPlay, showDeletedFrames, canvasIsReady, } = this.props; const { stopFrame } = jobInstance; @@ -386,7 +390,7 @@ class AnnotationTopBarContainer extends React.PureComponent { frameFrom, jobInstance.stopFrame, ); - if (newFrame !== frameNumber && newFrame !== null) { + if (newFrame !== frameNumber && newFrame !== null && canvasIsReady && isAbleToChangeFrame()) { if (playing) { onSwitchPlay(false); } @@ -394,7 +398,7 @@ class AnnotationTopBarContainer extends React.PureComponent { if (nextButtonType === 'regular') { this.changeFrame(newFrame); } else if (nextButtonType === 'filtered') { - this.searchAnnotations(newFrame, stopFrame); + searchAnnotations(jobInstance, newFrame, stopFrame); } else { this.searchEmptyFrame(newFrame, stopFrame); } @@ -403,7 +407,8 @@ class AnnotationTopBarContainer extends React.PureComponent { private onForward = async (): Promise => { const { - frameNumber, frameStep, jobInstance, playing, onSwitchPlay, showDeletedFrames, + frameNumber, frameStep, jobInstance, playing, + onSwitchPlay, showDeletedFrames, canvasIsReady, } = this.props; const newFrame = await jobInstance.frames.search( @@ -412,7 +417,7 @@ class AnnotationTopBarContainer extends React.PureComponent { jobInstance.stopFrame, ); - if (newFrame !== frameNumber && newFrame !== null) { + if (newFrame !== frameNumber && newFrame !== null && canvasIsReady) { if (playing) { onSwitchPlay(false); } @@ -422,12 +427,13 @@ class AnnotationTopBarContainer extends React.PureComponent { private onLastFrame = async (): Promise => { const { - frameNumber, jobInstance, playing, onSwitchPlay, showDeletedFrames, + frameNumber, jobInstance, playing, + onSwitchPlay, showDeletedFrames, canvasIsReady, } = this.props; const newFrame = await jobInstance.frames.search({ notDeleted: !showDeletedFrames }, jobInstance.stopFrame, frameNumber); - if (newFrame !== frameNumber && frameNumber !== null) { + if (newFrame !== frameNumber && frameNumber !== null && canvasIsReady) { if (playing) { onSwitchPlay(false); } @@ -435,6 +441,22 @@ class AnnotationTopBarContainer extends React.PureComponent { } }; + private searchAnnotations = (direction: 'forward' | 'backward'): void => { + const { + frameNumber, jobInstance, + canvasIsReady, searchAnnotations, + } = this.props; + const { startFrame, stopFrame } = jobInstance; + + if (isAbleToChangeFrame() && canvasIsReady) { + if (direction === 'forward' && frameNumber + 1 <= stopFrame) { + searchAnnotations(jobInstance, frameNumber + 1, stopFrame); + } else if (direction === 'backward' && frameNumber - 1 >= startFrame) { + searchAnnotations(jobInstance, frameNumber - 1, startFrame); + } + } + }; + private onSetPreviousButtonType = (type: 'regular' | 'filtered' | 'empty'): void => { this.setState({ prevButtonType: type, @@ -526,8 +548,10 @@ class AnnotationTopBarContainer extends React.PureComponent { }; private onDeleteFrame = (): void => { - const { deleteFrame, frameNumber, jobInstance } = this.props; - if (jobInstance.type !== JobType.GROUND_TRUTH) deleteFrame(frameNumber); + const { + deleteFrame, frameNumber, jobInstance, canvasIsReady, + } = this.props; + if (canvasIsReady && jobInstance.type !== JobType.GROUND_TRUTH) deleteFrame(frameNumber); }; private onRestoreFrame = (): void => { @@ -630,13 +654,6 @@ class AnnotationTopBarContainer extends React.PureComponent { } } - private searchAnnotations(start: number, stop: number): void { - const { jobInstance, searchAnnotations } = this.props; - if (isAbleToChangeFrame()) { - searchAnnotations(jobInstance, start, stop); - } - } - private searchEmptyFrame(start: number, stop: number): void { const { jobInstance, searchEmptyFrame } = this.props; if (isAbleToChangeFrame()) { @@ -657,7 +674,6 @@ class AnnotationTopBarContainer extends React.PureComponent { undoAction, redoAction, workspace, - canvasIsReady, keyMap, ranges, normalizedKeyMap, @@ -665,158 +681,69 @@ class AnnotationTopBarContainer extends React.PureComponent { annotationFilters, initialOpenGuide, toolsBlockerState, - searchAnnotations, switchNavigationBlocked, } = this.props; - const preventDefault = (event: KeyboardEvent | undefined): void => { - if (event) { - event.preventDefault(); - } - }; - - const subKeyMap = { - UNDO: keyMap.UNDO, - REDO: keyMap.REDO, - DELETE_FRAME: keyMap.DELETE_FRAME, - NEXT_FRAME: keyMap.NEXT_FRAME, - PREV_FRAME: keyMap.PREV_FRAME, - FORWARD_FRAME: keyMap.FORWARD_FRAME, - BACKWARD_FRAME: keyMap.BACKWARD_FRAME, - SEARCH_FORWARD: keyMap.SEARCH_FORWARD, - SEARCH_BACKWARD: keyMap.SEARCH_BACKWARD, - PLAY_PAUSE: keyMap.PLAY_PAUSE, - FOCUS_INPUT_FRAME: keyMap.FOCUS_INPUT_FRAME, - }; - - const handlers = { - UNDO: (event: KeyboardEvent | undefined) => { - preventDefault(event); - if (undoAction) { - this.undo(); - } - }, - REDO: (event: KeyboardEvent | undefined) => { - preventDefault(event); - if (redoAction) { - this.redo(); - } - }, - DELETE_FRAME: (event: KeyboardEvent | undefined) => { - preventDefault(event); - if (canvasIsReady) { - this.onDeleteFrame(); - } - }, - NEXT_FRAME: (event: KeyboardEvent | undefined) => { - preventDefault(event); - if (canvasIsReady) { - this.onNextFrame(); - } - }, - PREV_FRAME: (event: KeyboardEvent | undefined) => { - preventDefault(event); - if (canvasIsReady) { - this.onPrevFrame(); - } - }, - FORWARD_FRAME: (event: KeyboardEvent | undefined) => { - preventDefault(event); - if (canvasIsReady) { - this.onForward(); - } - }, - BACKWARD_FRAME: (event: KeyboardEvent | undefined) => { - preventDefault(event); - if (canvasIsReady) { - this.onBackward(); - } - }, - SEARCH_FORWARD: (event: KeyboardEvent | undefined) => { - preventDefault(event); - if (frameNumber + 1 <= stopFrame && canvasIsReady && isAbleToChangeFrame()) { - searchAnnotations(jobInstance, frameNumber + 1, stopFrame); - } - }, - SEARCH_BACKWARD: (event: KeyboardEvent | undefined) => { - preventDefault(event); - if (frameNumber - 1 >= startFrame && canvasIsReady && isAbleToChangeFrame()) { - searchAnnotations(jobInstance, frameNumber - 1, startFrame); - } - }, - PLAY_PAUSE: (event: KeyboardEvent | undefined) => { - preventDefault(event); - this.onSwitchPlay(); - }, - FOCUS_INPUT_FRAME: (event: KeyboardEvent | undefined) => { - preventDefault(event); - if (this.inputFrameRef.current) { - this.inputFrameRef.current.focus(); - } - }, - }; - return ( - <> - - - + ); } } diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index ab340183f498..c9e90ef4fdc8 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -57,7 +57,9 @@ const defaultState: AnnotationState = { requestedId: null, groundTruthJobFramesMeta: null, groundTruthInstance: null, - queryParameters: {}, + queryParameters: { + initialOpenGuide: false, + }, instance: null, attributes: {}, fetching: false, diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index a1e54cca61eb..171b375b84a1 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -706,7 +706,7 @@ export interface AnnotationState { requestedId: number | null; instance: Job | null | undefined; queryParameters: { - initialOpenGuide?: boolean; + initialOpenGuide: boolean; }; groundTruthJobFramesMeta: FramesMetaData | null; groundTruthInstance: Job | null; From df2ffca49abfc58e854e9ff0f7a5d946550bc442 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 15 Feb 2024 12:02:47 +0200 Subject: [PATCH 07/43] Navigation added --- .../single-shape-sidebar.tsx | 189 ++++++++++++------ .../single-shape-workspace/styles.scss | 13 ++ 2 files changed, 143 insertions(+), 59 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 1a150ff4f06b..f8dd9fbfa712 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -2,8 +2,10 @@ // // SPDX-License-Identifier: MIT -import { useDispatch, useSelector } from 'react-redux'; -import React, { useEffect, useReducer } from 'react'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import React, { + useCallback, useEffect, useReducer, useRef, +} from 'react'; import Layout, { SiderProps } from 'antd/lib/layout'; import { Row, Col } from 'antd/lib/grid'; import Text from 'antd/lib/typography/Text'; @@ -20,6 +22,9 @@ import { changeFrameAsync, changeWorkspace, saveAnnotationsAsync } from 'actions import { Job, Label, LabelType } from 'cvat-core-wrapper'; import { ActionUnion, createAction } from 'utils/redux'; import LabelSelector from 'components/label-selector/label-selector'; +import { Button } from 'antd'; +import Icon from '@ant-design/icons/lib/components/Icon'; +import { NextIcon, PreviousIcon } from 'icons'; const getState = (): CombinedState => getCVATStore().getState(); @@ -30,6 +35,7 @@ enum ReducerActionType { SWITCH_NAVIGATE_EMPTY_ONLY = 'SWITCH_NAVIGATE_EMPTY_ONLY', SET_ACTIVE_LABEL = 'SET_ACTIVE_LABEL', SET_POINTS_COUNT = 'SET_POINTS_COUNT', + SET_FRAMES = 'SET_FRAMES', } export const reducerActions = { @@ -52,9 +58,10 @@ export const reducerActions = { }) ), setPointsCount: (pointsCount: number) => ( - createAction(ReducerActionType.SET_POINTS_COUNT, { - pointsCount, - }) + createAction(ReducerActionType.SET_POINTS_COUNT, { pointsCount }) + ), + setFrames: (frames: number[]) => ( + createAction(ReducerActionType.SET_FRAMES, { frames }) ), }; @@ -67,6 +74,7 @@ interface State { labels: Label[]; label: Label | null; labelType: LabelType; + frames: number[]; } const reducer = (state: State, action: ActionUnion): State => { @@ -125,6 +133,13 @@ const reducer = (state: State, action: ActionUnion): Stat }; } + if (action.type === ReducerActionType.SET_FRAMES) { + return { + ...state, + frames: action.payload.frames, + }; + } + return state; }; @@ -136,8 +151,13 @@ function cancelCurrentCanvasOp(canvas: Canvas): void { function SingleShapeSidebar(): JSX.Element { const appDispatch = useDispatch(); - const isCanvasReady = useSelector((_state: CombinedState) => _state.annotation.canvas.ready); - const jobInstance = useSelector((_state: CombinedState) => _state.annotation.job.instance); + const { + isCanvasReady, + jobInstance, + } = useSelector((_state: CombinedState) => ({ + isCanvasReady: _state.annotation.canvas.ready, + jobInstance: _state.annotation.job.instance, + }), shallowEqual); const [state, dispatch] = useReducer(reducer, { sidebarCollabased: false, @@ -148,80 +168,125 @@ function SingleShapeSidebar(): JSX.Element { labels: (jobInstance as Job).labels.filter((label) => label.type !== LabelType.TAG), label: (jobInstance as Job).labels[0] || null, labelType: (jobInstance as Job).labels[0]?.type || LabelType.ANY, + frames: [], }); - let message = ''; - if (state.labelType === LabelType.POINTS) { - message = `${state.pointsCount === 1 ? 'one point' : `${state.pointsCount} points`}`; - } else { - message = `${state.labelType === LabelType.ELLIPSE ? 'an ellipse' : `a ${state.labelType}`}`; - } + const nextFrame = useCallback((): boolean => { + const frame = getState().annotation.player.frame.number; + const next = state.frames.find((_frame) => _frame > frame) || null; + if (typeof next === 'number') { + appDispatch(changeFrameAsync(next)); + return true; + } - const siderProps: SiderProps = { - className: 'cvat-single-shape-annotation-sidebar', - theme: 'light', - width: 300, - collapsedWidth: 0, - reverseArrow: true, - collapsible: true, - trigger: null, - collapsed: state.sidebarCollabased, - }; + return false; + }, [state.frames]); - const runDrawing = (): void => { - const canvas = getState().annotation.canvas.instance; - canvas?.draw({ - enabled: true, - shapeType: state.labelType, - numberOfPoints: state.pointsCount, - crosshair: true, - }); + const prevFrame = useCallback((): boolean => { + const frame = getState().annotation.player.frame.number; + const prev = state.frames.findLast((_frame) => _frame < frame) || null; + if (typeof prev === 'number') { + appDispatch(changeFrameAsync(prev)); + return true; + } + + return false; + }, [state.frames]); + + const canvasInitializerRef = useRef<() => void | null>(); + canvasInitializerRef.current = (): void => { + const canvas = getState().annotation.canvas.instance as Canvas; + cancelCurrentCanvasOp(canvas); + + if (isCanvasReady && state.label && state.labelType !== LabelType.ANY) { + canvas.draw({ + enabled: true, + shapeType: state.labelType, + numberOfPoints: state.pointsCount, + crosshair: true, + }); + } }; useEffect(() => { - cancelCurrentCanvasOp( - getState().annotation.canvas.instance as Canvas, - ); + const canvas = getState().annotation.canvas.instance as Canvas; + cancelCurrentCanvasOp(canvas); + return () => { + cancelCurrentCanvasOp(canvas); + }; + }, []); - if (isCanvasReady && state.label && state.labelType !== LabelType.ANY) { - runDrawing(); + useEffect(() => { + if (canvasInitializerRef.current) { + canvasInitializerRef?.current(); } }, [isCanvasReady, state.label, state.labelType, state.pointsCount]); + useEffect(() => { + (async () => { + const job = jobInstance as Job; + const framesToBeVisited = []; + + let frame = job.startFrame; + while (frame !== null) { + const foundFrame = await job.frames + .search({ notDeleted: true }, frame, job.stopFrame); + if (foundFrame !== null) { + framesToBeVisited.push(foundFrame); + frame = foundFrame !== job.stopFrame ? foundFrame + 1 : null; + if (foundFrame !== job.stopFrame) { + frame = foundFrame + 1; + } + } + } + + dispatch(reducerActions.setFrames(framesToBeVisited)); + if (framesToBeVisited.length) { + appDispatch(changeFrameAsync(framesToBeVisited[0])); + } + })(); + }, [state.navigateOnlyEmpty]); + useEffect(() => { const canvas = getState().annotation.canvas.instance as Canvas; const onDrawDone = (): void => { - const { - annotation: { - player: { - frame: { - number: frame, - }, - }, - }, - } = getState(); - - const { stopFrame } = jobInstance as Job; - if (frame < stopFrame) { + setTimeout(() => { if (state.autoNextFrame) { - appDispatch(changeFrameAsync(frame + 1)); - } - } else { - appDispatch(changeWorkspace(Workspace.STANDARD)); - if (state.saveOnFinish) { - appDispatch(saveAnnotationsAsync()); + if (!nextFrame()) { + appDispatch(changeWorkspace(Workspace.STANDARD)); + if (state.saveOnFinish) { + appDispatch(saveAnnotationsAsync()); + } + } + } else if (canvasInitializerRef?.current) { + canvasInitializerRef.current(); } - } + }, 100); }; - cancelCurrentCanvasOp(canvas); (canvas as Canvas).html().addEventListener('canvas.drawn', onDrawDone); - return (() => { - cancelCurrentCanvasOp(canvas); (canvas as Canvas).html().removeEventListener('canvas.drawn', onDrawDone); }); - }, []); + }, [nextFrame, state.autoNextFrame, state.saveOnFinish]); + + let message = ''; + if (state.labelType === LabelType.POINTS) { + message = `${state.pointsCount === 1 ? 'one point' : `${state.pointsCount} points`}`; + } else { + message = `${state.labelType === LabelType.ELLIPSE ? 'an ellipse' : `a ${state.labelType}`}`; + } + + const siderProps: SiderProps = { + className: 'cvat-single-shape-annotation-sidebar', + theme: 'light', + width: 300, + collapsedWidth: 0, + reverseArrow: true, + collapsible: true, + trigger: null, + collapsed: state.sidebarCollabased, + }; return ( @@ -337,6 +402,12 @@ function SingleShapeSidebar(): JSX.Element { + + + + + + { state.label !== null && state.labelType !== LabelType.ANY && ( diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss index dcaaae3f421a..42c7608f3061 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss @@ -38,4 +38,17 @@ bottom: 0; position: absolute; } + + .cvat-single-shape-annotation-sidebar-navigation-block { + margin-top: $grid-unit-size * 4; + + > div:nth-child(1) { + display: flex; + justify-content: space-between; + + > button { + width: $grid-unit-size * 16; + } + } + } } From 5bdbb8ce3dd15c6ff419e76044536af2a8ddf077 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 15 Feb 2024 13:09:27 +0200 Subject: [PATCH 08/43] Minor refactoring --- .../single-shape-sidebar.tsx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index f8dd9fbfa712..efb0522020ea 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -2,7 +2,9 @@ // // SPDX-License-Identifier: MIT -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { + shallowEqual, useDispatch, useSelector, useStore, +} from 'react-redux'; import React, { useCallback, useEffect, useReducer, useRef, } from 'react'; @@ -13,20 +15,17 @@ import Checkbox from 'antd/lib/checkbox'; import InputNumber from 'antd/lib/input-number'; import Select from 'antd/lib/select'; import Paragraph from 'antd/lib/typography/Paragraph'; - +import Button from 'antd/lib/button'; import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; -import { getCVATStore } from 'cvat-store'; -import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; +import Icon from '@ant-design/icons/lib/components/Icon'; + +import { NextIcon, PreviousIcon } from 'icons'; import { CombinedState, Workspace } from 'reducers'; -import { changeFrameAsync, changeWorkspace, saveAnnotationsAsync } from 'actions/annotation-actions'; +import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; import { Job, Label, LabelType } from 'cvat-core-wrapper'; import { ActionUnion, createAction } from 'utils/redux'; +import { changeFrameAsync, changeWorkspace, saveAnnotationsAsync } from 'actions/annotation-actions'; import LabelSelector from 'components/label-selector/label-selector'; -import { Button } from 'antd'; -import Icon from '@ant-design/icons/lib/components/Icon'; -import { NextIcon, PreviousIcon } from 'icons'; - -const getState = (): CombinedState => getCVATStore().getState(); enum ReducerActionType { SWITCH_SIDEBAR_COLLAPSED = 'SWITCH_SIDEBAR_COLLAPSED', @@ -151,6 +150,7 @@ function cancelCurrentCanvasOp(canvas: Canvas): void { function SingleShapeSidebar(): JSX.Element { const appDispatch = useDispatch(); + const store = useStore(); const { isCanvasReady, jobInstance, @@ -172,7 +172,7 @@ function SingleShapeSidebar(): JSX.Element { }); const nextFrame = useCallback((): boolean => { - const frame = getState().annotation.player.frame.number; + const frame = store.getState().annotation.player.frame.number; const next = state.frames.find((_frame) => _frame > frame) || null; if (typeof next === 'number') { appDispatch(changeFrameAsync(next)); @@ -183,7 +183,7 @@ function SingleShapeSidebar(): JSX.Element { }, [state.frames]); const prevFrame = useCallback((): boolean => { - const frame = getState().annotation.player.frame.number; + const frame = store.getState().annotation.player.frame.number; const prev = state.frames.findLast((_frame) => _frame < frame) || null; if (typeof prev === 'number') { appDispatch(changeFrameAsync(prev)); @@ -195,7 +195,7 @@ function SingleShapeSidebar(): JSX.Element { const canvasInitializerRef = useRef<() => void | null>(); canvasInitializerRef.current = (): void => { - const canvas = getState().annotation.canvas.instance as Canvas; + const canvas = store.getState().annotation.canvas.instance as Canvas; cancelCurrentCanvasOp(canvas); if (isCanvasReady && state.label && state.labelType !== LabelType.ANY) { @@ -209,7 +209,7 @@ function SingleShapeSidebar(): JSX.Element { }; useEffect(() => { - const canvas = getState().annotation.canvas.instance as Canvas; + const canvas = store.getState().annotation.canvas.instance as Canvas; cancelCurrentCanvasOp(canvas); return () => { cancelCurrentCanvasOp(canvas); @@ -248,7 +248,7 @@ function SingleShapeSidebar(): JSX.Element { }, [state.navigateOnlyEmpty]); useEffect(() => { - const canvas = getState().annotation.canvas.instance as Canvas; + const canvas = store.getState().annotation.canvas.instance as Canvas; const onDrawDone = (): void => { setTimeout(() => { if (state.autoNextFrame) { From 76fe91448390ed8b2c9632be56f3f06d6e17c3a5 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 15 Feb 2024 14:46:53 +0200 Subject: [PATCH 09/43] Some code improvements --- cvat-ui/src/actions/annotation-actions.ts | 5 ++- .../single-shape-sidebar.tsx | 37 ++++++++++++------- cvat-ui/src/reducers/annotation-reducer.ts | 2 + 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 458a4c5353fa..fd0da5370354 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -704,7 +704,9 @@ export function changeFrameAsync( Math.round(1000 / frameSpeed) - currentTime + (state.annotation.player.frame.changeTime as number), ); - const { states, maxZ, minZ } = await fetchAnnotations(toFrame); + const { + states, maxZ, minZ, history, + } = await fetchAnnotations(toFrame); dispatch({ type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS, payload: { @@ -713,6 +715,7 @@ export function changeFrameAsync( filename: data.filename, relatedFiles: data.relatedFiles, states, + history, minZ, maxZ, curZ: maxZ, diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index efb0522020ea..70d20fea5024 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -154,9 +154,11 @@ function SingleShapeSidebar(): JSX.Element { const { isCanvasReady, jobInstance, + frame, } = useSelector((_state: CombinedState) => ({ isCanvasReady: _state.annotation.canvas.ready, jobInstance: _state.annotation.job.instance, + frame: _state.annotation.player.frame.number, }), shallowEqual); const [state, dispatch] = useReducer(reducer, { @@ -172,7 +174,6 @@ function SingleShapeSidebar(): JSX.Element { }); const nextFrame = useCallback((): boolean => { - const frame = store.getState().annotation.player.frame.number; const next = state.frames.find((_frame) => _frame > frame) || null; if (typeof next === 'number') { appDispatch(changeFrameAsync(next)); @@ -180,10 +181,9 @@ function SingleShapeSidebar(): JSX.Element { } return false; - }, [state.frames]); + }, [state.frames, frame]); const prevFrame = useCallback((): boolean => { - const frame = store.getState().annotation.player.frame.number; const prev = state.frames.findLast((_frame) => _frame < frame) || null; if (typeof prev === 'number') { appDispatch(changeFrameAsync(prev)); @@ -191,7 +191,7 @@ function SingleShapeSidebar(): JSX.Element { } return false; - }, [state.frames]); + }, [state.frames, frame]); const canvasInitializerRef = useRef<() => void | null>(); canvasInitializerRef.current = (): void => { @@ -227,16 +227,13 @@ function SingleShapeSidebar(): JSX.Element { const job = jobInstance as Job; const framesToBeVisited = []; - let frame = job.startFrame; - while (frame !== null) { + let searchFrom = job.startFrame; + while (searchFrom !== null) { const foundFrame = await job.frames - .search({ notDeleted: true }, frame, job.stopFrame); + .search({ notDeleted: true }, searchFrom, job.stopFrame); if (foundFrame !== null) { framesToBeVisited.push(foundFrame); - frame = foundFrame !== job.stopFrame ? foundFrame + 1 : null; - if (foundFrame !== job.stopFrame) { - frame = foundFrame + 1; - } + searchFrom = foundFrame < job.stopFrame ? foundFrame + 1 : null; } } @@ -404,8 +401,22 @@ function SingleShapeSidebar(): JSX.Element { - - + + { state.label !== null && state.labelType !== LabelType.ANY && ( diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index c9e90ef4fdc8..ff41b50e1503 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -291,6 +291,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { filename, relatedFiles, states, + history, minZ, maxZ, curZ, @@ -319,6 +320,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { activatedStateID: updateActivatedStateID(states, activatedStateID), highlightedConflict: null, states, + history, zLayer: { min: minZ, max: maxZ, From b7af7932d0e04f0aace6a0f7523fe9b7353f1c78 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Sun, 18 Feb 2024 17:09:00 +0200 Subject: [PATCH 10/43] Simplified core api to search empty frames --- cvat-core/src/annotations-collection.ts | 35 +++++++++- cvat-core/src/session-implementation.ts | 42 ++---------- cvat-core/src/session.ts | 15 +---- cvat-ui/src/actions/annotation-actions.ts | 64 ++++--------------- .../single-shape-sidebar.tsx | 7 +- .../annotation-page/top-bar/top-bar.tsx | 22 ++----- cvat-ui/src/reducers/index.ts | 1 - cvat-ui/src/reducers/notifications-reducer.ts | 17 ----- 8 files changed, 62 insertions(+), 141 deletions(-) diff --git a/cvat-core/src/annotations-collection.ts b/cvat-core/src/annotations-collection.ts index 86221e2bbefa..5d38268bd133 100644 --- a/cvat-core/src/annotations-collection.ts +++ b/cvat-core/src/annotations-collection.ts @@ -1208,17 +1208,30 @@ export default class Collection { }; } - searchEmpty(frameFrom: number, frameTo: number): number | null { + _searchEmpty( + frameFrom: number, + frameTo: number, + searchParameters: { + allowDeletedFrames: boolean, + }, + ): number | null { + const { allowDeletedFrames } = searchParameters; const sign = Math.sign(frameTo - frameFrom); 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) { + continue; + } + if (frame in this.shapes && this.shapes[frame].some((shape) => !shape.removed)) { continue; } + if (frame in this.tags && this.tags[frame].some((tag) => !tag.removed)) { continue; } + const filteredTracks = this.tracks.filter((track) => !track.removed); let found = false; for (const track of filteredTracks) { @@ -1241,14 +1254,32 @@ export default class Collection { return null; } - search(filters: string[], frameFrom: number, frameTo: number): number | null { + search( + filters: object[], + frameFrom: number, + frameTo: number, + searchParameters: { + allowDeletedFrames: boolean, + }, + ): number | null { + const { allowDeletedFrames } = searchParameters; const sign = Math.sign(frameTo - frameFrom); const filtersStr = JSON.stringify(filters); const linearSearch = filtersStr.match(/"var":"width"/) || filtersStr.match(/"var":"height"/); + // handle special case when we need an empty frame + const emptyFrameFilter = '[{"and":[{"==":[{"var":"isEmptyFrame"},true]}]}]'; + if (filters.length === 1 && filtersStr === emptyFrameFilter) { + return this._searchEmpty(frameFrom, frameTo, searchParameters); + } + 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) { + continue; + } + // 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 tracks diff --git a/cvat-core/src/session-implementation.ts b/cvat-core/src/session-implementation.ts index 71230a170776..7194bd8c6a0d 100644 --- a/cvat-core/src/session-implementation.ts +++ b/cvat-core/src/session-implementation.ts @@ -231,7 +231,7 @@ export function implementJob(Job) { return annotationsData; }; - Job.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) { + Job.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo, searchParameters) { if (!Array.isArray(filters)) { throw new ArgumentError('Filters must be an array'); } @@ -248,23 +248,7 @@ export function implementJob(Job) { throw new ArgumentError('The stop frame is out of the job'); } - return getCollection(this).search(filters, frameFrom, frameTo); - }; - - Job.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) { - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } - - if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { - throw new ArgumentError('The start frame is out of the job'); - } - - if (frameTo < this.startFrame || frameTo > this.stopFrame) { - throw new ArgumentError('The stop frame is out of the job'); - } - - return getCollection(this).searchEmpty(frameFrom, frameTo); + return getCollection(this).search(filters, frameFrom, frameTo, searchParameters); }; Job.prototype.annotations.save.implementation = async function (onUpdate) { @@ -682,7 +666,7 @@ export function implementTask(Task) { throw new ArgumentError(`Frame ${frame} does not exist in the task`); } - const result = await getAnnotations(this, frame, allTracks, filters, null); + const result = await getAnnotations(this, frame, allTracks, filters); const deletedFrames = await getDeletedFrames('task', this.id); if (frame in deletedFrames) { return []; @@ -691,7 +675,7 @@ export function implementTask(Task) { return result; }; - Task.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) { + Task.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo, searchParameters) { if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) { throw new ArgumentError('The filters argument must be an array of strings'); } @@ -708,23 +692,7 @@ export function implementTask(Task) { throw new ArgumentError('The stop frame is out of the task'); } - return getCollection(this).search(filters, frameFrom, frameTo); - }; - - Task.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) { - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } - - if (frameFrom < 0 || frameFrom >= this.size) { - throw new ArgumentError('The start frame is out of the task'); - } - - if (frameTo < 0 || frameTo >= this.size) { - throw new ArgumentError('The stop frame is out of the task'); - } - - return getCollection(this).searchEmpty(frameFrom, frameTo); + return getCollection(this).search(filters, frameFrom, frameTo, searchParameters); }; Task.prototype.annotations.save.implementation = async function (onUpdate) { diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index aa62950ee398..ca85247bdc7c 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -82,23 +82,14 @@ function buildDuplicatedAPI(prototype) { return result; }, - async search(filters, frameFrom, frameTo) { + async search(filters, frameFrom, frameTo, searchParameters) { const result = await PluginRegistry.apiWrapper.call( this, prototype.annotations.search, filters, frameFrom, frameTo, - ); - return result; - }, - - async searchEmpty(frameFrom, frameTo) { - const result = await PluginRegistry.apiWrapper.call( - this, - prototype.annotations.searchEmpty, - frameFrom, - frameTo, + searchParameters, ); return result; }, @@ -324,7 +315,6 @@ export class Session { slice: CallableFunction; clear: CallableFunction; search: CallableFunction; - searchEmpty: CallableFunction; upload: CallableFunction; select: CallableFunction; import: CallableFunction; @@ -377,7 +367,6 @@ export class Session { slice: Object.getPrototypeOf(this).annotations.slice.bind(this), clear: Object.getPrototypeOf(this).annotations.clear.bind(this), search: Object.getPrototypeOf(this).annotations.search.bind(this), - searchEmpty: Object.getPrototypeOf(this).annotations.searchEmpty.bind(this), upload: Object.getPrototypeOf(this).annotations.upload.bind(this), select: Object.getPrototypeOf(this).annotations.select.bind(this), import: Object.getPrototypeOf(this).annotations.import.bind(this), diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index fd0da5370354..5a27b6fadf2a 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -175,7 +175,6 @@ export enum AnnotationActionTypes { SWITCH_Z_LAYER = 'SWITCH_Z_LAYER', ADD_Z_LAYER = 'ADD_Z_LAYER', SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED', - SEARCH_EMPTY_FRAME_FAILED = 'SEARCH_EMPTY_FRAME_FAILED', CHANGE_WORKSPACE = 'CHANGE_WORKSPACE', SAVE_LOGS_SUCCESS = 'SAVE_LOGS_SUCCESS', SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED', @@ -1243,7 +1242,12 @@ export function changeGroupColorAsync(group: number, color: string): ThunkAction }; } -export function searchAnnotationsAsync(sessionInstance: NonNullable, frameFrom: number, frameTo: number): ThunkAction { +export function searchAnnotationsAsync( + sessionInstance: NonNullable, + frameFrom: number, + frameTo: number, + filters?: object[], +): ThunkAction { return async (dispatch: ActionCreator, getState): Promise => { try { const { @@ -1251,22 +1255,17 @@ export function searchAnnotationsAsync(sessionInstance: NonNullable 0 ? frame < frameTo : frame > frameTo) { - frame = await sessionInstance.annotations.search(filters, frame + sign, frameTo); - } else { - frame = null; - } - } + const frame = await sessionInstance.annotations + .search( + filters || setupFilters, + frameFrom, + frameTo, + { allowDeletedFrames: showDeletedFrames }, + ); if (frame !== null) { dispatch(changeFrameAsync(frame)); } @@ -1281,41 +1280,6 @@ export function searchAnnotationsAsync(sessionInstance: NonNullable, frameFrom: number, frameTo: number): ThunkAction { - return async (dispatch: ActionCreator, getState): Promise => { - try { - const { - settings: { - player: { showDeletedFrames }, - }, - } = getState(); - - const sign = Math.sign(frameTo - frameFrom); - let frame = await sessionInstance.annotations.searchEmpty(frameFrom, frameTo); - while (frame !== null) { - const isDeleted = (await sessionInstance.frames.get(frame)).deleted; - if (!isDeleted || showDeletedFrames) { - break; - } else if (sign > 0 ? frame < frameTo : frame > frameTo) { - frame = await sessionInstance.annotations.searchEmpty(frame + sign, frameTo); - } else { - frame = null; - } - } - if (frame !== null) { - dispatch(changeFrameAsync(frame)); - } - } catch (error) { - dispatch({ - type: AnnotationActionTypes.SEARCH_EMPTY_FRAME_FAILED, - payload: { - error, - }, - }); - } - }; -} - const ShapeTypeToControl: Record = { [ShapeType.RECTANGLE]: ActiveControl.DRAW_RECTANGLE, [ShapeType.POLYLINE]: ActiveControl.DRAW_POLYLINE, diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 70d20fea5024..e99212b7d935 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -174,7 +174,7 @@ function SingleShapeSidebar(): JSX.Element { }); const nextFrame = useCallback((): boolean => { - const next = state.frames.find((_frame) => _frame > frame) || null; + const next = state.frames.find((_frame) => _frame > frame); if (typeof next === 'number') { appDispatch(changeFrameAsync(next)); return true; @@ -184,7 +184,7 @@ function SingleShapeSidebar(): JSX.Element { }, [state.frames, frame]); const prevFrame = useCallback((): boolean => { - const prev = state.frames.findLast((_frame) => _frame < frame) || null; + const prev = state.frames.find((_frame) => _frame < frame); if (typeof prev === 'number') { appDispatch(changeFrameAsync(prev)); return true; @@ -229,8 +229,7 @@ function SingleShapeSidebar(): JSX.Element { let searchFrom = job.startFrame; while (searchFrom !== null) { - const foundFrame = await job.frames - .search({ notDeleted: true }, searchFrom, job.stopFrame); + const foundFrame = await job.annotations.search(searchFrom, job.stopFrame, [{ and: [{ '==': [{ var: 'isEmptyFrame' }, true] }] }]); if (foundFrame !== null) { framesToBeVisited.push(foundFrame); searchFrom = foundFrame < job.stopFrame ? foundFrame + 1 : null; 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 85a4b9c025f7..aa2b2dc047c9 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 @@ -17,7 +17,6 @@ import { redoActionAsync, saveAnnotationsAsync, searchAnnotationsAsync, - searchEmptyFrameAsync, setForceExitAnnotationFlag as setForceExitAnnotationFlagAction, showFilters as showFiltersAction, showStatistics as showStatisticsAction, @@ -80,8 +79,7 @@ interface DispatchToProps { showFilters(sessionInstance: any): void; undo(): void; redo(): void; - searchAnnotations(sessionInstance: any, frameFrom: number, frameTo: number): void; - searchEmptyFrame(sessionInstance: any, frameFrom: number, frameTo: number): void; + searchAnnotations(sessionInstance: any, frameFrom: number, frameTo: number, filters?: object[]): void; setForceExitAnnotationFlag(forceExit: boolean): void; changeWorkspace(workspace: Workspace): void; onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState): void; @@ -178,11 +176,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { redo(): void { dispatch(redoActionAsync()); }, - searchAnnotations(sessionInstance: any, frameFrom: number, frameTo: number): void { - dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo)); - }, - searchEmptyFrame(sessionInstance: any, frameFrom: number, frameTo: number): void { - dispatch(searchEmptyFrameAsync(sessionInstance, frameFrom, frameTo)); + searchAnnotations(sessionInstance: any, frameFrom: number, frameTo: number, filters?: object[]): void { + dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo, filters)); }, changeWorkspace(workspace: Workspace): void { dispatch(changeWorkspaceAction(workspace)); @@ -371,7 +366,7 @@ class AnnotationTopBarContainer extends React.PureComponent { } else if (prevButtonType === 'filtered') { searchAnnotations(jobInstance, newFrame, startFrame); } else { - this.searchEmptyFrame(newFrame, startFrame); + searchAnnotations(jobInstance, newFrame, startFrame, [{ and: [{ '==': [{ var: 'isEmptyFrame' }, true] }] }]); } } }; @@ -400,7 +395,7 @@ class AnnotationTopBarContainer extends React.PureComponent { } else if (nextButtonType === 'filtered') { searchAnnotations(jobInstance, newFrame, stopFrame); } else { - this.searchEmptyFrame(newFrame, stopFrame); + searchAnnotations(jobInstance, newFrame, stopFrame, [{ and: [{ '==': [{ var: 'isEmptyFrame' }, true] }] }]); } } }; @@ -654,13 +649,6 @@ class AnnotationTopBarContainer extends React.PureComponent { } } - private searchEmptyFrame(start: number, stop: number): void { - const { jobInstance, searchEmptyFrame } = this.props; - if (isAbleToChangeFrame()) { - searchEmptyFrame(jobInstance, start, stop); - } - } - public render(): JSX.Element { const { nextButtonType, prevButtonType } = this.state; const { diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index 171b375b84a1..8853bb7a62b9 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -513,7 +513,6 @@ export interface NotificationsState { undo: null | ErrorState; redo: null | ErrorState; search: null | ErrorState; - searchEmptyFrame: null | ErrorState; deleteFrame: null | ErrorState; restoreFrame: null | ErrorState; savingLogs: null | ErrorState; diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index 16a6353bebea..20b40d159c37 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -109,7 +109,6 @@ const defaultState: NotificationsState = { undo: null, redo: null, search: null, - searchEmptyFrame: null, deleteFrame: null, restoreFrame: null, savingLogs: null, @@ -1196,22 +1195,6 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } - case AnnotationActionTypes.SEARCH_EMPTY_FRAME_FAILED: { - return { - ...state, - errors: { - ...state.errors, - annotation: { - ...state.errors.annotation, - searchEmptyFrame: { - message: 'Could not search an empty frame', - reason: action.payload.error, - shouldLog: !(action.payload.error instanceof ServerError), - }, - }, - }, - }; - } case AnnotationActionTypes.SAVE_LOGS_FAILED: { return { ...state, From a05ec82567a7020f81d9ded95cae741c82cc64de Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Sun, 18 Feb 2024 21:23:15 +0200 Subject: [PATCH 11/43] Many refactoring --- cvat-core/src/annotations-collection.ts | 53 +++++--- cvat-core/src/annotations-filter.ts | 2 +- cvat-core/src/session-implementation.ts | 32 +++-- cvat-core/src/session.ts | 3 +- cvat-core/tests/api/annotations.cjs | 22 ++-- cvat-ui/src/actions/annotation-actions.ts | 12 +- cvat-ui/src/base.scss | 1 + .../single-shape-sidebar.tsx | 113 ++++++++++++++---- .../single-shape-workspace/styles.scss | 6 +- .../annotation-page/top-bar/top-bar.tsx | 34 ++++-- 10 files changed, 197 insertions(+), 81 deletions(-) diff --git a/cvat-core/src/annotations-collection.ts b/cvat-core/src/annotations-collection.ts index 5d38268bd133..ed380838fa10 100644 --- a/cvat-core/src/annotations-collection.ts +++ b/cvat-core/src/annotations-collection.ts @@ -1212,7 +1212,7 @@ export default class Collection { frameFrom: number, frameTo: number, searchParameters: { - allowDeletedFrames: boolean, + allowDeletedFrames: boolean; }, ): number | null { const { allowDeletedFrames } = searchParameters; @@ -1255,26 +1255,52 @@ export default class Collection { } search( - filters: object[], frameFrom: number, frameTo: number, searchParameters: { - allowDeletedFrames: boolean, + allowDeletedFrames: boolean; + annotationsFilters?: object[]; + generalFilters?: { + isEmptyFrame?: boolean; + }; }, ): number | null { const { allowDeletedFrames } = searchParameters; - const sign = Math.sign(frameTo - frameFrom); - const filtersStr = JSON.stringify(filters); - const linearSearch = filtersStr.match(/"var":"width"/) || filtersStr.match(/"var":"height"/); + let { annotationsFilters } = searchParameters; + + if ('generalFilters' in searchParameters) { + // if we are looking for en empty frame, run a dedicated algorithm + if (searchParameters.generalFilters.isEmptyFrame) { + return this._searchEmpty(frameFrom, frameTo, { allowDeletedFrames }); + } - // handle special case when we need an empty frame - const emptyFrameFilter = '[{"and":[{"==":[{"var":"isEmptyFrame"},true]}]}]'; - if (filters.length === 1 && filtersStr === emptyFrameFilter) { - return this._searchEmpty(frameFrom, frameTo, searchParameters); + // not empty frames corresponds to default behaviour of the function with empty annotation filters + annotationsFilters = []; } + const sign = Math.sign(frameTo - frameFrom); const predicate = sign > 0 ? (frame) => frame <= frameTo : (frame) => frame >= frameTo; const update = sign > 0 ? (frame) => frame + 1 : (frame) => frame - 1; + + // if not looking for an emty frame nor frame with annotations, return the next frame + // check if deleted frames are allowed additionally + if (!annotationsFilters) { + let frame = frameFrom; + while (predicate(frame)) { + if (!allowDeletedFrames && this.frameMeta[frame].deleted) { + frame = update(frame); + continue; + } + + return frame; + } + + return null; + } + + const filtersStr = JSON.stringify(annotationsFilters); + 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) { continue; @@ -1298,13 +1324,8 @@ export default class Collection { .filter((track) => !track.removed); statesData.push(...tracks.map((track) => track.get(frame)).filter((state) => !state.outside)); - // Nothing to filtering, go to the next iteration - if (!statesData.length) { - continue; - } - // Filtering - const filtered = this.annotationsFilter.filter(statesData, filters); + const filtered = this.annotationsFilter.filter(statesData, annotationsFilters); if (filtered.length) { return frame; } diff --git a/cvat-core/src/annotations-filter.ts b/cvat-core/src/annotations-filter.ts index c47c15ff334b..2cb715fe33ef 100644 --- a/cvat-core/src/annotations-filter.ts +++ b/cvat-core/src/annotations-filter.ts @@ -89,7 +89,7 @@ export default class AnnotationsFilter { return objects; } - filter(statesData: SerializedData[], filters: string[]): number[] { + filter(statesData: SerializedData[], filters: object[]): number[] { if (!filters.length) return statesData.map((stateData): number => stateData.clientID); const converted = this._convertObjects(statesData); return converted diff --git a/cvat-core/src/session-implementation.ts b/cvat-core/src/session-implementation.ts index 7194bd8c6a0d..975805afb115 100644 --- a/cvat-core/src/session-implementation.ts +++ b/cvat-core/src/session-implementation.ts @@ -231,9 +231,17 @@ export function implementJob(Job) { return annotationsData; }; - Job.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo, searchParameters) { - if (!Array.isArray(filters)) { - throw new ArgumentError('Filters must be an array'); + Job.prototype.annotations.search.implementation = function (frameFrom, frameTo, searchParameters) { + if ('annotationsFilters' in searchParameters && !Array.isArray(searchParameters.annotationsFilters)) { + throw new ArgumentError('Annotations filters must be an array'); + } + + if ('generalFilters' in searchParameters && typeof searchParameters.generalFilters.isEmptyFrame !== 'boolean') { + throw new ArgumentError('General filter isEmptyFrame must be a boolean'); + } + + if ('annotationsFilters' in searchParameters && 'generalFilters' in searchParameters) { + throw new ArgumentError('Both annotations filters and general fiters could not be used together'); } if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { @@ -248,7 +256,7 @@ export function implementJob(Job) { throw new ArgumentError('The stop frame is out of the job'); } - return getCollection(this).search(filters, frameFrom, frameTo, searchParameters); + return getCollection(this).search(frameFrom, frameTo, searchParameters); }; Job.prototype.annotations.save.implementation = async function (onUpdate) { @@ -675,9 +683,17 @@ export function implementTask(Task) { return result; }; - Task.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo, searchParameters) { - if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) { - throw new ArgumentError('The filters argument must be an array of strings'); + Task.prototype.annotations.search.implementation = function (frameFrom, frameTo, searchParameters) { + if ('annotationsFilters' in searchParameters && !Array.isArray(searchParameters.annotationsFilters)) { + throw new ArgumentError('Annotations filters must be an array'); + } + + if ('generalFilters' in searchParameters && typeof searchParameters.generalFilters.isEmptyFrame !== 'boolean') { + throw new ArgumentError('General filter isEmptyFrame must be a boolean'); + } + + if ('annotationsFilters' in searchParameters && 'generalFilters' in searchParameters) { + throw new ArgumentError('Both annotations filters and general fiters could not be used together'); } if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { @@ -692,7 +708,7 @@ export function implementTask(Task) { throw new ArgumentError('The stop frame is out of the task'); } - return getCollection(this).search(filters, frameFrom, frameTo, searchParameters); + return getCollection(this).search(frameFrom, frameTo, searchParameters); }; Task.prototype.annotations.save.implementation = async function (onUpdate) { diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index ca85247bdc7c..54d87cc9b24c 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -82,11 +82,10 @@ function buildDuplicatedAPI(prototype) { return result; }, - async search(filters, frameFrom, frameTo, searchParameters) { + async search(frameFrom, frameTo, searchParameters) { const result = await PluginRegistry.apiWrapper.call( this, prototype.annotations.search, - filters, frameFrom, frameTo, searchParameters, diff --git a/cvat-core/tests/api/annotations.cjs b/cvat-core/tests/api/annotations.cjs index fc3e9d00fea8..488451e3d4ba 100644 --- a/cvat-core/tests/api/annotations.cjs +++ b/cvat-core/tests/api/annotations.cjs @@ -956,29 +956,29 @@ describe('Feature: search frame', () => { test('applying different filters', async () => { const job = (await 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); + let frame = await job.annotations.search(495, 994, { annotationsFilters: JSON.parse('[{"and":[{"==":[{"var":"type"},"tag"]}]}]') }); expect(frame).toBe(500); - frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"tag"]},{"==":[{"var":"label"},"bicycle"]}]}]'), 495, 994); + frame = await job.annotations.search(495, 994, { annotationsFilters: JSON.parse('[{"and":[{"==":[{"var":"type"},"tag"]},{"==":[{"var":"label"},"bicycle"]}]}]') }); expect(frame).toBe(500); - frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"track"]},{"==":[{"var":"label"},"bicycle"]}]}]'), 495, 994); + frame = await job.annotations.search(495, 994, { annotationsFilters: JSON.parse('[{"and":[{"==":[{"var":"type"},"track"]},{"==":[{"var":"label"},"bicycle"]}]}]') }); expect(frame).toBe(null); - frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"shape"]},{"==":[{"var":"shape"},"rectangle"]}]}]'), 495, 994); + frame = await job.annotations.search(495, 994, { annotationsFilters: JSON.parse('[{"and":[{"==":[{"var":"type"},"shape"]},{"==":[{"var":"shape"},"rectangle"]}]}]') }); expect(frame).toBe(510); - frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"shape"]},{"==":[{"var":"shape"},"rectangle"]}]}]'), 511, 994); + frame = await job.annotations.search(511, 994, { annotationsFilters: JSON.parse('[{"and":[{"==":[{"var":"type"},"shape"]},{"==":[{"var":"shape"},"rectangle"]}]}]') }); expect(frame).toBe(null); - frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"shape"]},{"==":[{"var":"shape"},"polygon"]}]}]'), 511, 994); + frame = await job.annotations.search(511, 994, { annotationsFilters: JSON.parse('[{"and":[{"==":[{"var":"type"},"shape"]},{"==":[{"var":"shape"},"polygon"]}]}]') }); expect(frame).toBe(520); - frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"attr.motorcycle.model"},"some text for test"]}]}]'), 495, 994); + frame = await job.annotations.search(495, 994, { annotationsFilters: JSON.parse('[{"and":[{"==":[{"var":"attr.motorcycle.model"},"some text for test"]}]}]') }); 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); + frame = await job.annotations.search(495, 994, { annotationsFilters: JSON.parse('[{"and":[{"==":[{"var":"attr.motorcycle.model"},"some text for test"]},{"==":[{"var":"shape"},"ellipse"]}]}]') }); expect(frame).toBe(null); - frame = await job.annotations.search(JSON.parse('[{"and":[{"<=":[450,{"var":"width"},550]}]}]'), 540, 994); + frame = await job.annotations.search(540, 994, { annotationsFilters: JSON.parse('[{"and":[{"<=":[450,{"var":"width"},550]}]}]') }); expect(frame).toBe(563); - frame = await job.annotations.search(JSON.parse('[{"and":[{"<=":[450,{"var":"width"},550]}]}]'), 588, 994); + frame = await job.annotations.search(588, 994, { annotationsFilters: JSON.parse('[{"and":[{"<=":[450,{"var":"width"},550]}]}]') }); expect(frame).toBe(null); - frame = await job.annotations.search(JSON.parse('[{"and":[{">=":[{"var":"width"},500]},{"<=":[{"var":"height"},300]}]}]'), 540, 994); + frame = await job.annotations.search(540, 994, { annotationsFilters: JSON.parse('[{"and":[{">=":[{"var":"width"},500]},{"<=":[{"var":"height"},300]}]}]') }); expect(frame).toBe(575); }); }); diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 5a27b6fadf2a..52a7b743d9b5 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -1246,7 +1246,9 @@ export function searchAnnotationsAsync( sessionInstance: NonNullable, frameFrom: number, frameTo: number, - filters?: object[], + generalFilters?: { + isEmptyFrame: boolean; + }, ): ThunkAction { return async (dispatch: ActionCreator, getState): Promise => { try { @@ -1255,16 +1257,18 @@ export function searchAnnotationsAsync( player: { showDeletedFrames }, }, annotation: { - annotations: { filters: setupFilters }, + annotations: { filters }, }, } = getState(); const frame = await sessionInstance.annotations .search( - filters || setupFilters, frameFrom, frameTo, - { allowDeletedFrames: showDeletedFrames }, + { + allowDeletedFrames: showDeletedFrames, + ...({ generalFilters } || { annotationsFilters: filters }), + }, ); if (frame !== null) { dispatch(changeFrameAsync(frame)); diff --git a/cvat-ui/src/base.scss b/cvat-ui/src/base.scss index 41589d11bd50..63ed8cdc8191 100644 --- a/cvat-ui/src/base.scss +++ b/cvat-ui/src/base.scss @@ -21,6 +21,7 @@ $background-color-1: white; $background-color-2: #f1f1f1; $notification-background-color-1: #d9ecff; $notification-border-color-1: #1890ff; +$important-info-background-color: #1890ff; $transparent-color: rgba(0, 0, 0, 0%); $player-slider-color: #979797; $player-buttons-color: #242424; diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index e99212b7d935..258f78208784 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -14,7 +14,6 @@ import Text from 'antd/lib/typography/Text'; import Checkbox from 'antd/lib/checkbox'; import InputNumber from 'antd/lib/input-number'; import Select from 'antd/lib/select'; -import Paragraph from 'antd/lib/typography/Paragraph'; import Button from 'antd/lib/button'; import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; import Icon from '@ant-design/icons/lib/components/Icon'; @@ -26,6 +25,7 @@ import { Job, Label, LabelType } from 'cvat-core-wrapper'; import { ActionUnion, createAction } from 'utils/redux'; import { changeFrameAsync, changeWorkspace, saveAnnotationsAsync } from 'actions/annotation-actions'; import LabelSelector from 'components/label-selector/label-selector'; +import { Alert } from 'antd'; enum ReducerActionType { SWITCH_SIDEBAR_COLLAPSED = 'SWITCH_SIDEBAR_COLLAPSED', @@ -155,10 +155,12 @@ function SingleShapeSidebar(): JSX.Element { isCanvasReady, jobInstance, frame, + keyMap, } = useSelector((_state: CombinedState) => ({ isCanvasReady: _state.annotation.canvas.ready, - jobInstance: _state.annotation.job.instance, + jobInstance: _state.annotation.job.instance as Job, frame: _state.annotation.player.frame.number, + keyMap: _state.shortcuts.normalizedKeyMap, }), shallowEqual); const [state, dispatch] = useReducer(reducer, { @@ -167,9 +169,9 @@ function SingleShapeSidebar(): JSX.Element { saveOnFinish: true, navigateOnlyEmpty: true, pointsCount: 1, - labels: (jobInstance as Job).labels.filter((label) => label.type !== LabelType.TAG), - label: (jobInstance as Job).labels[0] || null, - labelType: (jobInstance as Job).labels[0]?.type || LabelType.ANY, + labels: jobInstance.labels.filter((label) => label.type !== LabelType.TAG), + label: jobInstance.labels[0] || null, + labelType: jobInstance.labels[0]?.type || LabelType.ANY, frames: [], }); @@ -180,11 +182,18 @@ function SingleShapeSidebar(): JSX.Element { return true; } + if (state.saveOnFinish) { + // if the latest image does not have objects to be annotated and user clicks "Next" + // we should save the job + appDispatch(changeWorkspace(Workspace.STANDARD)); + appDispatch(saveAnnotationsAsync()); + } + return false; - }, [state.frames, frame]); + }, [state.frames, state.saveOnFinish, frame]); const prevFrame = useCallback((): boolean => { - const prev = state.frames.find((_frame) => _frame < frame); + const prev = state.frames.findLast((_frame) => _frame < frame); if (typeof prev === 'number') { appDispatch(changeFrameAsync(prev)); return true; @@ -224,15 +233,28 @@ function SingleShapeSidebar(): JSX.Element { useEffect(() => { (async () => { - const job = jobInstance as Job; const framesToBeVisited = []; - let searchFrom = job.startFrame; + let searchFrom: number | null = jobInstance.startFrame; while (searchFrom !== null) { - const foundFrame = await job.annotations.search(searchFrom, job.stopFrame, [{ and: [{ '==': [{ var: 'isEmptyFrame' }, true] }] }]); + const foundFrame: number | null = await jobInstance.annotations.search( + searchFrom, + jobInstance.stopFrame, + { + allowDeletedFrames: false, + ...(state.navigateOnlyEmpty ? { + generalFilters: { + isEmptyFrame: true, + }, + } : {}), + }, + ); + if (foundFrame !== null) { framesToBeVisited.push(foundFrame); - searchFrom = foundFrame < job.stopFrame ? foundFrame + 1 : null; + searchFrom = foundFrame < jobInstance.stopFrame ? foundFrame + 1 : null; + } else { + searchFrom = null; } } @@ -297,6 +319,51 @@ function SingleShapeSidebar(): JSX.Element { > {state.sidebarCollabased ? : } + { state.label !== null && state.labelType !== LabelType.ANY && ( + + + + Annotate + {` ${(state.label as Label).name} `} + on the image, using + {` ${message} `} + + )} + /> + +
  • + Click + {' Next '} + if already annotated or there is nothing to be annotated +
  • +
  • + Hold + {' [Alt] '} + button to avoid drawing on click +
  • +
  • + Press + {` ${keyMap.UNDO} `} + to undo a created object +
  • +
  • + Press + {` ${keyMap.CANCEL} `} + to reset drawing process +
  • + + )} + /> + +
    + )} Label selector @@ -409,7 +476,15 @@ function SingleShapeSidebar(): JSX.Element { Previous - { state.label !== null && state.labelType !== LabelType.ANY && ( - - -
    - - Annotate - {` ${(state.label as Label).name} `} - on the image, using - {` ${message} `} - -
    - -
    - )} ); } diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss index 42c7608f3061..82185f58a79e 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss @@ -20,6 +20,7 @@ } } + .cvat-single-shape-annotation-sidebar-label, .cvat-single-shape-annotation-sidebar-label-type, .cvat-single-shape-annotation-sidebar-points-count, .cvat-single-shape-annotation-sidebar-label-select, @@ -34,9 +35,8 @@ row-gap: 0; text-align: center; font-size: large; - padding-top: 32px; - bottom: 0; - position: absolute; + margin-top: $grid-unit-size * 5; + margin-bottom: $grid-unit-size; } .cvat-single-shape-annotation-sidebar-navigation-block { 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 aa2b2dc047c9..5c0e68309e30 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 @@ -75,11 +75,18 @@ interface DispatchToProps { onChangeFrame(frame: number, fillBuffer?: boolean, frameStep?: number): void; onSwitchPlay(playing: boolean): void; onSaveAnnotation(): void; - showStatistics(sessionInstance: any): void; - showFilters(sessionInstance: any): void; + showStatistics(sessionInstance: Job): void; + showFilters(): void; undo(): void; redo(): void; - searchAnnotations(sessionInstance: any, frameFrom: number, frameTo: number, filters?: object[]): void; + searchAnnotations( + sessionInstance: Job, + frameFrom: number, + frameTo: number, + generalFilters?: { + isEmptyFrame: boolean; + }, + ): void; setForceExitAnnotationFlag(forceExit: boolean): void; changeWorkspace(workspace: Workspace): void; onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState): void; @@ -163,7 +170,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { onSaveAnnotation(): void { dispatch(saveAnnotationsAsync()); }, - showStatistics(sessionInstance: any): void { + showStatistics(sessionInstance: Job): void { dispatch(collectStatisticsAsync(sessionInstance)); dispatch(showStatisticsAction(true)); }, @@ -176,8 +183,15 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { redo(): void { dispatch(redoActionAsync()); }, - searchAnnotations(sessionInstance: any, frameFrom: number, frameTo: number, filters?: object[]): void { - dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo, filters)); + searchAnnotations( + sessionInstance: Job, + frameFrom: number, + frameTo: number, + generalFilters?: { + isEmptyFrame: boolean; + }, + ): void { + dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo, generalFilters)); }, changeWorkspace(workspace: Workspace): void { dispatch(changeWorkspaceAction(workspace)); @@ -289,8 +303,8 @@ class AnnotationTopBarContainer extends React.PureComponent { }; private showFilters = (): void => { - const { jobInstance, showFilters } = this.props; - showFilters(jobInstance); + const { showFilters } = this.props; + showFilters(); }; private onSwitchPlay = (): void => { @@ -366,7 +380,7 @@ class AnnotationTopBarContainer extends React.PureComponent { } else if (prevButtonType === 'filtered') { searchAnnotations(jobInstance, newFrame, startFrame); } else { - searchAnnotations(jobInstance, newFrame, startFrame, [{ and: [{ '==': [{ var: 'isEmptyFrame' }, true] }] }]); + searchAnnotations(jobInstance, newFrame, startFrame, { isEmptyFrame: true }); } } }; @@ -395,7 +409,7 @@ class AnnotationTopBarContainer extends React.PureComponent { } else if (nextButtonType === 'filtered') { searchAnnotations(jobInstance, newFrame, stopFrame); } else { - searchAnnotations(jobInstance, newFrame, stopFrame, [{ and: [{ '==': [{ var: 'isEmptyFrame' }, true] }] }]); + searchAnnotations(jobInstance, newFrame, stopFrame, { isEmptyFrame: true }); } } }; From fd7b09db029aa81998bfaea27d0b5c2107e76fea Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 19 Feb 2024 09:57:04 +0200 Subject: [PATCH 12/43] Improvements and bugfixes --- .../single-shape-sidebar.tsx | 179 ++++++++++++------ .../single-shape-workspace/styles.scss | 11 +- 2 files changed, 132 insertions(+), 58 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 258f78208784..aad1d5da1a0b 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -15,6 +15,7 @@ import Checkbox from 'antd/lib/checkbox'; import InputNumber from 'antd/lib/input-number'; import Select from 'antd/lib/select'; import Button from 'antd/lib/button'; +import Alert from 'antd/lib/alert'; import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; import Icon from '@ant-design/icons/lib/components/Icon'; @@ -25,13 +26,16 @@ import { Job, Label, LabelType } from 'cvat-core-wrapper'; import { ActionUnion, createAction } from 'utils/redux'; import { changeFrameAsync, changeWorkspace, saveAnnotationsAsync } from 'actions/annotation-actions'; import LabelSelector from 'components/label-selector/label-selector'; -import { Alert } from 'antd'; + +import GlobalHotKeys from 'utils/mousetrap-react'; +import CVATTooltip from 'components/common/cvat-tooltip'; enum ReducerActionType { SWITCH_SIDEBAR_COLLAPSED = 'SWITCH_SIDEBAR_COLLAPSED', SWITCH_AUTO_NEXT_FRAME = 'SWITCH_AUTO_NEXT_FRAME', SWITCH_AUTOSAVE_ON_FINISH = 'SWITCH_AUTOSAVE_ON_FINISH', SWITCH_NAVIGATE_EMPTY_ONLY = 'SWITCH_NAVIGATE_EMPTY_ONLY', + SWITCH_COUNT_OF_POINTS_IS_PREDEFINED = 'SWITCH_COUNT_OF_POINTS_IS_PREDEFINED', SET_ACTIVE_LABEL = 'SET_ACTIVE_LABEL', SET_POINTS_COUNT = 'SET_POINTS_COUNT', SET_FRAMES = 'SET_FRAMES', @@ -50,6 +54,9 @@ export const reducerActions = { switchNavigateEmptyOnly: () => ( createAction(ReducerActionType.SWITCH_NAVIGATE_EMPTY_ONLY) ), + switchCountOfPointsIsPredefined: () => ( + createAction(ReducerActionType.SWITCH_COUNT_OF_POINTS_IS_PREDEFINED) + ), setActiveLabel: (label: Label, type?: LabelType) => ( createAction(ReducerActionType.SET_ACTIVE_LABEL, { label, @@ -69,6 +76,7 @@ interface State { autoNextFrame: boolean; saveOnFinish: boolean; navigateOnlyEmpty: boolean; + pointsCountIsPredefined: boolean; pointsCount: number; labels: Label[]; label: Label | null; @@ -116,6 +124,13 @@ const reducer = (state: State, action: ActionUnion): Stat }; } + if (action.type === ReducerActionType.SWITCH_COUNT_OF_POINTS_IS_PREDEFINED) { + return { + ...state, + pointsCountIsPredefined: !state.pointsCountIsPredefined, + }; + } + if (action.type === ReducerActionType.SET_ACTIVE_LABEL) { return { ...state, @@ -155,12 +170,14 @@ function SingleShapeSidebar(): JSX.Element { isCanvasReady, jobInstance, frame, + normalizedKeyMap, keyMap, } = useSelector((_state: CombinedState) => ({ isCanvasReady: _state.annotation.canvas.ready, jobInstance: _state.annotation.job.instance as Job, frame: _state.annotation.player.frame.number, - keyMap: _state.shortcuts.normalizedKeyMap, + keyMap: _state.shortcuts.keyMap, + normalizedKeyMap: _state.shortcuts.normalizedKeyMap, }), shallowEqual); const [state, dispatch] = useReducer(reducer, { @@ -168,6 +185,7 @@ function SingleShapeSidebar(): JSX.Element { autoNextFrame: true, saveOnFinish: true, navigateOnlyEmpty: true, + pointsCountIsPredefined: true, pointsCount: 1, labels: jobInstance.labels.filter((label) => label.type !== LabelType.TAG), label: jobInstance.labels[0] || null, @@ -211,7 +229,7 @@ function SingleShapeSidebar(): JSX.Element { canvas.draw({ enabled: true, shapeType: state.labelType, - numberOfPoints: state.pointsCount, + numberOfPoints: state.pointsCountIsPredefined ? state.pointsCount : undefined, crosshair: true, }); } @@ -226,10 +244,8 @@ function SingleShapeSidebar(): JSX.Element { }, []); useEffect(() => { - if (canvasInitializerRef.current) { - canvasInitializerRef?.current(); - } - }, [isCanvasReady, state.label, state.labelType, state.pointsCount]); + (canvasInitializerRef.current || (() => {}))(); + }, [isCanvasReady, state.label, state.labelType, state.pointsCount, state.pointsCountIsPredefined]); useEffect(() => { (async () => { @@ -306,8 +322,36 @@ function SingleShapeSidebar(): JSX.Element { collapsed: state.sidebarCollabased, }; + const subKeyMap = { + CANCEL: keyMap.CANCEL, + NEXT_FRAME: keyMap.NEXT_FRAME, + PREV_FRAME: keyMap.PREV_FRAME, + SWITCH_DRAW_MODE: keyMap.SWITCH_DRAW_MODE, + }; + const handlers = { + CANCEL: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + (canvasInitializerRef.current || (() => {}))(); + }, + NEXT_FRAME: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + nextFrame(); + }, + PREV_FRAME: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + prevFrame(); + }, + SWITCH_DRAW_MODE: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + const canvas = store.getState().annotation.canvas.instance as Canvas; + canvas.draw({ enabled: false }); + (canvasInitializerRef.current || (() => {}))(); + }, + }; + return ( + {/* eslint-disable-next-line */}
  • Press - {` ${keyMap.UNDO} `} + {` ${normalizedKeyMap.UNDO} `} to undo a created object
  • Press - {` ${keyMap.CANCEL} `} + {` ${normalizedKeyMap.CANCEL} `} to reset drawing process
  • +
  • + Press + {` ${normalizedKeyMap.SWITCH_DRAW_MODE} `} + to finish drawing process +
  • )} /> @@ -406,29 +455,6 @@ function SingleShapeSidebar(): JSX.Element {
    ) : null } - { state.label && [LabelType.POLYGON, LabelType.POLYLINE, LabelType.POINTS].includes(state.labelType) ? ( - <> - - - Number of points - - - - - { - if (value !== null) { - dispatch(reducerActions.setPointsCount(value)); - } - }} - /> - - - - ) : null } + + + { + dispatch(reducerActions.switchCountOfPointsIsPredefined()); + }} + > + Predefined number of points + + + + { state.label && + [LabelType.POLYGON, LabelType.POLYLINE, LabelType.POINTS].includes(state.labelType) && + state.pointsCountIsPredefined ? ( + <> + + + Number of points + + + + + { + if (value !== null) { + dispatch(reducerActions.setPointsCount(value)); + } + }} + /> + + + + ) : null } - - + + + + + + diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss index 82185f58a79e..0fe9f15942e4 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss @@ -4,8 +4,13 @@ @import 'base'; +.cvat-single-shape-workspace { + height: 100%; +} + .cvat-single-shape-annotation-sidebar { - padding: 8px; + padding: $grid-unit-size; + overflow: auto; .cvat-single-shape-annotation-sidebar-label-select, .cvat-single-shape-annotation-sidebar-label-type-selector { @@ -27,8 +32,9 @@ .cvat-single-shape-annotation-sidebar-points-count-input, .cvat-single-shape-annotation-sidebar-auto-next-frame-checkbox, .cvat-single-shape-annotation-sidebar-navigate-empty-checkbox, + .cvat-single-shape-annotation-sidebar-predefined-pounts-count-checkbox, .cvat-single-shape-annotation-sidebar-auto-save-checkbox { - margin-top: 8px; + margin-top: $grid-unit-size; } .cvat-single-shape-annotation-sidebar-hint { @@ -41,6 +47,7 @@ .cvat-single-shape-annotation-sidebar-navigation-block { margin-top: $grid-unit-size * 4; + padding-bottom: $grid-unit-size * 2; > div:nth-child(1) { display: flex; From 253a8f840fa8c808da4e7c5bfe7ff000e039475c Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 19 Feb 2024 10:18:05 +0200 Subject: [PATCH 13/43] Minor fixes --- cvat-ui/src/containers/annotation-page/annotation-page.tsx | 4 ---- cvat-ui/src/reducers/annotation-reducer.ts | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/cvat-ui/src/containers/annotation-page/annotation-page.tsx b/cvat-ui/src/containers/annotation-page/annotation-page.tsx index 575ada42989e..9b0d963f381e 100644 --- a/cvat-ui/src/containers/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/containers/annotation-page/annotation-page.tsx @@ -81,10 +81,6 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps { } } - if (searchParams.size) { - own.history.replace(own.history.location.pathname); - } - return { getJob(): void { dispatch(getJobAsync({ diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index ff41b50e1503..36e53b496188 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -171,7 +171,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { if (queryParameters.initialWorkspace !== Workspace.STANDARD3D) { workspaceSelected = queryParameters.initialWorkspace; } - workspaceSelected = workspaceSelected || (isReview ? Workspace.REVIEW : Workspace.SINGLE_SHAPE); + workspaceSelected = workspaceSelected || (isReview ? Workspace.REVIEW : Workspace.STANDARD); } else { workspaceSelected = Workspace.STANDARD3D; activeShapeType = ShapeType.CUBOID; From 5a9dc61db274f9eaffc78264ce227443d14b68a0 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 19 Feb 2024 10:19:47 +0200 Subject: [PATCH 14/43] Updated changelog --- changelog.d/20240219_101835_boris_single_object_mode.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/20240219_101835_boris_single_object_mode.md diff --git a/changelog.d/20240219_101835_boris_single_object_mode.md b/changelog.d/20240219_101835_boris_single_object_mode.md new file mode 100644 index 000000000000..8d6706187b27 --- /dev/null +++ b/changelog.d/20240219_101835_boris_single_object_mode.md @@ -0,0 +1,4 @@ +### Added + +- Single shape annotation mode allowing to easily annotate scenarious where a user +only needs to draw one object on one image () From 9c81aaf3fa5af21d782eeab71acb2def742535d2 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 19 Feb 2024 10:20:39 +0200 Subject: [PATCH 15/43] Updated versiob --- cvat-core/package.json | 2 +- cvat-ui/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cvat-core/package.json b/cvat-core/package.json index 9e025040d04b..81a1937817f8 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "14.1.1", + "version": "15.0.0", "type": "module", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 21fe7cd13727..b8d4b1e5259c 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.62.0", + "version": "1.63.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { From c27baf9b94cb4300daaf612ab30a372017d528e8 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 19 Feb 2024 10:32:09 +0200 Subject: [PATCH 16/43] Minor fix --- .../src/containers/annotation-page/annotation-page.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cvat-ui/src/containers/annotation-page/annotation-page.tsx b/cvat-ui/src/containers/annotation-page/annotation-page.tsx index 9b0d963f381e..52e1cc0fd99b 100644 --- a/cvat-ui/src/containers/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/containers/annotation-page/annotation-page.tsx @@ -81,6 +81,16 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps { } } + const initialSize = searchParams.size; + searchParams.delete('frame'); + searchParams.delete('serverID'); + searchParams.delete('type'); + searchParams.delete('openGuide'); + + if (searchParams.size !== initialSize) { + own.history.replace(`${own.history.location.pathname}?${searchParams.toString()}`); + } + return { getJob(): void { dispatch(getJobAsync({ From 6420185cbcd31e504d4b48728f1586ba3ef9b9fc Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 19 Feb 2024 11:34:37 +0200 Subject: [PATCH 17/43] Fixed test --- cvat-ui/src/actions/annotation-actions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 99165cf09733..31c9beccc7c9 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -1267,7 +1267,9 @@ export function searchAnnotationsAsync( frameTo, { allowDeletedFrames: showDeletedFrames, - ...({ generalFilters } || { annotationsFilters: filters }), + ...( + generalFilters ? { generalFilters } : { annotationsFilters: filters } + ), }, ); if (frame !== null) { From a82cb62394859cd4c4778ed0eba6f7c3dda0bda3 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 19 Feb 2024 20:13:34 +0200 Subject: [PATCH 18/43] Additional query parameters --- cvat-ui/src/actions/annotation-actions.ts | 6 ++++-- .../single-shape-sidebar/single-shape-sidebar.tsx | 14 ++++++++++++-- .../containers/annotation-page/annotation-page.tsx | 12 +++++++++--- cvat-ui/src/reducers/annotation-reducer.ts | 5 ++++- cvat-ui/src/reducers/index.ts | 2 ++ 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 31c9beccc7c9..e0729adb6e8d 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -875,8 +875,10 @@ export function getJobAsync({ initialFrame: number | null; initialFilters: object[]; queryParameters: { - initialOpenGuide?: boolean; - initialWorkspace?: Workspace; + initialOpenGuide: boolean; + initialWorkspace: Workspace | null; + defaultLabel: string | null; + defaultPointsCount: number | null; } }): ThunkAction { return async (dispatch: ActionCreator, getState): Promise => { diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index aad1d5da1a0b..70459a567107 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -172,12 +172,16 @@ function SingleShapeSidebar(): JSX.Element { frame, normalizedKeyMap, keyMap, + defaultLabel, + defaultPointsCount, } = useSelector((_state: CombinedState) => ({ isCanvasReady: _state.annotation.canvas.ready, jobInstance: _state.annotation.job.instance as Job, frame: _state.annotation.player.frame.number, keyMap: _state.shortcuts.keyMap, normalizedKeyMap: _state.shortcuts.normalizedKeyMap, + defaultLabel: _state.annotation.job.queryParameters.defaultLabel, + defaultPointsCount: _state.annotation.job.queryParameters.defaultPointsCount, }), shallowEqual); const [state, dispatch] = useReducer(reducer, { @@ -186,9 +190,9 @@ function SingleShapeSidebar(): JSX.Element { saveOnFinish: true, navigateOnlyEmpty: true, pointsCountIsPredefined: true, - pointsCount: 1, + pointsCount: defaultPointsCount || 1, labels: jobInstance.labels.filter((label) => label.type !== LabelType.TAG), - label: jobInstance.labels[0] || null, + label: null, labelType: jobInstance.labels[0]?.type || LabelType.ANY, frames: [], }); @@ -236,6 +240,12 @@ function SingleShapeSidebar(): JSX.Element { }; useEffect(() => { + const labelInstance = (defaultLabel ? jobInstance.labels + .find((_label) => _label.name === defaultLabel) : jobInstance.labels[0]); + if (labelInstance) { + dispatch(reducerActions.setActiveLabel(labelInstance)); + } + const canvas = store.getState().annotation.canvas.instance as Canvas; cancelCurrentCanvasOp(canvas); return () => { diff --git a/cvat-ui/src/containers/annotation-page/annotation-page.tsx b/cvat-ui/src/containers/annotation-page/annotation-page.tsx index 52e1cc0fd99b..19929971a06f 100644 --- a/cvat-ui/src/containers/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/containers/annotation-page/annotation-page.tsx @@ -64,9 +64,13 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps { const searchParams = new URLSearchParams(window.location.search); const initialFilters: object[] = []; const initialOpenGuide = searchParams.has('openGuide'); + + const parsedPointsCount = +(searchParams.get('defaultPointsCount') || 'NaN'); + const defaultLabel = searchParams.get('defaultLabel') || null; + const defaultPointsCount = Number.isInteger(parsedPointsCount) && parsedPointsCount >= 1 ? parsedPointsCount : null; const initialWorkspace = Object.entries(Workspace).find(([key]) => ( - key === searchParams.get('openWorkspace')?.toUpperCase() - )); + key === searchParams.get('defaultWorkspace')?.toUpperCase() + )) || null; const parsedFrame = +(searchParams.get('frame') || 'NaN'); const initialFrame = Number.isInteger(parsedFrame) && parsedFrame >= 0 ? parsedFrame : null; @@ -100,7 +104,9 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps { initialFilters, queryParameters: { initialOpenGuide, - ...(initialWorkspace ? { initialWorkspace: initialWorkspace[1] } : {}), + defaultLabel, + defaultPointsCount, + ...(initialWorkspace ? { initialWorkspace: initialWorkspace[1] } : { initialWorkspace }), }, })); }, diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index fc6a26c089b7..945bfbcadd45 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -59,6 +59,8 @@ const defaultState: AnnotationState = { groundTruthInstance: null, queryParameters: { initialOpenGuide: false, + defaultLabel: null, + defaultPointsCount: null, }, instance: null, attributes: {}, @@ -166,7 +168,6 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { let workspaceSelected = null; let activeShapeType = defaultLabel && defaultLabel.type !== 'any' ? defaultLabel.type : ShapeType.RECTANGLE; - if (job.dimension === DimensionType.DIMENSION_2D) { if (queryParameters.initialWorkspace !== Workspace.STANDARD3D) { workspaceSelected = queryParameters.initialWorkspace; @@ -198,6 +199,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { groundTruthJobFramesMeta, queryParameters: { initialOpenGuide: queryParameters.initialOpenGuide, + defaultLabel: queryParameters.defaultLabel, + defaultPointsCount: queryParameters.defaultPointsCount, }, }, annotations: { diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index 0bda530fd114..76e202d81bd0 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -705,6 +705,8 @@ export interface AnnotationState { instance: Job | null | undefined; queryParameters: { initialOpenGuide: boolean; + defaultLabel: string | null; + defaultPointsCount: number | null; }; groundTruthJobFramesMeta: FramesMetaData | null; groundTruthInstance: Job | null; From db76f1c01ac671347005efbe97a553edd80dd51c Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 19 Feb 2024 20:45:52 +0200 Subject: [PATCH 19/43] Minor improvements --- .../single-shape-sidebar.tsx | 30 ++++++++++--------- .../single-shape-workspace/styles.scss | 2 +- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 70459a567107..43279c504cca 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -20,11 +20,11 @@ import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; import Icon from '@ant-design/icons/lib/components/Icon'; import { NextIcon, PreviousIcon } from 'icons'; -import { CombinedState, Workspace } from 'reducers'; +import { CombinedState } from 'reducers'; import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; import { Job, Label, LabelType } from 'cvat-core-wrapper'; import { ActionUnion, createAction } from 'utils/redux'; -import { changeFrameAsync, changeWorkspace, saveAnnotationsAsync } from 'actions/annotation-actions'; +import { changeFrameAsync, saveAnnotationsAsync } from 'actions/annotation-actions'; import LabelSelector from 'components/label-selector/label-selector'; import GlobalHotKeys from 'utils/mousetrap-react'; @@ -204,15 +204,17 @@ function SingleShapeSidebar(): JSX.Element { return true; } - if (state.saveOnFinish) { + if (state.saveOnFinish && jobInstance.annotations.hasUnsavedChanges()) { // if the latest image does not have objects to be annotated and user clicks "Next" - // we should save the job - appDispatch(changeWorkspace(Workspace.STANDARD)); - appDispatch(saveAnnotationsAsync()); + // we should save the job if there are unsaved changes + appDispatch(saveAnnotationsAsync()).then(() => { + // update state to re-render component after the job saved unsaved changes + dispatch(reducerActions.setFrames([...state.frames])); + }); } return false; - }, [state.frames, state.saveOnFinish, frame]); + }, [state.frames, state.saveOnFinish, frame, jobInstance]); const prevFrame = useCallback((): boolean => { const prev = state.frames.findLast((_frame) => _frame < frame); @@ -297,7 +299,6 @@ function SingleShapeSidebar(): JSX.Element { setTimeout(() => { if (state.autoNextFrame) { if (!nextFrame()) { - appDispatch(changeWorkspace(Workspace.STANDARD)); if (state.saveOnFinish) { appDispatch(saveAnnotationsAsync()); } @@ -555,12 +556,13 @@ function SingleShapeSidebar(): JSX.Element { // allow clicking the button even if this is latest frame // if automatic saving at the end is enabled // e.g. when the latest frame does not contain objects to be annotated - disabled={state.frames.length === 0 || - ( - !state.saveOnFinish && - !jobInstance.annotations.hasUnsavedChanges() && - state.frames[state.frames.length - 1] <= frame - )} + disabled={state.frames.length === 0 || ( + state.frames[state.frames.length - 1] <= frame && ( + !state.saveOnFinish || (state.saveOnFinish && + !jobInstance.annotations.hasUnsavedChanges() + ) + ) + )} size='large' onClick={nextFrame} icon={} diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss index 0fe9f15942e4..201b2036993d 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss @@ -53,7 +53,7 @@ display: flex; justify-content: space-between; - > button { + button { width: $grid-unit-size * 16; } } From 2546f194033949ed71826e41f2968af0f8311967 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 19 Feb 2024 20:59:57 +0200 Subject: [PATCH 20/43] Refactoring --- .../single-shape-sidebar/single-shape-sidebar.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 43279c504cca..a1a99f62a3eb 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -226,7 +226,7 @@ function SingleShapeSidebar(): JSX.Element { return false; }, [state.frames, frame]); - const canvasInitializerRef = useRef<() => void | null>(); + const canvasInitializerRef = useRef<() => void | null>(() => {}); canvasInitializerRef.current = (): void => { const canvas = store.getState().annotation.canvas.instance as Canvas; cancelCurrentCanvasOp(canvas); @@ -256,7 +256,7 @@ function SingleShapeSidebar(): JSX.Element { }, []); useEffect(() => { - (canvasInitializerRef.current || (() => {}))(); + canvasInitializerRef.current(); }, [isCanvasReady, state.label, state.labelType, state.pointsCount, state.pointsCountIsPredefined]); useEffect(() => { @@ -303,7 +303,7 @@ function SingleShapeSidebar(): JSX.Element { appDispatch(saveAnnotationsAsync()); } } - } else if (canvasInitializerRef?.current) { + } else { canvasInitializerRef.current(); } }, 100); @@ -342,7 +342,7 @@ function SingleShapeSidebar(): JSX.Element { const handlers = { CANCEL: (event: KeyboardEvent | undefined) => { event?.preventDefault(); - (canvasInitializerRef.current || (() => {}))(); + canvasInitializerRef.current(); }, NEXT_FRAME: (event: KeyboardEvent | undefined) => { event?.preventDefault(); @@ -356,7 +356,7 @@ function SingleShapeSidebar(): JSX.Element { event?.preventDefault(); const canvas = store.getState().annotation.canvas.instance as Canvas; canvas.draw({ enabled: false }); - (canvasInitializerRef.current || (() => {}))(); + canvasInitializerRef.current(); }, }; From c80bd99d711ae4d0897426bce22fbeb7d251d7e4 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 20 Feb 2024 00:17:04 +0200 Subject: [PATCH 21/43] Canvas refactoring --- cvat-canvas/package.json | 2 +- .../src/typescript/canvasController.ts | 35 ----- cvat-canvas/src/typescript/canvasView.ts | 138 +++++------------- cvat-canvas/src/typescript/drawHandler.ts | 59 +++++--- cvat-canvas/src/typescript/masksHandler.ts | 2 + cvat-ui/src/actions/annotation-actions.ts | 8 +- .../single-shape-sidebar.tsx | 70 +++++---- 7 files changed, 129 insertions(+), 185 deletions(-) diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index f66d9ea9d74a..97302daace22 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.19.1", + "version": "2.20.0", "type": "module", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", diff --git a/cvat-canvas/src/typescript/canvasController.ts b/cvat-canvas/src/typescript/canvasController.ts index 510145a64c4d..adf6f708ad88 100644 --- a/cvat-canvas/src/typescript/canvasController.ts +++ b/cvat-canvas/src/typescript/canvasController.ts @@ -45,13 +45,6 @@ export interface CanvasController { zoom(x: number, y: number, direction: number): void; draw(drawData: DrawData): void; edit(editData: MasksEditData): void; - interact(interactionData: InteractionData): void; - merge(mergeData: MergeData): void; - split(splitData: SplitData): void; - group(groupData: GroupData): void; - join(joinData: JoinData): void; - slice(sliceData: SliceData): void; - selectRegion(enabled: boolean): void; enableDrag(x: number, y: number): void; drag(x: number, y: number): void; disableDrag(): void; @@ -107,34 +100,6 @@ export class CanvasControllerImpl implements CanvasController { this.model.edit(editData); } - public interact(interactionData: InteractionData): void { - this.model.interact(interactionData); - } - - public merge(mergeData: MergeData): void { - this.model.merge(mergeData); - } - - public split(splitData: SplitData): void { - this.model.split(splitData); - } - - public group(groupData: GroupData): void { - this.model.group(groupData); - } - - public join(joinData: JoinData): void { - this.model.join(joinData); - } - - public slice(sliceData: SliceData): void { - this.model.slice(sliceData); - } - - public selectRegion(enable: boolean): void { - this.model.selectRegion(enable); - } - public get geometry(): Geometry { return this.model.geometry; } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 4582d87a84f7..3c553d8f1b04 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -232,6 +232,16 @@ export class CanvasViewImpl implements CanvasView, Listener { } } + private dispatchCanceledEvent(): void { + this.mode = Mode.IDLE; + const event: CustomEvent = new CustomEvent('canvas.canceled', { + bubbles: false, + cancelable: true, + }); + + this.canvas.dispatchEvent(event); + } + private onInteraction = ( shapes: InteractionResult[] | null, shapesUpdated = true, @@ -256,16 +266,7 @@ export class CanvasViewImpl implements CanvasView, Listener { } if (shapes === null || isDone) { - const event: CustomEvent = new CustomEvent('canvas.canceled', { - bubbles: false, - cancelable: true, - }); - - this.canvas.dispatchEvent(event); - this.mode = Mode.IDLE; - this.controller.interact({ - enabled: false, - }); + this.dispatchCanceledEvent(); } }; @@ -290,13 +291,7 @@ export class CanvasViewImpl implements CanvasView, Listener { const [state] = this.controller.objects .filter((_state: any): boolean => _state.clientID === clientID); this.onEditDone(state, points); - - const event: CustomEvent = new CustomEvent('canvas.canceled', { - bubbles: false, - cancelable: true, - }); - - this.canvas.dispatchEvent(event); + this.dispatchCanceledEvent(); return; } @@ -317,10 +312,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas.dispatchEvent(event); } else if (!continueDraw) { - this.canvas.dispatchEvent(new CustomEvent('canvas.canceled', { - bubbles: false, - cancelable: true, - })); + this.dispatchCanceledEvent(); } if (continueDraw) { @@ -335,7 +327,8 @@ export class CanvasViewImpl implements CanvasView, Listener { ); } else { // when draw stops from inside canvas (for example if use predefined number of points) - this.controller.draw({ enabled: false }); + this.mode = Mode.IDLE; + this.canvas.style.cursor = ''; } }; @@ -361,6 +354,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private onEditDone = (state: any, points: number[], rotation?: number): void => { this.canvas.style.cursor = ''; + this.mode = Mode.IDLE; if (state && points) { const event: CustomEvent = new CustomEvent('canvas.edited', { bubbles: false, @@ -374,18 +368,12 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas.dispatchEvent(event); } else { - const event: CustomEvent = new CustomEvent('canvas.canceled', { - bubbles: false, - cancelable: true, - }); - - this.canvas.dispatchEvent(event); + this.dispatchCanceledEvent(); } for (const clientID of Object.keys(this.innerObjectsFlags.editHidden)) { this.setupInnerFlags(+clientID, 'editHidden', false); } - this.mode = Mode.IDLE; }; private onMergeDone = (objects: any[] | null, duration?: number): void => { @@ -399,18 +387,11 @@ export class CanvasViewImpl implements CanvasView, Listener { }, }); + this.mode = Mode.IDLE; this.canvas.dispatchEvent(event); } else { - const event: CustomEvent = new CustomEvent('canvas.canceled', { - bubbles: false, - cancelable: true, - }); - - this.canvas.dispatchEvent(event); + this.dispatchCanceledEvent(); } - - this.controller.merge({ enabled: false }); - this.mode = Mode.IDLE; }; private onSplitDone = (object?: any, duration?: number): void => { @@ -425,23 +406,23 @@ export class CanvasViewImpl implements CanvasView, Listener { }, }); + this.canvas.style.cursor = ''; + this.mode = Mode.IDLE; + this.splitHandler.split({ enabled: false }); this.canvas.dispatchEvent(event); } else { - const event: CustomEvent = new CustomEvent('canvas.canceled', { - bubbles: false, - cancelable: true, - }); - - this.canvas.dispatchEvent(event); + this.dispatchCanceledEvent(); } - - this.controller.split({ enabled: false }); - this.mode = Mode.IDLE; }; private onSelectDone = (objects?: any[], duration?: number): void => { + if (this.mode === Mode.JOIN) { + this.onMessage(null, 'join'); + } + if (objects && typeof duration !== 'undefined') { if (this.mode === Mode.GROUP && objects.length > 1) { + this.mode = Mode.IDLE; this.canvas.dispatchEvent(new CustomEvent('canvas.groupped', { bubbles: false, cancelable: true, @@ -451,6 +432,7 @@ export class CanvasViewImpl implements CanvasView, Listener { }, })); } else if (this.mode === Mode.JOIN && objects.length > 1) { + this.mode = Mode.IDLE; let [left, top, right, bottom] = objects[0].points.slice(-4); objects.forEach((state) => { const [curLeft, curTop, curRight, curBottom] = state.points.slice(-4); @@ -487,26 +469,14 @@ export class CanvasViewImpl implements CanvasView, Listener { }).catch(this.onError); } } else { - const event: CustomEvent = new CustomEvent('canvas.canceled', { - bubbles: false, - cancelable: true, - }); - - this.canvas.dispatchEvent(event); + this.dispatchCanceledEvent(); } - - if (this.mode === Mode.GROUP) { - this.controller.group({ enabled: false }); - } else if (this.mode === Mode.JOIN) { - this.controller.join({ enabled: false }); - this.onMessage(null, 'join'); - } - - this.mode = Mode.IDLE; }; private onSliceDone = (state?: any, results?: number[][], duration?: number): void => { if (state && results && typeof duration !== 'undefined') { + this.mode = Mode.IDLE; + this.sliceHandler.slice({ enabled: false }); this.canvas.dispatchEvent(new CustomEvent('canvas.sliced', { bubbles: false, cancelable: true, @@ -517,40 +487,22 @@ export class CanvasViewImpl implements CanvasView, Listener { }, })); } else { - const event: CustomEvent = new CustomEvent('canvas.canceled', { - bubbles: false, - cancelable: true, - }); - - this.canvas.dispatchEvent(event); + this.dispatchCanceledEvent(); } - - this.controller.slice({ enabled: false }); - this.mode = Mode.IDLE; }; private onRegionSelected = (points?: number[]): void => { if (points) { - const event: CustomEvent = new CustomEvent('canvas.regionselected', { + this.canvas.dispatchEvent(new CustomEvent('canvas.regionselected', { bubbles: false, cancelable: true, detail: { points, }, - }); - - this.canvas.dispatchEvent(event); + })); } else { - const event: CustomEvent = new CustomEvent('canvas.canceled', { - bubbles: false, - cancelable: true, - }); - - this.canvas.dispatchEvent(event); + this.dispatchCanceledEvent(); } - - this.controller.selectRegion(false); - this.mode = Mode.IDLE; }; private onFindObject = (e: MouseEvent): void => { @@ -1711,19 +1663,15 @@ export class CanvasViewImpl implements CanvasView, Listener { if (data.enabled) { this.canvas.style.cursor = 'copy'; this.mode = Mode.MERGE; - } else { - this.canvas.style.cursor = ''; + this.mergeHandler.merge(data); } - this.mergeHandler.merge(data); } else if (reason === UpdateReasons.SPLIT) { const data: SplitData = this.controller.splitData; if (data.enabled) { this.canvas.style.cursor = 'copy'; this.mode = Mode.SPLIT; - } else { - this.canvas.style.cursor = ''; + this.splitHandler.split(data); } - this.splitHandler.split(data); } else if ([UpdateReasons.JOIN, UpdateReasons.GROUP].includes(reason)) { let data: GroupData | JoinData = null; if (reason === UpdateReasons.GROUP) { @@ -1745,18 +1693,12 @@ export class CanvasViewImpl implements CanvasView, Listener { objectType: ['shape'], }); } - - if (data.enabled) { - this.canvas.style.cursor = 'copy'; - } else { - this.canvas.style.cursor = ''; - } } else if (reason === UpdateReasons.SLICE) { const data = this.controller.sliceData; if (data.enabled && this.mode === Mode.IDLE) { this.mode = Mode.SLICE; + this.sliceHandler.slice(data); } - this.sliceHandler.slice(data); } else if (reason === UpdateReasons.SELECT) { this.objectSelector.push(this.controller.selected); if (this.mode === Mode.MERGE) { @@ -1805,8 +1747,8 @@ export class CanvasViewImpl implements CanvasView, Listener { }), ); } - this.mode = Mode.IDLE; this.canvas.style.cursor = ''; + this.dispatchCanceledEvent(); } else if (reason === UpdateReasons.DATA_FAILED) { this.onError(model.exception, 'data fetching'); } else if (reason === UpdateReasons.DESTROY) { diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index 75127693b297..156255ffeeb0 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -409,8 +409,6 @@ export class DrawHandlerImpl implements DrawHandler { if (this.crosshair) { this.removeCrosshair(); } - - this.onDrawDone(null); } private initDrawing(): void { @@ -426,9 +424,12 @@ export class DrawHandlerImpl implements DrawHandler { const points = readPointsFromShape((e.target as any as { instance: SVG.Rect }).instance); const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(points, true); const { shapeType, redraw: clientID } = this.drawData; - this.release(); - if (this.canceled) return; + if (this.canceled) { + return; + } + + this.release(); if (checkConstraint('rectangle', [xtl, ytl, xbr, ybr])) { this.onDrawDone({ clientID, @@ -436,6 +437,8 @@ export class DrawHandlerImpl implements DrawHandler { points: [xtl, ytl, xbr, ybr], }, Date.now() - this.startTimestamp); + } else { + this.onDrawDone(null); } }) .on('drawupdate', (): void => { @@ -467,11 +470,13 @@ export class DrawHandlerImpl implements DrawHandler { }; this.canvas.on('mousedown.draw', (e: MouseEvent): void => { - if (initialPoint.x === null || initialPoint.y === null) { - const translated = translateToSVG(this.canvas.node as any as SVGSVGElement, [e.clientX, e.clientY]); - [initialPoint.x, initialPoint.y] = translated; - } else { - this.drawInstance.fire('drawstop'); + if (e.button === 0 && !e.altKey) { + if (initialPoint.x === null || initialPoint.y === null) { + const translated = translateToSVG(this.canvas.node as any as SVGSVGElement, [e.clientX, e.clientY]); + [initialPoint.x, initialPoint.y] = translated; + } else { + this.drawInstance.fire('drawstop'); + } } }); @@ -492,9 +497,12 @@ export class DrawHandlerImpl implements DrawHandler { this.drawInstance.off('drawstop'); const points = this.getFinalEllipseCoordinates(readPointsFromShape(this.drawInstance), false); const { shapeType, redraw: clientID } = this.drawData; - this.release(); - if (this.canceled) return; + if (this.canceled) { + return; + } + + this.release(); if (checkConstraint('ellipse', points)) { this.onDrawDone( { @@ -504,6 +512,8 @@ export class DrawHandlerImpl implements DrawHandler { }, Date.now() - this.startTimestamp, ); + } else { + this.onDrawDone(null); } }); } @@ -623,9 +633,12 @@ export class DrawHandlerImpl implements DrawHandler { const { points, box } = shapeType === 'cuboid' ? this.getFinalCuboidCoordinates(targetPoints) : this.getFinalPolyshapeCoordinates(targetPoints, true); - this.release(); - if (this.canceled) return; + if (this.canceled) { + return; + } + + this.release(); if (checkConstraint(shapeType, points, box)) { if (shapeType === 'cuboid') { this.onDrawDone( @@ -636,6 +649,8 @@ export class DrawHandlerImpl implements DrawHandler { } this.onDrawDone({ clientID, shapeType, points }, Date.now() - this.startTimestamp); + } else { + this.onDrawDone(null); } }); } @@ -699,9 +714,12 @@ export class DrawHandlerImpl implements DrawHandler { const points = readPointsFromShape((e.target as any as { instance: SVG.Rect }).instance); const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(points, true); const { shapeType, redraw: clientID } = this.drawData; - this.release(); - if (this.canceled) return; + if (this.canceled) { + return; + } + + this.release(); if (checkConstraint('cuboid', [xtl, ytl, xbr, ybr])) { const d = { x: (xbr - xtl) * 0.1, y: (ybr - ytl) * 0.1 }; this.onDrawDone({ @@ -710,6 +728,8 @@ export class DrawHandlerImpl implements DrawHandler { clientID, }, Date.now() - this.startTimestamp); + } else { + this.onDrawDone(null); } }) .on('drawupdate', (): void => { @@ -765,9 +785,12 @@ export class DrawHandlerImpl implements DrawHandler { }); const { shapeType, redraw: clientID } = this.drawData; - this.release(); - if (this.canceled) return; + if (this.canceled) { + return; + } + + this.release(); if (checkConstraint('skeleton', [xtl, ytl, xbr, ybr])) { this.onDrawDone({ clientID, @@ -775,6 +798,8 @@ export class DrawHandlerImpl implements DrawHandler { elements, }, Date.now() - this.startTimestamp); + } else { + this.onDrawDone(null); } }) .on('drawupdate', (): void => { diff --git a/cvat-canvas/src/typescript/masksHandler.ts b/cvat-canvas/src/typescript/masksHandler.ts index cd7aa58d69fe..74edc885800e 100644 --- a/cvat-canvas/src/typescript/masksHandler.ts +++ b/cvat-canvas/src/typescript/masksHandler.ts @@ -627,6 +627,8 @@ export class MasksHandlerImpl implements MasksHandler { enabled: true, shapeType: 'mask', }; + + this.onDrawRepeat({ enabled: true, shapeType: 'mask' }); this.onDrawRepeat(newDrawData); return; } diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index e0729adb6e8d..718cfd059d83 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -1393,6 +1393,10 @@ export function repeatDrawShapeAsync(): ThunkAction { activeControl = ShapeTypeToControl[activeShapeType]; + if (canvasInstance instanceof Canvas) { + canvasInstance.cancel(); + } + dispatch({ type: AnnotationActionTypes.REPEAT_DRAW_SHAPE, payload: { @@ -1400,10 +1404,6 @@ export function repeatDrawShapeAsync(): ThunkAction { }, }); - if (canvasInstance instanceof Canvas) { - canvasInstance.cancel(); - } - const [activeLabel] = labels.filter((label: any) => label.id === activeLabelID); if (!activeLabel) { throw new Error(`Label with ID ${activeLabelID}, was not found`); diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index a1a99f62a3eb..211119d1d2c2 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -157,7 +157,8 @@ const reducer = (state: State, action: ActionUnion): Stat return state; }; -function cancelCurrentCanvasOp(canvas: Canvas): void { +function cancelCurrentCanvasOp(state: CombinedState): void { + const canvas = state.annotation.canvas.instance as Canvas; if (canvas.mode() !== CanvasMode.IDLE) { canvas.cancel(); } @@ -229,8 +230,6 @@ function SingleShapeSidebar(): JSX.Element { const canvasInitializerRef = useRef<() => void | null>(() => {}); canvasInitializerRef.current = (): void => { const canvas = store.getState().annotation.canvas.instance as Canvas; - cancelCurrentCanvasOp(canvas); - if (isCanvasReady && state.label && state.labelType !== LabelType.ANY) { canvas.draw({ enabled: true, @@ -241,6 +240,40 @@ function SingleShapeSidebar(): JSX.Element { } }; + useEffect(() => { + const canvas = store.getState().annotation.canvas.instance as Canvas; + const onDrawDone = (): void => { + setTimeout(() => { + if (state.autoNextFrame) { + if (!nextFrame()) { + if (state.saveOnFinish) { + appDispatch(saveAnnotationsAsync()); + } + } + } else { + canvasInitializerRef.current(); + } + }, 100); + }; + + const onCancel = (): void => { + // canvas.drawn should be triggered after canvas.cancel + // event in a usual scenario (when user drawn something) + // but there are some cases when only canvas.cancel is triggered (e.g. when drawn shape was not correct) + // in this case need to re-run drawing process + canvasInitializerRef.current(); + }; + + (canvas as Canvas).html().addEventListener('canvas.drawn', onDrawDone); + (canvas as Canvas).html().addEventListener('canvas.canceled', onCancel); + return (() => { + // should be prior mount use effect to remove event handlers before final cancel() is called + + (canvas as Canvas).html().removeEventListener('canvas.drawn', onDrawDone); + (canvas as Canvas).html().removeEventListener('canvas.canceled', onCancel); + }); + }, [nextFrame, state.autoNextFrame, state.saveOnFinish]); + useEffect(() => { const labelInstance = (defaultLabel ? jobInstance.labels .find((_label) => _label.name === defaultLabel) : jobInstance.labels[0]); @@ -248,14 +281,14 @@ function SingleShapeSidebar(): JSX.Element { dispatch(reducerActions.setActiveLabel(labelInstance)); } - const canvas = store.getState().annotation.canvas.instance as Canvas; - cancelCurrentCanvasOp(canvas); + cancelCurrentCanvasOp(store.getState()); return () => { - cancelCurrentCanvasOp(canvas); + cancelCurrentCanvasOp(store.getState()); }; }, []); useEffect(() => { + cancelCurrentCanvasOp(store.getState()); canvasInitializerRef.current(); }, [isCanvasReady, state.label, state.labelType, state.pointsCount, state.pointsCountIsPredefined]); @@ -293,28 +326,6 @@ function SingleShapeSidebar(): JSX.Element { })(); }, [state.navigateOnlyEmpty]); - useEffect(() => { - const canvas = store.getState().annotation.canvas.instance as Canvas; - const onDrawDone = (): void => { - setTimeout(() => { - if (state.autoNextFrame) { - if (!nextFrame()) { - if (state.saveOnFinish) { - appDispatch(saveAnnotationsAsync()); - } - } - } else { - canvasInitializerRef.current(); - } - }, 100); - }; - - (canvas as Canvas).html().addEventListener('canvas.drawn', onDrawDone); - return (() => { - (canvas as Canvas).html().removeEventListener('canvas.drawn', onDrawDone); - }); - }, [nextFrame, state.autoNextFrame, state.saveOnFinish]); - let message = ''; if (state.labelType === LabelType.POINTS) { message = `${state.pointsCount === 1 ? 'one point' : `${state.pointsCount} points`}`; @@ -342,7 +353,7 @@ function SingleShapeSidebar(): JSX.Element { const handlers = { CANCEL: (event: KeyboardEvent | undefined) => { event?.preventDefault(); - canvasInitializerRef.current(); + (store.getState().annotation.canvas.instance as Canvas).cancel(); }, NEXT_FRAME: (event: KeyboardEvent | undefined) => { event?.preventDefault(); @@ -356,7 +367,6 @@ function SingleShapeSidebar(): JSX.Element { event?.preventDefault(); const canvas = store.getState().annotation.canvas.instance as Canvas; canvas.draw({ enabled: false }); - canvasInitializerRef.current(); }, }; From 0ee714b5aaf22d684ad46f5cf2c8444c424c640b Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 20 Feb 2024 00:20:52 +0200 Subject: [PATCH 22/43] Removed skeletons from mode --- .../single-shape-sidebar/single-shape-sidebar.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 211119d1d2c2..da73627f5492 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -192,7 +192,7 @@ function SingleShapeSidebar(): JSX.Element { navigateOnlyEmpty: true, pointsCountIsPredefined: true, pointsCount: defaultPointsCount || 1, - labels: jobInstance.labels.filter((label) => label.type !== LabelType.TAG), + labels: jobInstance.labels.filter((label) => label.type !== LabelType.TAG && label.type !== LabelType.SKELETON), label: null, labelType: jobInstance.labels[0]?.type || LabelType.ANY, frames: [], @@ -470,7 +470,6 @@ function SingleShapeSidebar(): JSX.Element { -
    From f93545820f6939ca63c6abd130676dad79a380c9 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 20 Feb 2024 00:29:01 +0200 Subject: [PATCH 23/43] Fixed one more issue from develop --- cvat-core/src/frames.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cvat-core/src/frames.ts b/cvat-core/src/frames.ts index 412cecde88d8..0fb48c5482e3 100644 --- a/cvat-core/src/frames.ts +++ b/cvat-core/src/frames.ts @@ -612,7 +612,11 @@ export async function findFrame( const offset = filters.offset || 1; let meta; if (!frameDataCache[jobID]) { - meta = await serverProxy.frames.getMeta('job', jobID); + const serverMeta = await serverProxy.frames.getMeta('job', jobID); + meta = { + ...serverMeta, + deleted_frames: Object.fromEntries(serverMeta.deleted_frames.map((_frame) => [_frame, true])), + }; } else { meta = frameDataCache[jobID].meta; } From 059fec1cc3843bea062f06d2e162ce7940374d48 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 20 Feb 2024 00:35:03 +0200 Subject: [PATCH 24/43] Updated license headers --- cvat-canvas/src/typescript/canvasController.ts | 2 +- cvat-canvas/src/typescript/canvasView.ts | 2 +- cvat-canvas/src/typescript/drawHandler.ts | 2 +- cvat-core/src/annotations-filter.ts | 2 +- cvat-core/src/frames.ts | 2 +- cvat-core/src/session.ts | 2 +- cvat-core/tests/api/annotations.cjs | 2 +- cvat-ui/src/base.scss | 1 - .../standard-workspace/objects-side-bar/issues-list.tsx | 2 +- .../standard-workspace/objects-side-bar/objects-list-header.tsx | 1 + .../tag-annotation-sidebar/tag-annotation-sidebar.tsx | 2 +- cvat-ui/src/utils/filter-annotations.ts | 2 +- 12 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cvat-canvas/src/typescript/canvasController.ts b/cvat-canvas/src/typescript/canvasController.ts index adf6f708ad88..9acf58e95fc2 100644 --- a/cvat-canvas/src/typescript/canvasController.ts +++ b/cvat-canvas/src/typescript/canvasController.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 3c553d8f1b04..e2fea53e388e 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index 156255ffeeb0..1b6a7261c261 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-core/src/annotations-filter.ts b/cvat-core/src/annotations-filter.ts index 2cb715fe33ef..58c9e82a63e5 100644 --- a/cvat-core/src/annotations-filter.ts +++ b/cvat-core/src/annotations-filter.ts @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-core/src/frames.ts b/cvat-core/src/frames.ts index 0fb48c5482e3..a6aa0a668238 100644 --- a/cvat-core/src/frames.ts +++ b/cvat-core/src/frames.ts @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index 1a532bbcd317..e1e8f46c732a 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-core/tests/api/annotations.cjs b/cvat-core/tests/api/annotations.cjs index 488451e3d4ba..724ecfa06c95 100644 --- a/cvat-core/tests/api/annotations.cjs +++ b/cvat-core/tests/api/annotations.cjs @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-ui/src/base.scss b/cvat-ui/src/base.scss index 63ed8cdc8191..41589d11bd50 100644 --- a/cvat-ui/src/base.scss +++ b/cvat-ui/src/base.scss @@ -21,7 +21,6 @@ $background-color-1: white; $background-color-2: #f1f1f1; $notification-background-color-1: #d9ecff; $notification-border-color-1: #1890ff; -$important-info-background-color: #1890ff; $transparent-color: rgba(0, 0, 0, 0%); $player-slider-color: #979797; $player-buttons-color: #242424; diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/issues-list.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/issues-list.tsx index aec8f0375e37..463089d414ae 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/issues-list.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/issues-list.tsx @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx index cf0fe11b87ac..304e6a1f225c 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx index a3cb4bb366da..0ab291beca97 100644 --- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-ui/src/utils/filter-annotations.ts b/cvat-ui/src/utils/filter-annotations.ts index 0e167e7abbee..4e9b48c5b771 100644 --- a/cvat-ui/src/utils/filter-annotations.ts +++ b/cvat-ui/src/utils/filter-annotations.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT From 632cda1b4674a4b87792fd088492db485513a074 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 20 Feb 2024 00:36:46 +0200 Subject: [PATCH 25/43] Updated cscc --- .../annotation-page/single-shape-workspace/styles.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss index 201b2036993d..bb74da19b4cd 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss @@ -15,13 +15,13 @@ .cvat-single-shape-annotation-sidebar-label-select, .cvat-single-shape-annotation-sidebar-label-type-selector { .ant-select { - width: 160px; + width: $grid-unit-size * 20; } } .cvat-single-shape-annotation-sidebar-points-count-input { .ant-input-number { - width: 160px; + width: $grid-unit-size * 20; } } From 4e5b361aa2bdfb9f93c5a39895dd55e8e1591510 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 20 Feb 2024 09:36:35 +0200 Subject: [PATCH 26/43] Fixed test --- cvat-ui/src/actions/annotation-actions.ts | 8 ++++---- .../single-shape-sidebar/single-shape-sidebar.tsx | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 718cfd059d83..09508c7e1d81 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -1444,6 +1444,10 @@ export function redrawShapeAsync(): ThunkAction { const [state] = states.filter((_state: any): boolean => _state.clientID === activatedStateID); if (state && state.objectType !== ObjectType.TAG) { const activeControl = ShapeTypeToControl[state.shapeType as ShapeType] || ActiveControl.CURSOR; + if (canvasInstance instanceof Canvas) { + canvasInstance.cancel(); + } + dispatch({ type: AnnotationActionTypes.REPEAT_DRAW_SHAPE, payload: { @@ -1451,10 +1455,6 @@ export function redrawShapeAsync(): ThunkAction { }, }); - if (canvasInstance instanceof Canvas) { - canvasInstance.cancel(); - } - canvasInstance.draw({ skeletonSVG: state.shapeType === ShapeType.SKELETON ? state.label.structure.svg : undefined, enabled: true, diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index da73627f5492..8a2fdedd403f 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -230,7 +230,7 @@ function SingleShapeSidebar(): JSX.Element { const canvasInitializerRef = useRef<() => void | null>(() => {}); canvasInitializerRef.current = (): void => { const canvas = store.getState().annotation.canvas.instance as Canvas; - if (isCanvasReady && state.label && state.labelType !== LabelType.ANY) { + if (isCanvasReady && canvas.mode() !== CanvasMode.DRAW && state.label && state.labelType !== LabelType.ANY) { canvas.draw({ enabled: true, shapeType: state.labelType, @@ -261,13 +261,15 @@ function SingleShapeSidebar(): JSX.Element { // event in a usual scenario (when user drawn something) // but there are some cases when only canvas.cancel is triggered (e.g. when drawn shape was not correct) // in this case need to re-run drawing process - canvasInitializerRef.current(); + setTimeout(() => { + canvasInitializerRef.current(); + }); }; (canvas as Canvas).html().addEventListener('canvas.drawn', onDrawDone); (canvas as Canvas).html().addEventListener('canvas.canceled', onCancel); return (() => { - // should be prior mount use effect to remove event handlers before final cancel() is called + // should stay prior mount useEffect to remove event handlers before final cancel() is called (canvas as Canvas).html().removeEventListener('canvas.drawn', onDrawDone); (canvas as Canvas).html().removeEventListener('canvas.canceled', onCancel); From b29f747ad798c2e4593797e18e480b32ce9f7582 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 20 Feb 2024 09:57:37 +0200 Subject: [PATCH 27/43] Fixed corner case --- cvat-canvas/src/typescript/drawHandler.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index 1b6a7261c261..b7e9cbb90130 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -388,8 +388,11 @@ export class DrawHandlerImpl implements DrawHandler { } // Clear drawing this.drawInstance.draw('stop'); - } else if (this.drawInstance && this.drawData.shapeType === 'ellipse' && !this.drawData.initialState) { - this.drawInstance.fire('drawstop'); + } else { + this.onDrawDone(null); + if (this.drawInstance && this.drawData.shapeType === 'ellipse' && !this.drawData.initialState) { + this.drawInstance.fire('drawstop'); + } } if (this.pointsGroup) { From 3c313ddc09d2b374d77984625cbeb2ed27a3d778 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 20 Feb 2024 10:41:11 +0200 Subject: [PATCH 28/43] Fixed merge --- cvat-canvas/src/typescript/canvasView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index e2fea53e388e..d4ab384b69a5 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1663,8 +1663,8 @@ export class CanvasViewImpl implements CanvasView, Listener { if (data.enabled) { this.canvas.style.cursor = 'copy'; this.mode = Mode.MERGE; - this.mergeHandler.merge(data); } + this.mergeHandler.merge(data); } else if (reason === UpdateReasons.SPLIT) { const data: SplitData = this.controller.splitData; if (data.enabled) { From 0c70b8ff9b725abad14ce39062ee3be4d7652956 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 20 Feb 2024 20:55:05 +0200 Subject: [PATCH 29/43] Fixed several issues reported by Maxim --- .../single-shape-sidebar.tsx | 115 ++++++++++-------- .../single-shape-workspace/styles.scss | 7 ++ 2 files changed, 69 insertions(+), 53 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 8a2fdedd403f..07c2ac6fa67c 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -198,6 +198,7 @@ function SingleShapeSidebar(): JSX.Element { frames: [], }); + const savingRef = useRef(false); const nextFrame = useCallback((): boolean => { const next = state.frames.find((_frame) => _frame > frame); if (typeof next === 'number') { @@ -205,12 +206,15 @@ function SingleShapeSidebar(): JSX.Element { return true; } - if (state.saveOnFinish && jobInstance.annotations.hasUnsavedChanges()) { + if (state.saveOnFinish && jobInstance.annotations.hasUnsavedChanges() && !savingRef.current) { // if the latest image does not have objects to be annotated and user clicks "Next" // we should save the job if there are unsaved changes + savingRef.current = true; appDispatch(saveAnnotationsAsync()).then(() => { // update state to re-render component after the job saved unsaved changes dispatch(reducerActions.setFrames([...state.frames])); + }).finally(() => { + savingRef.current = false; }); } @@ -245,11 +249,7 @@ function SingleShapeSidebar(): JSX.Element { const onDrawDone = (): void => { setTimeout(() => { if (state.autoNextFrame) { - if (!nextFrame()) { - if (state.saveOnFinish) { - appDispatch(saveAnnotationsAsync()); - } - } + nextFrame(); } else { canvasInitializerRef.current(); } @@ -372,6 +372,7 @@ function SingleShapeSidebar(): JSX.Element { }, }; + const isPolylabel = [LabelType.POINTS, LabelType.POLYGON, LabelType.POLYLINE].includes(state.labelType); return ( @@ -420,16 +421,20 @@ function SingleShapeSidebar(): JSX.Element { {` ${normalizedKeyMap.UNDO} `} to undo a created object -
  • - Press - {` ${normalizedKeyMap.CANCEL} `} - to reset drawing process -
  • -
  • - Press - {` ${normalizedKeyMap.SWITCH_DRAW_MODE} `} - to finish drawing process -
  • + { (isPolylabel && (!state.pointsCountIsPredefined || state.pointsCount > 1)) && ( + <> +
  • + Press + {` ${normalizedKeyMap.CANCEL} `} + to reset drawing process +
  • +
  • + Press + {` ${normalizedKeyMap.SWITCH_DRAW_MODE} `} + to finish drawing process +
  • + + ) } )} /> @@ -482,6 +487,7 @@ function SingleShapeSidebar(): JSX.Element { { + (window.document.activeElement as HTMLInputElement)?.blur(); dispatch(reducerActions.switchAutoNextFrame()); }} > @@ -494,6 +500,7 @@ function SingleShapeSidebar(): JSX.Element { { + (window.document.activeElement as HTMLInputElement)?.blur(); dispatch(reducerActions.switchAutoSaveOnFinish()); }} > @@ -506,6 +513,7 @@ function SingleShapeSidebar(): JSX.Element { { + (window.document.activeElement as HTMLInputElement)?.blur(); dispatch(reducerActions.switchNavigateEmptyOnly()); }} > @@ -513,43 +521,44 @@ function SingleShapeSidebar(): JSX.Element { - - - { - dispatch(reducerActions.switchCountOfPointsIsPredefined()); - }} - > - Predefined number of points - - - - { state.label && - [LabelType.POLYGON, LabelType.POLYLINE, LabelType.POINTS].includes(state.labelType) && - state.pointsCountIsPredefined ? ( - <> - - - Number of points - - - - - { - if (value !== null) { - dispatch(reducerActions.setPointsCount(value)); - } - }} - /> - - - - ) : null } + { isPolylabel && ( + + + { + (window.document.activeElement as HTMLInputElement)?.blur(); + dispatch(reducerActions.switchCountOfPointsIsPredefined()); + }} + > + Predefined number of points + + + + )} + { isPolylabel && state.pointsCountIsPredefined ? ( + <> + + + Number of points + + + + + { + if (value !== null) { + dispatch(reducerActions.setPointsCount(value)); + } + }} + /> + + + + ) : null } diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss index bb74da19b4cd..b8e3bb702620 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss @@ -37,6 +37,13 @@ margin-top: $grid-unit-size; } + .cvat-single-shape-annotation-sidebar-auto-next-frame-checkbox, + .cvat-single-shape-annotation-sidebar-auto-save-checkbox, + .cvat-single-shape-annotation-sidebar-navigate-empty-checkbox, + .cvat-single-shape-annotation-sidebar-predefined-pounts-count-checkbox { + user-select: none; + } + .cvat-single-shape-annotation-sidebar-hint { row-gap: 0; text-align: center; From ce8b76e2f6c7c79efba160ba231e8da1c245fef1 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 20 Feb 2024 21:02:07 +0200 Subject: [PATCH 30/43] Fixed conditional rendering --- .../single-shape-sidebar.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 07c2ac6fa67c..434834673815 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -421,19 +421,20 @@ function SingleShapeSidebar(): JSX.Element { {` ${normalizedKeyMap.UNDO} `} to undo a created object + { (!isPolylabel || !state.pointsCountIsPredefined || state.pointsCount > 1) && ( +
  • + Press + {` ${normalizedKeyMap.CANCEL} `} + to reset drawing process +
  • + ) } + { (isPolylabel && (!state.pointsCountIsPredefined || state.pointsCount > 1)) && ( - <> -
  • - Press - {` ${normalizedKeyMap.CANCEL} `} - to reset drawing process -
  • -
  • - Press - {` ${normalizedKeyMap.SWITCH_DRAW_MODE} `} - to finish drawing process -
  • - +
  • + Press + {` ${normalizedKeyMap.SWITCH_DRAW_MODE} `} + to finish drawing process +
  • ) } )} From 59f475a04a00be4c74ca350b34b67c308f4c1618 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 20 Feb 2024 21:12:28 +0200 Subject: [PATCH 31/43] Added progress bar --- .../single-shape-sidebar/single-shape-sidebar.tsx | 10 ++++++++++ .../annotation-page/single-shape-workspace/styles.scss | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 434834673815..0464d3672452 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -16,6 +16,7 @@ import InputNumber from 'antd/lib/input-number'; import Select from 'antd/lib/select'; import Button from 'antd/lib/button'; import Alert from 'antd/lib/alert'; +import Progress from 'antd/lib/progress'; import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; import Icon from '@ant-design/icons/lib/components/Icon'; @@ -373,6 +374,9 @@ function SingleShapeSidebar(): JSX.Element { }; const isPolylabel = [LabelType.POINTS, LabelType.POLYGON, LabelType.POLYLINE].includes(state.labelType); + const progress = Math.round((state.frames.length ? + ((state.frames.indexOf(frame) + 1) * 100) / (state.frames.length || 1) : 0 + )); return ( @@ -402,6 +406,12 @@ function SingleShapeSidebar(): JSX.Element { )} /> + Date: Tue, 20 Feb 2024 21:19:32 +0200 Subject: [PATCH 32/43] Improved progress --- .../single-shape-sidebar/single-shape-sidebar.tsx | 7 +++---- .../annotation-page/single-shape-workspace/styles.scss | 6 ++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 0464d3672452..83bfe30a221f 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -374,9 +374,8 @@ function SingleShapeSidebar(): JSX.Element { }; const isPolylabel = [LabelType.POINTS, LabelType.POLYGON, LabelType.POLYLINE].includes(state.labelType); - const progress = Math.round((state.frames.length ? - ((state.frames.indexOf(frame) + 1) * 100) / (state.frames.length || 1) : 0 - )); + const imageIndex = state.frames.indexOf(frame) + 1; + const progress = Math.round((state.frames.length ? (imageIndex * 100) / (state.frames.length || 1) : 0)); return ( @@ -407,10 +406,10 @@ function SingleShapeSidebar(): JSX.Element { )} /> `${imageIndex} of ${state.frames.length || 1}`} /> Date: Tue, 20 Feb 2024 21:27:06 +0200 Subject: [PATCH 33/43] Removed extra code --- .../single-shape-sidebar.tsx | 29 ++----------------- .../single-shape-workspace/styles.scss | 7 ++++- 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 83bfe30a221f..2451f03d563a 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -17,7 +17,6 @@ import Select from 'antd/lib/select'; import Button from 'antd/lib/button'; import Alert from 'antd/lib/alert'; import Progress from 'antd/lib/progress'; -import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; import Icon from '@ant-design/icons/lib/components/Icon'; import { NextIcon, PreviousIcon } from 'icons'; @@ -32,7 +31,6 @@ import GlobalHotKeys from 'utils/mousetrap-react'; import CVATTooltip from 'components/common/cvat-tooltip'; enum ReducerActionType { - SWITCH_SIDEBAR_COLLAPSED = 'SWITCH_SIDEBAR_COLLAPSED', SWITCH_AUTO_NEXT_FRAME = 'SWITCH_AUTO_NEXT_FRAME', SWITCH_AUTOSAVE_ON_FINISH = 'SWITCH_AUTOSAVE_ON_FINISH', SWITCH_NAVIGATE_EMPTY_ONLY = 'SWITCH_NAVIGATE_EMPTY_ONLY', @@ -43,9 +41,6 @@ enum ReducerActionType { } export const reducerActions = { - switchSidebarCollapsed: () => ( - createAction(ReducerActionType.SWITCH_SIDEBAR_COLLAPSED) - ), switchAutoNextFrame: () => ( createAction(ReducerActionType.SWITCH_AUTO_NEXT_FRAME) ), @@ -73,7 +68,6 @@ export const reducerActions = { }; interface State { - sidebarCollabased: boolean; autoNextFrame: boolean; saveOnFinish: boolean; navigateOnlyEmpty: boolean; @@ -97,13 +91,6 @@ const reducer = (state: State, action: ActionUnion): Stat return minimalPoints; }; - if (action.type === ReducerActionType.SWITCH_SIDEBAR_COLLAPSED) { - return { - ...state, - sidebarCollabased: !state.sidebarCollabased, - }; - } - if (action.type === ReducerActionType.SWITCH_AUTO_NEXT_FRAME) { return { ...state, @@ -187,7 +174,6 @@ function SingleShapeSidebar(): JSX.Element { }), shallowEqual); const [state, dispatch] = useReducer(reducer, { - sidebarCollabased: false, autoNextFrame: true, saveOnFinish: true, navigateOnlyEmpty: true, @@ -344,7 +330,6 @@ function SingleShapeSidebar(): JSX.Element { reverseArrow: true, collapsible: true, trigger: null, - collapsed: state.sidebarCollabased, }; const subKeyMap = { @@ -379,17 +364,6 @@ function SingleShapeSidebar(): JSX.Element { return ( - {/* eslint-disable-next-line */} - { - dispatch(reducerActions.switchSidebarCollapsed()); - }} - > - {state.sidebarCollabased ? : } - { state.label !== null && state.labelType !== LabelType.ANY && ( @@ -413,12 +387,13 @@ function SingleShapeSidebar(): JSX.Element { />
  • Click {' Next '} - if already annotated or there is nothing to be annotated + if there is nothing to annotate
  • Hold diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss index 5ccc83f5f711..f1042d486fd7 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss @@ -48,7 +48,6 @@ row-gap: 0; text-align: center; font-size: large; - margin-top: $grid-unit-size * 5; margin-bottom: $grid-unit-size; } @@ -62,6 +61,12 @@ } } + .cvat-single-shape-annotation-sidebar-ux-hints { + ul { + padding-left: $grid-unit-size * 2; + } + } + .cvat-single-shape-annotation-sidebar-navigation-block { margin-top: $grid-unit-size * 4; padding-bottom: $grid-unit-size * 2; From ed749485a3cde73f826b235f7d6b398537e5e12a Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 20 Feb 2024 21:38:16 +0200 Subject: [PATCH 34/43] Adjusted save button --- .../single-shape-sidebar.tsx | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 2451f03d563a..6d7f49f63950 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -19,7 +19,7 @@ import Alert from 'antd/lib/alert'; import Progress from 'antd/lib/progress'; import Icon from '@ant-design/icons/lib/components/Icon'; -import { NextIcon, PreviousIcon } from 'icons'; +import { NextIcon, PreviousIcon, SaveIcon } from 'icons'; import { CombinedState } from 'reducers'; import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; import { Job, Label, LabelType } from 'cvat-core-wrapper'; @@ -358,9 +358,20 @@ function SingleShapeSidebar(): JSX.Element { }, }; + const isFirstFrame = state.frames[0] >= frame; + const isLastFrame = state.frames[state.frames.length - 1] <= frame; + const isPreviousDisabled = state.frames.length === 0 || isFirstFrame; + const isNextDisabled = state.frames.length === 0 || ( + isLastFrame && ( + !state.saveOnFinish || (state.saveOnFinish && + !jobInstance.annotations.hasUnsavedChanges() + ) + ) + ); const isPolylabel = [LabelType.POINTS, LabelType.POLYGON, LabelType.POLYLINE].includes(state.labelType); const imageIndex = state.frames.indexOf(frame) + 1; const progress = Math.round((state.frames.length ? (imageIndex * 100) / (state.frames.length || 1) : 0)); + return ( @@ -548,7 +559,7 @@ function SingleShapeSidebar(): JSX.Element { - + From 9cca32452eb0086824466d3e06843cbae123f305 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 21 Feb 2024 20:05:53 +0200 Subject: [PATCH 35/43] Applied changes from the latest meeting --- cvat-ui/src/actions/annotation-actions.ts | 11 + .../single-shape-sidebar.tsx | 212 +++++------------- .../single-shape-workspace/styles.scss | 29 +-- .../top-bar/player-buttons.tsx | 152 +++++++------ .../top-bar/player-navigation.tsx | 35 ++- .../annotation-page/top-bar/top-bar.tsx | 113 +++++----- .../annotation-page/top-bar/top-bar.tsx | 60 ++--- cvat-ui/src/reducers/annotation-reducer.ts | 11 + cvat-ui/src/reducers/index.ts | 7 + 9 files changed, 276 insertions(+), 354 deletions(-) diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 09508c7e1d81..091fe1e80d55 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -23,6 +23,7 @@ import { CombinedState, ContextMenuType, FrameSpeed, + NavigationType, ObjectType, OpenCVTool, Rotation, @@ -183,6 +184,7 @@ export enum AnnotationActionTypes { CANVAS_ERROR_OCCURRED = 'CANVAS_ERROR_OCCURRED', SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG = 'SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG', SWITCH_NAVIGATION_BLOCKED = 'SWITCH_NAVIGATION_BLOCKED', + SET_NAVIGATION_TYPE = 'SET_NAVIGATION_TYPE', DELETE_FRAME = 'DELETE_FRAME', DELETE_FRAME_SUCCESS = 'DELETE_FRAME_SUCCESS', DELETE_FRAME_FAILED = 'DELETE_FRAME_FAILED', @@ -1485,6 +1487,15 @@ export function switchNavigationBlocked(navigationBlocked: boolean): AnyAction { }; } +export function setNavigationType(navigationType: NavigationType): AnyAction { + return { + type: AnnotationActionTypes.SET_NAVIGATION_TYPE, + payload: { + navigationType, + }, + }; +} + export function deleteFrameAsync(frame: number): ThunkAction { return async (dispatch: ActionCreator): Promise => { const { jobInstance } = receiveAnnotationsParameters(); diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 6d7f49f63950..a50a39d1dde0 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -11,33 +11,28 @@ import React, { import Layout, { SiderProps } from 'antd/lib/layout'; import { Row, Col } from 'antd/lib/grid'; import Text from 'antd/lib/typography/Text'; -import Checkbox from 'antd/lib/checkbox'; +import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox'; import InputNumber from 'antd/lib/input-number'; import Select from 'antd/lib/select'; -import Button from 'antd/lib/button'; import Alert from 'antd/lib/alert'; -import Progress from 'antd/lib/progress'; -import Icon from '@ant-design/icons/lib/components/Icon'; +import Modal from 'antd/lib/modal'; -import { NextIcon, PreviousIcon, SaveIcon } from 'icons'; -import { CombinedState } from 'reducers'; +import { CombinedState, NavigationType } from 'reducers'; import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; -import { Job, Label, LabelType } from 'cvat-core-wrapper'; +import { + Job, JobState, Label, LabelType, +} from 'cvat-core-wrapper'; import { ActionUnion, createAction } from 'utils/redux'; -import { changeFrameAsync, saveAnnotationsAsync } from 'actions/annotation-actions'; +import { changeFrameAsync, saveAnnotationsAsync, setNavigationType } from 'actions/annotation-actions'; import LabelSelector from 'components/label-selector/label-selector'; - import GlobalHotKeys from 'utils/mousetrap-react'; -import CVATTooltip from 'components/common/cvat-tooltip'; enum ReducerActionType { SWITCH_AUTO_NEXT_FRAME = 'SWITCH_AUTO_NEXT_FRAME', SWITCH_AUTOSAVE_ON_FINISH = 'SWITCH_AUTOSAVE_ON_FINISH', - SWITCH_NAVIGATE_EMPTY_ONLY = 'SWITCH_NAVIGATE_EMPTY_ONLY', SWITCH_COUNT_OF_POINTS_IS_PREDEFINED = 'SWITCH_COUNT_OF_POINTS_IS_PREDEFINED', SET_ACTIVE_LABEL = 'SET_ACTIVE_LABEL', SET_POINTS_COUNT = 'SET_POINTS_COUNT', - SET_FRAMES = 'SET_FRAMES', } export const reducerActions = { @@ -47,9 +42,6 @@ export const reducerActions = { switchAutoSaveOnFinish: () => ( createAction(ReducerActionType.SWITCH_AUTOSAVE_ON_FINISH) ), - switchNavigateEmptyOnly: () => ( - createAction(ReducerActionType.SWITCH_NAVIGATE_EMPTY_ONLY) - ), switchCountOfPointsIsPredefined: () => ( createAction(ReducerActionType.SWITCH_COUNT_OF_POINTS_IS_PREDEFINED) ), @@ -62,21 +54,16 @@ export const reducerActions = { setPointsCount: (pointsCount: number) => ( createAction(ReducerActionType.SET_POINTS_COUNT, { pointsCount }) ), - setFrames: (frames: number[]) => ( - createAction(ReducerActionType.SET_FRAMES, { frames }) - ), }; interface State { autoNextFrame: boolean; saveOnFinish: boolean; - navigateOnlyEmpty: boolean; pointsCountIsPredefined: boolean; pointsCount: number; labels: Label[]; label: Label | null; labelType: LabelType; - frames: number[]; } const reducer = (state: State, action: ActionUnion): State => { @@ -98,13 +85,6 @@ const reducer = (state: State, action: ActionUnion): Stat }; } - if (action.type === ReducerActionType.SWITCH_NAVIGATE_EMPTY_ONLY) { - return { - ...state, - navigateOnlyEmpty: !state.navigateOnlyEmpty, - }; - } - if (action.type === ReducerActionType.SWITCH_AUTOSAVE_ON_FINISH) { return { ...state, @@ -135,13 +115,6 @@ const reducer = (state: State, action: ActionUnion): Stat }; } - if (action.type === ReducerActionType.SET_FRAMES) { - return { - ...state, - frames: action.payload.frames, - }; - } - return state; }; @@ -163,6 +136,7 @@ function SingleShapeSidebar(): JSX.Element { keyMap, defaultLabel, defaultPointsCount, + navigationType, } = useSelector((_state: CombinedState) => ({ isCanvasReady: _state.annotation.canvas.ready, jobInstance: _state.annotation.job.instance as Job, @@ -171,52 +145,54 @@ function SingleShapeSidebar(): JSX.Element { normalizedKeyMap: _state.shortcuts.normalizedKeyMap, defaultLabel: _state.annotation.job.queryParameters.defaultLabel, defaultPointsCount: _state.annotation.job.queryParameters.defaultPointsCount, + navigationType: _state.annotation.player.navigationType, }), shallowEqual); const [state, dispatch] = useReducer(reducer, { autoNextFrame: true, saveOnFinish: true, - navigateOnlyEmpty: true, pointsCountIsPredefined: true, pointsCount: defaultPointsCount || 1, labels: jobInstance.labels.filter((label) => label.type !== LabelType.TAG && label.type !== LabelType.SKELETON), label: null, labelType: jobInstance.labels[0]?.type || LabelType.ANY, - frames: [], }); const savingRef = useRef(false); - const nextFrame = useCallback((): boolean => { - const next = state.frames.find((_frame) => _frame > frame); - if (typeof next === 'number') { - appDispatch(changeFrameAsync(next)); - return true; - } - - if (state.saveOnFinish && jobInstance.annotations.hasUnsavedChanges() && !savingRef.current) { - // if the latest image does not have objects to be annotated and user clicks "Next" - // we should save the job if there are unsaved changes - savingRef.current = true; - appDispatch(saveAnnotationsAsync()).then(() => { - // update state to re-render component after the job saved unsaved changes - dispatch(reducerActions.setFrames([...state.frames])); - }).finally(() => { - savingRef.current = false; - }); - } - - return false; - }, [state.frames, state.saveOnFinish, frame, jobInstance]); - - const prevFrame = useCallback((): boolean => { - const prev = state.frames.findLast((_frame) => _frame < frame); - if (typeof prev === 'number') { - appDispatch(changeFrameAsync(prev)); - return true; - } - - return false; - }, [state.frames, frame]); + const nextFrame = useCallback((): void => { + (frame < jobInstance.stopFrame ? jobInstance.annotations.search(frame + 1, jobInstance.stopFrame, { + allowDeletedFrames: false, + ...(navigationType === NavigationType.EMPTY ? { + generalFilters: { + isEmptyFrame: true, + }, + } : {}), + }) : Promise.resolve(null)).then((foundFrame: number | null) => { + if (typeof foundFrame === 'number') { + appDispatch(changeFrameAsync(foundFrame)); + } else if (state.saveOnFinish && jobInstance.annotations.hasUnsavedChanges() && !savingRef.current) { + savingRef.current = true; + Modal.confirm({ + title: 'You finished the job', + content: 'Please, confirm further action', + cancelText: 'Stay on the page', + okText: 'Submit results', + onOk: () => { + appDispatch(saveAnnotationsAsync()).then(() => { + jobInstance.state = JobState.COMPLETED; + return jobInstance.save(); + }).then(() => { + Modal.info({ + closable: false, + title: 'Annotations submitted', + content: 'You may close the window', + }); + }); + }, + }); + } + }); + }, [state.saveOnFinish, frame, jobInstance, navigationType]); const canvasInitializerRef = useRef<() => void | null>(() => {}); canvasInitializerRef.current = (): void => { @@ -270,6 +246,7 @@ function SingleShapeSidebar(): JSX.Element { dispatch(reducerActions.setActiveLabel(labelInstance)); } + appDispatch(setNavigationType(NavigationType.EMPTY)); cancelCurrentCanvasOp(store.getState()); return () => { cancelCurrentCanvasOp(store.getState()); @@ -281,40 +258,6 @@ function SingleShapeSidebar(): JSX.Element { canvasInitializerRef.current(); }, [isCanvasReady, state.label, state.labelType, state.pointsCount, state.pointsCountIsPredefined]); - useEffect(() => { - (async () => { - const framesToBeVisited = []; - - let searchFrom: number | null = jobInstance.startFrame; - while (searchFrom !== null) { - const foundFrame: number | null = await jobInstance.annotations.search( - searchFrom, - jobInstance.stopFrame, - { - allowDeletedFrames: false, - ...(state.navigateOnlyEmpty ? { - generalFilters: { - isEmptyFrame: true, - }, - } : {}), - }, - ); - - if (foundFrame !== null) { - framesToBeVisited.push(foundFrame); - searchFrom = foundFrame < jobInstance.stopFrame ? foundFrame + 1 : null; - } else { - searchFrom = null; - } - } - - dispatch(reducerActions.setFrames(framesToBeVisited)); - if (framesToBeVisited.length) { - appDispatch(changeFrameAsync(framesToBeVisited[0])); - } - })(); - }, [state.navigateOnlyEmpty]); - let message = ''; if (state.labelType === LabelType.POINTS) { message = `${state.pointsCount === 1 ? 'one point' : `${state.pointsCount} points`}`; @@ -334,23 +277,14 @@ function SingleShapeSidebar(): JSX.Element { const subKeyMap = { CANCEL: keyMap.CANCEL, - NEXT_FRAME: keyMap.NEXT_FRAME, - PREV_FRAME: keyMap.PREV_FRAME, SWITCH_DRAW_MODE: keyMap.SWITCH_DRAW_MODE, }; + const handlers = { CANCEL: (event: KeyboardEvent | undefined) => { event?.preventDefault(); (store.getState().annotation.canvas.instance as Canvas).cancel(); }, - NEXT_FRAME: (event: KeyboardEvent | undefined) => { - event?.preventDefault(); - nextFrame(); - }, - PREV_FRAME: (event: KeyboardEvent | undefined) => { - event?.preventDefault(); - prevFrame(); - }, SWITCH_DRAW_MODE: (event: KeyboardEvent | undefined) => { event?.preventDefault(); const canvas = store.getState().annotation.canvas.instance as Canvas; @@ -358,20 +292,7 @@ function SingleShapeSidebar(): JSX.Element { }, }; - const isFirstFrame = state.frames[0] >= frame; - const isLastFrame = state.frames[state.frames.length - 1] <= frame; - const isPreviousDisabled = state.frames.length === 0 || isFirstFrame; - const isNextDisabled = state.frames.length === 0 || ( - isLastFrame && ( - !state.saveOnFinish || (state.saveOnFinish && - !jobInstance.annotations.hasUnsavedChanges() - ) - ) - ); const isPolylabel = [LabelType.POINTS, LabelType.POLYGON, LabelType.POLYLINE].includes(state.labelType); - const imageIndex = state.frames.indexOf(frame) + 1; - const progress = Math.round((state.frames.length ? (imageIndex * 100) / (state.frames.length || 1) : 0)); - return ( @@ -390,12 +311,6 @@ function SingleShapeSidebar(): JSX.Element { )} /> - `${imageIndex} of ${state.frames.length || 1}`} - /> { + checked={navigationType === NavigationType.EMPTY} + onChange={(event: CheckboxChangeEvent): void => { (window.document.activeElement as HTMLInputElement)?.blur(); - dispatch(reducerActions.switchNavigateEmptyOnly()); + if (event.target.checked) { + appDispatch(setNavigationType(NavigationType.EMPTY)); + } else { + appDispatch(setNavigationType(NavigationType.REGULAR)); + } }} > Navigate only empty frames @@ -555,33 +474,6 @@ function SingleShapeSidebar(): JSX.Element { ) : null } - - - - - - - - - - ); } diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss index f1042d486fd7..b6b241199c67 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss @@ -30,13 +30,16 @@ .cvat-single-shape-annotation-sidebar-points-count, .cvat-single-shape-annotation-sidebar-label-select, .cvat-single-shape-annotation-sidebar-points-count-input, - .cvat-single-shape-annotation-sidebar-auto-next-frame-checkbox, .cvat-single-shape-annotation-sidebar-navigate-empty-checkbox, .cvat-single-shape-annotation-sidebar-predefined-pounts-count-checkbox, .cvat-single-shape-annotation-sidebar-auto-save-checkbox { margin-top: $grid-unit-size; } + .cvat-single-shape-annotation-sidebar-auto-next-frame-checkbox { + margin-top: $grid-unit-size * 3; + } + .cvat-single-shape-annotation-sidebar-auto-next-frame-checkbox, .cvat-single-shape-annotation-sidebar-auto-save-checkbox, .cvat-single-shape-annotation-sidebar-navigate-empty-checkbox, @@ -51,33 +54,9 @@ margin-bottom: $grid-unit-size; } - .cvat-single-shape-annotation-sidebar-progress { - margin-bottom: $grid-unit-size; - text-align: center; - - .ant-progress-outer { - margin-right: 0; - padding-right: 0; - } - } - .cvat-single-shape-annotation-sidebar-ux-hints { ul { padding-left: $grid-unit-size * 2; } } - - .cvat-single-shape-annotation-sidebar-navigation-block { - margin-top: $grid-unit-size * 4; - padding-bottom: $grid-unit-size * 2; - - > div:nth-child(1) { - display: flex; - justify-content: space-between; - - button { - width: $grid-unit-size * 16; - } - } - } } diff --git a/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx b/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx index a94a7a0977a2..07eac1f5a794 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx @@ -2,13 +2,14 @@ // // SPDX-License-Identifier: MIT -import React from 'react'; +import React, { CSSProperties } from 'react'; import { Col } from 'antd/lib/grid'; import Icon from '@ant-design/icons'; import Popover from 'antd/lib/popover'; import CVATTooltip from 'components/common/cvat-tooltip'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; +import { NavigationType, Workspace } from 'reducers'; import { FirstIcon, BackJumpIcon, @@ -31,9 +32,9 @@ interface Props { previousFrameShortcut: string; forwardShortcut: string; backwardShortcut: string; - prevButtonType: string; - nextButtonType: string; keyMap: KeyMap; + workspace: Workspace; + navigationType: NavigationType; onSwitchPlay(): void; onPrevFrame(): void; onNextFrame(): void; @@ -42,8 +43,7 @@ interface Props { onFirstFrame(): void; onLastFrame(): void; onSearchAnnotations(direction: 'forward' | 'backward'): void; - setPrevButton(type: 'regular' | 'filtered' | 'empty'): void; - setNextButton(type: 'regular' | 'filtered' | 'empty'): void; + setNavigationType(navigationType: NavigationType): void; } function PlayerButtons(props: Props): JSX.Element { @@ -54,9 +54,9 @@ function PlayerButtons(props: Props): JSX.Element { previousFrameShortcut, forwardShortcut, backwardShortcut, - prevButtonType, - nextButtonType, keyMap, + navigationType, + workspace, onSwitchPlay, onPrevFrame, onNextFrame, @@ -64,20 +64,21 @@ function PlayerButtons(props: Props): JSX.Element { onBackward, onFirstFrame, onLastFrame, + setNavigationType, onSearchAnnotations, - setPrevButton, - setNextButton, } = props; const subKeyMap = { NEXT_FRAME: keyMap.NEXT_FRAME, PREV_FRAME: keyMap.PREV_FRAME, - FORWARD_FRAME: keyMap.FORWARD_FRAME, - BACKWARD_FRAME: keyMap.BACKWARD_FRAME, - SEARCH_FORWARD: keyMap.SEARCH_FORWARD, - SEARCH_BACKWARD: keyMap.SEARCH_BACKWARD, - PLAY_PAUSE: keyMap.PLAY_PAUSE, - FOCUS_INPUT_FRAME: keyMap.FOCUS_INPUT_FRAME, + ...(workspace !== Workspace.SINGLE_SHAPE ? { + FORWARD_FRAME: keyMap.FORWARD_FRAME, + BACKWARD_FRAME: keyMap.BACKWARD_FRAME, + SEARCH_FORWARD: keyMap.SEARCH_FORWARD, + SEARCH_BACKWARD: keyMap.SEARCH_BACKWARD, + PLAY_PAUSE: keyMap.PLAY_PAUSE, + FOCUS_INPUT_FRAME: keyMap.FOCUS_INPUT_FRAME, + } : {}), }; const handlers = { @@ -89,26 +90,28 @@ function PlayerButtons(props: Props): JSX.Element { event?.preventDefault(); onPrevFrame(); }, - FORWARD_FRAME: (event: KeyboardEvent | undefined) => { - event?.preventDefault(); - onForward(); - }, - BACKWARD_FRAME: (event: KeyboardEvent | undefined) => { - event?.preventDefault(); - onBackward(); - }, - SEARCH_FORWARD: (event: KeyboardEvent | undefined) => { - event?.preventDefault(); - onSearchAnnotations('forward'); - }, - SEARCH_BACKWARD: (event: KeyboardEvent | undefined) => { - event?.preventDefault(); - onSearchAnnotations('backward'); - }, - PLAY_PAUSE: (event: KeyboardEvent | undefined) => { - event?.preventDefault(); - onSwitchPlay(); - }, + ...(workspace !== Workspace.SINGLE_SHAPE ? { + FORWARD_FRAME: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + onForward(); + }, + BACKWARD_FRAME: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + onBackward(); + }, + SEARCH_FORWARD: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + onSearchAnnotations('forward'); + }, + SEARCH_BACKWARD: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + onSearchAnnotations('backward'); + }, + PLAY_PAUSE: (event: KeyboardEvent | undefined) => { + event?.preventDefault(); + onSwitchPlay(); + }, + } : {}), }; const prevRegularText = 'Go back'; @@ -120,7 +123,7 @@ function PlayerButtons(props: Props): JSX.Element { let prevButton = ; let prevButtonTooltipMessage = prevRegularText; - if (prevButtonType === 'filtered') { + if (navigationType === NavigationType.FILTERED) { prevButton = ( ); prevButtonTooltipMessage = prevFilteredText; - } else if (prevButtonType === 'empty') { + } else if (navigationType === NavigationType.EMPTY) { prevButton = ( ); @@ -138,24 +141,39 @@ function PlayerButtons(props: Props): JSX.Element { let nextButton = ; let nextButtonTooltipMessage = nextRegularText; - if (nextButtonType === 'filtered') { + if (navigationType === NavigationType.FILTERED) { nextButton = ( ); nextButtonTooltipMessage = nextFilteredText; - } else if (nextButtonType === 'empty') { + } else if (navigationType === NavigationType.EMPTY) { nextButton = ; nextButtonTooltipMessage = nextEmptyText; } + const disabledStyle: CSSProperties = { + pointerEvents: 'none', + opacity: 0.5, + }; + return ( - + - + { - setPrevButton('regular'); - }} + onClick={() => setNavigationType(NavigationType.REGULAR)} /> { - setPrevButton('filtered'); - }} + onClick={() => setNavigationType(NavigationType.FILTERED)} /> { - setPrevButton('empty'); - }} + onClick={() => setNavigationType(NavigationType.EMPTY)} /> @@ -199,11 +211,21 @@ function PlayerButtons(props: Props): JSX.Element { {!playing ? ( - + ) : ( - + )} @@ -216,27 +238,21 @@ function PlayerButtons(props: Props): JSX.Element { { - setNextButton('regular'); - }} + onClick={() => setNavigationType(NavigationType.REGULAR)} /> { - setNextButton('filtered'); - }} + onClick={() => setNavigationType(NavigationType.FILTERED)} /> { - setNextButton('empty'); - }} + onClick={() => setNavigationType(NavigationType.EMPTY)} /> @@ -247,10 +263,20 @@ function PlayerButtons(props: Props): JSX.Element { - + - + ); 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 dbc254bb6d07..e9aceaa32c9f 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 @@ -3,12 +3,13 @@ // // SPDX-License-Identifier: MIT -import React, { useState, useEffect, useCallback } from 'react'; +import React, { + useState, useEffect, useCallback, CSSProperties, +} from 'react'; import { Row, Col } from 'antd/lib/grid'; import Icon, { LinkOutlined, DeleteOutlined } from '@ant-design/icons'; import Slider from 'antd/lib/slider'; import InputNumber from 'antd/lib/input-number'; -import Input from 'antd/lib/input'; import Text from 'antd/lib/typography/Text'; import Modal from 'antd/lib/modal'; @@ -16,6 +17,7 @@ import { RestoreIcon } from 'icons'; import CVATTooltip from 'components/common/cvat-tooltip'; import { clamp } from 'utils/math'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; +import { Workspace } from 'reducers'; interface Props { startFrame: number; @@ -28,8 +30,9 @@ interface Props { deleteFrameAvailable: boolean; deleteFrameShortcut: string; focusFrameInputShortcut: string; - inputFrameRef: React.RefObject; + inputFrameRef: React.RefObject; keyMap: KeyMap; + workspace: Workspace; onSliderChange(value: number): void; onInputChange(value: number): void; onURLIconClick(): void; @@ -51,13 +54,14 @@ function PlayerNavigation(props: Props): JSX.Element { inputFrameRef, ranges, keyMap, + workspace, + deleteFrameAvailable, onSliderChange, onInputChange, onURLIconClick, onDeleteFrame, onRestoreFrame, switchNavigationBlocked, - deleteFrameAvailable, } = props; const [frameInputValue, setFrameInputValue] = useState(frameNumber); @@ -106,19 +110,33 @@ function PlayerNavigation(props: Props): JSX.Element { }, }; + const disabledStyle: CSSProperties = { + pointerEvents: 'none', + opacity: 0.5, + }; + const deleteFrameIcon = !frameDeleted ? ( - + ) : ( - + ); return ( <> - + { workspace !== Workspace.SINGLE_SHAPE && } @@ -127,7 +145,7 @@ function PlayerNavigation(props: Props): JSX.Element { min={startFrame} max={stopFrame} value={frameNumber || 0} - onChange={onSliderChange} + onChange={workspace !== Workspace.SINGLE_SHAPE ? onSliderChange : undefined} /> {!!ranges && ( @@ -170,6 +188,7 @@ function PlayerNavigation(props: Props): JSX.Element { ref={inputFrameRef} className='cvat-player-frame-selector' type='number' + disabled={workspace === Workspace.SINGLE_SHAPE} value={frameInputValue} onChange={(value: number | undefined | string | null) => { if (typeof value !== 'undefined' && value !== null) { 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 05ee3cb49b10..7af232e3332e 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 @@ -4,11 +4,10 @@ // SPDX-License-Identifier: MIT import React from 'react'; -import Input from 'antd/lib/input'; import { Col, Row } from 'antd/lib/grid'; import { - ActiveControl, CombinedState, ToolsBlockerState, Workspace, + ActiveControl, CombinedState, NavigationType, ToolsBlockerState, Workspace, } from 'reducers'; import { Job } from 'cvat-core-wrapper'; import { usePlugins } from 'utils/hooks'; @@ -24,7 +23,7 @@ interface Props { frameNumber: number; frameFilename: string; frameDeleted: boolean; - inputFrameRef: React.RefObject
  • Click - {' Next '} + {' Skip '} if there is nothing to annotate
  • diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss index b6b241199c67..ff2772f57fd3 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/styles.scss @@ -59,4 +59,12 @@ padding-left: $grid-unit-size * 2; } } + + .cvat-single-shape-annotation-sidebar-skip-wrapper { + margin-bottom: $grid-unit-size; + + button { + width: $grid-unit-size * 35.5; + } + } } From c1bc40733d6c609e3440c1299ec86114f9023589 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 22 Feb 2024 12:25:42 +0200 Subject: [PATCH 39/43] Fixed empty labels --- .../single-shape-sidebar.tsx | 78 +++++++++++++------ .../single-shape-workspace/styles.scss | 6 ++ 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 8243cefc5091..15b041011f57 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -156,7 +156,7 @@ function SingleShapeSidebar(): JSX.Element { pointsCount: defaultPointsCount || 1, labels: jobInstance.labels.filter((label) => label.type !== LabelType.TAG && label.type !== LabelType.SKELETON), label: null, - labelType: jobInstance.labels[0]?.type || LabelType.ANY, + labelType: LabelType.ANY, }); const savingRef = useRef(false); @@ -176,24 +176,35 @@ function SingleShapeSidebar(): JSX.Element { promise.then((foundFrame: number | null) => { if (typeof foundFrame === 'number') { appDispatch(changeFrameAsync(foundFrame)); - } else if (state.saveOnFinish && jobInstance.annotations.hasUnsavedChanges() && !savingRef.current) { - savingRef.current = true; + } else if (state.saveOnFinish && !savingRef.current) { Modal.confirm({ title: 'You finished the job', content: 'Please, confirm further action', cancelText: 'Stay on the page', okText: 'Submit results', onOk: () => { - appDispatch(saveAnnotationsAsync()).then(() => { - jobInstance.state = JobState.COMPLETED; - return jobInstance.save(); - }).then(() => { + function reset(): void { + savingRef.current = false; + } + + function showSubmittedInfo(): void { Modal.info({ closable: false, title: 'Annotations submitted', content: 'You may close the window', }); - }); + } + + savingRef.current = true; + if (jobInstance.annotations.hasUnsavedChanges()) { + appDispatch(saveAnnotationsAsync(() => { + jobInstance.state = JobState.COMPLETED; + jobInstance.save().then(showSubmittedInfo).finally(reset); + })).catch(reset); + } else { + jobInstance.state = JobState.COMPLETED; + jobInstance.save().then(showSubmittedInfo).finally(reset); + } }, }); } @@ -247,7 +258,7 @@ function SingleShapeSidebar(): JSX.Element { useEffect(() => { const labelInstance = (defaultLabel ? jobInstance.labels - .find((_label) => _label.name === defaultLabel) : jobInstance.labels[0]); + .find((_label) => _label.name === defaultLabel) : state.labels[0] || null); if (labelInstance) { dispatch(reducerActions.setActiveLabel(labelInstance)); } @@ -298,7 +309,20 @@ function SingleShapeSidebar(): JSX.Element { }, }; + if (!state.labels.length) { + return ( + +
    + No available labels found +
    +
    + ); + } + const isPolylabel = [LabelType.POINTS, LabelType.POLYGON, LabelType.POLYLINE].includes(state.labelType); + const withLabelsSelector = state.labels.length > 1; + const withLabelTypeSelector = state.label && state.label.type === 'any'; + return ( @@ -371,21 +395,25 @@ function SingleShapeSidebar(): JSX.Element { )} - - - Label selector - - - - - dispatch(reducerActions.setActiveLabel(label))} - /> - - - { state.label && state.label.type === 'any' ? ( + { withLabelsSelector && ( + <> + + + Label selector + + + + + dispatch(reducerActions.setActiveLabel(label))} + /> + + + + )} + { withLabelTypeSelector && ( <> @@ -411,7 +439,7 @@ function SingleShapeSidebar(): JSX.Element { - ) : null } + )} Date: Thu, 22 Feb 2024 12:42:14 +0200 Subject: [PATCH 40/43] Added return to initial navitation type --- cvat-canvas/src/typescript/masksHandler.ts | 1 - .../single-shape-sidebar/single-shape-sidebar.tsx | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cvat-canvas/src/typescript/masksHandler.ts b/cvat-canvas/src/typescript/masksHandler.ts index 74edc885800e..72530039f379 100644 --- a/cvat-canvas/src/typescript/masksHandler.ts +++ b/cvat-canvas/src/typescript/masksHandler.ts @@ -138,7 +138,6 @@ export class MasksHandlerImpl implements MasksHandler { this.isInsertion = false; this.redraw = null; this.drawnObjects = this.createDrawnObjectsArray(); - this.onDrawDone(null); } private releaseEdit(): void { diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 15b041011f57..91d9a74a73c2 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -65,6 +65,7 @@ interface State { labels: Label[]; label: Label | null; labelType: LabelType; + initialNavigationType: NavigationType; } const reducer = (state: State, action: ActionUnion): State => { @@ -157,6 +158,7 @@ function SingleShapeSidebar(): JSX.Element { labels: jobInstance.labels.filter((label) => label.type !== LabelType.TAG && label.type !== LabelType.SKELETON), label: null, labelType: LabelType.ANY, + initialNavigationType: navigationType, }); const savingRef = useRef(false); @@ -266,6 +268,7 @@ function SingleShapeSidebar(): JSX.Element { appDispatch(setNavigationType(NavigationType.EMPTY)); cancelCurrentCanvasOp(store.getState()); return () => { + appDispatch(setNavigationType(state.initialNavigationType)); cancelCurrentCanvasOp(store.getState()); }; }, []); From aae100f5d0e51bdd4c042d4f3df80c6b05f25999 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 22 Feb 2024 12:46:47 +0200 Subject: [PATCH 41/43] Correct unmounting --- .../annotation-page/canvas/views/canvas2d/brush-tools.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-tools.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-tools.tsx index 0b7883ada510..142967634002 100644 --- a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-tools.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-tools.tsx @@ -132,6 +132,10 @@ function BrushTools(): React.ReactPortal | null { const { offsetTop, offsetLeft } = canvasContainer.parentElement as HTMLElement; setTopLeft([offsetTop, offsetLeft]); } + + return () => { + dispatch(updateCanvasBrushTools({ visible: false })); + }; }, []); useEffect(() => { From 68fe4846b6c344ec093da2f7c1a4f9c42a2a191e Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 22 Feb 2024 13:38:26 +0200 Subject: [PATCH 42/43] Fixed tests --- cvat-canvas/src/typescript/masksHandler.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cvat-canvas/src/typescript/masksHandler.ts b/cvat-canvas/src/typescript/masksHandler.ts index 72530039f379..7a12789ee276 100644 --- a/cvat-canvas/src/typescript/masksHandler.ts +++ b/cvat-canvas/src/typescript/masksHandler.ts @@ -613,6 +613,8 @@ export class MasksHandlerImpl implements MasksHandler { ...(Number.isInteger(this.redraw) ? { clientID: this.redraw } : {}), }, Date.now() - this.startTimestamp, drawData.continue, this.drawData); } + } else { + this.onDrawDone(null); } } finally { this.releaseDraw(); From bf1dcdfccda3458862c089c1a3f3796d6354dded Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 23 Feb 2024 12:03:20 +0200 Subject: [PATCH 43/43] Renamed checkbox --- .../single-shape-sidebar/single-shape-sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx index 91d9a74a73c2..fd02f84e5d3b 100644 --- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx @@ -465,7 +465,7 @@ function SingleShapeSidebar(): JSX.Element { dispatch(reducerActions.switchAutoSaveOnFinish()); }} > - Automatically save after the latest frame + Automatically save when finish