Semi-automatic tools enhancements (Non-blocking UI, tips) (#3473)

main
Boris Sekachev 5 years ago committed by GitHub
parent d773f8c6a5
commit 59af610f12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -10,10 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Notification if the browser does not support nesassary API
- Additional inline tips in interactors with demo gifs (<https://github.com/openvinotoolkit/cvat/pull/3473>)
### Changed
- TDB
- Non-blocking UI when using interactors (<https://github.com/openvinotoolkit/cvat/pull/3473>)
- "Selected opacity" slider now defines opacity level for shapes being drawnSelected opacity (<https://github.com/openvinotoolkit/cvat/pull/3473>)
### Deprecated
@ -874,20 +876,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
```
## [Unreleased]
### Added
-
- TDB
### Changed
-
- TDB
### Deprecated
-
- TDB
### Removed
-
- TDB
### Fixed
-
- TDB
### Security
-
- TDB
```

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

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

@ -24,7 +24,6 @@ polyline.cvat_shape_action_opacity {
}
.cvat_shape_drawing_opacity {
fill-opacity: 0.2;
stroke-opacity: 1;
}
@ -161,9 +160,8 @@ polyline.cvat_canvas_shape_splitting {
.cvat_canvas_removable_interaction_point {
cursor:
url(
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K'
) 10 10,
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K')
10 10,
auto;
}

@ -59,6 +59,7 @@ export interface Configuration {
forceDisableEditing?: boolean;
intelligentPolygonCrop?: boolean;
forceFrameUpdate?: boolean;
creationOpacity?: number;
}
export interface DrawData {
@ -547,6 +548,16 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
}
// install default values for drawing method
if (drawData.enabled) {
if (drawData.shapeType === 'rectangle') {
this.data.drawData.rectDrawingMethod = drawData.rectDrawingMethod || RectDrawingMethod.CLASSIC;
}
if (drawData.shapeType === 'cuboid') {
this.data.drawData.cuboidDrawingMethod = drawData.cuboidDrawingMethod || CuboidDrawingMethod.CLASSIC;
}
}
this.notify(UpdateReasons.DRAW);
}
@ -656,6 +667,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.configuration.forceFrameUpdate = configuration.forceFrameUpdate;
}
if (typeof configuration.creationOpacity === 'number') {
this.data.configuration.creationOpacity = configuration.creationOpacity;
}
this.notify(UpdateReasons.CONFIG_UPDATED);
}

@ -998,6 +998,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.adoptedContent,
this.adoptedText,
this.autoborderHandler,
this.geometry,
this.configuration,
);
this.editHandler = new EditHandlerImpl(this.onEditDone.bind(this), this.adoptedContent, this.autoborderHandler);
this.mergeHandler = new MergeHandlerImpl(
@ -1026,6 +1028,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.onInteraction.bind(this),
this.adoptedContent,
this.geometry,
this.configuration,
);
// Setup event handlers
@ -1117,6 +1120,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.activate(activeElement);
this.editHandler.configurate(this.configuration);
this.drawHandler.configurate(this.configuration);
this.interactionHandler.configurate(this.configuration);
// remove if exist and not enabled
// this.setupObjects([]);

@ -50,6 +50,7 @@ export class DrawHandlerImpl implements DrawHandler {
private crosshair: Crosshair;
private drawData: DrawData;
private geometry: Geometry;
private configuration: Configuration;
private autoborderHandler: AutoborderHandler;
private autobordersEnabled: boolean;
@ -371,6 +372,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
});
}
@ -527,6 +529,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
});
this.drawPolyshape();
@ -597,6 +600,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
});
}
@ -654,6 +658,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
});
this.pasteShape();
@ -686,6 +691,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
});
this.pasteShape();
this.pastePolyshape();
@ -709,6 +715,7 @@ export class DrawHandlerImpl implements DrawHandler {
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'face-stroke': 'black',
'fill-opacity': this.configuration.creationOpacity,
});
this.pasteShape();
this.pastePolyshape();
@ -845,6 +852,8 @@ export class DrawHandlerImpl implements DrawHandler {
canvas: SVG.Container,
text: SVG.Container,
autoborderHandler: AutoborderHandler,
geometry: Geometry,
configuration: Configuration,
) {
this.autoborderHandler = autoborderHandler;
this.autobordersEnabled = false;
@ -855,7 +864,8 @@ export class DrawHandlerImpl implements DrawHandler {
this.initialized = false;
this.canceled = false;
this.drawData = null;
this.geometry = null;
this.geometry = geometry;
this.configuration = configuration;
this.crosshair = new Crosshair();
this.drawInstance = null;
this.pointsGroup = null;
@ -874,6 +884,20 @@ export class DrawHandlerImpl implements DrawHandler {
}
public configurate(configuration: Configuration): void {
this.configuration = configuration;
const isFillableRect = this.drawData
&& this.drawData.shapeType === 'rectangle'
&& (this.drawData.rectDrawingMethod === RectDrawingMethod.CLASSIC || this.drawData.initialState);
const isFillableCuboid = this.drawData
&& this.drawData.shapeType === 'cuboid'
&& (this.drawData.cuboidDrawingMethod === CuboidDrawingMethod.CLASSIC || this.drawData.initialState);
const isFilalblePolygon = this.drawData && this.drawData.shapeType === 'polygon';
if (this.drawInstance && (isFillableRect || isFillableCuboid || isFilalblePolygon)) {
this.drawInstance.fill({ opacity: configuration.creationOpacity });
}
if (typeof configuration.autoborders === 'boolean') {
this.autobordersEnabled = configuration.autoborders;
if (this.drawInstance) {

@ -8,16 +8,20 @@ import Crosshair from './crosshair';
import {
translateToSVG, PropType, stringifyPoints, translateToCanvas,
} from './shared';
import { InteractionData, InteractionResult, Geometry } from './canvasModel';
import {
InteractionData, InteractionResult, Geometry, Configuration,
} from './canvasModel';
export interface InteractionHandler {
transform(geometry: Geometry): void;
interact(interactData: InteractionData): void;
configurate(config: Configuration): void;
cancel(): void;
}
export class InteractionHandlerImpl implements InteractionHandler {
private onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void;
private configuration: Configuration;
private geometry: Geometry;
private canvas: SVG.Container;
private interactionData: InteractionData;
@ -196,7 +200,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
})
.fill({ opacity: this.configuration.creationOpacity, color: 'white' });
}
private initInteraction(): void {
@ -286,8 +291,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
'shape-rendering': 'geometricprecision',
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
stroke: erroredShape ? 'red' : 'black',
fill: 'none',
})
.fill({ opacity: this.configuration.creationOpacity, color: 'white' })
.addClass('cvat_canvas_interact_intermediate_shape');
this.selectize(true, this.drawnIntermediateShape, erroredShape);
} else {
@ -339,12 +344,14 @@ export class InteractionHandlerImpl implements InteractionHandler {
) => void,
canvas: SVG.Container,
geometry: Geometry,
configuration: Configuration,
) {
this.onInteraction = (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean): void => {
this.shapesWereUpdated = false;
onInteraction(shapes, shapesUpdated, isDone, this.threshold ? this.thresholdRectSize / 2 : null);
};
this.canvas = canvas;
this.configuration = configuration;
this.geometry = geometry;
this.shapesWereUpdated = false;
this.interactionShapes = [];
@ -465,6 +472,25 @@ export class InteractionHandlerImpl implements InteractionHandler {
}
}
public configurate(configuration: Configuration): void {
this.configuration = configuration;
if (this.drawnIntermediateShape) {
this.drawnIntermediateShape.fill({
opacity: configuration.creationOpacity,
});
}
// when interactRectangle
if (this.currentInteractionShape && this.currentInteractionShape.type === 'rect') {
this.currentInteractionShape.fill({ opacity: configuration.creationOpacity });
}
// when interactPoints with startwithbbox
if (this.interactionShapes[0] && this.interactionShapes[0].type === 'rect') {
this.interactionShapes[0].fill({ opacity: configuration.creationOpacity });
}
}
public cancel(): void {
this.release();
this.onInteraction(null);

@ -958,8 +958,12 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
},
paintOrientationLines() {
const fillColor = this.attr('fill');
const strokeColor = this.attr('stroke');
// style has higher priority than attr, so then try to fetch it if exists
// https://stackoverflow.com/questions/47088409/svg-attributes-beaten-by-cssstyle-in-priority]
// we use getComputedStyle to get actual, not-inlined css property (come from the corresponding css class)
const computedStyles = getComputedStyle(this.node);
const fillColor = computedStyles['fill'] || this.attr('fill');
const strokeColor = computedStyles['stroke'] || this.attr('stroke');
const selectedColor = this.attr('face-stroke') || '#b0bec5';
this.frontTopEdge.stroke({ color: selectedColor });
this.frontLeftEdge.stroke({ color: selectedColor });

@ -14,6 +14,10 @@ class MLModel {
this._framework = data.framework;
this._description = data.description;
this._type = data.type;
this._tip = {
message: data.help_message,
gif: data.animated_gif,
};
this._params = {
canvas: {
minPosVertices: data.min_pos_points,
@ -84,6 +88,17 @@ class MLModel {
canvas: { ...this._params.canvas },
};
}
/**
* @typedef {Object} MlModelTip
* @property {string} message A short message for a user about the model
* @property {string} gif A gif URL to be shawn to a user as an example
* @returns {MlModelTip}
* @readonly
*/
get tip() {
return { ...this._tip };
}
}
module.exports = MLModel;

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

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

@ -106,6 +106,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
showObjectsTextAlways,
workspace,
showProjections,
selectedOpacity,
} = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
@ -121,6 +122,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
forceDisableEditing: workspace === Workspace.REVIEW_WORKSPACE,
intelligentPolygonCrop,
showProjections,
creationOpacity: selectedOpacity,
});
this.initialSetup();
@ -166,7 +168,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
prevProps.showObjectsTextAlways !== showObjectsTextAlways ||
prevProps.automaticBordering !== automaticBordering ||
prevProps.showProjections !== showProjections ||
prevProps.intelligentPolygonCrop !== intelligentPolygonCrop
prevProps.intelligentPolygonCrop !== intelligentPolygonCrop ||
prevProps.selectedOpacity !== selectedOpacity
) {
canvasInstance.configure({
undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE,
@ -174,6 +177,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
autoborders: automaticBordering,
showProjections,
intelligentPolygonCrop,
creationOpacity: selectedOpacity,
});
}
@ -198,7 +202,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.activate(null);
const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`);
if (el) {
(el as any).instance.fill({ opacity: opacity / 100 });
(el as any).instance.fill({ opacity });
}
}
@ -214,7 +218,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
if (gridPattern) {
gridPattern.style.stroke = gridColor.toLowerCase();
gridPattern.style.opacity = `${gridOpacity / 100}`;
gridPattern.style.opacity = `${gridOpacity}`;
}
}
@ -225,10 +229,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
) {
const backgroundElement = window.document.getElementById('cvat_canvas_background');
if (backgroundElement) {
backgroundElement.style.filter =
`brightness(${brightnessLevel / 100})` +
`contrast(${contrastLevel / 100})` +
`saturate(${saturationLevel / 100})`;
const filter = `brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`;
backgroundElement.style.filter = filter;
}
}
@ -619,7 +621,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`);
if (el) {
((el as any) as SVGElement).setAttribute('fill-opacity', `${selectedOpacity / 100}`);
((el as any) as SVGElement).setAttribute('fill-opacity', `${selectedOpacity}`);
}
}
}
@ -648,7 +650,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
handler.nested.fill({ color: shapeColor });
}
(shapeView as any).instance.fill({ color: shapeColor, opacity: opacity / 100 });
(shapeView as any).instance.fill({ color: shapeColor, opacity });
(shapeView as any).instance.stroke({ color: outlined ? outlineColor : shapeColor });
}
}
@ -710,17 +712,15 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
if (gridPattern) {
gridPattern.style.stroke = gridColor.toLowerCase();
gridPattern.style.opacity = `${gridOpacity / 100}`;
gridPattern.style.opacity = `${gridOpacity}`;
}
canvasInstance.grid(gridSize, gridSize);
// Filters
const backgroundElement = window.document.getElementById('cvat_canvas_background');
if (backgroundElement) {
backgroundElement.style.filter =
`brightness(${brightnessLevel / 100})` +
`contrast(${contrastLevel / 100})` +
`saturate(${saturationLevel / 100})`;
const filter = `brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`;
backgroundElement.style.filter = filter;
}
const canvasWrapperElement = window.document
@ -823,7 +823,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
<ContextImage />
<Dropdown trigger='click' placement='topCenter' overlay={<ImageSetupsContent />}>
<Dropdown trigger={['click']} placement='topCenter' overlay={<ImageSetupsContent />}>
<UpOutlined className='cvat-canvas-image-setups-trigger' />
</Dropdown>

