diff --git a/CHANGELOG.md b/CHANGELOG.md index ff247905c991..cb0c298be44f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 () ### Changed -- TDB +- Non-blocking UI when using interactors () +- "Selected opacity" slider now defines opacity level for shapes being drawnSelected opacity () ### 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 ``` diff --git a/cvat-canvas/package-lock.json b/cvat-canvas/package-lock.json index abaab237db6e..a598d7215b3d 100644 --- a/cvat-canvas/package-lock.json +++ b/cvat-canvas/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.5.0", + "version": "2.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index 813dab0fdd45..ad5dd2897ff2 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -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": { diff --git a/cvat-canvas/src/scss/canvas.scss b/cvat-canvas/src/scss/canvas.scss index 993745ba4fbd..5e21171d2cdf 100644 --- a/cvat-canvas/src/scss/canvas.scss +++ b/cvat-canvas/src/scss/canvas.scss @@ -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; } diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index 0da3bce42a91..7732fea355a2 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -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); } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index f1a9f4662b00..1eceef6daf31 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -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([]); diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index 8bf38eee1d45..847375a612ab 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -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) { diff --git a/cvat-canvas/src/typescript/interactionHandler.ts b/cvat-canvas/src/typescript/interactionHandler.ts index dcb8101ef5e5..b7ede60495c7 100644 --- a/cvat-canvas/src/typescript/interactionHandler.ts +++ b/cvat-canvas/src/typescript/interactionHandler.ts @@ -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); diff --git a/cvat-canvas/src/typescript/svg.patch.ts b/cvat-canvas/src/typescript/svg.patch.ts index 060dde62e2a6..debebfd1f901 100644 --- a/cvat-canvas/src/typescript/svg.patch.ts +++ b/cvat-canvas/src/typescript/svg.patch.ts @@ -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 }); diff --git a/cvat-core/src/ml-model.js b/cvat-core/src/ml-model.js index e16cf24e27f2..010e674626b8 100644 --- a/cvat-core/src/ml-model.js +++ b/cvat-core/src/ml-model.js @@ -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; diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 7ca955400d11..f878779282c8 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.21.3", + "version": "1.22.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index e88d38f61e2d..977098b85317 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -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": { diff --git a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx index fb21708fd116..1afcc0c89f37 100644 --- a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx @@ -106,6 +106,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { showObjectsTextAlways, workspace, showProjections, + selectedOpacity, } = this.props; const { canvasInstance } = this.props as { canvasInstance: Canvas }; @@ -121,6 +122,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { forceDisableEditing: workspace === Workspace.REVIEW_WORKSPACE, intelligentPolygonCrop, showProjections, + creationOpacity: selectedOpacity, }); this.initialSetup(); @@ -166,7 +168,8 @@ export default class CanvasWrapperComponent extends React.PureComponent { 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 { autoborders: automaticBordering, showProjections, intelligentPolygonCrop, + creationOpacity: selectedOpacity, }); } @@ -198,7 +202,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { 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 { } 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 { ) { 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 { } 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 { 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 { } 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 { - }> + }> diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/interactor-tooltips.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/interactor-tooltips.tsx new file mode 100644 index 000000000000..6ee62ed1e6ae --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/interactor-tooltips.tsx @@ -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 ( +
+ {name ? ( + <> + {desc} + + You can prevent server requests holding + {' Ctrl '} + key + + + Positive points can be added by left-clicking the image. + {withNegativePoints ? ( + Negative points can be added by right-clicking the image. + ) : null} + + {gif ? Example gif : null} + + ) : ( + Select an interactor to see help message + )} +
+ ); +} + +export default React.memo(InteractorTooltips); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx index 9f4ff760b2df..52ded26dc771 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx @@ -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 { - 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 { 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 { 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 { }; private cancelListener = async (): Promise => { - 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 => { - const { - frame, - labels, - curZOrder, - jobInstance, - isActivated, - activeLabelID, - canvasInstance, - createAnnotations, - } = this.props; + private runInteractionRequest = async (interactionId: string): Promise => { + 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 { } }; + 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 => { const { isActivated, jobInstance, frame, curZOrder, fetchAnnotations, @@ -306,7 +347,6 @@ export class ToolsControlComponent extends React.PureComponent { 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 { }); }; + 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 { 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 { ); } + const minNegVertices = activeInteractor ? (activeInteractor.params.canvas.minNegVertices as number) : -1; + return ( <> @@ -587,8 +647,8 @@ export class ToolsControlComponent extends React.PureComponent { Interactor - - + + + + = 0} + {...(activeInteractor?.tip || {})} + /> + )} + > + + + @@ -725,7 +798,7 @@ export class ToolsControlComponent extends React.PureComponent { 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 { className: 'cvat-tools-control', }; - return !labels.length ? ( - - ) : ( + 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 ? ( <> + { + this.setState({ approxPolyAccuracy: value }); + }} + /> + + ) : null; + + const trackOrDetectModal: JSX.Element | null = + showDetectionContent || showTrackingContent ? ( Waiting for a server response.. - {trackingProgress !== null && ( - - )} + {showTrackingContent ? ( + + ) : null} - {isActivated && activeInteractor !== null && pointsRecieved ? ( - { - this.setState({ approxPolyAccuracy: value }); - }} - /> - ) : null} + ) : null; + + return showAnyContent ? ( + <> + {interactionContent} + {trackOrDetectModal} + ) : ( + ); } } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss index 6897e2f1cb87..1a8784e1acc9 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss @@ -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%; } diff --git a/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx b/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx index cc92a23131ee..d7d52d9ac7f6 100644 --- a/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx +++ b/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx @@ -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, diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 7da382b85df3..adbe7bffcdf5 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -182,8 +182,12 @@ export interface Model { framework: string; description: string; type: string; + tip: { + message: string; + gif: string; + }; params: { - canvas: Record; + canvas: Record; }; } diff --git a/cvat/apps/lambda_manager/views.py b/cvat/apps/lambda_manager/views.py index 53bb2c143892..f5a34217e244 100644 --- a/cvat/apps/lambda_manager/views.py +++ b/cvat/apps/lambda_manager/views.py @@ -113,6 +113,8 @@ def __init__(self, gateway, data): 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 @@ def to_dict(self): 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: diff --git a/serverless/openvino/dextr/nuclio/function.yaml b/serverless/openvino/dextr/nuclio/function.yaml index 825599428f62..b996c0d8285e 100644 --- a/serverless/openvino/dextr/nuclio/function.yaml +++ b/serverless/openvino/dextr/nuclio/function.yaml @@ -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 diff --git a/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml b/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml index 57b78b718de0..c0c4e6dcf35e 100644 --- a/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml +++ b/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml @@ -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 diff --git a/serverless/pytorch/shiyinzhang/iog/nuclio/function.yaml b/serverless/pytorch/shiyinzhang/iog/nuclio/function.yaml index a210195fcd54..6535525a1f92 100644 --- a/serverless/pytorch/shiyinzhang/iog/nuclio/function.yaml +++ b/serverless/pytorch/shiyinzhang/iog/nuclio/function.yaml @@ -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