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>)
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>)
- Added intelligent scissors blocking feature (<https://github.com/openvinotoolkit/cvat/pull/3510>)
### Changed

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

@ -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": {

@ -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;

@ -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);

@ -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<InteractionData, 'intermediateShape'>;
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();

@ -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;

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

@ -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": {

@ -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<SettingsState>): AnyAction {
return {
type: SettingsActionTypes.SET_SETTINGS,

@ -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<Props & DispatchToProps, State> {
@ -142,7 +149,10 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
public componentDidUpdate(prevProps: Props, prevState: State): void {
const { approxPolyAccuracy } = this.state;
const { isActivated, defaultApproxPolyAccuracy, canvasInstance } = this.props;
const {
isActivated, defaultApproxPolyAccuracy, canvasInstance, toolsBlockerState,
} = this.props;
if (!prevProps.isActivated && isActivated) {
// reset flags & states before using a tool
this.latestPoints = [];
@ -150,6 +160,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
approxPolyAccuracy: defaultApproxPolyAccuracy,
});
if (this.activeTool) {
this.activeTool.switchBlockMode(toolsBlockerState.algorithmsLocked);
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 {
@ -180,7 +195,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
private interactionListener = async (e: Event): Promise<void> => {
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<Props & DispatchToProps
shapesUpdated, isDone, threshold, shapes,
} = (e as CustomEvent).detail;
const pressedPoints = convertShapesForInteractor(shapes, 0).flat();
try {
if (shapesUpdated) {
this.latestPoints = await this.runCVAlgorithm(pressedPoints, threshold);
const approx = openCVWrapper.contours.approxPoly(
this.latestPoints,
thresholdFromAccuracy(approxPolyAccuracy),
false,
);
this.latestPoints = await this.runCVAlgorithm(pressedPoints,
toolsBlockerState.algorithmsLocked ? 0 : threshold);
let points = [];
if (toolsBlockerState.algorithmsLocked && this.latestPoints.length > 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<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> => {
const { activeImageModifiers } = this.state;
const {
@ -279,22 +323,24 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
if (!canvas) {
throw new Error('Element #cvat_canvas_background was not found');
}
if (!this.activeTool || pressedPoints.length === 0) return [];
const { width, height } = canvas;
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Canvas context is empty');
}
let imageData;
const [x, y] = pressedPoints.slice(-2);
const startX = Math.round(Math.max(0, x - threshold));
const startY = Math.round(Math.max(0, y - threshold));
const segmentWidth = Math.min(2 * threshold, width - startX);
const segmentHeight = Math.min(2 * threshold, height - startY);
const imageData = context.getImageData(startX, startY, segmentWidth, segmentHeight);
if (!this.activeTool) return [];
if (threshold !== 0) {
const segmentWidth = Math.min(2 * threshold, width - startX);
const segmentHeight = Math.min(2 * threshold, height - startY);
imageData = context.getImageData(startX, startY, segmentWidth, segmentHeight);
} else {
imageData = context.getImageData(0, 0, width, height);
}
// Handling via OpenCV.js
const points = await this.activeTool.run(pressedPoints, imageData, startX, startY);
return points;
@ -360,7 +406,8 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
<CVATTooltip title='Intelligent scissors' className='cvat-opencv-drawing-tool'>
<Button
onClick={() => {
this.activeTool = openCVWrapper.segmentation.intelligentScissorsFactory();
this.activeTool = openCVWrapper.segmentation
.intelligentScissorsFactory(this.onChangeToolsBlockerState);
canvasInstance.cancel();
onInteractionStart(this.activeTool, activeLabelID);
canvasInstance.interact({

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

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

@ -4,7 +4,7 @@
import React from 'react';
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 Button from 'antd/lib/button';
import Timeline from 'antd/lib/timeline';
@ -14,7 +14,7 @@ import AnnotationMenuContainer from 'containers/annotation-page/top-bar/annotati
import {
MainMenuIcon, SaveIcon, UndoIcon, RedoIcon,
} from 'icons';
import { ActiveControl } from 'reducers/interfaces';
import { ActiveControl, ToolsBlockerState } from 'reducers/interfaces';
import CVATTooltip from 'components/common/cvat-tooltip';
interface Props {
@ -26,11 +26,14 @@ interface Props {
undoShortcut: string;
redoShortcut: string;
drawShortcut: string;
switchToolsBlockerShortcut: string;
toolsBlockerState: ToolsBlockerState;
activeControl: ActiveControl;
onSaveAnnotation(): void;
onUndoClick(): void;
onRedoClick(): void;
onFinishDraw(): void;
onSwitchToolsBlockerState(): void;
}
function LeftGroup(props: Props): JSX.Element {
@ -43,11 +46,14 @@ function LeftGroup(props: Props): JSX.Element {
undoShortcut,
redoShortcut,
drawShortcut,
switchToolsBlockerShortcut,
activeControl,
toolsBlockerState,
onSaveAnnotation,
onUndoClick,
onRedoClick,
onFinishDraw,
onSwitchToolsBlockerState,
} = props;
const includesDoneButton = [
@ -58,6 +64,15 @@ function LeftGroup(props: Props): JSX.Element {
ActiveControl.OPENCV_TOOLS,
].includes(activeControl);
const includesToolsBlockerButton = [
ActiveControl.OPENCV_TOOLS,
ActiveControl.AI_TOOLS,
].includes(activeControl) && toolsBlockerState.buttonVisible;
const shouldEnableToolsBlockerOnClick = [
ActiveControl.OPENCV_TOOLS,
].includes(activeControl);
return (
<Col className='cvat-annotation-header-left-group'>
<Dropdown overlay={<AnnotationMenuContainer />}>
@ -113,6 +128,18 @@ function LeftGroup(props: Props): JSX.Element {
</Button>
</CVATTooltip>
) : 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>
);
}

@ -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}
/>
<Col className='cvat-annotation-header-player-group'>
<Row align='middle'>

@ -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<string, string>;
@ -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<Props, State> {
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<Props, State> {
searchAnnotations,
changeWorkspace,
switchPredictor,
toolsBlockerState,
} = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
@ -672,6 +696,8 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
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<Props, State> {
onUndoClick={this.undo}
onRedoClick={this.redo}
onFinishDraw={this.onFinishDraw}
onSwitchToolsBlockerState={this.onSwitchToolsBlockerState}
toolsBlockerState={toolsBlockerState}
jobInstance={jobInstance}
isTrainingActive={isTrainingActive}
activeControl={activeControl}

@ -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 {

@ -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 };
}

@ -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',

@ -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,
},
};
}

@ -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),
};
}

Loading…
Cancel
Save