diff --git a/CHANGELOG.md b/CHANGELOG.md index f22a8d61b455..17daf8b5ce3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added ability to export project as a dataset () and project with 3D tasks () - Additional inline tips in interactors with demo gifs () +- Added intelligent scissors blocking feature () ### Changed diff --git a/cvat-canvas/package-lock.json b/cvat-canvas/package-lock.json index e0a9208f81fe..8fde0d749f01 100644 --- a/cvat-canvas/package-lock.json +++ b/cvat-canvas/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index ad5dd2897ff2..ce264aaa2a46 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.6.0", + "version": "2.7.0", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", "scripts": { diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index 7732fea355a2..e59461f0bbbd 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -87,6 +87,7 @@ export interface InteractionData { shapeType: string; points: number[]; }; + onChangeToolsBlockerState?: (event: string) => void; } export interface InteractionResult { @@ -565,15 +566,14 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { if (![Mode.IDLE, Mode.INTERACT].includes(this.data.mode)) { throw Error(`Canvas is busy. Action: ${this.data.mode}`); } - - if (interactionData.enabled && !interactionData.intermediateShape) { + const thresholdChanged = this.data.interactionData.enableThreshold !== interactionData.enableThreshold; + if (interactionData.enabled && !interactionData.intermediateShape && !thresholdChanged) { if (this.data.interactionData.enabled) { throw new Error('Interaction has been already started'); } else if (!interactionData.shapeType) { throw new Error('A shape type was not specified'); } } - this.data.interactionData = interactionData; if (typeof this.data.interactionData.crosshair !== 'boolean') { this.data.interactionData.crosshair = true; diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 1eceef6daf31..40f864ed9085 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1279,7 +1279,9 @@ export class CanvasViewImpl implements CanvasView, Listener { } this.interactionHandler.interact(data); } else { - this.canvas.style.cursor = ''; + if (!data.enabled) { + this.canvas.style.cursor = ''; + } if (this.mode !== Mode.IDLE) { this.interactionHandler.interact(data); } @@ -1569,7 +1571,6 @@ export class CanvasViewImpl implements CanvasView, Listener { private addObjects(states: any[]): void { const { displayAllText } = this.configuration; - for (const state of states) { const points: number[] = state.points as number[]; const translatedPoints: number[] = this.translateToCanvas(points); diff --git a/cvat-canvas/src/typescript/interactionHandler.ts b/cvat-canvas/src/typescript/interactionHandler.ts index b7ede60495c7..37cedd981d64 100644 --- a/cvat-canvas/src/typescript/interactionHandler.ts +++ b/cvat-canvas/src/typescript/interactionHandler.ts @@ -8,6 +8,7 @@ import Crosshair from './crosshair'; import { translateToSVG, PropType, stringifyPoints, translateToCanvas, } from './shared'; + import { InteractionData, InteractionResult, Geometry, Configuration, } from './canvasModel'; @@ -34,6 +35,7 @@ export class InteractionHandlerImpl implements InteractionHandler { private thresholdRectSize: number; private intermediateShape: PropType; private drawnIntermediateShape: SVG.Shape; + private thresholdWasModified: boolean; private prepareResult(): InteractionResult[] { return this.interactionShapes.map( @@ -141,14 +143,15 @@ export class InteractionHandlerImpl implements InteractionHandler { _e.preventDefault(); _e.stopPropagation(); self.remove(); + this.shapesWereUpdated = true; + const shouldRaiseEvent = this.shouldRaiseEvent(_e.ctrlKey); this.interactionShapes = this.interactionShapes.filter( (shape: SVG.Shape): boolean => shape !== self, ); if (this.interactionData.startWithBox && this.interactionShapes.length === 1) { this.interactionShapes[0].style({ visibility: '' }); } - this.shapesWereUpdated = true; - if (this.shouldRaiseEvent(_e.ctrlKey)) { + if (shouldRaiseEvent) { this.onInteraction(this.prepareResult(), true, false); } }); @@ -207,10 +210,14 @@ export class InteractionHandlerImpl implements InteractionHandler { private initInteraction(): void { if (this.interactionData.crosshair) { this.addCrosshair(); + } else if (this.crosshair) { + this.removeCrosshair(); } - if (this.interactionData.enableThreshold) { this.addThreshold(); + } else if (this.threshold) { + this.threshold.remove(); + this.threshold = null; } } @@ -332,9 +339,27 @@ export class InteractionHandlerImpl implements InteractionHandler { const handler = shape.remember('_selectHandler'); if (handler && handler.nested) { handler.nested.fill(shape.attr('fill')); + // move green circle group(anchors) and polygon(lastChild) to the top of svg to make anchors hoverable + handler.parent.node.prepend(handler.nested.node); + handler.parent.node.prepend(handler.parent.node.lastChild); } } + private visualComponentsChanged(interactionData: InteractionData): boolean { + const allowedKeys = ['enabled', 'crosshair', 'enableThreshold', 'onChangeToolsBlockerState']; + if (Object.keys(interactionData).every((key: string): boolean => allowedKeys.includes(key))) { + if (this.interactionData.enableThreshold !== undefined && interactionData.enableThreshold !== undefined + && this.interactionData.enableThreshold !== interactionData.enableThreshold) { + return true; + } + if (this.interactionData.crosshair !== undefined && interactionData.crosshair !== undefined + && this.interactionData.crosshair !== interactionData.crosshair) { + return true; + } + } + return false; + } + public constructor( onInteraction: ( shapes: InteractionResult[] | null, @@ -376,7 +401,6 @@ export class InteractionHandlerImpl implements InteractionHandler { if (this.threshold) { this.threshold.center(x, y); } - if (this.interactionData.enableSliding && this.interactionShapes.length) { if (this.isWithinFrame(x, y)) { if (this.interactionData.enableThreshold && !this.isWithinThreshold(x, y)) return; @@ -399,6 +423,7 @@ export class InteractionHandlerImpl implements InteractionHandler { this.canvas.on('wheel.interaction', (e: WheelEvent): void => { if (e.ctrlKey) { if (this.threshold) { + this.thresholdWasModified = true; const { x, y } = this.cursorPosition; e.preventDefault(); if (e.deltaY > 0) { @@ -412,10 +437,24 @@ export class InteractionHandlerImpl implements InteractionHandler { } }); - document.body.addEventListener('keyup', (e: KeyboardEvent): void => { - if (e.keyCode === 17 && this.shouldRaiseEvent(false)) { - // 17 is ctrl - this.onInteraction(this.prepareResult(), true, false); + window.addEventListener('keyup', (e: KeyboardEvent): void => { + if (this.interactionData.enabled && e.keyCode === 17) { + if (this.interactionData.onChangeToolsBlockerState && !this.thresholdWasModified) { + this.interactionData.onChangeToolsBlockerState('keyup'); + } + if (this.shouldRaiseEvent(false)) { + // 17 is ctrl + this.onInteraction(this.prepareResult(), true, false); + } + } + }); + + window.addEventListener('keydown', (e: KeyboardEvent): void => { + if (this.interactionData.enabled && e.keyCode === 17) { + if (this.interactionData.onChangeToolsBlockerState && !this.thresholdWasModified) { + this.interactionData.onChangeToolsBlockerState('keydown'); + } + this.thresholdWasModified = false; } }); } @@ -461,6 +500,9 @@ export class InteractionHandlerImpl implements InteractionHandler { if (this.interactionData.startWithBox) { this.interactionShapes[0].style({ visibility: 'hidden' }); } + } else if (interactionData.enabled && this.visualComponentsChanged(interactionData)) { + this.interactionData = { ...this.interactionData, ...interactionData }; + this.initInteraction(); } else if (interactionData.enabled) { this.interactionData = interactionData; this.initInteraction(); diff --git a/cvat-core/src/ml-model.js b/cvat-core/src/ml-model.js index 010e674626b8..05aa01406e29 100644 --- a/cvat-core/src/ml-model.js +++ b/cvat-core/src/ml-model.js @@ -99,6 +99,14 @@ class MLModel { get tip() { return { ...this._tip }; } + + /** + * @param {(event:string)=>void} onChangeToolsBlockerState Set canvas onChangeToolsBlockerState callback + * @returns {void} + */ + set onChangeToolsBlockerState(onChangeToolsBlockerState) { + this._params.canvas.onChangeToolsBlockerState = onChangeToolsBlockerState; + } } module.exports = MLModel; diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 5d8f7f9a206d..496f22e7d357 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.22.0", + "version": "1.23.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 977098b85317..4bbffd0cc03f 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.22.0", + "version": "1.23.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/settings-actions.ts b/cvat-ui/src/actions/settings-actions.ts index 966743c47ee4..6e9b5bfb9b7c 100644 --- a/cvat-ui/src/actions/settings-actions.ts +++ b/cvat-ui/src/actions/settings-actions.ts @@ -3,7 +3,9 @@ // SPDX-License-Identifier: MIT import { AnyAction } from 'redux'; -import { GridColor, ColorBy, SettingsState } from 'reducers/interfaces'; +import { + GridColor, ColorBy, SettingsState, ToolsBlockerState, +} from 'reducers/interfaces'; export enum SettingsActionTypes { SWITCH_ROTATE_ALL = 'SWITCH_ROTATE_ALL', @@ -34,6 +36,7 @@ export enum SettingsActionTypes { CHANGE_CANVAS_BACKGROUND_COLOR = 'CHANGE_CANVAS_BACKGROUND_COLOR', SWITCH_SETTINGS_DIALOG = 'SWITCH_SETTINGS_DIALOG', SET_SETTINGS = 'SET_SETTINGS', + SWITCH_TOOLS_BLOCKER_STATE = 'SWITCH_TOOLS_BLOCKER_STATE', } export function changeShapesOpacity(opacity: number): AnyAction { @@ -280,6 +283,15 @@ export function changeDefaultApproxPolyAccuracy(approxPolyAccuracy: number): Any }; } +export function switchToolsBlockerState(toolsBlockerState: ToolsBlockerState): AnyAction { + return { + type: SettingsActionTypes.SWITCH_TOOLS_BLOCKER_STATE, + payload: { + toolsBlockerState, + }, + }; +} + export function setSettings(settings: Partial): AnyAction { return { type: SettingsActionTypes.SET_SETTINGS, diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx index d993af71cc0c..3afbecd87a6b 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx @@ -19,7 +19,7 @@ import getCore from 'cvat-core-wrapper'; import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper'; import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors'; import { - CombinedState, ActiveControl, OpenCVTool, ObjectType, ShapeType, + CombinedState, ActiveControl, OpenCVTool, ObjectType, ShapeType, ToolsBlockerState, } from 'reducers/interfaces'; import { interactWithCanvas, @@ -34,6 +34,7 @@ import ApproximationAccuracy, { thresholdFromAccuracy, } from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy'; import { ImageProcessing } from 'utils/opencv-wrapper/opencv-interfaces'; +import { switchToolsBlockerState } from 'actions/settings-actions'; import withVisibilityHandling from './handle-popover-visibility'; interface Props { @@ -46,6 +47,8 @@ interface Props { curZOrder: number; defaultApproxPolyAccuracy: number; frameData: any; + toolsBlockerState: ToolsBlockerState; + activeControl: ActiveControl; } interface DispatchToProps { @@ -54,6 +57,7 @@ interface DispatchToProps { createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void; fetchAnnotations(): void; changeFrame(toFrame: number, fillBuffer?: boolean, frameStep?: number, forceUpdate?: boolean):void; + onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState):void; } interface State { @@ -87,12 +91,13 @@ function mapStateToProps(state: CombinedState): Props { }, }, settings: { - workspace: { defaultApproxPolyAccuracy }, + workspace: { defaultApproxPolyAccuracy, toolsBlockerState }, }, } = state; return { isActivated: activeControl === ActiveControl.OPENCV_TOOLS, + activeControl, canvasInstance: canvasInstance as Canvas, defaultApproxPolyAccuracy, jobInstance, @@ -101,6 +106,7 @@ function mapStateToProps(state: CombinedState): Props { states, frame, frameData, + toolsBlockerState, }; } @@ -110,6 +116,7 @@ const mapDispatchToProps = { fetchAnnotations: fetchAnnotationsAsync, createAnnotations: createAnnotationsAsync, changeFrame: changeFrameAsync, + onSwitchToolsBlockerState: switchToolsBlockerState, }; class OpenCVControlComponent extends React.PureComponent { @@ -142,7 +149,10 @@ class OpenCVControlComponent extends React.PureComponent => { const { approxPolyAccuracy } = this.state; const { - createAnnotations, isActivated, jobInstance, frame, labels, curZOrder, canvasInstance, + createAnnotations, isActivated, jobInstance, frame, labels, curZOrder, canvasInstance, toolsBlockerState, } = this.props; const { activeLabelID } = this.state; if (!isActivated || !this.activeTool) { @@ -191,27 +206,41 @@ class OpenCVControlComponent extends React.PureComponent 2) { + // disable approximation for lastest two points to disable fickering + const [x, y] = this.latestPoints.slice(-2); + this.latestPoints.splice(this.latestPoints.length - 2, 2); + points = openCVWrapper.contours.approxPoly( + this.latestPoints, + thresholdFromAccuracy(approxPolyAccuracy), + false, + ); + points.push([x, y]); + } else { + points = openCVWrapper.contours.approxPoly( + this.latestPoints, + thresholdFromAccuracy(approxPolyAccuracy), + false, + ); + } canvasInstance.interact({ enabled: true, intermediateShape: { shapeType: ShapeType.POLYGON, - points: approx.flat(), + points: points.flat(), }, }); } if (isDone) { // need to recalculate without the latest sliding point - const finalPoints = await this.runCVAlgorithm(pressedPoints, threshold); + const finalPoints = await this.runCVAlgorithm(pressedPoints, + toolsBlockerState.algorithmsLocked ? 0 : threshold); const finalObject = new core.classes.ObjectState({ frame, objectType: ObjectType.SHAPE, @@ -234,6 +263,21 @@ class OpenCVControlComponent extends React.PureComponent { + const { + isActivated, toolsBlockerState, onSwitchToolsBlockerState, canvasInstance, + } = this.props; + if (isActivated && event === 'keyup') { + onSwitchToolsBlockerState({ algorithmsLocked: !toolsBlockerState.algorithmsLocked }); + canvasInstance.interact({ + enabled: true, + crosshair: toolsBlockerState.algorithmsLocked, + enableThreshold: toolsBlockerState.algorithmsLocked, + onChangeToolsBlockerState: this.onChangeToolsBlockerState, + }); + } + }; + private runImageModifier = async ():Promise => { const { activeImageModifiers } = this.state; const { @@ -279,22 +323,24 @@ class OpenCVControlComponent extends React.PureComponent ) : null} + {includesToolsBlockerButton ? ( + + + + ) : 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 5c2867b9df05..39742c3cb1f5 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 @@ -6,7 +6,9 @@ import React from 'react'; import Input from 'antd/lib/input'; import { Col, Row } from 'antd/lib/grid'; -import { ActiveControl, PredictorState, Workspace } from 'reducers/interfaces'; +import { + ActiveControl, PredictorState, ToolsBlockerState, Workspace, +} from 'reducers/interfaces'; import LeftGroup from './left-group'; import PlayerButtons from './player-buttons'; import PlayerNavigation from './player-navigation'; @@ -28,6 +30,7 @@ interface Props { undoShortcut: string; redoShortcut: string; drawShortcut: string; + switchToolsBlockerShortcut: string; playPauseShortcut: string; nextFrameShortcut: string; previousFrameShortcut: string; @@ -39,6 +42,7 @@ interface Props { predictor: PredictorState; isTrainingActive: boolean; activeControl: ActiveControl; + toolsBlockerState: ToolsBlockerState; changeWorkspace(workspace: Workspace): void; switchPredictor(predictorEnabled: boolean): void; showStatistics(): void; @@ -59,6 +63,7 @@ interface Props { onUndoClick(): void; onRedoClick(): void; onFinishDraw(): void; + onSwitchToolsBlockerState(): void; jobInstance: any; } @@ -79,6 +84,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { undoShortcut, redoShortcut, drawShortcut, + switchToolsBlockerShortcut, playPauseShortcut, nextFrameShortcut, previousFrameShortcut, @@ -89,6 +95,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { predictor, focusFrameInputShortcut, activeControl, + toolsBlockerState, showStatistics, switchPredictor, showFilters, @@ -109,6 +116,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { onUndoClick, onRedoClick, onFinishDraw, + onSwitchToolsBlockerState, jobInstance, isTrainingActive, } = props; @@ -125,10 +133,13 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { redoShortcut={redoShortcut} activeControl={activeControl} drawShortcut={drawShortcut} + switchToolsBlockerShortcut={switchToolsBlockerShortcut} + toolsBlockerState={toolsBlockerState} onSaveAnnotation={onSaveAnnotation} onUndoClick={onUndoClick} onRedoClick={onRedoClick} onFinishDraw={onFinishDraw} + onSwitchToolsBlockerState={onSwitchToolsBlockerState} /> 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 d14e382c37ee..d922a0000253 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 @@ -30,9 +30,10 @@ import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-ba import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas3d } from 'cvat-canvas3d-wrapper'; import { - CombinedState, FrameSpeed, Workspace, PredictorState, DimensionType, ActiveControl, + CombinedState, FrameSpeed, Workspace, PredictorState, DimensionType, ActiveControl, ToolsBlockerState, } from 'reducers/interfaces'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; +import { switchToolsBlockerState } from 'actions/settings-actions'; interface StateToProps { jobInstance: any; @@ -49,6 +50,7 @@ interface StateToProps { redoAction?: string; autoSave: boolean; autoSaveInterval: number; + toolsBlockerState: ToolsBlockerState; workspace: Workspace; keyMap: KeyMap; normalizedKeyMap: Record; @@ -72,6 +74,7 @@ interface DispatchToProps { setForceExitAnnotationFlag(forceExit: boolean): void; changeWorkspace(workspace: Workspace): void; switchPredictor(predictorEnabled: boolean): void; + onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState): void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -92,7 +95,7 @@ function mapStateToProps(state: CombinedState): StateToProps { }, settings: { player: { frameSpeed, frameStep }, - workspace: { autoSave, autoSaveInterval }, + workspace: { autoSave, autoSaveInterval, toolsBlockerState }, }, shortcuts: { keyMap, normalizedKeyMap }, plugins: { list }, @@ -113,6 +116,7 @@ function mapStateToProps(state: CombinedState): StateToProps { redoAction: history.redo.length ? history.redo[history.redo.length - 1][0] : undefined, autoSave, autoSaveInterval, + toolsBlockerState, workspace, keyMap, normalizedKeyMap, @@ -167,6 +171,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { dispatch(getPredictionsAsync()); } }, + onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState):void{ + dispatch(switchToolsBlockerState(toolsBlockerState)); + }, }; } @@ -431,6 +438,22 @@ class AnnotationTopBarContainer extends React.PureComponent { canvasInstance.draw({ enabled: false }); }; + private onSwitchToolsBlockerState = (): void => { + const { + toolsBlockerState, onSwitchToolsBlockerState, canvasInstance, activeControl, + } = this.props; + if (canvasInstance instanceof Canvas) { + if (activeControl.includes(ActiveControl.OPENCV_TOOLS)) { + canvasInstance.interact({ + enabled: true, + crosshair: toolsBlockerState.algorithmsLocked, + enableThreshold: toolsBlockerState.algorithmsLocked, + }); + } + } + onSwitchToolsBlockerState({ algorithmsLocked: !toolsBlockerState.algorithmsLocked }); + }; + private onURLIconClick = (): void => { const { frameNumber } = this.props; const { origin, pathname } = window.location; @@ -546,6 +569,7 @@ class AnnotationTopBarContainer extends React.PureComponent { searchAnnotations, changeWorkspace, switchPredictor, + toolsBlockerState, } = this.props; const preventDefault = (event: KeyboardEvent | undefined): void => { @@ -672,6 +696,8 @@ class AnnotationTopBarContainer extends React.PureComponent { undoShortcut={normalizedKeyMap.UNDO} redoShortcut={normalizedKeyMap.REDO} drawShortcut={normalizedKeyMap.SWITCH_DRAW_MODE} + // this shortcut is handled in interactionHandler.ts separatelly + switchToolsBlockerShortcut={normalizedKeyMap.SWITCH_TOOLS_BLOCKER_STATE} playPauseShortcut={normalizedKeyMap.PLAY_PAUSE} nextFrameShortcut={normalizedKeyMap.NEXT_FRAME} previousFrameShortcut={normalizedKeyMap.PREV_FRAME} @@ -683,6 +709,8 @@ class AnnotationTopBarContainer extends React.PureComponent { onUndoClick={this.undo} onRedoClick={this.redo} onFinishDraw={this.onFinishDraw} + onSwitchToolsBlockerState={this.onSwitchToolsBlockerState} + toolsBlockerState={toolsBlockerState} jobInstance={jobInstance} isTrainingActive={isTrainingActive} activeControl={activeControl} diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index f89e268d5453..ef11a8207463 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -185,6 +185,7 @@ export interface Model { framework: string; description: string; type: string; + onChangeToolsBlockerState: (event:string) => void; tip: { message: string; gif: string; @@ -195,6 +196,12 @@ export interface Model { } export type OpenCVTool = IntelligentScissors; + +export interface ToolsBlockerState { + algorithmsLocked?: boolean; + buttonVisible?: boolean; +} + export enum TaskStatus { ANNOTATION = 'annotation', REVIEW = 'validation', @@ -564,6 +571,7 @@ export interface WorkspaceSettingsState { showAllInterpolationTracks: boolean; intelligentPolygonCrop: boolean; defaultApproxPolyAccuracy: number; + toolsBlockerState: ToolsBlockerState; } export interface ShapesSettingsState { diff --git a/cvat-ui/src/reducers/settings-reducer.ts b/cvat-ui/src/reducers/settings-reducer.ts index 301ff9a7e324..69a945d6ade9 100644 --- a/cvat-ui/src/reducers/settings-reducer.ts +++ b/cvat-ui/src/reducers/settings-reducer.ts @@ -32,6 +32,10 @@ const defaultState: SettingsState = { showAllInterpolationTracks: false, intelligentPolygonCrop: true, defaultApproxPolyAccuracy: 9, + toolsBlockerState: { + algorithmsLocked: false, + buttonVisible: false, + }, }, player: { canvasBackgroundColor: '#ffffff', @@ -287,6 +291,15 @@ export default (state = defaultState, action: AnyAction): SettingsState => { }, }; } + case SettingsActionTypes.SWITCH_TOOLS_BLOCKER_STATE: { + return { + ...state, + workspace: { + ...state.workspace, + toolsBlockerState: { ...state.workspace.toolsBlockerState, ...action.payload.toolsBlockerState }, + }, + }; + } case SettingsActionTypes.SWITCH_SETTINGS_DIALOG: { return { ...state, @@ -320,6 +333,18 @@ export default (state = defaultState, action: AnyAction): SettingsState => { }, }; } + case AnnotationActionTypes.INTERACT_WITH_CANVAS: { + return { + ...state, + workspace: { + ...state.workspace, + toolsBlockerState: { + buttonVisible: true, + algorithmsLocked: false, + }, + }, + }; + } case AuthActionTypes.LOGOUT_SUCCESS: { return { ...defaultState }; } diff --git a/cvat-ui/src/reducers/shortcuts-reducer.ts b/cvat-ui/src/reducers/shortcuts-reducer.ts index ed219f9d0086..1af6d08ace1b 100644 --- a/cvat-ui/src/reducers/shortcuts-reducer.ts +++ b/cvat-ui/src/reducers/shortcuts-reducer.ts @@ -322,6 +322,13 @@ const defaultKeyMap = ({ action: 'keydown', applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D], }, + SWITCH_TOOLS_BLOCKER_STATE: { + name: 'Switch algorithm blocker', + description: 'Postpone running the algorithm for interaction tools', + sequences: ['сtrl'], + action: 'keydown', + applicable: [DimensionType.DIM_2D], + }, CHANGE_OBJECT_COLOR: { name: 'Change color', description: 'Set the next color for an activated shape', diff --git a/cvat-ui/src/utils/opencv-wrapper/intelligent-scissors.ts b/cvat-ui/src/utils/opencv-wrapper/intelligent-scissors.ts index 8fca230143f1..49483f7337da 100644 --- a/cvat-ui/src/utils/opencv-wrapper/intelligent-scissors.ts +++ b/cvat-ui/src/utils/opencv-wrapper/intelligent-scissors.ts @@ -14,6 +14,7 @@ export interface IntelligentScissorsParams { enableSliding: boolean; allowRemoveOnlyLast: boolean; minPosVertices: number; + onChangeToolsBlockerState: (event:string)=>void; }; } @@ -21,6 +22,7 @@ export interface IntelligentScissors { reset(): void; run(points: number[], image: ImageData, offsetX: number, offsetY: number): number[]; params: IntelligentScissorsParams; + switchBlockMode(mode?:boolean):void; } function applyOffset(points: Point[], offsetX: number, offsetY: number): Point[] { @@ -34,6 +36,7 @@ function applyOffset(points: Point[], offsetX: number, offsetY: number): Point[] export default class IntelligentScissorsImplementation implements IntelligentScissors { private cv: any; + private onChangeToolsBlockerState: (event:string)=>void; private scissors: { tool: any; state: { @@ -46,14 +49,20 @@ export default class IntelligentScissorsImplementation implements IntelligentSci } >; // point index : start index in path image: any | null; + blocked: boolean; }; }; - public constructor(cv: any) { + public constructor(cv: any, onChangeToolsBlockerState:(event:string)=>void) { this.cv = cv; + this.onChangeToolsBlockerState = onChangeToolsBlockerState; this.reset(); } + public switchBlockMode(mode:boolean): void { + this.scissors.state.blocked = mode; + } + public reset(): void { if (this.scissors && this.scissors.tool) { this.scissors.tool.delete(); @@ -66,6 +75,7 @@ export default class IntelligentScissorsImplementation implements IntelligentSci path: [], anchors: {}, image: null, + blocked: false, }, }; @@ -88,7 +98,6 @@ export default class IntelligentScissorsImplementation implements IntelligentSci const { tool, state } = scissors; const points = applyOffset(numberArrayToPoints(coordinates), offsetX, offsetY); - if (points.length > 1) { let matImage = null; const contour = new cv.Mat(); @@ -108,7 +117,6 @@ export default class IntelligentScissorsImplementation implements IntelligentSci delete state.anchors[+i]; } } - return [...state.path]; } @@ -118,14 +126,17 @@ export default class IntelligentScissorsImplementation implements IntelligentSci state.path = state.path.slice(0, state.anchors[points.length - 1].start); delete state.anchors[points.length - 1]; } - - tool.applyImage(matImage); - tool.buildMap(new cv.Point(prevX, prevY)); - tool.getContour(new cv.Point(curX, curY), contour); - const pathSegment = []; - for (let row = 0; row < contour.rows; row++) { - pathSegment.push(contour.intAt(row, 0) + offsetX, contour.intAt(row, 1) + offsetY); + if (!state.blocked) { + tool.applyImage(matImage); + tool.buildMap(new cv.Point(prevX, prevY)); + tool.getContour(new cv.Point(curX, curY), contour); + + for (let row = 0; row < contour.rows; row++) { + pathSegment.push(contour.intAt(row, 0) + offsetX, contour.intAt(row, 1) + offsetY); + } + } else { + pathSegment.push(curX + offsetX, curY + offsetY); } state.anchors[points.length - 1] = { point: cur, @@ -140,13 +151,13 @@ export default class IntelligentScissorsImplementation implements IntelligentSci contour.delete(); } } else { + state.path = []; state.path.push(...pointsToNumberArray(applyOffset(points.slice(-1), -offsetX, -offsetY))); state.anchors[0] = { point: points[0], start: 0, }; } - return [...state.path]; } @@ -167,6 +178,7 @@ export default class IntelligentScissorsImplementation implements IntelligentSci enableSliding: true, allowRemoveOnlyLast: true, minPosVertices: 1, + onChangeToolsBlockerState: this.onChangeToolsBlockerState, }, }; } diff --git a/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts b/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts index 4afb3e34dabc..c946fb194895 100644 --- a/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts +++ b/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts @@ -12,7 +12,7 @@ const core = getCore(); const baseURL = core.config.backendAPI.slice(0, -7); export interface Segmentation { - intelligentScissorsFactory: () => IntelligentScissors; + intelligentScissorsFactory: (onChangeToolsBlockerState:(event:string)=>void) => IntelligentScissors; } export interface Contours { @@ -126,7 +126,8 @@ export class OpenCVWrapper { } return { - intelligentScissorsFactory: () => new IntelligentScissorsImplementation(this.cv), + intelligentScissorsFactory: (onChangeToolsBlockerState:(event:string)=>void) => + new IntelligentScissorsImplementation(this.cv, onChangeToolsBlockerState), }; }