@ -0,0 +1,48 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Image from 'antd/lib/image';
import Paragraph from 'antd/lib/typography/Paragraph';
import Text from 'antd/lib/typography/Text';
interface Props {
name?: string;
gif?: string;
message?: string;
withNegativePoints?: boolean;
}
function InteractorTooltips(props: Props): JSX.Element {
const {
name, gif, message, withNegativePoints,
} = props;
const UNKNOWN_MESSAGE = 'Selected interactor does not have a help message';
const desc = message || UNKNOWN_MESSAGE;
return (
<div className='cvat-interactor-tip-container'>
{name ? (
<>
<Paragraph>{desc}</Paragraph>
<Paragraph>
<Text>You can prevent server requests holding</Text>
<Text strong>{' Ctrl '}</Text>
<Text>key</Text>
</Paragraph>
<Paragraph>
<Text>Positive points can be added by left-clicking the image. </Text>
{withNegativePoints ? (
<Text>Negative points can be added by right-clicking the image. </Text>
) : null}
</Paragraph>
{gif ? <Image className='cvat-interactor-tip-image' alt='Example gif' src={gif} /> : null}
</>
) : (
<Text>Select an interactor to see help message</Text>
)}
</div>
);
}
export default React.memo(InteractorTooltips);

@ -4,7 +4,7 @@
import React, { MutableRefObject } from 'react';
import { connect } from 'react-redux';
import Icon, { LoadingOutlined } from '@ant-design/icons';
import Icon, { LoadingOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import Popover from 'antd/lib/popover';
import Select from 'antd/lib/select';
import Button from 'antd/lib/button';
@ -16,6 +16,8 @@ import notification from 'antd/lib/notification';
import message from 'antd/lib/message';
import Progress from 'antd/lib/progress';
import InputNumber from 'antd/lib/input-number';
import Dropdown from 'antd/lib/dropdown';
import lodash from 'lodash';
import { AIToolsIcon } from 'icons';
import { Canvas, convertShapesForInteractor } from 'cvat-canvas-wrapper';
@ -37,6 +39,7 @@ import ApproximationAccuracy, {
thresholdFromAccuracy,
} from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy';
import withVisibilityHandling from './handle-popover-visibility';
import ToolsTooltips from './interactor-tooltips';
interface StateToProps {
canvasInstance: Canvas;
@ -111,10 +114,21 @@ interface State {
}
export class ToolsControlComponent extends React.PureComponent<Props, State> {
private interactionIsAborted: boolean;
private interactionIsDone: boolean;
private latestResponseResult: number[][];
private latestResult: number[][];
private interaction: {
id: string | null;
isAborted: boolean;
latestResponse: number[][];
latestResult: number[][];
latestRequest: null | {
interactor: Model;
data: {
frame: number;
neg_points: number[][];
pos_points: number[][];
};
} | null;
hideMessage: (() => void) | null;
};
public constructor(props: Props) {
super(props);
@ -130,10 +144,14 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
mode: 'interaction',
};
this.latestResponseResult = [];
this.latestResult = [];
this.interactionIsAborted = false;
this.interactionIsDone = false;
this.interaction = {
id: null,
isAborted: false,
latestResponse: [],
latestResult: [],
latestRequest: null,
hideMessage: null,
};
}
public componentDidMount(): void {
@ -145,32 +163,42 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
public componentDidUpdate(prevProps: Props, prevState: State): void {
const { isActivated, defaultApproxPolyAccuracy, canvasInstance } = this.props;
const { approxPolyAccuracy, activeInteractor } = this.state;
const { approxPolyAccuracy, mode } = this.state;
if (prevProps.isActivated && !isActivated) {
window.removeEventListener('contextmenu', this.contextmenuDisabler);
// hide interaction message if exists
if (this.interaction.hideMessage) {
this.interaction.hideMessage();
this.interaction.hideMessage = null;
}
} else if (!prevProps.isActivated && isActivated) {
// reset flags when start interaction/tracking
this.interaction = {
id: null,
isAborted: false,
latestResponse: [],
latestResult: [],
latestRequest: null,
hideMessage: null,
};
this.setState({
approxPolyAccuracy: defaultApproxPolyAccuracy,
pointsRecieved: false,
});
this.latestResult = [];
this.latestResponseResult = [];
this.interactionIsDone = false;
this.interactionIsAborted = false;
window.addEventListener('contextmenu', this.contextmenuDisabler);
}
if (prevState.approxPolyAccuracy !== approxPolyAccuracy) {
if (isActivated && activeInteractor !== null && this.latestResponseResult.length) {
this.approximateResponsePoints(this.latestResponseResult).then((points: number[][]) => {
this.latestResult = points;
if (isActivated && mode === 'interaction' && this.interaction.latestResponse.length) {
this.approximateResponsePoints(this.interaction.latestResponse).then((points: number[][]) => {
this.interaction.latestResult = points;
canvasInstance.interact({
enabled: true,
intermediateShape: {
shapeType: ShapeType.POLYGON,
points: this.latestResult.flat(),
points: this.interaction.latestResult.flat(),
},
});
});
@ -196,91 +224,67 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
};
private cancelListener = async (): Promise<void> => {
const { isActivated } = this.props;
const { fetching } = this.state;
this.latestResult = [];
if (isActivated) {
if (fetching && !this.interactionIsDone) {
// user pressed ESC
this.setState({ fetching: false });
this.interactionIsAborted = true;
}
if (fetching) {
// user pressed ESC
this.setState({ fetching: false });
this.interaction.isAborted = true;
}
};
private onInteraction = async (e: Event): Promise<void> => {
const {
frame,
labels,
curZOrder,
jobInstance,
isActivated,
activeLabelID,
canvasInstance,
createAnnotations,
} = this.props;
private runInteractionRequest = async (interactionId: string): Promise<void> => {
const { jobInstance, canvasInstance } = this.props;
const { activeInteractor, fetching } = this.state;
if (!isActivated) {
const { id, latestRequest } = this.interaction;
if (id !== interactionId || !latestRequest || fetching) {
// current interaction request is not relevant (new interaction session has started)
// or a user didn't add more points
// or one server request is on processing
return;
}
try {
this.interactionIsDone = (e as CustomEvent).detail.isDone;
const interactor = activeInteractor as Model;
const { interactor, data } = latestRequest;
this.interaction.latestRequest = null;
if ((e as CustomEvent).detail.shapesUpdated) {
try {
this.interaction.hideMessage = message.loading(`Waiting a response from ${activeInteractor?.name}..`, 0);
try {
// run server request
this.setState({ fetching: true });
try {
this.latestResponseResult = await core.lambda.call(jobInstance.task, interactor, {
frame,
pos_points: convertShapesForInteractor((e as CustomEvent).detail.shapes, 0),
neg_points: convertShapesForInteractor((e as CustomEvent).detail.shapes, 2),
});
const response = await core.lambda.call(jobInstance.task, interactor, data);
// approximation with cv.approxPolyDP
const approximated = await this.approximateResponsePoints(response);
this.latestResult = this.latestResponseResult;
if (this.interaction.id !== interactionId || this.interaction.isAborted) {
// new interaction session or the session is aborted
return;
}
if (this.interactionIsAborted) {
// while the server request
// user has cancelled interaction (for example pressed ESC)
// need to clean variables that have been just set
this.latestResult = [];
this.latestResponseResult = [];
return;
}
this.interaction.latestResponse = response;
this.interaction.latestResult = approximated;
this.latestResult = await this.approximateResponsePoints(this.latestResponseResult);
} finally {
this.setState({ fetching: false, pointsRecieved: !!this.latestResult.length });
this.setState({ pointsRecieved: !!response.length });
} finally {
if (this.interaction.id === interactionId && this.interaction.hideMessage) {
this.interaction.hideMessage();
this.interaction.hideMessage = null;
}
}
if (!this.latestResult.length) {
return;
this.setState({ fetching: false });
}
if (this.interactionIsDone && !fetching) {
const object = new core.classes.ObjectState({
frame,
objectType: ObjectType.SHAPE,
label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null,
shapeType: ShapeType.POLYGON,
points: this.latestResult.flat(),
occluded: false,
zOrder: curZOrder,
});
createAnnotations(jobInstance, frame, [object]);
} else {
if (this.interaction.latestResult.length) {
canvasInstance.interact({
enabled: true,
intermediateShape: {
shapeType: ShapeType.POLYGON,
points: this.latestResult.flat(),
points: this.interaction.latestResult.flat(),
},
});
}
setTimeout(() => this.runInteractionRequest(interactionId));
} catch (err) {
notification.error({
description: err.toString(),
@ -289,6 +293,43 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
}
};
private onInteraction = (e: Event): void => {
const { frame, isActivated } = this.props;
const { activeInteractor } = this.state;
if (!isActivated) {
return;
}
if (!this.interaction.id) {
this.interaction.id = lodash.uniqueId('interaction_');
}
const { shapesUpdated, isDone, shapes } = (e as CustomEvent).detail;
if (isDone) {
// make an object from current result
// do not make one more request
// prevent future requests if possible
this.interaction.isAborted = true;
this.interaction.latestRequest = null;
if (this.interaction.latestResult.length) {
this.constructFromPoints(this.interaction.latestResult);
}
} else if (shapesUpdated) {
const interactor = activeInteractor as Model;
this.interaction.latestRequest = {
interactor,
data: {
frame,
pos_points: convertShapesForInteractor(shapes, 0),
neg_points: convertShapesForInteractor(shapes, 2),
},
};
this.runInteractionRequest(this.interaction.id);
}
};
private onTracking = async (e: Event): Promise<void> => {
const {
isActivated, jobInstance, frame, curZOrder, fetchAnnotations,
@ -306,7 +347,6 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
return;
}
this.interactionIsDone = true;
try {
const { points } = (e as CustomEvent).detail.shapes[0];
const state = new core.classes.ObjectState({
@ -363,11 +403,29 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
});
};
private constructFromPoints(points: number[][]): void {
const {
frame, labels, curZOrder, jobInstance, activeLabelID, createAnnotations,
} = this.props;
const object = new core.classes.ObjectState({
frame,
objectType: ObjectType.SHAPE,
label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null,
shapeType: ShapeType.POLYGON,
points: points.flat(),
occluded: false,
zOrder: curZOrder,
});
createAnnotations(jobInstance, frame, [object]);
}
private async approximateResponsePoints(points: number[][]): Promise<number[][]> {
const { approxPolyAccuracy } = this.state;
if (points.length > 3) {
if (!openCVWrapper.isInitialized) {
const hide = message.loading('OpenCV.js initialization..');
const hide = message.loading('OpenCV.js initialization..', 0);
try {
await openCVWrapper.initialize(() => {});
} finally {
@ -580,6 +638,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
);
}
const minNegVertices = activeInteractor ? (activeInteractor.params.canvas.minNegVertices as number) : -1;
return (
<>
<Row justify='start'>
@ -587,8 +647,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
<Text className='cvat-text-color'>Interactor</Text>
</Col>
</Row>
<Row align='middle' justify='center'>
<Col span={24}>
<Row align='middle' justify='space-between'>
<Col span={22}>
<Select
style={{ width: '100%' }}
defaultValue={interactors[0].name}
@ -607,6 +667,19 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
)}
</Select>
</Col>
<Col span={2} className='cvat-interactors-tips-icon-container'>
<Dropdown
overlay={(
<ToolsTooltips
name={activeInteractor?.name}
withNegativePoints={minNegVertices >= 0}
{...(activeInteractor?.tip || {})}
/>
)}
>
<QuestionCircleOutlined />
</Dropdown>
</Col>
</Row>
<Row align='middle' justify='end'>
<Col>
@ -725,7 +798,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
interactors, detectors, trackers, isActivated, canvasInstance, labels,
} = this.props;
const {
fetching, trackingProgress, approxPolyAccuracy, activeInteractor, pointsRecieved,
fetching, trackingProgress, approxPolyAccuracy, pointsRecieved, mode,
} = this.state;
if (![...interactors, ...detectors, ...trackers].length) return null;
@ -749,35 +822,51 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
className: 'cvat-tools-control',
};
return !labels.length ? (
<Icon className=' cvat-tools-control cvat-disabled-canvas-control' component={AIToolsIcon} />
) : (
const showAnyContent = !!labels.length;
const showInteractionContent = isActivated && mode === 'interaction' && pointsRecieved;
const showDetectionContent = fetching && mode === 'detection';
const showTrackingContent = fetching && mode === 'tracking' && trackingProgress !== null;
const formattedTrackingProgress = showTrackingContent ? +((trackingProgress as number) * 100).toFixed(0) : null;
const interactionContent: JSX.Element | null = showInteractionContent ? (
<>
<ApproximationAccuracy
approxPolyAccuracy={approxPolyAccuracy}
onChange={(value: number) => {
this.setState({ approxPolyAccuracy: value });
}}
/>
</>
) : null;
const trackOrDetectModal: JSX.Element | null =
showDetectionContent || showTrackingContent ? (
<Modal
title='Making a server request'
zIndex={Number.MAX_SAFE_INTEGER}
visible={fetching}
visible
destroyOnClose
closable={false}
footer={[]}
>
<Text>Waiting for a server response..</Text>
<LoadingOutlined style={{ marginLeft: '10px' }} />
{trackingProgress !== null && (
<Progress percent={+(trackingProgress * 100).toFixed(0)} status='active' />
)}
{showTrackingContent ? (
<Progress percent={formattedTrackingProgress as number} status='active' />
) : null}
</Modal>
{isActivated && activeInteractor !== null && pointsRecieved ? (
<ApproximationAccuracy
approxPolyAccuracy={approxPolyAccuracy}
onChange={(value: number) => {
this.setState({ approxPolyAccuracy: value });
}}
/>
) : null}
) : null;
return showAnyContent ? (
<>
<CustomPopover {...dynamcPopoverPros} placement='right' content={this.renderPopoverContent()}>
<Icon {...dynamicIconProps} component={AIToolsIcon} />
</CustomPopover>
{interactionContent}
{trackOrDetectModal}
</>
) : (
<Icon className=' cvat-tools-control cvat-disabled-canvas-control' component={AIToolsIcon} />
);
}
}

@ -192,6 +192,24 @@
margin-top: 10px;
}
.cvat-interactors-tips-icon-container {
text-align: center;
font-size: 20px;
}
.cvat-interactor-tip-container {
background: $background-color-2;
padding: $grid-unit-size;
box-shadow: $box-shadow-base;
width: $grid-unit-size * 40;
text-align: center;
border-radius: 4px;
}
.cvat-interactor-tip-image {
width: $grid-unit-size * 37;
}
.cvat-draw-shape-popover-points-selector {
width: 100%;
}

@ -90,6 +90,7 @@ interface StateToProps {
maxZLayer: number;
curZLayer: number;
automaticBordering: boolean;
intelligentPolygonCrop: boolean;
switchableAutomaticBordering: boolean;
keyMap: KeyMap;
canvasBackgroundColor: string;
@ -188,9 +189,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
activatedAttributeID,
selectedStatesID,
annotations,
opacity,
opacity: opacity / 100,
colorBy,
selectedOpacity,
selectedOpacity: selectedOpacity / 100,
outlined,
outlineColor,
showBitmap,
@ -198,12 +199,12 @@ function mapStateToProps(state: CombinedState): StateToProps {
grid,
gridSize,
gridColor,
gridOpacity,
gridOpacity: gridOpacity / 100,
activeLabelID,
activeObjectType,
brightnessLevel,
contrastLevel,
saturationLevel,
brightnessLevel: brightnessLevel / 100,
contrastLevel: contrastLevel / 100,
saturationLevel: saturationLevel / 100,
resetZoom,
aamZoomMargin,
showObjectsTextAlways,

@ -182,8 +182,12 @@ export interface Model {
framework: string;
description: string;
type: string;
tip: {
message: string;
gif: string;
};
params: {
canvas: Record<string, unknown>;
canvas: Record<string, number | boolean>;
};
}

@ -113,6 +113,8 @@ class LambdaFunction:
self.min_pos_points = int(meta_anno.get('min_pos_points', 1))
self.min_neg_points = int(meta_anno.get('min_neg_points', -1))
self.startswith_box = bool(meta_anno.get('startswith_box', False))
self.animated_gif = meta_anno.get('animated_gif', '')
self.help_message = meta_anno.get('help_message', '')
self.gateway = gateway
def to_dict(self):
@ -129,7 +131,9 @@ class LambdaFunction:
response.update({
'min_pos_points': self.min_pos_points,
'min_neg_points': self.min_neg_points,
'startswith_box': self.startswith_box
'startswith_box': self.startswith_box,
'help_message': self.help_message,
'animated_gif': self.animated_gif
})
if self.kind is LambdaType.TRACKER:

@ -7,6 +7,8 @@ metadata:
spec:
framework: openvino
min_pos_points: 4
animated_gif: https://raw.githubusercontent.com/openvinotoolkit/cvat/0fbb19ae3846a017853d52e187f0ce149adced7d/site/content/en/images/dextr_example.gif
help_message: The interactor allows to get a mask of an object using its extreme points (more or equal than 4). You can add a point left-clicking the image
spec:
description: Deep Extreme Cut

@ -8,6 +8,8 @@ metadata:
framework: pytorch
min_pos_points: 1
min_neg_points: 0
animated_gif: https://raw.githubusercontent.com/openvinotoolkit/cvat/0fbb19ae3846a017853d52e187f0ce149adced7d/site/content/en/images/fbrs_example.gif
help_message: The interactor allows to get a mask for an object using positive points, and negative points
spec:
description: f-BRS interactive segmentation

@ -9,6 +9,8 @@ metadata:
min_pos_points: 1
min_neg_points: 0
startswith_box: true
animated_gif: https://raw.githubusercontent.com/openvinotoolkit/cvat/0fbb19ae3846a017853d52e187f0ce149adced7d/site/content/en/images/iog_example.gif
help_message: The interactor allows to get a mask of an object using its wrapping boundig box, positive, and negative points inside it
spec:
description: Interactive Object Segmentation with Inside-Outside Guidance

Loading…
Cancel
Save