diff --git a/CHANGELOG.md b/CHANGELOG.md index 66dcb96963e..71ef9865a1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support of cloud storage without copying data into CVAT: server part () - Filter `is_active` for user list () - Ability to export/import tasks () +- Explicit "Done" button when drawing any polyshapes () ### Changed @@ -20,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update of COCO format documentation () - Updated Webpack Dev Server config to add proxxy () - Update to Django 3.1.12 () +- Updated visibility for removable points in AI tools () +- Updated UI handling for IOG serverless function () ### Deprecated diff --git a/cvat-canvas/package-lock.json b/cvat-canvas/package-lock.json index 44d1ef1a1e0..b0ec3bcbc05 100644 --- a/cvat-canvas/package-lock.json +++ b/cvat-canvas/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.4.5", + "version": "2.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index 0b0438ab210..9fa50052e6a 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.4.5", + "version": "2.5.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 181fa15c195..ff0a50fc28b 100644 --- a/cvat-canvas/src/scss/canvas.scss +++ b/cvat-canvas/src/scss/canvas.scss @@ -155,6 +155,17 @@ polyline.cvat_canvas_shape_splitting { fill: blueviolet; } +.cvat_canvas_interact_intermediate_shape { + @extend .cvat_canvas_shape; +} + +.cvat_canvas_removable_interaction_point { + cursor: + url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K') + 10 10, + auto; +} + .svg_select_boundingRect { opacity: 0; pointer-events: none; diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index 780cca67267..1d6b009410b 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -77,10 +77,14 @@ export interface InteractionData { crosshair?: boolean; minPosVertices?: number; minNegVertices?: number; - enableNegVertices?: boolean; + startWithBox?: boolean; enableThreshold?: boolean; enableSliding?: boolean; allowRemoveOnlyLast?: boolean; + intermediateShape?: { + shapeType: string; + points: number[]; + }; } export interface InteractionResult { @@ -551,7 +555,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { throw Error(`Canvas is busy. Action: ${this.data.mode}`); } - if (interactionData.enabled) { + if (interactionData.enabled && !interactionData.intermediateShape) { if (this.data.interactionData.enabled) { throw new Error('Interaction has been already started'); } else if (!interactionData.shapeType) { diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 95e89b7d6ae..f1a9f4662b0 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -23,6 +23,7 @@ import consts from './consts'; import { translateToSVG, translateFromSVG, + translateToCanvas, pointsToNumberArray, parsePoints, displayShapeSize, @@ -103,7 +104,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private translateToCanvas(points: number[]): number[] { const { offset } = this.controller.geometry; - return points.map((coord: number): number => coord + offset); + return translateToCanvas(offset, points); } private translateFromCanvas(points: number[]): number[] { @@ -1267,9 +1268,11 @@ export class CanvasViewImpl implements CanvasView, Listener { } } else if (reason === UpdateReasons.INTERACT) { const data: InteractionData = this.controller.interactionData; - if (data.enabled && this.mode === Mode.IDLE) { - this.canvas.style.cursor = 'crosshair'; - this.mode = Mode.INTERACT; + if (data.enabled && (this.mode === Mode.IDLE || data.intermediateShape)) { + if (!data.intermediateShape) { + this.canvas.style.cursor = 'crosshair'; + this.mode = Mode.INTERACT; + } this.interactionHandler.interact(data); } else { this.canvas.style.cursor = ''; diff --git a/cvat-canvas/src/typescript/interactionHandler.ts b/cvat-canvas/src/typescript/interactionHandler.ts index 32371dbccce..c6526a4c334 100644 --- a/cvat-canvas/src/typescript/interactionHandler.ts +++ b/cvat-canvas/src/typescript/interactionHandler.ts @@ -5,7 +5,9 @@ import * as SVG from 'svg.js'; import consts from './consts'; import Crosshair from './crosshair'; -import { translateToSVG } from './shared'; +import { + translateToSVG, PropType, stringifyPoints, translateToCanvas, +} from './shared'; import { InteractionData, InteractionResult, Geometry } from './canvasModel'; export interface InteractionHandler { @@ -26,6 +28,8 @@ export class InteractionHandlerImpl implements InteractionHandler { private crosshair: Crosshair; private threshold: SVG.Rect | null; private thresholdRectSize: number; + private intermediateShape: PropType; + private drawnIntermediateShape: SVG.Shape; private prepareResult(): InteractionResult[] { return this.interactionShapes.map( @@ -65,8 +69,10 @@ export class InteractionHandlerImpl implements InteractionHandler { return enabled && !ctrlKey && !!interactionShapes.length; } - const minPosVerticesAchieved = typeof minPosVertices === 'undefined' || minPosVertices <= positiveShapes.length; - const minNegVerticesAchieved = typeof minNegVertices === 'undefined' || minPosVertices <= negativeShapes.length; + const minPosVerticesDefined = Number.isInteger(minPosVertices); + const minNegVerticesDefined = Number.isInteger(minNegVertices) && minNegVertices >= 0; + const minPosVerticesAchieved = !minPosVerticesDefined || minPosVertices <= positiveShapes.length; + const minNegVerticesAchieved = !minNegVerticesDefined || minNegVertices <= negativeShapes.length; const minimumVerticesAchieved = minPosVerticesAchieved && minNegVerticesAchieved; return enabled && !ctrlKey && minimumVerticesAchieved && shapesWereUpdated; } @@ -91,7 +97,7 @@ export class InteractionHandlerImpl implements InteractionHandler { private interactPoints(): void { const eventListener = (e: MouseEvent): void => { - if ((e.button === 0 || (e.button === 2 && this.interactionData.enableNegVertices)) && !e.altKey) { + if ((e.button === 0 || (e.button === 2 && this.interactionData.minNegVertices >= 0)) && !e.altKey) { e.preventDefault(); const [cx, cy] = translateToSVG((this.canvas.node as any) as SVGSVGElement, [e.clientX, e.clientY]); if (!this.isWithinFrame(cx, cy)) return; @@ -121,8 +127,10 @@ export class InteractionHandlerImpl implements InteractionHandler { } } + self.addClass('cvat_canvas_removable_interaction_point'); self.attr({ 'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale, + r: (consts.BASE_POINT_SIZE * 1.5) / this.geometry.scale, }); self.on('mousedown', (_e: MouseEvent): void => { @@ -132,6 +140,9 @@ export class InteractionHandlerImpl implements InteractionHandler { 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)) { this.onInteraction(this.prepareResult(), true, false); @@ -140,8 +151,10 @@ export class InteractionHandlerImpl implements InteractionHandler { }); self.on('mouseleave', (): void => { + self.removeClass('cvat_canvas_removable_interaction_point'); self.attr({ 'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale, + r: consts.BASE_POINT_SIZE / this.geometry.scale, }); self.off('mousedown'); @@ -153,7 +166,7 @@ export class InteractionHandlerImpl implements InteractionHandler { this.canvas.on('mousedown.interaction', eventListener); } - private interactRectangle(): void { + private interactRectangle(shouldFinish: boolean, onContinue?: () => void): void { let initialized = false; const eventListener = (e: MouseEvent): void => { if (e.button === 0 && !e.altKey) { @@ -170,11 +183,15 @@ export class InteractionHandlerImpl implements InteractionHandler { this.canvas.on('mousedown.interaction', eventListener); this.currentInteractionShape .on('drawstop', (): void => { + this.canvas.off('mousedown.interaction', eventListener); this.interactionShapes.push(this.currentInteractionShape); this.shapesWereUpdated = true; - this.canvas.off('mousedown.interaction', eventListener); - this.interact({ enabled: false }); + if (shouldFinish) { + this.interact({ enabled: false }); + } else if (onContinue) { + onContinue(); + } }) .addClass('cvat_canvas_shape_drawing') .attr({ @@ -194,15 +211,24 @@ export class InteractionHandlerImpl implements InteractionHandler { private startInteraction(): void { if (this.interactionData.shapeType === 'rectangle') { - this.interactRectangle(); + this.interactRectangle(true); } else if (this.interactionData.shapeType === 'points') { - this.interactPoints(); + if (this.interactionData.startWithBox) { + this.interactRectangle(false, (): void => this.interactPoints()); + } else { + this.interactPoints(); + } } else { throw new Error('Interactor implementation supports only rectangle and points'); } } private release(): void { + if (this.drawnIntermediateShape) { + this.drawnIntermediateShape.remove(); + this.drawnIntermediateShape = null; + } + if (this.crosshair) { this.removeCrosshair(); } @@ -241,6 +267,31 @@ export class InteractionHandlerImpl implements InteractionHandler { return imageX >= 0 && imageX < width && imageY >= 0 && imageY < height; } + private updateIntermediateShape(): void { + const { intermediateShape, geometry } = this; + if (this.drawnIntermediateShape) { + this.drawnIntermediateShape.remove(); + } + + if (!intermediateShape) return; + const { shapeType, points } = intermediateShape; + if (shapeType === 'polygon') { + this.drawnIntermediateShape = this.canvas + .polygon(stringifyPoints(translateToCanvas(geometry.offset, points))) + .attr({ + 'color-rendering': 'optimizeQuality', + 'shape-rendering': 'geometricprecision', + 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, + fill: 'none', + }) + .addClass('cvat_canvas_interact_intermediate_shape'); + } else { + throw new Error( + `Shape type "${shapeType}" was not implemented at interactionHandler::updateIntermediateShape`, + ); + } + } + public constructor( onInteraction: ( shapes: InteractionResult[] | null, @@ -264,6 +315,8 @@ export class InteractionHandlerImpl implements InteractionHandler { this.crosshair = new Crosshair(); this.threshold = null; this.thresholdRectSize = 300; + this.intermediateShape = null; + this.drawnIntermediateShape = null; this.cursorPosition = { x: 0, y: 0, @@ -334,8 +387,13 @@ export class InteractionHandlerImpl implements InteractionHandler { : [...this.interactionShapes]; for (const shape of shapesToBeScaled) { if (shape.type === 'circle') { - (shape as SVG.Circle).radius(consts.BASE_POINT_SIZE / this.geometry.scale); - shape.attr('stroke-width', consts.POINTS_STROKE_WIDTH / this.geometry.scale); + if (shape.hasClass('cvat_canvas_removable_interaction_point')) { + (shape as SVG.Circle).radius((consts.BASE_POINT_SIZE * 1.5) / this.geometry.scale); + shape.attr('stroke-width', consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale); + } else { + (shape as SVG.Circle).radius(consts.BASE_POINT_SIZE / this.geometry.scale); + shape.attr('stroke-width', consts.POINTS_STROKE_WIDTH / this.geometry.scale); + } } else { shape.attr('stroke-width', consts.BASE_STROKE_WIDTH / this.geometry.scale); } @@ -343,7 +401,13 @@ export class InteractionHandlerImpl implements InteractionHandler { } public interact(interactionData: InteractionData): void { - if (interactionData.enabled) { + if (interactionData.intermediateShape) { + this.intermediateShape = interactionData.intermediateShape; + this.updateIntermediateShape(); + if (this.interactionData.startWithBox) { + this.interactionShapes[0].style({ visibility: 'hidden' }); + } + } else if (interactionData.enabled) { this.interactionData = interactionData; this.initInteraction(); this.startInteraction(); diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts index 9ffa080ebda..452982a9e9b 100644 --- a/cvat-canvas/src/typescript/shared.ts +++ b/cvat-canvas/src/typescript/shared.ts @@ -181,3 +181,9 @@ export function vectorLength(vector: Vector2D): number { const sqrJ = vector.j ** 2; return Math.sqrt(sqrI + sqrJ); } + +export function translateToCanvas(offset: number, points: number[]): number[] { + return points.map((coord: number): number => coord + offset); +} + +export type PropType = T[Prop]; diff --git a/cvat-core/src/ml-model.js b/cvat-core/src/ml-model.js index 950f70043bf..e16cf24e27f 100644 --- a/cvat-core/src/ml-model.js +++ b/cvat-core/src/ml-model.js @@ -17,7 +17,8 @@ class MLModel { this._params = { canvas: { minPosVertices: data.min_pos_points, - enableNegVertices: true, + minNegVertices: data.min_neg_points, + startWithBox: data.startswith_box, }, }; } diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 3e5119d2b9e..d654c11a2b1 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.20.7", + "version": "1.21.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 38ab13b81f2..2158072dd07 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.20.7", + "version": "1.21.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/assets/fullscreen-icon.svg b/cvat-ui/src/assets/fullscreen-icon.svg index a5290f043fc..e620d72cdf9 100644 --- a/cvat-ui/src/assets/fullscreen-icon.svg +++ b/cvat-ui/src/assets/fullscreen-icon.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/cvat-ui/src/assets/info-icon.svg b/cvat-ui/src/assets/info-icon.svg index f2f7b1dd10e..96ca1120b01 100644 --- a/cvat-ui/src/assets/info-icon.svg +++ b/cvat-ui/src/assets/info-icon.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/cvat-ui/src/assets/main-menu-icon.svg b/cvat-ui/src/assets/main-menu-icon.svg index e712d571f74..4ad687e64e8 100644 --- a/cvat-ui/src/assets/main-menu-icon.svg +++ b/cvat-ui/src/assets/main-menu-icon.svg @@ -1 +1 @@ - + diff --git a/cvat-ui/src/assets/object-filter-icon.svg b/cvat-ui/src/assets/object-filter-icon.svg index b62e9623bbe..7ab2b729b33 100644 --- a/cvat-ui/src/assets/object-filter-icon.svg +++ b/cvat-ui/src/assets/object-filter-icon.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/cvat-ui/src/assets/redo-icon.svg b/cvat-ui/src/assets/redo-icon.svg index 61cf325add9..a0cdb866fc1 100644 --- a/cvat-ui/src/assets/redo-icon.svg +++ b/cvat-ui/src/assets/redo-icon.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/cvat-ui/src/assets/save-icon.svg b/cvat-ui/src/assets/save-icon.svg index a87e524ea67..9101c59eb6b 100644 --- a/cvat-ui/src/assets/save-icon.svg +++ b/cvat-ui/src/assets/save-icon.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/cvat-ui/src/assets/undo-icon.svg b/cvat-ui/src/assets/undo-icon.svg index d193a31408c..7575a69c14c 100644 --- a/cvat-ui/src/assets/undo-icon.svg +++ b/cvat-ui/src/assets/undo-icon.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx index d8c9c7615dd..f91233eaecc 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx @@ -19,7 +19,7 @@ import getCore from 'cvat-core-wrapper'; import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper'; import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors'; import { - CombinedState, ActiveControl, OpenCVTool, ObjectType, + CombinedState, ActiveControl, OpenCVTool, ObjectType, ShapeType, } from 'reducers/interfaces'; import { interactWithCanvas, @@ -93,17 +93,11 @@ const mapDispatchToProps = { class OpenCVControlComponent extends React.PureComponent { private activeTool: IntelligentScissors | null; - private interactiveStateID: number | null; - private interactionIsDone: boolean; public constructor(props: Props & DispatchToProps) { super(props); const { labels } = props; - this.activeTool = null; - this.interactiveStateID = null; - this.interactionIsDone = false; - this.state = { libraryInitialized: openCVWrapper.isInitialized, initializationError: false, @@ -115,7 +109,6 @@ class OpenCVControlComponent extends React.PureComponent _state.clientID === this.interactiveStateID)[0] || null; } - private cancelListener = async (): Promise => { - const { - fetchAnnotations, isActivated, jobInstance, frame, - } = this.props; - - if (isActivated) { - if (this.interactiveStateID !== null) { - const state = this.getInteractiveState(); - this.interactiveStateID = null; - await state.delete(frame); - fetchAnnotations(); - } - - await jobInstance.actions.freeze(false); - } - }; - private interactionListener = async (e: Event): Promise => { const { - fetchAnnotations, updateAnnotations, isActivated, jobInstance, frame, labels, curZOrder, + createAnnotations, isActivated, jobInstance, frame, labels, curZOrder, canvasInstance, } = this.props; const { activeLabelID } = this.state; if (!isActivated || !this.activeTool) { @@ -171,64 +139,36 @@ class OpenCVControlComponent extends React.PureComponent label.id === activeLabelID)[0], - points, + // need to recalculate without the latest sliding point + points: await this.runCVAlgorithm(pressedPoints, threshold), occluded: false, zOrder: curZOrder, }); - // need a clientID of a created object to interact with it further - // so, we do not use createAnnotationAction - const [clientID] = await jobInstance.annotations.put([object]); - this.interactiveStateID = clientID; - - // update annotations on a canvas - fetchAnnotations(); - return; - } - - const state = this.getInteractiveState(); - if ((e as CustomEvent).detail.isDone) { - const finalObject = new core.classes.ObjectState({ - frame: state.frame, - objectType: state.objectType, - label: state.label, - shapeType: state.shapeType, - // need to recalculate without the latest sliding point - points: points = await this.runCVAlgorithm(pressedPoints, threshold), - occluded: state.occluded, - zOrder: state.zOrder, - }); - this.interactiveStateID = null; - await state.delete(frame); - await jobInstance.actions.freeze(false); - await jobInstance.annotations.put([finalObject]); - fetchAnnotations(); - } else { - state.points = points; - updateAnnotations([state]); - fetchAnnotations(); + createAnnotations(jobInstance, frame, [finalObject]); } } catch (error) { notification.error({ description: error.toString(), - message: 'Processing error occured', + message: 'OpenCV.js processing error occured', }); } }; 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 f19971f1522..d859c80578e 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 @@ -85,15 +85,14 @@ function mapStateToProps(state: CombinedState): StateToProps { const mapDispatchToProps = { onInteractionStart: interactWithCanvas, updateAnnotations: updateAnnotationsAsync, - fetchAnnotations: fetchAnnotationsAsync, createAnnotations: createAnnotationsAsync, + fetchAnnotations: fetchAnnotationsAsync, }; type Props = StateToProps & DispatchToProps; interface State { activeInteractor: Model | null; activeLabelID: number; - interactiveStateID: number | null; activeTracker: Model | null; trackingProgress: number | null; trackingFrames: number; @@ -105,6 +104,7 @@ export class ToolsControlComponent extends React.PureComponent { private interactionIsAborted: boolean; private interactionIsDone: boolean; + private latestResult: number[]; public constructor(props: Props) { super(props); @@ -112,13 +112,13 @@ export class ToolsControlComponent extends React.PureComponent { activeInteractor: props.interactors.length ? props.interactors[0] : null, activeTracker: props.trackers.length ? props.trackers[0] : null, activeLabelID: props.labels.length ? props.labels[0].id : null, - interactiveStateID: null, trackingProgress: null, trackingFrames: 10, fetching: false, mode: 'interaction', }; + this.latestResult = []; this.interactionIsAborted = false; this.interactionIsDone = false; } @@ -149,12 +149,6 @@ export class ToolsControlComponent extends React.PureComponent { canvasInstance.html().removeEventListener('canvas.canceled', this.cancelListener); } - private getInteractiveState(): any | null { - const { states } = this.props; - const { interactiveStateID } = this.state; - return states.filter((_state: any): boolean => _state.clientID === interactiveStateID)[0] || null; - } - private contextmenuDisabler = (e: MouseEvent): void => { if ( e.target && @@ -166,10 +160,9 @@ export class ToolsControlComponent extends React.PureComponent { }; private cancelListener = async (): Promise => { - const { - isActivated, jobInstance, frame, fetchAnnotations, - } = this.props; - const { interactiveStateID, fetching } = this.state; + const { isActivated } = this.props; + const { fetching } = this.state; + this.latestResult = []; if (isActivated) { if (fetching && !this.interactionIsDone) { @@ -177,15 +170,6 @@ export class ToolsControlComponent extends React.PureComponent { this.setState({ fetching: false }); this.interactionIsAborted = true; } - - if (interactiveStateID !== null) { - const state = this.getInteractiveState(); - this.setState({ interactiveStateID: null }); - await state.delete(frame); - fetchAnnotations(); - } - - await jobInstance.actions.freeze(false); } }; @@ -197,28 +181,23 @@ export class ToolsControlComponent extends React.PureComponent { jobInstance, isActivated, activeLabelID, - fetchAnnotations, - updateAnnotations, + canvasInstance, + createAnnotations, } = this.props; - const { activeInteractor, interactiveStateID, fetching } = this.state; + const { activeInteractor, fetching } = this.state; if (!isActivated) { return; } try { - if (fetching) { - this.interactionIsDone = (e as CustomEvent).detail.isDone; - return; - } - + this.interactionIsDone = (e as CustomEvent).detail.isDone; const interactor = activeInteractor as Model; - let result = []; if ((e as CustomEvent).detail.shapesUpdated) { this.setState({ fetching: true }); try { - result = await core.lambda.call(jobInstance.task, interactor, { + this.latestResult = 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), @@ -227,6 +206,7 @@ export class ToolsControlComponent extends React.PureComponent { if (this.interactionIsAborted) { // while the server request // user has cancelled interaction (for example pressed ESC) + this.latestResult = []; return; } } finally { @@ -234,66 +214,30 @@ export class ToolsControlComponent extends React.PureComponent { } } - if (this.interactionIsDone) { - // while the server request, user has done interaction (for example pressed N) + if (!this.latestResult.length) { + return; + } + + 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: result.flat(), + points: this.latestResult.flat(), occluded: false, zOrder: curZOrder, }); - await jobInstance.annotations.put([object]); - fetchAnnotations(); + createAnnotations(jobInstance, frame, [object]); } else { - // no shape yet, then create it and save to collection - if (interactiveStateID === null) { - // freeze history for interaction time - // (points updating shouldn't cause adding new actions to history) - await jobInstance.actions.freeze(true); - const object = new core.classes.ObjectState({ - frame, - objectType: ObjectType.SHAPE, - label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null, + canvasInstance.interact({ + enabled: true, + intermediateShape: { shapeType: ShapeType.POLYGON, - points: result.flat(), - occluded: false, - zOrder: curZOrder, - }); - // need a clientID of a created object to interact with it further - // so, we do not use createAnnotationAction - const [clientID] = await jobInstance.annotations.put([object]); - - // update annotations on a canvas - fetchAnnotations(); - this.setState({ interactiveStateID: clientID }); - return; - } - - const state = this.getInteractiveState(); - if ((e as CustomEvent).detail.isDone) { - const finalObject = new core.classes.ObjectState({ - frame: state.frame, - objectType: state.objectType, - label: state.label, - shapeType: state.shapeType, - points: result.length ? result.flat() : state.points, - occluded: state.occluded, - zOrder: state.zOrder, - }); - this.setState({ interactiveStateID: null }); - await state.delete(frame); - await jobInstance.actions.freeze(false); - await jobInstance.annotations.put([finalObject]); - fetchAnnotations(); - } else { - state.points = result.flat(); - updateAnnotations([state]); - fetchAnnotations(); - } + points: this.latestResult.flat(), + }, + }); } } catch (err) { notification.error({ @@ -633,7 +577,7 @@ export class ToolsControlComponent extends React.PureComponent { private renderDetectorBlock(): JSX.Element { const { - jobInstance, detectors, curZOrder, frame, fetchAnnotations, + jobInstance, detectors, curZOrder, frame, } = this.props; if (!detectors.length) { @@ -672,8 +616,7 @@ export class ToolsControlComponent extends React.PureComponent { }), ); - await jobInstance.annotations.put(states); - fetchAnnotations(); + createAnnotationsAsync(jobInstance, frame, states); } catch (error) { notification.error({ description: error.toString(), diff --git a/cvat-ui/src/components/annotation-page/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss index dd53134f81e..969d63aa1d6 100644 --- a/cvat-ui/src/components/annotation-page/styles.scss +++ b/cvat-ui/src/components/annotation-page/styles.scss @@ -16,7 +16,7 @@ .ant-layout-header.cvat-annotation-header { background-color: $background-color-2; border-bottom: 1px solid $border-color-1; - height: 54px; + height: 48px; padding: 0; > div:first-child { @@ -39,14 +39,14 @@ .ant-btn.cvat-annotation-header-button { padding: 0; - width: 54px; - height: 54px; - text-align: center; + width: 48px; + height: 48px; user-select: none; color: $text-color; display: flex; flex-direction: column; align-items: center; + justify-content: center; margin: 0 3px; > span:not([role='img']) { @@ -55,21 +55,15 @@ } > span[role='img'] { - transform: scale(0.8); - padding: 3px; + font-size: 24px; } &:hover > span[role='img'] { - transform: scale(0.85); + transform: scale(1.1); } &:active > span[role='img'] { - transform: scale(0.8); - } - - > * { - display: block; - line-height: 0; + transform: scale(1.05); } &.filters-armed { @@ -135,7 +129,7 @@ button.cvat-predictor-button { } .cvat-annotation-header-player-group > div { - height: 54px; + height: 48px; line-height: 0; flex-wrap: nowrap; } @@ -212,9 +206,11 @@ button.cvat-predictor-button { justify-content: flex-end; > div { - display: block; - height: 54px; - margin-right: 15px; + display: flex; + height: 48px; + align-items: center; + justify-content: center; + padding-right: 8px; } } diff --git a/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx index dbbad65fed5..472cb5008dd 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { Col } from 'antd/lib/grid'; -import Icon from '@ant-design/icons'; +import Icon, { CheckOutlined } from '@ant-design/icons'; import Modal from 'antd/lib/modal'; import Button from 'antd/lib/button'; import Timeline from 'antd/lib/timeline'; @@ -14,6 +14,8 @@ import AnnotationMenuContainer from 'containers/annotation-page/top-bar/annotati import { MainMenuIcon, SaveIcon, UndoIcon, RedoIcon, } from 'icons'; +import { ActiveControl } from 'reducers/interfaces'; +import CVATTooltip from 'components/common/cvat-tooltip'; interface Props { saving: boolean; @@ -23,9 +25,12 @@ interface Props { saveShortcut: string; undoShortcut: string; redoShortcut: string; + drawShortcut: string; + activeControl: ActiveControl; onSaveAnnotation(): void; onUndoClick(): void; onRedoClick(): void; + onFinishDraw(): void; } function LeftGroup(props: Props): JSX.Element { @@ -37,11 +42,22 @@ function LeftGroup(props: Props): JSX.Element { saveShortcut, undoShortcut, redoShortcut, + drawShortcut, + activeControl, onSaveAnnotation, onUndoClick, onRedoClick, + onFinishDraw, } = props; + const includesDoneButton = [ + ActiveControl.DRAW_POLYGON, + ActiveControl.DRAW_POLYLINE, + ActiveControl.DRAW_POINTS, + ActiveControl.AI_TOOLS, + ActiveControl.OPENCV_TOOLS, + ].includes(activeControl); + return ( }> @@ -50,44 +66,53 @@ function LeftGroup(props: Props): JSX.Element { Menu - - - + + + + + + + + + + {includesDoneButton ? ( + + + + ) : null} ); } diff --git a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx index d64c338a2e0..5c2867b9df0 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx @@ -6,7 +6,7 @@ import React from 'react'; import Input from 'antd/lib/input'; import { Col, Row } from 'antd/lib/grid'; -import { PredictorState, Workspace } from 'reducers/interfaces'; +import { ActiveControl, PredictorState, Workspace } from 'reducers/interfaces'; import LeftGroup from './left-group'; import PlayerButtons from './player-buttons'; import PlayerNavigation from './player-navigation'; @@ -27,6 +27,7 @@ interface Props { saveShortcut: string; undoShortcut: string; redoShortcut: string; + drawShortcut: string; playPauseShortcut: string; nextFrameShortcut: string; previousFrameShortcut: string; @@ -37,6 +38,7 @@ interface Props { focusFrameInputShortcut: string; predictor: PredictorState; isTrainingActive: boolean; + activeControl: ActiveControl; changeWorkspace(workspace: Workspace): void; switchPredictor(predictorEnabled: boolean): void; showStatistics(): void; @@ -56,6 +58,7 @@ interface Props { onURLIconClick(): void; onUndoClick(): void; onRedoClick(): void; + onFinishDraw(): void; jobInstance: any; } @@ -75,6 +78,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { saveShortcut, undoShortcut, redoShortcut, + drawShortcut, playPauseShortcut, nextFrameShortcut, previousFrameShortcut, @@ -84,6 +88,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { nextButtonType, predictor, focusFrameInputShortcut, + activeControl, showStatistics, switchPredictor, showFilters, @@ -103,6 +108,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { onURLIconClick, onUndoClick, onRedoClick, + onFinishDraw, jobInstance, isTrainingActive, } = props; @@ -117,9 +123,12 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { saveShortcut={saveShortcut} undoShortcut={undoShortcut} redoShortcut={redoShortcut} + activeControl={activeControl} + drawShortcut={drawShortcut} onSaveAnnotation={onSaveAnnotation} onUndoClick={onUndoClick} onRedoClick={onRedoClick} + onFinishDraw={onFinishDraw} /> diff --git a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx index 5e1bf0b6747..d14e382c37e 100644 --- a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx @@ -30,7 +30,7 @@ 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, + CombinedState, FrameSpeed, Workspace, PredictorState, DimensionType, ActiveControl, } from 'reducers/interfaces'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; @@ -55,6 +55,7 @@ interface StateToProps { canvasInstance: Canvas | Canvas3d; forceExit: boolean; predictor: PredictorState; + activeControl: ActiveControl; isTrainingActive: boolean; } @@ -85,7 +86,7 @@ function mapStateToProps(state: CombinedState): StateToProps { history, }, job: { instance: jobInstance }, - canvas: { ready: canvasIsReady, instance: canvasInstance }, + canvas: { ready: canvasIsReady, instance: canvasInstance, activeControl }, workspace, predictor, }, @@ -118,6 +119,7 @@ function mapStateToProps(state: CombinedState): StateToProps { canvasInstance, forceExit, predictor, + activeControl, isTrainingActive: list.PREDICT, }; } @@ -416,6 +418,19 @@ class AnnotationTopBarContainer extends React.PureComponent { } }; + private onFinishDraw = (): void => { + const { activeControl, canvasInstance } = this.props; + if ( + [ActiveControl.AI_TOOLS, ActiveControl.OPENCV_TOOLS].includes(activeControl) && + canvasInstance instanceof Canvas + ) { + canvasInstance.interact({ enabled: false }); + return; + } + + canvasInstance.draw({ enabled: false }); + }; + private onURLIconClick = (): void => { const { frameNumber } = this.props; const { origin, pathname } = window.location; @@ -526,10 +541,11 @@ class AnnotationTopBarContainer extends React.PureComponent { normalizedKeyMap, canvasInstance, predictor, + isTrainingActive, + activeControl, searchAnnotations, changeWorkspace, switchPredictor, - isTrainingActive, } = this.props; const preventDefault = (event: KeyboardEvent | undefined): void => { @@ -655,6 +671,7 @@ class AnnotationTopBarContainer extends React.PureComponent { saveShortcut={normalizedKeyMap.SAVE_JOB} undoShortcut={normalizedKeyMap.UNDO} redoShortcut={normalizedKeyMap.REDO} + drawShortcut={normalizedKeyMap.SWITCH_DRAW_MODE} playPauseShortcut={normalizedKeyMap.PLAY_PAUSE} nextFrameShortcut={normalizedKeyMap.NEXT_FRAME} previousFrameShortcut={normalizedKeyMap.PREV_FRAME} @@ -665,8 +682,10 @@ class AnnotationTopBarContainer extends React.PureComponent { focusFrameInputShortcut={normalizedKeyMap.FOCUS_INPUT_FRAME} onUndoClick={this.undo} onRedoClick={this.redo} + onFinishDraw={this.onFinishDraw} jobInstance={jobInstance} isTrainingActive={isTrainingActive} + activeControl={activeControl} /> ); diff --git a/cvat-ui/src/cvat-canvas-wrapper.ts b/cvat-ui/src/cvat-canvas-wrapper.ts index 3b0c1e21174..10bd8fbc559 100644 --- a/cvat-ui/src/cvat-canvas-wrapper.ts +++ b/cvat-ui/src/cvat-canvas-wrapper.ts @@ -22,7 +22,7 @@ export function convertShapesForInteractor(shapes: InteractionResult[], button: }; return shapes - .filter((shape: InteractionResult): boolean => shape.shapeType === 'points' && shape.button === button) + .filter((shape: InteractionResult): boolean => shape.button === button) .map((shape: InteractionResult): number[] => shape.points) .flat() .reduce(reducer, []); diff --git a/cvat/apps/lambda_manager/views.py b/cvat/apps/lambda_manager/views.py index 079d5789454..53bb2c14389 100644 --- a/cvat/apps/lambda_manager/views.py +++ b/cvat/apps/lambda_manager/views.py @@ -111,6 +111,7 @@ def __init__(self, gateway, data): # display name for the function self.name = meta_anno.get('name', self.id) 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.gateway = gateway @@ -127,6 +128,7 @@ def to_dict(self): if self.kind is LambdaType.INTERACTOR: response.update({ 'min_pos_points': self.min_pos_points, + 'min_neg_points': self.min_neg_points, 'startswith_box': self.startswith_box }) @@ -166,8 +168,9 @@ def invoke(self, db_task, data): elif self.kind == LambdaType.INTERACTOR: payload.update({ "image": self._get_image(db_task, data["frame"], quality), - "pos_points": data["pos_points"], - "neg_points": data["neg_points"] + "pos_points": data["pos_points"][2:] if self.startswith_box else data["pos_points"], + "neg_points": data["neg_points"], + "obj_bbox": data["pos_points"][0:2] if self.startswith_box else None }) elif self.kind == LambdaType.REID: payload.update({ diff --git a/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml b/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml index 50a25753d7c..57b78b718de 100644 --- a/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml +++ b/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml @@ -7,6 +7,7 @@ metadata: spec: framework: pytorch min_pos_points: 1 + min_neg_points: 0 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 f84f543ced1..a210195fcd5 100644 --- a/serverless/pytorch/shiyinzhang/iog/nuclio/function.yaml +++ b/serverless/pytorch/shiyinzhang/iog/nuclio/function.yaml @@ -7,6 +7,7 @@ metadata: spec: framework: pytorch min_pos_points: 1 + min_neg_points: 0 startswith_box: true spec: diff --git a/serverless/pytorch/shiyinzhang/iog/nuclio/model_handler.py b/serverless/pytorch/shiyinzhang/iog/nuclio/model_handler.py index 5d972915d35..c16cd84a10e 100644 --- a/serverless/pytorch/shiyinzhang/iog/nuclio/model_handler.py +++ b/serverless/pytorch/shiyinzhang/iog/nuclio/model_handler.py @@ -50,10 +50,10 @@ def handle(self, image, bbox, pos_points, neg_points, threshold): # extract a crop with padding from the image crop_padding = 30 crop_bbox = [ - max(bbox[0] - crop_padding, 0), - max(bbox[1] - crop_padding, 0), - min(bbox[2] + crop_padding, image.width - 1), - min(bbox[3] + crop_padding, image.height - 1) + max(bbox[0][0] - crop_padding, 0), + max(bbox[0][1] - crop_padding, 0), + min(bbox[1][0] + crop_padding, image.width - 1), + min(bbox[1][1] + crop_padding, image.height - 1) ] crop_shape = ( int(crop_bbox[2] - crop_bbox[0] + 1), # width diff --git a/tests/cypress/integration/actions_objects/case_37_object_make_copy.js b/tests/cypress/integration/actions_objects/case_37_object_make_copy.js index ac543c3006c..3e25c046e7c 100644 --- a/tests/cypress/integration/actions_objects/case_37_object_make_copy.js +++ b/tests/cypress/integration/actions_objects/case_37_object_make_copy.js @@ -135,7 +135,7 @@ context('Object make a copy.', () => { .rightclick('right'); // When click in the center of polyline: is being covered by another element: { it('Copy a shape with holding "Ctrl".', () => { const keyCodeC = 67; const keyCodeV = 86; - cy.get('.cvat_canvas_shape').first().trigger('mousemove').should('have.class', 'cvat_canvas_shape_activated'); - cy.get('body').type('{ctrl}', {release: false}); // Hold + cy.get('.cvat_canvas_shape') + .first() + .trigger('mousemove') + .should('have.class', 'cvat_canvas_shape_activated'); + cy.get('body').type('{ctrl}', { release: false }); // Hold cy.get('body') - .trigger('keydown', {keyCode: keyCodeC, ctrlKey: true}) + .trigger('keydown', { keyCode: keyCodeC, ctrlKey: true }) .trigger('keyup') - .trigger('keydown', {keyCode: keyCodeV, ctrlKey: true}) + .trigger('keydown', { keyCode: keyCodeV, ctrlKey: true }) .trigger('keyup'); cy.get('.cvat-canvas-container').click(400, 300); cy.get('.cvat-canvas-container').click(500, 300); diff --git a/tests/cypress/integration/actions_objects/case_54_redraw_feature.js b/tests/cypress/integration/actions_objects/case_54_redraw_feature.js index 9bb6e647807..fec851a4ce7 100644 --- a/tests/cypress/integration/actions_objects/case_54_redraw_feature.js +++ b/tests/cypress/integration/actions_objects/case_54_redraw_feature.js @@ -131,7 +131,7 @@ context('Redraw feature.', () => { it('Draw and redraw a cuboid.', () => { cy.createCuboid(createCuboidShape2Points); - cy.get('.cvat-canvas-container').trigger('mousemove', 300, 400); + cy.get('.cvat-canvas-container').trigger('mousemove', 350, 400); cy.get('#cvat_canvas_shape_5').should('have.class', 'cvat_canvas_shape_activated'); cy.get('body').trigger('keydown', { keyCode: keyCodeN, shiftKey: true }); // Start redraw the cuboid cy.get('.cvat-canvas-container')