Intelligent scissors disabling feature (#3510)

* disabling feature

* excess points bugfix

* ctrl interaction

* ui bugfix

* trackers block mode removal

* Ctrl press fix

* approximation disabled only for block mode

* architectural improvements

* code refactoring

* renamed switchBlockMode

* added comments

* callback signature change

* polygon finish fix

* fixed bugs

* removed unnecessary if

* final recalculation threshold disable

* delete points bugfix

* update changelog

* update package versions
main
Kirill Lakhov 5 years ago committed by GitHub
parent cef42b69e9
commit 1da3c96b5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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 (<https://github.com/openvinotoolkit/cvat/pull/3365>) - Added ability to export project as a dataset (<https://github.com/openvinotoolkit/cvat/pull/3365>)
and project with 3D tasks (<https://github.com/openvinotoolkit/cvat/pull/3502>) and project with 3D tasks (<https://github.com/openvinotoolkit/cvat/pull/3502>)
- Additional inline tips in interactors with demo gifs (<https://github.com/openvinotoolkit/cvat/pull/3473>) - Additional inline tips in interactors with demo gifs (<https://github.com/openvinotoolkit/cvat/pull/3473>)
- Added intelligent scissors blocking feature (<https://github.com/openvinotoolkit/cvat/pull/3510>)
### Changed ### Changed

@ -1,6 +1,6 @@
{ {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.6.0", "version": "2.7.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

@ -1,6 +1,6 @@
{ {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.6.0", "version": "2.7.0",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library", "description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts", "main": "src/canvas.ts",
"scripts": { "scripts": {

@ -87,6 +87,7 @@ export interface InteractionData {
shapeType: string; shapeType: string;
points: number[]; points: number[];
}; };
onChangeToolsBlockerState?: (event: string) => void;
} }
export interface InteractionResult { export interface InteractionResult {
@ -565,15 +566,14 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
if (![Mode.IDLE, Mode.INTERACT].includes(this.data.mode)) { if (![Mode.IDLE, Mode.INTERACT].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`); throw Error(`Canvas is busy. Action: ${this.data.mode}`);
} }
const thresholdChanged = this.data.interactionData.enableThreshold !== interactionData.enableThreshold;
if (interactionData.enabled && !interactionData.intermediateShape) { if (interactionData.enabled && !interactionData.intermediateShape && !thresholdChanged) {
if (this.data.interactionData.enabled) { if (this.data.interactionData.enabled) {
throw new Error('Interaction has been already started'); throw new Error('Interaction has been already started');
} else if (!interactionData.shapeType) { } else if (!interactionData.shapeType) {
throw new Error('A shape type was not specified'); throw new Error('A shape type was not specified');
} }
} }
this.data.interactionData = interactionData; this.data.interactionData = interactionData;
if (typeof this.data.interactionData.crosshair !== 'boolean') { if (typeof this.data.interactionData.crosshair !== 'boolean') {
this.data.interactionData.crosshair = true; this.data.interactionData.crosshair = true;

@ -1279,7 +1279,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
this.interactionHandler.interact(data); this.interactionHandler.interact(data);
} else { } else {
this.canvas.style.cursor = ''; if (!data.enabled) {
this.canvas.style.cursor = '';
}
if (this.mode !== Mode.IDLE) { if (this.mode !== Mode.IDLE) {
this.interactionHandler.interact(data); this.interactionHandler.interact(data);
} }
@ -1569,7 +1571,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
private addObjects(states: any[]): void { private addObjects(states: any[]): void {
const { displayAllText } = this.configuration; const { displayAllText } = this.configuration;
for (const state of states) { for (const state of states) {
const points: number[] = state.points as number[]; const points: number[] = state.points as number[];
const translatedPoints: number[] = this.translateToCanvas(points); const translatedPoints: number[] = this.translateToCanvas(points);

@ -8,6 +8,7 @@ import Crosshair from './crosshair';
import { import {
translateToSVG, PropType, stringifyPoints, translateToCanvas, translateToSVG, PropType, stringifyPoints, translateToCanvas,
} from './shared'; } from './shared';
import { import {
InteractionData, InteractionResult, Geometry, Configuration, InteractionData, InteractionResult, Geometry, Configuration,
} from './canvasModel'; } from './canvasModel';
@ -34,6 +35,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
private thresholdRectSize: number; private thresholdRectSize: number;
private intermediateShape: PropType<InteractionData, 'intermediateShape'>; private intermediateShape: PropType<InteractionData, 'intermediateShape'>;
private drawnIntermediateShape: SVG.Shape; private drawnIntermediateShape: SVG.Shape;
private thresholdWasModified: boolean;
private prepareResult(): InteractionResult[] { private prepareResult(): InteractionResult[] {
return this.interactionShapes.map( return this.interactionShapes.map(
@ -141,14 +143,15 @@ export class InteractionHandlerImpl implements InteractionHandler {
_e.preventDefault(); _e.preventDefault();
_e.stopPropagation(); _e.stopPropagation();
self.remove(); self.remove();
this.shapesWereUpdated = true;
const shouldRaiseEvent = this.shouldRaiseEvent(_e.ctrlKey);
this.interactionShapes = this.interactionShapes.filter( this.interactionShapes = this.interactionShapes.filter(
(shape: SVG.Shape): boolean => shape !== self, (shape: SVG.Shape): boolean => shape !== self,
); );
if (this.interactionData.startWithBox && this.interactionShapes.length === 1) { if (this.interactionData.startWithBox && this.interactionShapes.length === 1) {
this.interactionShapes[0].style({ visibility: '' }); this.interactionShapes[0].style({ visibility: '' });
} }
this.shapesWereUpdated = true; if (shouldRaiseEvent) {
if (this.shouldRaiseEvent(_e.ctrlKey)) {
this.onInteraction(this.prepareResult(), true, false); this.onInteraction(this.prepareResult(), true, false);
} }
}); });
@ -207,10 +210,14 @@ export class InteractionHandlerImpl implements InteractionHandler {
private initInteraction(): void { private initInteraction(): void {
if (this.interactionData.crosshair) { if (this.interactionData.crosshair) {
this.addCrosshair(); this.addCrosshair();
} else if (this.crosshair) {
this.removeCrosshair();
} }
if (this.interactionData.enableThreshold) { if (this.interactionData.enableThreshold) {
this.addThreshold(); 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'); const handler = shape.remember('_selectHandler');
if (handler && handler.nested) { if (handler && handler.nested) {
handler.nested.fill(shape.attr('fill')); 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( public constructor(
onInteraction: ( onInteraction: (
shapes: InteractionResult[] | null, shapes: InteractionResult[] | null,
@ -376,7 +401,6 @@ export class InteractionHandlerImpl implements InteractionHandler {
if (this.threshold) { if (this.threshold) {
this.threshold.center(x, y); this.threshold.center(x, y);
} }
if (this.interactionData.enableSliding && this.interactionShapes.length) { if (this.interactionData.enableSliding && this.interactionShapes.length) {
if (this.isWithinFrame(x, y)) { if (this.isWithinFrame(x, y)) {
if (this.interactionData.enableThreshold && !this.isWithinThreshold(x, y)) return; 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 => { this.canvas.on('wheel.interaction', (e: WheelEvent): void => {
if (e.ctrlKey) { if (e.ctrlKey) {
if (this.threshold) { if (this.threshold) {
this.thresholdWasModified = true;
const { x, y } = this.cursorPosition; const { x, y } = this.cursorPosition;
e.preventDefault(); e.preventDefault();
if (e.deltaY > 0) { if (e.deltaY > 0) {
@ -412,10 +437,24 @@ export class InteractionHandlerImpl implements InteractionHandler {
} }
}); });
document.body.addEventListener('keyup', (e: KeyboardEvent): void => { window.addEventListener('keyup', (e: KeyboardEvent): void => {
if (e.keyCode === 17 && this.shouldRaiseEvent(false)) { if (this.interactionData.enabled && e.keyCode === 17) {
// 17 is ctrl if (this.interactionData.onChangeToolsBlockerState && !this.thresholdWasModified) {
this.onInteraction(this.prepareResult(), true, false); 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) { if (this.interactionData.startWithBox) {
this.interactionShapes[0].style({ visibility: 'hidden' }); this.interactionShapes[0].style({ visibility: 'hidden' });
} }
} else if (interactionData.enabled && this.visualComponentsChanged(interactionData)) {
this.interactionData = { ...this.interactionData, ...interactionData };
this.initInteraction();
} else if (interactionData.enabled) { } else if (interactionData.enabled) {
this.interactionData = interactionData; this.interactionData = interactionData;
this.initInteraction(); this.initInteraction();

@ -99,6 +99,14 @@ class MLModel {
get tip() { get tip() {
return { ...this._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; module.exports = MLModel;

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.22.0", "version": "1.23.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.22.0", "version": "1.23.0",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {

@ -3,7 +3,9 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { AnyAction } from 'redux'; import { AnyAction } from 'redux';
import { GridColor, ColorBy, SettingsState } from 'reducers/interfaces'; import {
GridColor, ColorBy, SettingsState, ToolsBlockerState,
} from 'reducers/interfaces';
export enum SettingsActionTypes { export enum SettingsActionTypes {
SWITCH_ROTATE_ALL = 'SWITCH_ROTATE_ALL', SWITCH_ROTATE_ALL = 'SWITCH_ROTATE_ALL',
@ -34,6 +36,7 @@ export enum SettingsActionTypes {
CHANGE_CANVAS_BACKGROUND_COLOR = 'CHANGE_CANVAS_BACKGROUND_COLOR', CHANGE_CANVAS_BACKGROUND_COLOR = 'CHANGE_CANVAS_BACKGROUND_COLOR',
SWITCH_SETTINGS_DIALOG = 'SWITCH_SETTINGS_DIALOG', SWITCH_SETTINGS_DIALOG = 'SWITCH_SETTINGS_DIALOG',
SET_SETTINGS = 'SET_SETTINGS', SET_SETTINGS = 'SET_SETTINGS',
SWITCH_TOOLS_BLOCKER_STATE = 'SWITCH_TOOLS_BLOCKER_STATE',
} }
export function changeShapesOpacity(opacity: number): AnyAction { 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<SettingsState>): AnyAction { export function setSettings(settings: Partial<SettingsState>): AnyAction {
return { return {
type: SettingsActionTypes.SET_SETTINGS, type: SettingsActionTypes.SET_SETTINGS,

@ -19,7 +19,7 @@ import getCore from 'cvat-core-wrapper';
import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper'; import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper';
import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors'; import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors';
import { import {
CombinedState, ActiveControl, OpenCVTool, ObjectType, ShapeType, CombinedState, ActiveControl, OpenCVTool, ObjectType, ShapeType, ToolsBlockerState,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { import {
interactWithCanvas, interactWithCanvas,
@ -34,6 +34,7 @@ import ApproximationAccuracy, {
thresholdFromAccuracy, thresholdFromAccuracy,
} from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy'; } from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy';
import { ImageProcessing } from 'utils/opencv-wrapper/opencv-interfaces'; import { ImageProcessing } from 'utils/opencv-wrapper/opencv-interfaces';
import { switchToolsBlockerState } from 'actions/settings-actions';
import withVisibilityHandling from './handle-popover-visibility'; import withVisibilityHandling from './handle-popover-visibility';
interface Props { interface Props {
@ -46,6 +47,8 @@ interface Props {
curZOrder: number; curZOrder: number;
defaultApproxPolyAccuracy: number; defaultApproxPolyAccuracy: number;
frameData: any; frameData: any;
toolsBlockerState: ToolsBlockerState;
activeControl: ActiveControl;
} }
interface DispatchToProps { interface DispatchToProps {
@ -54,6 +57,7 @@ interface DispatchToProps {
createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void; createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void;
fetchAnnotations(): void; fetchAnnotations(): void;
changeFrame(toFrame: number, fillBuffer?: boolean, frameStep?: number, forceUpdate?: boolean):void; changeFrame(toFrame: number, fillBuffer?: boolean, frameStep?: number, forceUpdate?: boolean):void;
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState):void;
} }
interface State { interface State {
@ -87,12 +91,13 @@ function mapStateToProps(state: CombinedState): Props {
}, },
}, },
settings: { settings: {
workspace: { defaultApproxPolyAccuracy }, workspace: { defaultApproxPolyAccuracy, toolsBlockerState },
}, },
} = state; } = state;
return { return {
isActivated: activeControl === ActiveControl.OPENCV_TOOLS, isActivated: activeControl === ActiveControl.OPENCV_TOOLS,
activeControl,
canvasInstance: canvasInstance as Canvas, canvasInstance: canvasInstance as Canvas,
defaultApproxPolyAccuracy, defaultApproxPolyAccuracy,
jobInstance, jobInstance,
@ -101,6 +106,7 @@ function mapStateToProps(state: CombinedState): Props {
states, states,
frame, frame,
frameData, frameData,
toolsBlockerState,
}; };
} }
@ -110,6 +116,7 @@ const mapDispatchToProps = {
fetchAnnotations: fetchAnnotationsAsync, fetchAnnotations: fetchAnnotationsAsync,
createAnnotations: createAnnotationsAsync, createAnnotations: createAnnotationsAsync,
changeFrame: changeFrameAsync, changeFrame: changeFrameAsync,
onSwitchToolsBlockerState: switchToolsBlockerState,
}; };
class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps, State> { class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps, State> {
@ -142,7 +149,10 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
public componentDidUpdate(prevProps: Props, prevState: State): void { public componentDidUpdate(prevProps: Props, prevState: State): void {
const { approxPolyAccuracy } = this.state; const { approxPolyAccuracy } = this.state;
const { isActivated, defaultApproxPolyAccuracy, canvasInstance } = this.props; const {
isActivated, defaultApproxPolyAccuracy, canvasInstance, toolsBlockerState,
} = this.props;
if (!prevProps.isActivated && isActivated) { if (!prevProps.isActivated && isActivated) {
// reset flags & states before using a tool // reset flags & states before using a tool
this.latestPoints = []; this.latestPoints = [];
@ -150,6 +160,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
approxPolyAccuracy: defaultApproxPolyAccuracy, approxPolyAccuracy: defaultApproxPolyAccuracy,
}); });
if (this.activeTool) { if (this.activeTool) {
this.activeTool.switchBlockMode(toolsBlockerState.algorithmsLocked);
this.activeTool.reset(); this.activeTool.reset();
} }
} }
@ -169,6 +180,10 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
}); });
} }
} }
if (prevProps.toolsBlockerState.algorithmsLocked !== toolsBlockerState.algorithmsLocked &&
!!this.activeTool?.switchBlockMode) {
this.activeTool.switchBlockMode(toolsBlockerState.algorithmsLocked);
}
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
@ -180,7 +195,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
private interactionListener = async (e: Event): Promise<void> => { private interactionListener = async (e: Event): Promise<void> => {
const { approxPolyAccuracy } = this.state; const { approxPolyAccuracy } = this.state;
const { const {
createAnnotations, isActivated, jobInstance, frame, labels, curZOrder, canvasInstance, createAnnotations, isActivated, jobInstance, frame, labels, curZOrder, canvasInstance, toolsBlockerState,
} = this.props; } = this.props;
const { activeLabelID } = this.state; const { activeLabelID } = this.state;
if (!isActivated || !this.activeTool) { if (!isActivated || !this.activeTool) {
@ -191,27 +206,41 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
shapesUpdated, isDone, threshold, shapes, shapesUpdated, isDone, threshold, shapes,
} = (e as CustomEvent).detail; } = (e as CustomEvent).detail;
const pressedPoints = convertShapesForInteractor(shapes, 0).flat(); const pressedPoints = convertShapesForInteractor(shapes, 0).flat();
try { try {
if (shapesUpdated) { if (shapesUpdated) {
this.latestPoints = await this.runCVAlgorithm(pressedPoints, threshold); this.latestPoints = await this.runCVAlgorithm(pressedPoints,
const approx = openCVWrapper.contours.approxPoly( toolsBlockerState.algorithmsLocked ? 0 : threshold);
this.latestPoints, let points = [];
thresholdFromAccuracy(approxPolyAccuracy), if (toolsBlockerState.algorithmsLocked && this.latestPoints.length > 2) {
false, // 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({ canvasInstance.interact({
enabled: true, enabled: true,
intermediateShape: { intermediateShape: {
shapeType: ShapeType.POLYGON, shapeType: ShapeType.POLYGON,
points: approx.flat(), points: points.flat(),
}, },
}); });
} }
if (isDone) { if (isDone) {
// need to recalculate without the latest sliding point // 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({ const finalObject = new core.classes.ObjectState({
frame, frame,
objectType: ObjectType.SHAPE, objectType: ObjectType.SHAPE,
@ -234,6 +263,21 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
} }
}; };
private onChangeToolsBlockerState = (event:string):void => {
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<void> => { private runImageModifier = async ():Promise<void> => {
const { activeImageModifiers } = this.state; const { activeImageModifiers } = this.state;
const { const {
@ -279,22 +323,24 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
if (!canvas) { if (!canvas) {
throw new Error('Element #cvat_canvas_background was not found'); throw new Error('Element #cvat_canvas_background was not found');
} }
if (!this.activeTool || pressedPoints.length === 0) return [];
const { width, height } = canvas; const { width, height } = canvas;
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
if (!context) { if (!context) {
throw new Error('Canvas context is empty'); throw new Error('Canvas context is empty');
} }
let imageData;
const [x, y] = pressedPoints.slice(-2); const [x, y] = pressedPoints.slice(-2);
const startX = Math.round(Math.max(0, x - threshold)); const startX = Math.round(Math.max(0, x - threshold));
const startY = Math.round(Math.max(0, y - threshold)); const startY = Math.round(Math.max(0, y - threshold));
const segmentWidth = Math.min(2 * threshold, width - startX); if (threshold !== 0) {
const segmentHeight = Math.min(2 * threshold, height - startY); const segmentWidth = Math.min(2 * threshold, width - startX);
const imageData = context.getImageData(startX, startY, segmentWidth, segmentHeight); const segmentHeight = Math.min(2 * threshold, height - startY);
imageData = context.getImageData(startX, startY, segmentWidth, segmentHeight);
if (!this.activeTool) return []; } else {
imageData = context.getImageData(0, 0, width, height);
}
// Handling via OpenCV.js // Handling via OpenCV.js
const points = await this.activeTool.run(pressedPoints, imageData, startX, startY); const points = await this.activeTool.run(pressedPoints, imageData, startX, startY);
return points; return points;
@ -360,7 +406,8 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
<CVATTooltip title='Intelligent scissors' className='cvat-opencv-drawing-tool'> <CVATTooltip title='Intelligent scissors' className='cvat-opencv-drawing-tool'>
<Button <Button
onClick={() => { onClick={() => {
this.activeTool = openCVWrapper.segmentation.intelligentScissorsFactory(); this.activeTool = openCVWrapper.segmentation
.intelligentScissorsFactory(this.onChangeToolsBlockerState);
canvasInstance.cancel(); canvasInstance.cancel();
onInteractionStart(this.activeTool, activeLabelID); onInteractionStart(this.activeTool, activeLabelID);
canvasInstance.interact({ canvasInstance.interact({

@ -25,7 +25,7 @@ import range from 'utils/range';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper'; import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper';
import { import {
CombinedState, ActiveControl, Model, ObjectType, ShapeType, CombinedState, ActiveControl, Model, ObjectType, ShapeType, ToolsBlockerState,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { import {
interactWithCanvas, interactWithCanvas,
@ -38,6 +38,7 @@ import LabelSelector from 'components/label-selector/label-selector';
import ApproximationAccuracy, { import ApproximationAccuracy, {
thresholdFromAccuracy, thresholdFromAccuracy,
} from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy'; } from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy';
import { switchToolsBlockerState } from 'actions/settings-actions';
import withVisibilityHandling from './handle-popover-visibility'; import withVisibilityHandling from './handle-popover-visibility';
import ToolsTooltips from './interactor-tooltips'; import ToolsTooltips from './interactor-tooltips';
@ -55,6 +56,7 @@ interface StateToProps {
curZOrder: number; curZOrder: number;
aiToolsRef: MutableRefObject<any>; aiToolsRef: MutableRefObject<any>;
defaultApproxPolyAccuracy: number; defaultApproxPolyAccuracy: number;
toolsBlockerState: ToolsBlockerState;
} }
interface DispatchToProps { interface DispatchToProps {
@ -62,6 +64,7 @@ interface DispatchToProps {
updateAnnotations(statesToUpdate: any[]): void; updateAnnotations(statesToUpdate: any[]): void;
createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void; createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void;
fetchAnnotations(): void; fetchAnnotations(): void;
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState):void;
} }
const core = getCore(); const core = getCore();
@ -75,6 +78,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { instance: canvasInstance, activeControl } = annotation.canvas; const { instance: canvasInstance, activeControl } = annotation.canvas;
const { models } = state; const { models } = state;
const { interactors, detectors, trackers } = models; const { interactors, detectors, trackers } = models;
const { toolsBlockerState } = state.settings.workspace;
return { return {
interactors, interactors,
@ -90,6 +94,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
curZOrder: annotation.annotations.zLayer.cur, curZOrder: annotation.annotations.zLayer.cur,
aiToolsRef: annotation.aiToolsRef, aiToolsRef: annotation.aiToolsRef,
defaultApproxPolyAccuracy: settings.workspace.defaultApproxPolyAccuracy, defaultApproxPolyAccuracy: settings.workspace.defaultApproxPolyAccuracy,
toolsBlockerState,
}; };
} }
@ -98,6 +103,7 @@ const mapDispatchToProps = {
updateAnnotations: updateAnnotationsAsync, updateAnnotations: updateAnnotationsAsync,
createAnnotations: createAnnotationsAsync, createAnnotations: createAnnotationsAsync,
fetchAnnotations: fetchAnnotationsAsync, fetchAnnotations: fetchAnnotationsAsync,
onSwitchToolsBlockerState: switchToolsBlockerState,
}; };
type Props = StateToProps & DispatchToProps; type Props = StateToProps & DispatchToProps;
@ -200,6 +206,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
shapeType: ShapeType.POLYGON, shapeType: ShapeType.POLYGON,
points: this.interaction.latestResult.flat(), points: this.interaction.latestResult.flat(),
}, },
onChangeToolsBlockerState: this.onChangeToolsBlockerState,
}); });
}); });
} }
@ -281,6 +288,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
shapeType: ShapeType.POLYGON, shapeType: ShapeType.POLYGON,
points: this.interaction.latestResult.flat(), points: this.interaction.latestResult.flat(),
}, },
onChangeToolsBlockerState: this.onChangeToolsBlockerState,
}); });
} }
@ -403,6 +411,15 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
}); });
}; };
private onChangeToolsBlockerState = (event:string):void => {
const { isActivated, onSwitchToolsBlockerState } = this.props;
if (isActivated && event === 'keydown') {
onSwitchToolsBlockerState({ algorithmsLocked: true });
} else if (isActivated && event === 'keyup') {
onSwitchToolsBlockerState({ algorithmsLocked: false });
}
};
private constructFromPoints(points: number[][]): void { private constructFromPoints(points: number[][]): void {
const { const {
frame, labels, curZOrder, jobInstance, activeLabelID, createAnnotations, frame, labels, curZOrder, jobInstance, activeLabelID, createAnnotations,
@ -611,6 +628,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
}); });
onInteractionStart(activeTracker, activeLabelID); onInteractionStart(activeTracker, activeLabelID);
const { onSwitchToolsBlockerState } = this.props;
onSwitchToolsBlockerState({ buttonVisible: false });
} }
}} }}
> >
@ -693,12 +712,12 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
if (activeInteractor) { if (activeInteractor) {
canvasInstance.cancel(); canvasInstance.cancel();
activeInteractor.onChangeToolsBlockerState = this.onChangeToolsBlockerState;
canvasInstance.interact({ canvasInstance.interact({
shapeType: 'points', shapeType: 'points',
enabled: true, enabled: true,
...activeInteractor.params.canvas, ...activeInteractor.params.canvas,
}); });
onInteractionStart(activeInteractor, activeLabelID); onInteractionStart(activeInteractor, activeLabelID);
} }
}} }}
@ -753,6 +772,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
); );
createAnnotations(jobInstance, frame, states); createAnnotations(jobInstance, frame, states);
const { onSwitchToolsBlockerState } = this.props;
onSwitchToolsBlockerState({ buttonVisible: false });
} catch (error) { } catch (error) {
notification.error({ notification.error({
description: error.toString(), description: error.toString(),
@ -776,7 +797,10 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
</Text> </Text>
</Col> </Col>
</Row> </Row>
<Tabs type='card' tabBarGutter={8}> <Tabs
type='card'
tabBarGutter={8}
>
<Tabs.TabPane key='interactors' tab='Interactors'> <Tabs.TabPane key='interactors' tab='Interactors'>
{this.renderLabelBlock()} {this.renderLabelBlock()}
{this.renderInteractorBlock()} {this.renderInteractorBlock()}

@ -128,6 +128,10 @@ button.cvat-predictor-button {
pointer-events: none; pointer-events: none;
} }
.cvat-button-active.ant-btn {
color: $border-color-hover;
}
.cvat-annotation-header-player-group > div { .cvat-annotation-header-player-group > div {
height: 48px; height: 48px;
line-height: 0; line-height: 0;

@ -4,7 +4,7 @@
import React from 'react'; import React from 'react';
import { Col } from 'antd/lib/grid'; import { Col } from 'antd/lib/grid';
import Icon, { CheckOutlined } from '@ant-design/icons'; import Icon, { StopOutlined, CheckOutlined } from '@ant-design/icons';
import Modal from 'antd/lib/modal'; import Modal from 'antd/lib/modal';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import Timeline from 'antd/lib/timeline'; import Timeline from 'antd/lib/timeline';
@ -14,7 +14,7 @@ import AnnotationMenuContainer from 'containers/annotation-page/top-bar/annotati
import { import {
MainMenuIcon, SaveIcon, UndoIcon, RedoIcon, MainMenuIcon, SaveIcon, UndoIcon, RedoIcon,
} from 'icons'; } from 'icons';
import { ActiveControl } from 'reducers/interfaces'; import { ActiveControl, ToolsBlockerState } from 'reducers/interfaces';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
interface Props { interface Props {
@ -26,11 +26,14 @@ interface Props {
undoShortcut: string; undoShortcut: string;
redoShortcut: string; redoShortcut: string;
drawShortcut: string; drawShortcut: string;
switchToolsBlockerShortcut: string;
toolsBlockerState: ToolsBlockerState;
activeControl: ActiveControl; activeControl: ActiveControl;
onSaveAnnotation(): void; onSaveAnnotation(): void;
onUndoClick(): void; onUndoClick(): void;
onRedoClick(): void; onRedoClick(): void;
onFinishDraw(): void; onFinishDraw(): void;
onSwitchToolsBlockerState(): void;
} }
function LeftGroup(props: Props): JSX.Element { function LeftGroup(props: Props): JSX.Element {
@ -43,11 +46,14 @@ function LeftGroup(props: Props): JSX.Element {
undoShortcut, undoShortcut,
redoShortcut, redoShortcut,
drawShortcut, drawShortcut,
switchToolsBlockerShortcut,
activeControl, activeControl,
toolsBlockerState,
onSaveAnnotation, onSaveAnnotation,
onUndoClick, onUndoClick,
onRedoClick, onRedoClick,
onFinishDraw, onFinishDraw,
onSwitchToolsBlockerState,
} = props; } = props;
const includesDoneButton = [ const includesDoneButton = [
@ -58,6 +64,15 @@ function LeftGroup(props: Props): JSX.Element {
ActiveControl.OPENCV_TOOLS, ActiveControl.OPENCV_TOOLS,
].includes(activeControl); ].includes(activeControl);
const includesToolsBlockerButton = [
ActiveControl.OPENCV_TOOLS,
ActiveControl.AI_TOOLS,
].includes(activeControl) && toolsBlockerState.buttonVisible;
const shouldEnableToolsBlockerOnClick = [
ActiveControl.OPENCV_TOOLS,
].includes(activeControl);
return ( return (
<Col className='cvat-annotation-header-left-group'> <Col className='cvat-annotation-header-left-group'>
<Dropdown overlay={<AnnotationMenuContainer />}> <Dropdown overlay={<AnnotationMenuContainer />}>
@ -113,6 +128,18 @@ function LeftGroup(props: Props): JSX.Element {
</Button> </Button>
</CVATTooltip> </CVATTooltip>
) : null} ) : null}
{includesToolsBlockerButton ? (
<CVATTooltip overlay={`Press "${switchToolsBlockerShortcut}" to postpone running the algorithm `}>
<Button
type='link'
className={`cvat-annotation-header-button ${toolsBlockerState.algorithmsLocked ? 'cvat-button-active' : ''}`}
onClick={shouldEnableToolsBlockerOnClick ? onSwitchToolsBlockerState : undefined}
>
<StopOutlined />
Block
</Button>
</CVATTooltip>
) : null}
</Col> </Col>
); );
} }

@ -6,7 +6,9 @@ import React from 'react';
import Input from 'antd/lib/input'; import Input from 'antd/lib/input';
import { Col, Row } from 'antd/lib/grid'; 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 LeftGroup from './left-group';
import PlayerButtons from './player-buttons'; import PlayerButtons from './player-buttons';
import PlayerNavigation from './player-navigation'; import PlayerNavigation from './player-navigation';
@ -28,6 +30,7 @@ interface Props {
undoShortcut: string; undoShortcut: string;
redoShortcut: string; redoShortcut: string;
drawShortcut: string; drawShortcut: string;
switchToolsBlockerShortcut: string;
playPauseShortcut: string; playPauseShortcut: string;
nextFrameShortcut: string; nextFrameShortcut: string;
previousFrameShortcut: string; previousFrameShortcut: string;
@ -39,6 +42,7 @@ interface Props {
predictor: PredictorState; predictor: PredictorState;
isTrainingActive: boolean; isTrainingActive: boolean;
activeControl: ActiveControl; activeControl: ActiveControl;
toolsBlockerState: ToolsBlockerState;
changeWorkspace(workspace: Workspace): void; changeWorkspace(workspace: Workspace): void;
switchPredictor(predictorEnabled: boolean): void; switchPredictor(predictorEnabled: boolean): void;
showStatistics(): void; showStatistics(): void;
@ -59,6 +63,7 @@ interface Props {
onUndoClick(): void; onUndoClick(): void;
onRedoClick(): void; onRedoClick(): void;
onFinishDraw(): void; onFinishDraw(): void;
onSwitchToolsBlockerState(): void;
jobInstance: any; jobInstance: any;
} }
@ -79,6 +84,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
undoShortcut, undoShortcut,
redoShortcut, redoShortcut,
drawShortcut, drawShortcut,
switchToolsBlockerShortcut,
playPauseShortcut, playPauseShortcut,
nextFrameShortcut, nextFrameShortcut,
previousFrameShortcut, previousFrameShortcut,
@ -89,6 +95,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
predictor, predictor,
focusFrameInputShortcut, focusFrameInputShortcut,
activeControl, activeControl,
toolsBlockerState,
showStatistics, showStatistics,
switchPredictor, switchPredictor,
showFilters, showFilters,
@ -109,6 +116,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
onUndoClick, onUndoClick,
onRedoClick, onRedoClick,
onFinishDraw, onFinishDraw,
onSwitchToolsBlockerState,
jobInstance, jobInstance,
isTrainingActive, isTrainingActive,
} = props; } = props;
@ -125,10 +133,13 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
redoShortcut={redoShortcut} redoShortcut={redoShortcut}
activeControl={activeControl} activeControl={activeControl}
drawShortcut={drawShortcut} drawShortcut={drawShortcut}
switchToolsBlockerShortcut={switchToolsBlockerShortcut}
toolsBlockerState={toolsBlockerState}
onSaveAnnotation={onSaveAnnotation} onSaveAnnotation={onSaveAnnotation}
onUndoClick={onUndoClick} onUndoClick={onUndoClick}
onRedoClick={onRedoClick} onRedoClick={onRedoClick}
onFinishDraw={onFinishDraw} onFinishDraw={onFinishDraw}
onSwitchToolsBlockerState={onSwitchToolsBlockerState}
/> />
<Col className='cvat-annotation-header-player-group'> <Col className='cvat-annotation-header-player-group'>
<Row align='middle'> <Row align='middle'>

@ -30,9 +30,10 @@ import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-ba
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper'; import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { import {
CombinedState, FrameSpeed, Workspace, PredictorState, DimensionType, ActiveControl, CombinedState, FrameSpeed, Workspace, PredictorState, DimensionType, ActiveControl, ToolsBlockerState,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { switchToolsBlockerState } from 'actions/settings-actions';
interface StateToProps { interface StateToProps {
jobInstance: any; jobInstance: any;
@ -49,6 +50,7 @@ interface StateToProps {
redoAction?: string; redoAction?: string;
autoSave: boolean; autoSave: boolean;
autoSaveInterval: number; autoSaveInterval: number;
toolsBlockerState: ToolsBlockerState;
workspace: Workspace; workspace: Workspace;
keyMap: KeyMap; keyMap: KeyMap;
normalizedKeyMap: Record<string, string>; normalizedKeyMap: Record<string, string>;
@ -72,6 +74,7 @@ interface DispatchToProps {
setForceExitAnnotationFlag(forceExit: boolean): void; setForceExitAnnotationFlag(forceExit: boolean): void;
changeWorkspace(workspace: Workspace): void; changeWorkspace(workspace: Workspace): void;
switchPredictor(predictorEnabled: boolean): void; switchPredictor(predictorEnabled: boolean): void;
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState): void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
@ -92,7 +95,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
}, },
settings: { settings: {
player: { frameSpeed, frameStep }, player: { frameSpeed, frameStep },
workspace: { autoSave, autoSaveInterval }, workspace: { autoSave, autoSaveInterval, toolsBlockerState },
}, },
shortcuts: { keyMap, normalizedKeyMap }, shortcuts: { keyMap, normalizedKeyMap },
plugins: { list }, plugins: { list },
@ -113,6 +116,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
redoAction: history.redo.length ? history.redo[history.redo.length - 1][0] : undefined, redoAction: history.redo.length ? history.redo[history.redo.length - 1][0] : undefined,
autoSave, autoSave,
autoSaveInterval, autoSaveInterval,
toolsBlockerState,
workspace, workspace,
keyMap, keyMap,
normalizedKeyMap, normalizedKeyMap,
@ -167,6 +171,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(getPredictionsAsync()); dispatch(getPredictionsAsync());
} }
}, },
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState):void{
dispatch(switchToolsBlockerState(toolsBlockerState));
},
}; };
} }
@ -431,6 +438,22 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
canvasInstance.draw({ enabled: false }); 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 => { private onURLIconClick = (): void => {
const { frameNumber } = this.props; const { frameNumber } = this.props;
const { origin, pathname } = window.location; const { origin, pathname } = window.location;
@ -546,6 +569,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
searchAnnotations, searchAnnotations,
changeWorkspace, changeWorkspace,
switchPredictor, switchPredictor,
toolsBlockerState,
} = this.props; } = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => { const preventDefault = (event: KeyboardEvent | undefined): void => {
@ -672,6 +696,8 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
undoShortcut={normalizedKeyMap.UNDO} undoShortcut={normalizedKeyMap.UNDO}
redoShortcut={normalizedKeyMap.REDO} redoShortcut={normalizedKeyMap.REDO}
drawShortcut={normalizedKeyMap.SWITCH_DRAW_MODE} drawShortcut={normalizedKeyMap.SWITCH_DRAW_MODE}
// this shortcut is handled in interactionHandler.ts separatelly
switchToolsBlockerShortcut={normalizedKeyMap.SWITCH_TOOLS_BLOCKER_STATE}
playPauseShortcut={normalizedKeyMap.PLAY_PAUSE} playPauseShortcut={normalizedKeyMap.PLAY_PAUSE}
nextFrameShortcut={normalizedKeyMap.NEXT_FRAME} nextFrameShortcut={normalizedKeyMap.NEXT_FRAME}
previousFrameShortcut={normalizedKeyMap.PREV_FRAME} previousFrameShortcut={normalizedKeyMap.PREV_FRAME}
@ -683,6 +709,8 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
onUndoClick={this.undo} onUndoClick={this.undo}
onRedoClick={this.redo} onRedoClick={this.redo}
onFinishDraw={this.onFinishDraw} onFinishDraw={this.onFinishDraw}
onSwitchToolsBlockerState={this.onSwitchToolsBlockerState}
toolsBlockerState={toolsBlockerState}
jobInstance={jobInstance} jobInstance={jobInstance}
isTrainingActive={isTrainingActive} isTrainingActive={isTrainingActive}
activeControl={activeControl} activeControl={activeControl}

@ -185,6 +185,7 @@ export interface Model {
framework: string; framework: string;
description: string; description: string;
type: string; type: string;
onChangeToolsBlockerState: (event:string) => void;
tip: { tip: {
message: string; message: string;
gif: string; gif: string;
@ -195,6 +196,12 @@ export interface Model {
} }
export type OpenCVTool = IntelligentScissors; export type OpenCVTool = IntelligentScissors;
export interface ToolsBlockerState {
algorithmsLocked?: boolean;
buttonVisible?: boolean;
}
export enum TaskStatus { export enum TaskStatus {
ANNOTATION = 'annotation', ANNOTATION = 'annotation',
REVIEW = 'validation', REVIEW = 'validation',
@ -564,6 +571,7 @@ export interface WorkspaceSettingsState {
showAllInterpolationTracks: boolean; showAllInterpolationTracks: boolean;
intelligentPolygonCrop: boolean; intelligentPolygonCrop: boolean;
defaultApproxPolyAccuracy: number; defaultApproxPolyAccuracy: number;
toolsBlockerState: ToolsBlockerState;
} }
export interface ShapesSettingsState { export interface ShapesSettingsState {

@ -32,6 +32,10 @@ const defaultState: SettingsState = {
showAllInterpolationTracks: false, showAllInterpolationTracks: false,
intelligentPolygonCrop: true, intelligentPolygonCrop: true,
defaultApproxPolyAccuracy: 9, defaultApproxPolyAccuracy: 9,
toolsBlockerState: {
algorithmsLocked: false,
buttonVisible: false,
},
}, },
player: { player: {
canvasBackgroundColor: '#ffffff', 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: { case SettingsActionTypes.SWITCH_SETTINGS_DIALOG: {
return { return {
...state, ...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: { case AuthActionTypes.LOGOUT_SUCCESS: {
return { ...defaultState }; return { ...defaultState };
} }

@ -322,6 +322,13 @@ const defaultKeyMap = ({
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D], 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: { CHANGE_OBJECT_COLOR: {
name: 'Change color', name: 'Change color',
description: 'Set the next color for an activated shape', description: 'Set the next color for an activated shape',

@ -14,6 +14,7 @@ export interface IntelligentScissorsParams {
enableSliding: boolean; enableSliding: boolean;
allowRemoveOnlyLast: boolean; allowRemoveOnlyLast: boolean;
minPosVertices: number; minPosVertices: number;
onChangeToolsBlockerState: (event:string)=>void;
}; };
} }
@ -21,6 +22,7 @@ export interface IntelligentScissors {
reset(): void; reset(): void;
run(points: number[], image: ImageData, offsetX: number, offsetY: number): number[]; run(points: number[], image: ImageData, offsetX: number, offsetY: number): number[];
params: IntelligentScissorsParams; params: IntelligentScissorsParams;
switchBlockMode(mode?:boolean):void;
} }
function applyOffset(points: Point[], offsetX: number, offsetY: number): Point[] { 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 { export default class IntelligentScissorsImplementation implements IntelligentScissors {
private cv: any; private cv: any;
private onChangeToolsBlockerState: (event:string)=>void;
private scissors: { private scissors: {
tool: any; tool: any;
state: { state: {
@ -46,14 +49,20 @@ export default class IntelligentScissorsImplementation implements IntelligentSci
} }
>; // point index : start index in path >; // point index : start index in path
image: any | null; image: any | null;
blocked: boolean;
}; };
}; };
public constructor(cv: any) { public constructor(cv: any, onChangeToolsBlockerState:(event:string)=>void) {
this.cv = cv; this.cv = cv;
this.onChangeToolsBlockerState = onChangeToolsBlockerState;
this.reset(); this.reset();
} }
public switchBlockMode(mode:boolean): void {
this.scissors.state.blocked = mode;
}
public reset(): void { public reset(): void {
if (this.scissors && this.scissors.tool) { if (this.scissors && this.scissors.tool) {
this.scissors.tool.delete(); this.scissors.tool.delete();
@ -66,6 +75,7 @@ export default class IntelligentScissorsImplementation implements IntelligentSci
path: [], path: [],
anchors: {}, anchors: {},
image: null, image: null,
blocked: false,
}, },
}; };
@ -88,7 +98,6 @@ export default class IntelligentScissorsImplementation implements IntelligentSci
const { tool, state } = scissors; const { tool, state } = scissors;
const points = applyOffset(numberArrayToPoints(coordinates), offsetX, offsetY); const points = applyOffset(numberArrayToPoints(coordinates), offsetX, offsetY);
if (points.length > 1) { if (points.length > 1) {
let matImage = null; let matImage = null;
const contour = new cv.Mat(); const contour = new cv.Mat();
@ -108,7 +117,6 @@ export default class IntelligentScissorsImplementation implements IntelligentSci
delete state.anchors[+i]; delete state.anchors[+i];
} }
} }
return [...state.path]; 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); state.path = state.path.slice(0, state.anchors[points.length - 1].start);
delete state.anchors[points.length - 1]; 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 = []; const pathSegment = [];
for (let row = 0; row < contour.rows; row++) { if (!state.blocked) {
pathSegment.push(contour.intAt(row, 0) + offsetX, contour.intAt(row, 1) + offsetY); 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] = { state.anchors[points.length - 1] = {
point: cur, point: cur,
@ -140,13 +151,13 @@ export default class IntelligentScissorsImplementation implements IntelligentSci
contour.delete(); contour.delete();
} }
} else { } else {
state.path = [];
state.path.push(...pointsToNumberArray(applyOffset(points.slice(-1), -offsetX, -offsetY))); state.path.push(...pointsToNumberArray(applyOffset(points.slice(-1), -offsetX, -offsetY)));
state.anchors[0] = { state.anchors[0] = {
point: points[0], point: points[0],
start: 0, start: 0,
}; };
} }
return [...state.path]; return [...state.path];
} }
@ -167,6 +178,7 @@ export default class IntelligentScissorsImplementation implements IntelligentSci
enableSliding: true, enableSliding: true,
allowRemoveOnlyLast: true, allowRemoveOnlyLast: true,
minPosVertices: 1, minPosVertices: 1,
onChangeToolsBlockerState: this.onChangeToolsBlockerState,
}, },
}; };
} }

@ -12,7 +12,7 @@ const core = getCore();
const baseURL = core.config.backendAPI.slice(0, -7); const baseURL = core.config.backendAPI.slice(0, -7);
export interface Segmentation { export interface Segmentation {
intelligentScissorsFactory: () => IntelligentScissors; intelligentScissorsFactory: (onChangeToolsBlockerState:(event:string)=>void) => IntelligentScissors;
} }
export interface Contours { export interface Contours {
@ -126,7 +126,8 @@ export class OpenCVWrapper {
} }
return { return {
intelligentScissorsFactory: () => new IntelligentScissorsImplementation(this.cv), intelligentScissorsFactory: (onChangeToolsBlockerState:(event:string)=>void) =>
new IntelligentScissorsImplementation(this.cv, onChangeToolsBlockerState),
}; };
} }

Loading…
Cancel
Save