diff --git a/CHANGELOG.md b/CHANGELOG.md index f37cf7d43f1..8a2f7bc279e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Objects sorting option in the sidebar, by z order. Additional visualization when the sorting is applied () - Added YOLOv5 serverless function NVIDIA GPU support () +- Mask tools are supported now (brush, eraser, polygon-plus, polygon-minus, returning masks +from online detectors & interactors) () - Added Webhooks () ### Changed diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index c9cee7a7b47..eb1aa7d7281 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.15.4", + "version": "2.16.0", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", "scripts": { @@ -17,6 +17,8 @@ ], "dependencies": { "@types/polylabel": "^1.0.5", + "@types/fabric": "^4.5.7", + "fabric": "^5.2.1", "polylabel": "^1.1.0", "svg.draggable.js": "2.2.2", "svg.draw.js": "^2.0.4", diff --git a/cvat-canvas/src/scss/canvas.scss b/cvat-canvas/src/scss/canvas.scss index 7c124118d00..c90d8b81963 100644 --- a/cvat-canvas/src/scss/canvas.scss +++ b/cvat-canvas/src/scss/canvas.scss @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corp +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -72,6 +72,10 @@ polyline.cvat_shape_drawing_opacity { fill: darkmagenta; } +image.cvat_canvas_shape_grouping { + visibility: hidden; +} + .cvat_canvas_shape_region_selection { @extend .cvat_shape_action_dasharray; @extend .cvat_shape_action_opacity; @@ -340,6 +344,11 @@ g.cvat_canvas_shape_occluded { height: 100%; } +.cvat_masks_canvas_wrapper { + z-index: 3; + display: none; +} + #cvat_canvas_attachment_board { position: absolute; z-index: 4; diff --git a/cvat-canvas/src/typescript/canvas.ts b/cvat-canvas/src/typescript/canvas.ts index aec02854a44..9b4c6c60d11 100644 --- a/cvat-canvas/src/typescript/canvas.ts +++ b/cvat-canvas/src/typescript/canvas.ts @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -8,6 +9,7 @@ import { MergeData, SplitData, GroupData, + MasksEditData, InteractionData as _InteractionData, InteractionResult as _InteractionResult, CanvasModel, @@ -38,6 +40,7 @@ interface Canvas { interact(interactionData: InteractionData): void; draw(drawData: DrawData): void; + edit(editData: MasksEditData): void; group(groupData: GroupData): void; split(splitData: SplitData): void; merge(mergeData: MergeData): void; @@ -129,6 +132,10 @@ class CanvasImpl implements Canvas { this.model.draw(drawData); } + public edit(editData: MasksEditData): void { + this.model.edit(editData); + } + public split(splitData: SplitData): void { this.model.split(splitData); } diff --git a/cvat-canvas/src/typescript/canvasController.ts b/cvat-canvas/src/typescript/canvasController.ts index 7ec577f5780..d8b50fc05db 100644 --- a/cvat-canvas/src/typescript/canvasController.ts +++ b/cvat-canvas/src/typescript/canvasController.ts @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -15,6 +16,7 @@ import { Mode, InteractionData, Configuration, + MasksEditData, } from './canvasModel'; export interface CanvasController { @@ -24,6 +26,7 @@ export interface CanvasController { readonly focusData: FocusData; readonly activeElement: ActiveElement; readonly drawData: DrawData; + readonly editData: MasksEditData; readonly interactionData: InteractionData; readonly mergeData: MergeData; readonly splitData: SplitData; @@ -35,6 +38,7 @@ export interface CanvasController { zoom(x: number, y: number, direction: number): void; draw(drawData: DrawData): void; + edit(editData: MasksEditData): void; interact(interactionData: InteractionData): void; merge(mergeData: MergeData): void; split(splitData: SplitData): void; @@ -91,6 +95,10 @@ export class CanvasControllerImpl implements CanvasController { this.model.draw(drawData); } + public edit(editData: MasksEditData): void { + this.model.edit(editData); + } + public interact(interactionData: InteractionData): void { this.model.interact(interactionData); } @@ -143,6 +151,10 @@ export class CanvasControllerImpl implements CanvasController { return this.model.drawData; } + public get editData(): MasksEditData { + return this.model.editData; + } + public get interactionData(): InteractionData { return this.model.interactionData; } diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index 79502ca3447..b7591f02277 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -52,6 +53,12 @@ export enum CuboidDrawingMethod { CORNER_POINTS = 'By 4 points', } +export enum ColorBy { + INSTANCE = 'Instance', + GROUP = 'Group', + LABEL = 'Label', +} + export interface Configuration { smoothImage?: boolean; autoborders?: boolean; @@ -65,15 +72,23 @@ export interface Configuration { intelligentPolygonCrop?: boolean; forceFrameUpdate?: boolean; CSSImageFilter?: string; - colorBy?: string; + colorBy?: ColorBy; selectedShapeOpacity?: number; shapeOpacity?: number; controlPointsSize?: number; outlinedBorders?: string | false; } +export interface BrushTool { + type: 'brush' | 'eraser' | 'polygon-plus' | 'polygon-minus'; + color: string; + form: 'circle' | 'square'; + size: number; +} + export interface DrawData { enabled: boolean; + continue?: boolean; shapeType?: string; rectDrawingMethod?: RectDrawingMethod; cuboidDrawingMethod?: CuboidDrawingMethod; @@ -81,7 +96,10 @@ export interface DrawData { numberOfPoints?: number; initialState?: any; crosshair?: boolean; + brushTool?: BrushTool; redraw?: number; + onDrawDone?: (data: object) => void; + onUpdateConfiguration?: (configuration: { brushTool?: Pick }) => void; } export interface InteractionData { @@ -107,12 +125,19 @@ export interface InteractionResult { button: number; } -export interface EditData { +export interface PolyEditData { enabled: boolean; state: any; pointID: number; } +export interface MasksEditData { + enabled: boolean; + state?: any; + brushTool?: BrushTool; + onUpdateConfiguration?: (configuration: { brushTool?: Pick }) => void; +} + export interface GroupData { enabled: boolean; } @@ -146,6 +171,7 @@ export enum UpdateReasons { INTERACT = 'interact', DRAW = 'draw', + EDIT = 'edit', MERGE = 'merge', SPLIT = 'split', GROUP = 'group', @@ -186,6 +212,7 @@ export interface CanvasModel { readonly focusData: FocusData; readonly activeElement: ActiveElement; readonly drawData: DrawData; + readonly editData: MasksEditData; readonly interactionData: InteractionData; readonly mergeData: MergeData; readonly splitData: SplitData; @@ -208,6 +235,7 @@ export interface CanvasModel { grid(stepX: number, stepY: number): void; draw(drawData: DrawData): void; + edit(editData: MasksEditData): void; group(groupData: GroupData): void; split(splitData: SplitData): void; merge(mergeData: MergeData): void; @@ -226,6 +254,50 @@ export interface CanvasModel { destroy(): void; } +const defaultData = { + drawData: { + enabled: false, + }, + editData: { + enabled: false, + }, + interactionData: { + enabled: false, + }, + mergeData: { + enabled: false, + }, + groupData: { + enabled: false, + }, + splitData: { + enabled: false, + }, +}; + +function hasShapeIsBeingDrawn(): boolean { + const [element] = window.document.getElementsByClassName('cvat_canvas_shape_drawing'); + if (element) { + return !!(element as any).instance.remember('_paintHandler'); + } + + return false; +} + +function disableInternalSVGDrawing(data: DrawData | MasksEditData, currentData: DrawData | MasksEditData): boolean { + // P.S. spaghetti code, but probably significant refactoring needed to find a better solution + // when it is a mask drawing/editing using polygon fill + // a user needs to close drawing/editing twice + // first close stops internal drawing/editing with svg.js + // the second one stops drawing/editing mask itself + + return !data.enabled && currentData.enabled && + (('shapeType' in currentData && currentData.shapeType === 'mask') || + ('state' in currentData && currentData.state.shapeType === 'mask')) && + currentData.brushTool?.type?.startsWith('polygon-') && + hasShapeIsBeingDrawn(); +} + export class CanvasModelImpl extends MasterImpl implements CanvasModel { private data: { activeElement: ActiveElement; @@ -247,6 +319,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { top: number; zLayer: number | null; drawData: DrawData; + editData: MasksEditData; interactionData: InteractionData; mergeData: MergeData; groupData: GroupData; @@ -278,7 +351,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { intelligentPolygonCrop: false, forceFrameUpdate: false, CSSImageFilter: '', - colorBy: 'Label', + colorBy: ColorBy.LABEL, selectedShapeOpacity: 0.5, shapeOpacity: 0.2, outlinedBorders: false, @@ -311,25 +384,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { scale: 1, top: 0, zLayer: null, - drawData: { - enabled: false, - initialState: null, - }, - interactionData: { - enabled: false, - }, - mergeData: { - enabled: false, - }, - groupData: { - enabled: false, - }, - splitData: { - enabled: false, - }, selected: null, mode: Mode.IDLE, exception: null, + ...defaultData, }; } @@ -561,9 +619,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { throw new Error('Skeleton template must be specified when drawing a skeleton'); } - if (this.data.drawData.enabled) { - throw new Error('Drawing has been already started'); - } else if (!drawData.shapeType && !drawData.initialState) { + if (!drawData.shapeType && !drawData.initialState) { throw new Error('A shape type is not specified'); } else if (typeof drawData.numberOfPoints !== 'undefined') { if (drawData.shapeType === 'polygon' && drawData.numberOfPoints < 3) { @@ -585,6 +641,11 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { return; } } else { + if (disableInternalSVGDrawing(drawData, this.data.drawData)) { + this.notify(UpdateReasons.DRAW); + return; + } + this.data.drawData = { ...drawData }; if (this.data.drawData.initialState) { this.data.drawData.shapeType = this.data.drawData.initialState.shapeType; @@ -604,6 +665,33 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { this.notify(UpdateReasons.DRAW); } + public edit(editData: MasksEditData): void { + if (![Mode.IDLE, Mode.EDIT].includes(this.data.mode)) { + throw Error(`Canvas is busy. Action: ${this.data.mode}`); + } + + if (editData.enabled && !editData.state) { + throw Error('State must be specified when call edit() editing process'); + } + + if (this.data.editData.enabled && editData.enabled && + editData.state.clientID !== this.data.editData.state.clientID + ) { + throw Error('State cannot be updated during editing, need to finish current editing first'); + } + + if (editData.enabled) { + this.data.editData = { ...editData }; + } else if (disableInternalSVGDrawing(editData, this.data.editData)) { + this.notify(UpdateReasons.EDIT); + return; + } else { + this.data.editData = { enabled: false }; + } + + this.notify(UpdateReasons.EDIT); + } + public interact(interactionData: InteractionData): void { if (![Mode.IDLE, Mode.INTERACT].includes(this.data.mode)) { throw Error(`Canvas is busy. Action: ${this.data.mode}`); @@ -735,7 +823,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { if (['string', 'boolean'].includes(typeof configuration.outlinedBorders)) { this.data.configuration.outlinedBorders = configuration.outlinedBorders; } - if (['Instance', 'Group', 'Label'].includes(configuration.colorBy)) { + if (Object.values(ColorBy).includes(configuration.colorBy)) { this.data.configuration.colorBy = configuration.colorBy; } @@ -754,6 +842,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { } public cancel(): void { + this.data = { + ...this.data, + ...defaultData, + }; this.notify(UpdateReasons.CANCEL); } @@ -837,6 +929,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { return { ...this.data.drawData }; } + public get editData(): MasksEditData { + return { ...this.data.editData }; + } + public get interactionData(): InteractionData { return { ...this.data.interactionData }; } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 9402a813d24..1de41214e67 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1,9 +1,10 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corp +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import polylabel from 'polylabel'; +import { fabric } from 'fabric'; import * as SVG from 'svg.js'; import 'svg.draggable.js'; @@ -13,6 +14,7 @@ import 'svg.select.js'; import { CanvasController } from './canvasController'; import { Listener, Master } from './master'; import { DrawHandler, DrawHandlerImpl } from './drawHandler'; +import { MasksHandler, MasksHandlerImpl } from './masksHandler'; import { EditHandler, EditHandlerImpl } from './editHandler'; import { MergeHandler, MergeHandlerImpl } from './mergeHandler'; import { SplitHandler, SplitHandlerImpl } from './splitHandler'; @@ -38,6 +40,8 @@ import { readPointsFromShape, setupSkeletonEdges, makeSVGFromTemplate, + imageDataToDataURL, + expandChannels, } from './shared'; import { CanvasModel, @@ -54,6 +58,7 @@ import { Configuration, InteractionResult, InteractionData, + ColorBy, } from './canvasModel'; export interface CanvasView { @@ -65,6 +70,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private text: SVGSVGElement; private adoptedText: SVG.Container; private background: HTMLCanvasElement; + private masksContent: HTMLCanvasElement; private bitmap: HTMLCanvasElement; private grid: SVGSVGElement; private content: SVGSVGElement; @@ -82,6 +88,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private drawnIssueRegions: Record; private geometry: Geometry; private drawHandler: DrawHandler; + private masksHandler: MasksHandler; private editHandler: EditHandler; private mergeHandler: MergeHandler; private splitHandler: SplitHandler; @@ -95,6 +102,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private snapToAngleResize: number; private innerObjectsFlags: { drawHidden: Record; + editHidden: Record; }; private set mode(value: Mode) { @@ -180,7 +188,7 @@ export class CanvasViewImpl implements CanvasView, Listener { return this.innerObjectsFlags.drawHidden[clientID] || false; } - private setupInnerFlags(clientID: number, path: 'drawHidden', value: boolean): void { + private setupInnerFlags(clientID: number, path: 'drawHidden' | 'editHidden', value: boolean): void { this.innerObjectsFlags[path][clientID] = value; const shape = this.svgShapes[clientID]; const text = this.svgTexts[clientID]; @@ -253,7 +261,7 @@ export class CanvasViewImpl implements CanvasView, Listener { } } - private onDrawDone(data: any | null, duration: number, continueDraw?: boolean): void { + private onDrawDone(data: any | null, duration: number, continueDraw?: boolean, prevDrawData?: DrawData): void { const hiddenBecauseOfDraw = Object.keys(this.innerObjectsFlags.drawHidden) .map((_clientID): number => +_clientID); if (hiddenBecauseOfDraw.length) { @@ -266,16 +274,16 @@ export class CanvasViewImpl implements CanvasView, Listener { const { clientID, elements } = data as any; const points = data.points || elements.map((el: any) => el.points).flat(); if (typeof clientID === 'number') { + const [state] = this.controller.objects + .filter((_state: any): boolean => _state.clientID === clientID); + this.onEditDone(state, points); + const event: CustomEvent = new CustomEvent('canvas.canceled', { bubbles: false, cancelable: true, }); this.canvas.dispatchEvent(event); - - const [state] = this.controller.objects.filter((_state: any): boolean => _state.clientID === clientID); - - this.onEditDone(state, points); return; } @@ -296,23 +304,50 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas.dispatchEvent(event); } else if (!continueDraw) { - const event: CustomEvent = new CustomEvent('canvas.canceled', { + this.canvas.dispatchEvent(new CustomEvent('canvas.canceled', { bubbles: false, cancelable: true, - }); - - this.canvas.dispatchEvent(event); + })); } - if (!continueDraw) { - this.mode = Mode.IDLE; - this.controller.draw({ - enabled: false, - }); + if (continueDraw) { + this.canvas.dispatchEvent( + new CustomEvent('canvas.drawstart', { + bubbles: false, + cancelable: true, + detail: { + drawData: prevDrawData, + }, + }), + ); + } else { + // when draw stops from inside canvas (for example if use predefined number of points) + this.controller.draw({ enabled: false }); } } - private onEditDone(state: any, points: number[], rotation?: number): void { + private onEditStart = (state?: any): void => { + this.canvas.style.cursor = 'crosshair'; + this.deactivate(); + this.canvas.dispatchEvent( + new CustomEvent('canvas.editstart', { + bubbles: false, + cancelable: true, + detail: { + state, + }, + }), + ); + + if (state && state.shapeType === 'mask') { + this.setupInnerFlags(state.clientID, 'editHidden', true); + } + + this.mode = Mode.EDIT; + }; + + private onEditDone = (state: any, points: number[], rotation?: number): void => { + this.canvas.style.cursor = ''; if (state && points) { const event: CustomEvent = new CustomEvent('canvas.edited', { bubbles: false, @@ -334,8 +369,11 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas.dispatchEvent(event); } + for (const clientID of Object.keys(this.innerObjectsFlags.editHidden)) { + this.setupInnerFlags(+clientID, 'editHidden', false); + } this.mode = Mode.IDLE; - } + }; private onMergeDone(objects: any[] | null, duration?: number): void { if (objects) { @@ -358,10 +396,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas.dispatchEvent(event); } - this.controller.merge({ - enabled: false, - }); - + this.controller.merge({ enabled: false }); this.mode = Mode.IDLE; } @@ -386,10 +421,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas.dispatchEvent(event); } - this.controller.split({ - enabled: false, - }); - + this.controller.split({ enabled: false }); this.mode = Mode.IDLE; } @@ -413,10 +445,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas.dispatchEvent(event); } - this.controller.group({ - enabled: false, - }); - + this.controller.group({ enabled: false }); this.mode = Mode.IDLE; } @@ -459,7 +488,6 @@ export class CanvasViewImpl implements CanvasView, Listener { }); this.canvas.dispatchEvent(event); - e.preventDefault(); } } @@ -523,6 +551,7 @@ export class CanvasViewImpl implements CanvasView, Listener { // Transform handlers this.drawHandler.transform(this.geometry); + this.masksHandler.transform(this.geometry); this.editHandler.transform(this.geometry); this.zoomHandler.transform(this.geometry); this.autoborderHandler.transform(this.geometry); @@ -533,7 +562,11 @@ export class CanvasViewImpl implements CanvasView, Listener { private transformCanvas(): void { // Transform canvas for (const obj of [ - this.background, this.grid, this.content, this.bitmap, this.attachmentBoard, + this.background, + this.grid, + this.content, + this.bitmap, + this.attachmentBoard, ]) { obj.style.transform = `scale(${this.geometry.scale}) rotate(${this.geometry.angle}deg)`; } @@ -545,6 +578,7 @@ export class CanvasViewImpl implements CanvasView, Listener { for (const element of [ ...window.document.getElementsByClassName('svg_select_points'), ...window.document.getElementsByClassName('svg_select_points_rot'), + ...window.document.getElementsByClassName('svg_select_boundingRect'), ]) { element.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.geometry.scale}`); element.setAttribute('r', `${this.configuration.controlPointsSize / this.geometry.scale}`); @@ -606,6 +640,7 @@ export class CanvasViewImpl implements CanvasView, Listener { // Transform handlers this.drawHandler.transform(this.geometry); + this.masksHandler.transform(this.geometry); this.editHandler.transform(this.geometry); this.zoomHandler.transform(this.geometry); this.autoborderHandler.transform(this.geometry); @@ -614,7 +649,7 @@ export class CanvasViewImpl implements CanvasView, Listener { } private resizeCanvas(): void { - for (const obj of [this.background, this.grid, this.bitmap]) { + for (const obj of [this.background, this.masksContent, this.grid, this.bitmap]) { obj.style.width = `${this.geometry.image.width}px`; obj.style.height = `${this.geometry.image.height}px`; } @@ -685,6 +720,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private setupObjects(states: any[]): void { const created = []; const updated = []; + for (const state of states) { if (!(state.clientID in this.drawnStates)) { created.push(state); @@ -831,15 +867,7 @@ export class CanvasViewImpl implements CanvasView, Listener { const { points } = state; this.onEditDone(state, points.slice(0, pointID * 2).concat(points.slice(pointID * 2 + 2))); } else if (e.shiftKey) { - this.canvas.dispatchEvent( - new CustomEvent('canvas.editstart', { - bubbles: false, - cancelable: true, - }), - ); - - this.mode = Mode.EDIT; - this.deactivate(); + this.onEditStart(state); this.editHandler.edit({ enabled: true, state, @@ -901,6 +929,7 @@ export class CanvasViewImpl implements CanvasView, Listener { deepSelect: true, pointSize: (2 * this.configuration.controlPointsSize) / this.geometry.scale, rotationPoint: shape.type === 'rect' || shape.type === 'ellipse', + pointsExclude: shape.type === 'image' ? ['lt', 'rt', 'rb', 'lb', 't', 'r', 'b', 'l'] : [], pointType(cx: number, cy: number): SVG.Circle { const circle: SVG.Circle = this.nested .circle(this.options.pointSize) @@ -974,6 +1003,19 @@ export class CanvasViewImpl implements CanvasView, Listener { title.textContent = 'Hold Shift to snap angle'; rotationPoint.appendChild(title); } + + if (value && shape.type === 'image') { + const [boundingRect] = window.document.getElementsByClassName('svg_select_boundingRect'); + if (boundingRect) { + (boundingRect as SVGRectElement).style.opacity = '1'; + boundingRect.setAttribute('fill', 'none'); + boundingRect.setAttribute('stroke', shape.attr('stroke')); + boundingRect.setAttribute('stroke-width', `${consts.BASE_STROKE_WIDTH / this.geometry.scale}px`); + if (shape.hasClass('cvat_canvas_shape_occluded')) { + boundingRect.setAttribute('stroke-dasharray', '5'); + } + } + } } private onShiftKeyDown = (e: KeyboardEvent): void => { @@ -981,7 +1023,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_SHIFT; if (this.activeElement) { const shape = this.svgShapes[this.activeElement.clientID]; - if (shape && shape.hasClass('cvat_canvas_shape_activated')) { + if (shape && shape?.remember('_selectHandler')?.options?.rotationPoint) { if (this.drawnStates[this.activeElement.clientID]?.shapeType === 'skeleton') { const wrappingRect = (shape as any).children().find((child: SVG.Element) => child.type === 'rect'); if (wrappingRect) { @@ -1000,7 +1042,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_DEFAULT; if (this.activeElement) { const shape = this.svgShapes[this.activeElement.clientID]; - if (shape && shape.hasClass('cvat_canvas_shape_activated')) { + if (shape && shape?.remember('_selectHandler')?.options?.rotationPoint) { if (this.drawnStates[this.activeElement.clientID]?.shapeType === 'skeleton') { const wrappingRect = (shape as any).children().find((child: SVG.Element) => child.type === 'rect'); if (wrappingRect) { @@ -1036,6 +1078,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_DEFAULT; this.innerObjectsFlags = { drawHidden: {}, + editHidden: {}, }; // Create HTML elements @@ -1043,6 +1086,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.text = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.adoptedText = SVG.adopt((this.text as any) as HTMLElement) as SVG.Container; this.background = window.document.createElement('canvas'); + this.masksContent = window.document.createElement('canvas'); this.bitmap = window.document.createElement('canvas'); // window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); @@ -1110,6 +1154,7 @@ export class CanvasViewImpl implements CanvasView, Listener { // Setup content this.text.setAttribute('id', 'cvat_canvas_text_content'); this.background.setAttribute('id', 'cvat_canvas_background'); + this.masksContent.setAttribute('id', 'cvat_canvas_masks_content'); this.content.setAttribute('id', 'cvat_canvas_content'); this.bitmap.setAttribute('id', 'cvat_canvas_bitmap'); this.bitmap.style.display = 'none'; @@ -1131,6 +1176,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas.appendChild(this.loadingAnimation); this.canvas.appendChild(this.text); this.canvas.appendChild(this.background); + this.canvas.appendChild(this.masksContent); this.canvas.appendChild(this.bitmap); this.canvas.appendChild(this.grid); this.canvas.appendChild(this.content); @@ -1146,7 +1192,15 @@ export class CanvasViewImpl implements CanvasView, Listener { this.geometry, this.configuration, ); - this.editHandler = new EditHandlerImpl(this.onEditDone.bind(this), this.adoptedContent, this.autoborderHandler); + this.masksHandler = new MasksHandlerImpl( + this.onDrawDone.bind(this), + this.controller.draw.bind(this.controller), + this.onEditStart, + this.onEditDone, + this.drawHandler, + this.masksContent, + ); + this.editHandler = new EditHandlerImpl(this.onEditDone, this.adoptedContent, this.autoborderHandler); this.mergeHandler = new MergeHandlerImpl( this.onMergeDone.bind(this), this.onFindObject.bind(this), @@ -1177,12 +1231,12 @@ export class CanvasViewImpl implements CanvasView, Listener { ); // Setup event handlers - this.content.addEventListener('dblclick', (e: MouseEvent): void => { + this.canvas.addEventListener('dblclick', (e: MouseEvent): void => { this.controller.fit(); e.preventDefault(); }); - this.content.addEventListener('mousedown', (event): void => { + this.canvas.addEventListener('mousedown', (event): void => { if ([0, 1].includes(event.button)) { if ( [Mode.IDLE, Mode.DRAG_CANVAS, Mode.MERGE, Mode.SPLIT] @@ -1197,7 +1251,7 @@ export class CanvasViewImpl implements CanvasView, Listener { window.document.addEventListener('keydown', this.onShiftKeyDown); window.document.addEventListener('keyup', this.onShiftKeyUp); - this.content.addEventListener('wheel', (event): void => { + this.canvas.addEventListener('wheel', (event): void => { if (event.ctrlKey) return; const { offset } = this.controller.geometry; const point = translateToSVG(this.content, [event.clientX, event.clientY]); @@ -1211,7 +1265,7 @@ export class CanvasViewImpl implements CanvasView, Listener { event.preventDefault(); }); - this.content.addEventListener('mousemove', (e): void => { + this.canvas.addEventListener('mousemove', (e): void => { this.controller.drag(e.clientX, e.clientY); if (this.mode !== Mode.IDLE) return; @@ -1244,32 +1298,41 @@ export class CanvasViewImpl implements CanvasView, Listener { const { configuration } = model; const updateShapeViews = (states: DrawnState[], parentState?: DrawnState): void => { - for (const state of states) { - const { fill, stroke, 'fill-opacity': fillOpacity } = this.getShapeColorization(state, { configuration, parentState }); - const shapeView = window.document.getElementById(`cvat_canvas_shape_${state.clientID}`); + for (const drawnState of states) { + const { + fill, stroke, 'fill-opacity': fillOpacity, + } = this.getShapeColorization(drawnState, { parentState }); + const shapeView = window.document.getElementById(`cvat_canvas_shape_${drawnState.clientID}`); + const [objectState] = this.controller.objects + .filter((_state: any) => _state.clientID === drawnState.clientID); if (shapeView) { const handler = (shapeView as any).instance.remember('_selectHandler'); if (handler && handler.nested) { handler.nested.fill({ color: fill }); } + if (drawnState.shapeType === 'mask') { + // if there are masks, we need to redraw them + this.deleteObjects([drawnState]); + this.addObjects([objectState]); + continue; + } + (shapeView as any).instance .fill({ color: fill, opacity: fillOpacity }) .stroke({ color: stroke }); } - if (state.elements) { - updateShapeViews(state.elements, state); + if (drawnState.elements) { + updateShapeViews(drawnState.elements, drawnState); } } }; - if (configuration.shapeOpacity !== this.configuration.shapeOpacity || + const withUpdatingShapeViews = configuration.shapeOpacity !== this.configuration.shapeOpacity || configuration.selectedShapeOpacity !== this.configuration.selectedShapeOpacity || configuration.outlinedBorders !== this.configuration.outlinedBorders || - configuration.colorBy !== this.configuration.colorBy) { - updateShapeViews(Object.values(this.drawnStates)); - } + configuration.colorBy !== this.configuration.colorBy; if (configuration.displayAllText && !this.configuration.displayAllText) { for (const i in this.drawnStates) { @@ -1298,6 +1361,10 @@ export class CanvasViewImpl implements CanvasView, Listener { } this.configuration = configuration; + if (withUpdatingShapeViews) { + updateShapeViews(Object.values(this.drawnStates)); + } + if (recreateText) { const states = this.controller.objects; for (const key of Object.keys(this.drawnStates)) { @@ -1327,6 +1394,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.activate(activeElement); this.editHandler.configurate(this.configuration); this.drawHandler.configurate(this.configuration); + this.masksHandler.configurate(this.configuration); this.autoborderHandler.configurate(this.configuration); this.interactionHandler.configurate(this.configuration); this.transformCanvas(); @@ -1495,19 +1563,46 @@ export class CanvasViewImpl implements CanvasView, Listener { } } else if (reason === UpdateReasons.DRAW) { const data: DrawData = this.controller.drawData; - if (data.enabled && this.mode === Mode.IDLE) { - this.canvas.style.cursor = 'crosshair'; - this.mode = Mode.DRAW; - if (typeof data.redraw === 'number') { - this.setupInnerFlags(data.redraw, 'drawHidden', true); + if (data.enabled && [Mode.IDLE, Mode.DRAW].includes(this.mode)) { + if (data.shapeType !== 'mask') { + this.drawHandler.draw(data, this.geometry); + } else { + this.masksHandler.draw(data); } - this.drawHandler.draw(data, this.geometry); - } else { + + if (this.mode === Mode.IDLE) { + this.canvas.style.cursor = 'crosshair'; + this.mode = Mode.DRAW; + this.canvas.dispatchEvent( + new CustomEvent('canvas.drawstart', { + bubbles: false, + cancelable: true, + detail: { + drawData: data, + }, + }), + ); + + if (typeof data.redraw === 'number') { + this.setupInnerFlags(data.redraw, 'drawHidden', true); + } + } + } else if (this.mode !== Mode.IDLE) { this.canvas.style.cursor = ''; - if (this.mode !== Mode.IDLE) { + this.mode = Mode.IDLE; + if (this.masksHandler.enabled) { + this.masksHandler.draw(data); + } else { this.drawHandler.draw(data, this.geometry); } } + } else if (reason === UpdateReasons.EDIT) { + const data = this.controller.editData; + if (data.enabled && data.state.shapeType === 'mask') { + this.masksHandler.edit(data); + } else if (this.masksHandler.enabled) { + this.masksHandler.edit(data); + } } else if (reason === UpdateReasons.INTERACT) { const data: InteractionData = this.controller.interactionData; if (data.enabled && (this.mode === Mode.IDLE || data.intermediateShape)) { @@ -1561,7 +1656,11 @@ export class CanvasViewImpl implements CanvasView, Listener { } } else if (reason === UpdateReasons.CANCEL) { if (this.mode === Mode.DRAW) { - this.drawHandler.cancel(); + if (this.masksHandler.enabled) { + this.masksHandler.cancel(); + } else { + this.drawHandler.cancel(); + } } else if (this.mode === Mode.INTERACT) { this.interactionHandler.cancel(); } else if (this.mode === Mode.MERGE) { @@ -1573,7 +1672,11 @@ export class CanvasViewImpl implements CanvasView, Listener { } else if (this.mode === Mode.SELECT_REGION) { this.regionSelector.cancel(); } else if (this.mode === Mode.EDIT) { - this.editHandler.cancel(); + if (this.masksHandler.enabled) { + this.masksHandler.cancel(); + } else { + this.editHandler.cancel(); + } } else if (this.mode === Mode.DRAG_CANVAS) { this.canvas.dispatchEvent( new CustomEvent('canvas.dragstop', { @@ -1684,6 +1787,22 @@ export class CanvasViewImpl implements CanvasView, Listener { ctx.fill(); } + if (state.shapeType === 'mask') { + const { points } = state; + const [left, top, right, bottom] = points.slice(-4); + const imageBitmap = expandChannels(255, 255, 255, points, 4); + imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1, + (dataURL: string) => new Promise((resolve) => { + const img = document.createElement('img'); + img.addEventListener('load', () => { + ctx.drawImage(img, left, top); + URL.revokeObjectURL(dataURL); + resolve(); + }); + img.src = dataURL; + })); + } + if (state.shapeType === 'cuboid') { for (let i = 0; i < 5; i++) { const points = [ @@ -1737,20 +1856,19 @@ export class CanvasViewImpl implements CanvasView, Listener { } private getShapeColorization(state: any, opts: { - configuration?: Configuration, parentState?: any, } = {}): { fill: string; stroke: string, 'fill-opacity': number } { const { shapeType } = state; const parentShapeType = opts.parentState?.shapeType; - const configuration = opts.configuration || this.configuration; + const { configuration } = this; const { colorBy, shapeOpacity, outlinedBorders } = configuration; let shapeColor = ''; - if (colorBy === 'Instance') { + if (colorBy === ColorBy.INSTANCE) { shapeColor = state.color; - } else if (colorBy === 'Group') { + } else if (colorBy === ColorBy.GROUP) { shapeColor = state.group.color; - } else if (colorBy === 'Label') { + } else if (colorBy === ColorBy.LABEL) { shapeColor = state.label.color; } const outlinedColor = parentShapeType === 'skeleton' ? 'black' : outlinedBorders || shapeColor; @@ -1820,6 +1938,13 @@ export class CanvasViewImpl implements CanvasView, Listener { state.points.length !== drawnState.points.length || state.points.some((p: number, id: number): boolean => p !== drawnState.points[id]) ) { + if (state.shapeType === 'mask') { + // if masks points were updated, draw from scratch + this.deleteObjects([this.drawnStates[+clientID]]); + this.addObjects([state]); + continue; + } + const translatedPoints: number[] = this.translateToCanvas(state.points); if (state.shapeType === 'rectangle') { @@ -1884,21 +2009,25 @@ export class CanvasViewImpl implements CanvasView, Listener { } } - if (drawnState.label.id !== state.label.id || drawnState.color !== state.color) { + if ( + drawnState.label.id !== state.label.id || + drawnState.group.id !== state.group.id || + drawnState.group.color !== state.group.color || + drawnState.color !== state.color + ) { // update shape color if necessary if (shape) { - shape.attr({ - ...this.getShapeColorization(state), - }); + if (state.shapeType === 'mask') { + // if masks points were updated, draw from scratch + this.deleteObjects([this.drawnStates[+clientID]]); + this.addObjects([state]); + continue; + } else { + shape.attr({ ...this.getShapeColorization(state) }); + } } } - if ( - drawnState.group.id !== state.group.id || drawnState.group.color !== state.group.color - ) { - shape.attr({ ...this.getShapeColorization(state) }); - } - this.drawnStates[state.clientID] = this.saveState(state); } } @@ -1931,28 +2060,32 @@ export class CanvasViewImpl implements CanvasView, Listener { const { displayAllText } = this.configuration; for (const state of states) { const points: number[] = state.points as number[]; - const translatedPoints: number[] = this.translateToCanvas(points); // TODO: Use enums after typification cvat-core - if (state.shapeType === 'rectangle') { - this.svgShapes[state.clientID] = this.addRect(translatedPoints, state); + if (state.shapeType === 'mask') { + this.svgShapes[state.clientID] = this.addMask(points, state); } else if (state.shapeType === 'skeleton') { this.svgShapes[state.clientID] = this.addSkeleton(state); } else { - const stringified = this.stringifyToCanvas(translatedPoints); - - if (state.shapeType === 'polygon') { - this.svgShapes[state.clientID] = this.addPolygon(stringified, state); - } else if (state.shapeType === 'polyline') { - this.svgShapes[state.clientID] = this.addPolyline(stringified, state); - } else if (state.shapeType === 'points') { - this.svgShapes[state.clientID] = this.addPoints(stringified, state); - } else if (state.shapeType === 'ellipse') { - this.svgShapes[state.clientID] = this.addEllipse(stringified, state); - } else if (state.shapeType === 'cuboid') { - this.svgShapes[state.clientID] = this.addCuboid(stringified, state); + const translatedPoints: number[] = this.translateToCanvas(points); + if (state.shapeType === 'rectangle') { + this.svgShapes[state.clientID] = this.addRect(translatedPoints, state); } else { - continue; + const stringified = this.stringifyToCanvas(translatedPoints); + + if (state.shapeType === 'polygon') { + this.svgShapes[state.clientID] = this.addPolygon(stringified, state); + } else if (state.shapeType === 'polyline') { + this.svgShapes[state.clientID] = this.addPolyline(stringified, state); + } else if (state.shapeType === 'points') { + this.svgShapes[state.clientID] = this.addPoints(stringified, state); + } else if (state.shapeType === 'ellipse') { + this.svgShapes[state.clientID] = this.addEllipse(stringified, state); + } else if (state.shapeType === 'cuboid') { + this.svgShapes[state.clientID] = this.addCuboid(stringified, state); + } else { + continue; + } } } @@ -2027,8 +2160,19 @@ export class CanvasViewImpl implements CanvasView, Listener { const drawnState = this.drawnStates[clientID]; const shape = this.svgShapes[clientID]; - shape.removeClass('cvat_canvas_shape_activated'); + if (drawnState.shapeType === 'points') { + this.svgShapes[clientID] + .remember('_selectHandler').nested + .removeClass('cvat_canvas_shape_activated'); + } else { + shape.removeClass('cvat_canvas_shape_activated'); + } shape.removeClass('cvat_canvas_shape_draggable'); + if (drawnState.shapeType === 'mask') { + shape.attr('opacity', `${this.configuration.shapeOpacity}`); + } else { + shape.attr('fill-opacity', `${this.configuration.shapeOpacity}`); + } if (!drawnState.pinned) { (shape as any).off('dragstart'); @@ -2044,6 +2188,10 @@ export class CanvasViewImpl implements CanvasView, Listener { (shape as any).attr('projections', false); } + if (drawnState.shapeType === 'mask') { + (shape as any).off('mousedown'); + } + (shape as any).off('resizestart'); (shape as any).off('resizing'); (shape as any).off('resizedone'); @@ -2109,7 +2257,20 @@ export class CanvasViewImpl implements CanvasView, Listener { return; } - shape.addClass('cvat_canvas_shape_activated'); + if (state.shapeType === 'points') { + this.svgShapes[clientID] + .remember('_selectHandler').nested + .addClass('cvat_canvas_shape_activated'); + } else { + shape.addClass('cvat_canvas_shape_activated'); + } + + if (state.shapeType === 'mask') { + shape.attr('opacity', `${this.configuration.selectedShapeOpacity}`); + } else { + shape.attr('fill-opacity', `${this.configuration.selectedShapeOpacity}`); + } + if (state.shapeType === 'points') { this.content.append(this.svgShapes[clientID].remember('_selectHandler').nested.node); } else { @@ -2137,7 +2298,9 @@ export class CanvasViewImpl implements CanvasView, Listener { if (!state.pinned) { shape.addClass('cvat_canvas_shape_draggable'); (shape as any) - .draggable() + .draggable({ + ...(state.shapeType === 'mask' ? { snapToGrid: 1 } : {}), + }) .on('dragstart', (): void => { this.mode = Mode.DRAG; hideText(); @@ -2153,10 +2316,29 @@ export class CanvasViewImpl implements CanvasView, Listener { showText(); const p1 = e.detail.handler.startPoints.point; const p2 = e.detail.p; - const delta = 1; const dx2 = (p1.x - p2.x) ** 2; const dy2 = (p1.y - p2.y) ** 2; - if (Math.sqrt(dx2 + dy2) >= delta) { + if (Math.sqrt(dx2 + dy2) > 0) { + if (shape.type === 'image') { + const { points } = state; + const x = Math.trunc(shape.x()) - this.geometry.offset; + const y = Math.trunc(shape.y()) - this.geometry.offset; + points.splice(-4); + points.push(x, y, x + shape.width() - 1, y + shape.height() - 1); + this.drawnStates[state.clientID].points = points; + this.onEditDone(state, points); + this.canvas.dispatchEvent( + new CustomEvent('canvas.dragshape', { + bubbles: false, + cancelable: true, + detail: { + id: state.clientID, + }, + }), + ); + + return; + } // these points does not take into account possible transformations, applied on the element // so, if any (like rotation) we need to map them to canvas coordinate space let points = readPointsFromShape(shape); @@ -2213,66 +2395,75 @@ export class CanvasViewImpl implements CanvasView, Listener { this.mode = Mode.IDLE; }; - (shape as any) - .resize({ - snapToGrid: 0.1, - snapToAngle: this.snapToAngleResize, - }) - .on('resizestart', (): void => { - this.mode = Mode.RESIZE; - resized = false; - hideDirection(); - hideText(); - if (state.shapeType === 'rectangle' || state.shapeType === 'ellipse') { - shapeSizeElement = displayShapeSize(this.adoptedContent, this.adoptedText); - } - (shape as any).on('remove.resize', () => { - // disable internal resize events of SVG.js - window.dispatchEvent(new MouseEvent('mouseup')); - resizeFinally(); - }); - }) - .on('resizing', (): void => { - resized = true; - if (shapeSizeElement) { - shapeSizeElement.update(shape); - } - }) - .on('resizedone', (): void => { - (shape as any).off('remove.resize'); - resizeFinally(); - showDirection(); - showText(); - if (resized) { - let rotation = shape.transform().rotation || 0; - - // be sure, that rotation in range [0; 360] - while (rotation < 0) rotation += 360; - rotation %= 360; - - // these points does not take into account possible transformations, applied on the element - // so, if any (like rotation) we need to map them to canvas coordinate space - let points = readPointsFromShape(shape); - - // let's keep current points, but they could be rewritten in updateObjects - this.drawnStates[clientID].points = this.translateFromCanvas(points); - this.drawnStates[clientID].rotation = rotation; - if (rotation) { - points = this.translatePointsFromRotatedShape(shape, points); + if (state.shapeType !== 'mask') { + (shape as any) + .resize({ + snapToGrid: 0.1, + snapToAngle: this.snapToAngleResize, + }) + .on('resizestart', (): void => { + this.mode = Mode.RESIZE; + resized = false; + hideDirection(); + hideText(); + if (state.shapeType === 'rectangle' || state.shapeType === 'ellipse') { + shapeSizeElement = displayShapeSize(this.adoptedContent, this.adoptedText); } + (shape as any).on('remove.resize', () => { + // disable internal resize events of SVG.js + window.dispatchEvent(new MouseEvent('mouseup')); + resizeFinally(); + }); + }) + .on('resizing', (): void => { + resized = true; + if (shapeSizeElement) { + shapeSizeElement.update(shape); + } + }) + .on('resizedone', (): void => { + (shape as any).off('remove.resize'); + resizeFinally(); + showDirection(); + showText(); + if (resized) { + let rotation = shape.transform().rotation || 0; - this.onEditDone(state, this.translateFromCanvas(points), rotation); - this.canvas.dispatchEvent( - new CustomEvent('canvas.resizeshape', { - bubbles: false, - cancelable: true, - detail: { - id: state.clientID, - }, - }), - ); + // be sure, that rotation in range [0; 360] + while (rotation < 0) rotation += 360; + rotation %= 360; + + // these points does not take into account possible transformations, applied on the element + // so, if any (like rotation) we need to map them to canvas coordinate space + let points = readPointsFromShape(shape); + + // let's keep current points, but they could be rewritten in updateObjects + this.drawnStates[clientID].points = this.translateFromCanvas(points); + this.drawnStates[clientID].rotation = rotation; + if (rotation) { + points = this.translatePointsFromRotatedShape(shape, points); + } + + this.onEditDone(state, this.translateFromCanvas(points), rotation); + this.canvas.dispatchEvent( + new CustomEvent('canvas.resizeshape', { + bubbles: false, + cancelable: true, + detail: { + id: state.clientID, + }, + }), + ); + } + }); + } else { + (shape as any).on('dblclick', (e: MouseEvent) => { + if (e.shiftKey) { + this.controller.edit({ enabled: true, state }); + e.stopPropagation(); } }); + } this.canvas.dispatchEvent( new CustomEvent('canvas.activated', { @@ -2605,6 +2796,41 @@ export class CanvasViewImpl implements CanvasView, Listener { return cube; } + private addMask(points: number[], state: any): SVG.Image { + const colorization = this.getShapeColorization(state); + const color = fabric.Color.fromHex(colorization.fill).getSource(); + const [left, top, right, bottom] = points.slice(-4); + const imageBitmap = expandChannels(color[0], color[1], color[2], points, 4); + + const image = this.adoptedContent.image().attr({ + clientID: state.clientID, + 'color-rendering': 'optimizeQuality', + id: `cvat_canvas_shape_${state.clientID}`, + 'shape-rendering': 'geometricprecision', + 'data-z-order': state.zOrder, + opacity: colorization['fill-opacity'], + stroke: colorization.stroke, + }).addClass('cvat_canvas_shape'); + image.move(this.geometry.offset + left, this.geometry.offset + top); + + imageDataToDataURL( + imageBitmap, + right - left + 1, + bottom - top + 1, + (dataURL: string) => new Promise((resolve, reject) => { + image.loaded(() => { + resolve(); + }); + image.error(() => { + reject(); + }); + image.load(dataURL); + }), + ); + + return image; + } + private addSkeleton(state: any): any { const skeleton = (this.adoptedContent as any) .group() @@ -2689,7 +2915,7 @@ export class CanvasViewImpl implements CanvasView, Listener { } }; - const mousemove = (e: MouseEvent) => { + const mousemove = (e: MouseEvent): void => { if (this.mode === Mode.IDLE) { // stop propagation to canvas where it calls another canvas.moved // and does not allow to activate an element diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index 5b2076de39d..60ca0c48691 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -77,7 +77,12 @@ function checkConstraint(shapeType: string, points: number[], box: Box | null = export class DrawHandlerImpl implements DrawHandler { // callback is used to notify about creating new shape - private onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void; + private onDrawDoneDefault: ( + data: object | null, + duration?: number, + continueDraw?: boolean, + prevDrawData?: DrawData, + ) => void; private startTimestamp: number; private canvas: SVG.Container; private text: SVG.Container; @@ -344,6 +349,15 @@ export class DrawHandlerImpl implements DrawHandler { this.crosshair.hide(); } + private onDrawDone(...args: any[]): void { + if (this.drawData.onDrawDone) { + this.drawData.onDrawDone.call(this, ...args); + return; + } + + this.onDrawDoneDefault.call(this, ...args); + } + private release(): void { if (!this.initialized) { // prevents recursive calls @@ -838,10 +852,6 @@ export class DrawHandlerImpl implements DrawHandler { this.getFinalCuboidCoordinates(targetPoints) : this.getFinalPolyshapeCoordinates(targetPoints, true); - if (!e.detail.originalEvent.ctrlKey) { - this.release(); - } - if (checkConstraint(shapeType, points, box)) { this.onDrawDone( { @@ -855,8 +865,13 @@ export class DrawHandlerImpl implements DrawHandler { }, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey, + this.drawData, ); } + + if (!e.detail.originalEvent.ctrlKey) { + this.release(); + } }); } @@ -893,10 +908,6 @@ export class DrawHandlerImpl implements DrawHandler { this.drawInstance.on('done', (e: CustomEvent): void => { const points = readPointsFromShape((e.target as any as { instance: SVG.Rect }).instance); const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(points, !this.drawData.initialState.rotation); - if (!e.detail.originalEvent.ctrlKey) { - this.release(); - } - if (checkConstraint('rectangle', [xtl, ytl, xbr, ybr])) { this.onDrawDone( { @@ -911,8 +922,13 @@ export class DrawHandlerImpl implements DrawHandler { }, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey, + this.drawData, ); } + + if (!e.detail.originalEvent.ctrlKey) { + this.release(); + } }); } @@ -932,11 +948,6 @@ export class DrawHandlerImpl implements DrawHandler { const points = this.getFinalEllipseCoordinates( readPointsFromShape((e.target as any as { instance: SVG.Ellipse }).instance), false, ); - - if (!e.detail.originalEvent.ctrlKey) { - this.release(); - } - if (checkConstraint('ellipse', points)) { this.onDrawDone( { @@ -951,8 +962,13 @@ export class DrawHandlerImpl implements DrawHandler { }, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey, + this.drawData, ); } + + if (!e.detail.originalEvent.ctrlKey) { + this.release(); + } }); } @@ -1044,15 +1060,16 @@ export class DrawHandlerImpl implements DrawHandler { rotation: this.drawData.initialState.rotation, }; - if (!e.detail.originalEvent.ctrlKey) { - this.release(); - } - this.onDrawDone( result, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey, + this.drawData, ); + + if (!e.detail.originalEvent.ctrlKey) { + this.release(); + } }); this.canvas.on('mousemove.draw', (): void => { @@ -1213,7 +1230,7 @@ export class DrawHandlerImpl implements DrawHandler { } public constructor( - onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void, + onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean, prevDrawData?: DrawData) => void, canvas: SVG.Container, text: SVG.Container, autoborderHandler: AutoborderHandler, @@ -1226,7 +1243,7 @@ export class DrawHandlerImpl implements DrawHandler { this.outlinedBorders = configuration.outlinedBorders || 'black'; this.autobordersEnabled = false; this.startTimestamp = Date.now(); - this.onDrawDone = onDrawDone; + this.onDrawDoneDefault = onDrawDone; this.canvas = canvas; this.text = text; this.initialized = false; diff --git a/cvat-canvas/src/typescript/editHandler.ts b/cvat-canvas/src/typescript/editHandler.ts index 5abb52b5b50..1e907ba1c3a 100644 --- a/cvat-canvas/src/typescript/editHandler.ts +++ b/cvat-canvas/src/typescript/editHandler.ts @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -7,11 +8,11 @@ import 'svg.select.js'; import consts from './consts'; import { translateFromSVG, pointsToNumberArray } from './shared'; -import { EditData, Geometry, Configuration } from './canvasModel'; +import { PolyEditData, Geometry, Configuration } from './canvasModel'; import { AutoborderHandler } from './autoborderHandler'; export interface EditHandler { - edit(editData: EditData): void; + edit(editData: PolyEditData): void; transform(geometry: Geometry): void; configurate(configuration: Configuration): void; cancel(): void; @@ -22,7 +23,7 @@ export class EditHandlerImpl implements EditHandler { private autoborderHandler: AutoborderHandler; private geometry: Geometry | null; private canvas: SVG.Container; - private editData: EditData | null; + private editData: PolyEditData | null; private editedShape: SVG.Shape | null; private editLine: SVG.PolyLine | null; private clones: SVG.Polygon[]; diff --git a/cvat-canvas/src/typescript/groupHandler.ts b/cvat-canvas/src/typescript/groupHandler.ts index f4792eb7836..f97a54c4899 100644 --- a/cvat-canvas/src/typescript/groupHandler.ts +++ b/cvat-canvas/src/typescript/groupHandler.ts @@ -1,11 +1,11 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import * as SVG from 'svg.js'; import { GroupData } from './canvasModel'; - -import { translateToSVG } from './shared'; +import { expandChannels, imageDataToDataURL, translateToSVG } from './shared'; export interface GroupHandler { group(groupData: GroupData): void; @@ -31,6 +31,7 @@ export class GroupHandlerImpl implements GroupHandler { private initialized: boolean; private statesToBeGroupped: any[]; private highlightedShapes: Record; + private groupingCopies: Record; private getSelectionBox( event: MouseEvent, @@ -106,11 +107,7 @@ export class GroupHandlerImpl implements GroupHandler { (state: any): boolean => state.clientID === clientID, )[0]; - if (objectState) { - this.statesToBeGroupped.push(objectState); - this.highlightedShapes[clientID] = shape; - (shape as any).addClass('cvat_canvas_shape_grouping'); - } + this.appendToSelection(objectState); } } } @@ -164,6 +161,7 @@ export class GroupHandlerImpl implements GroupHandler { this.canvas = canvas; this.statesToBeGroupped = []; this.highlightedShapes = {}; + this.groupingCopies = {}; this.selectionRect = null; this.initialized = false; this.startSelectionPoint = { @@ -185,23 +183,67 @@ export class GroupHandlerImpl implements GroupHandler { } } + private appendToSelection(objectState: any): void { + const { clientID } = objectState; + + const shape = this.canvas.select(`#cvat_canvas_shape_${clientID}`).first(); + if (shape) { + if (objectState.shapeType === 'mask') { + const { points } = objectState; + const colorRGB = [139, 0, 139]; + const [left, top, right, bottom] = points.slice(-4); + const imageBitmap = expandChannels(colorRGB[0], colorRGB[1], colorRGB[2], points, 4); + + const bbox = shape.bbox(); + const image = this.canvas.image().attr({ + 'color-rendering': 'optimizeQuality', + 'shape-rendering': 'geometricprecision', + 'data-z-order': Number.MAX_SAFE_INTEGER, + 'grouping-copy-for': clientID, + }).move(bbox.x, bbox.y); + this.groupingCopies[clientID] = image; + + imageDataToDataURL( + imageBitmap, + right - left + 1, + bottom - top + 1, + (dataURL: string) => new Promise((resolve, reject) => { + image.loaded(() => { + resolve(); + }); + image.error(() => { + reject(); + }); + image.load(dataURL); + }), + ); + } + + this.statesToBeGroupped.push(objectState); + this.highlightedShapes[clientID] = shape; + shape.addClass('cvat_canvas_shape_grouping'); + } + } + public select(objectState: any): void { const stateIndexes = this.statesToBeGroupped.map((state): number => state.clientID); - const includes = stateIndexes.indexOf(objectState.clientID); + const { clientID } = objectState; + const includes = stateIndexes.indexOf(clientID); if (includes !== -1) { - const shape = this.highlightedShapes[objectState.clientID]; + const shape = this.highlightedShapes[clientID]; this.statesToBeGroupped.splice(includes, 1); if (shape) { - delete this.highlightedShapes[objectState.clientID]; + if (this.groupingCopies[clientID]) { + // remove clones for masks + this.groupingCopies[clientID].remove(); + delete this.groupingCopies[clientID]; + } + + delete this.highlightedShapes[clientID]; shape.removeClass('cvat_canvas_shape_grouping'); } } else { - const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first(); - if (shape) { - this.statesToBeGroupped.push(objectState); - this.highlightedShapes[objectState.clientID] = shape; - shape.addClass('cvat_canvas_shape_grouping'); - } + this.appendToSelection(objectState); } } @@ -210,8 +252,14 @@ export class GroupHandlerImpl implements GroupHandler { const shape = this.highlightedShapes[state.clientID]; shape.removeClass('cvat_canvas_shape_grouping'); } + + for (const shape of Object.values(this.groupingCopies)) { + shape.remove(); + } + this.statesToBeGroupped = []; this.highlightedShapes = {}; + this.groupingCopies = {}; if (this.selectionRect) { this.selectionRect.remove(); this.selectionRect = null; diff --git a/cvat-canvas/src/typescript/interactionHandler.ts b/cvat-canvas/src/typescript/interactionHandler.ts index 443e9038a67..42370d186e6 100644 --- a/cvat-canvas/src/typescript/interactionHandler.ts +++ b/cvat-canvas/src/typescript/interactionHandler.ts @@ -6,7 +6,7 @@ import * as SVG from 'svg.js'; import consts from './consts'; import Crosshair from './crosshair'; import { - translateToSVG, PropType, stringifyPoints, translateToCanvas, + translateToSVG, PropType, stringifyPoints, translateToCanvas, expandChannels, imageDataToDataURL, } from './shared'; import { @@ -304,6 +304,33 @@ export class InteractionHandlerImpl implements InteractionHandler { .fill({ opacity: this.selectedShapeOpacity, color: 'white' }) .addClass('cvat_canvas_interact_intermediate_shape'); this.selectize(true, this.drawnIntermediateShape, erroredShape); + } else if (shapeType === 'mask') { + const [left, top, right, bottom] = points.slice(-4); + const imageBitmap = expandChannels(255, 255, 255, points, 4); + + const image = this.canvas.image().attr({ + 'color-rendering': 'optimizeQuality', + 'shape-rendering': 'geometricprecision', + 'pointer-events': 'none', + opacity: 0.5, + }).addClass('cvat_canvas_interact_intermediate_shape'); + image.move(this.geometry.offset, this.geometry.offset); + this.drawnIntermediateShape = image; + + imageDataToDataURL( + imageBitmap, + right - left + 1, + bottom - top + 1, + (dataURL: string) => new Promise((resolve, reject) => { + image.loaded(() => { + resolve(); + }); + image.error(() => { + reject(); + }); + image.load(dataURL); + }), + ); } else { throw new Error( `Shape type "${shapeType}" was not implemented at interactionHandler::updateIntermediateShape`, diff --git a/cvat-canvas/src/typescript/masksHandler.ts b/cvat-canvas/src/typescript/masksHandler.ts new file mode 100644 index 00000000000..e0512c06664 --- /dev/null +++ b/cvat-canvas/src/typescript/masksHandler.ts @@ -0,0 +1,655 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { fabric } from 'fabric'; + +import { + DrawData, MasksEditData, Geometry, Configuration, BrushTool, ColorBy, +} from './canvasModel'; +import consts from './consts'; +import { DrawHandler } from './drawHandler'; +import { + PropType, computeWrappingBox, alphaChannelOnly, expandChannels, imageDataToDataURL, +} from './shared'; + +interface WrappingBBox { + left: number; + top: number; + right: number; + bottom: number; +} + +export interface MasksHandler { + draw(drawData: DrawData): void; + edit(state: MasksEditData): void; + configurate(configuration: Configuration): void; + transform(geometry: Geometry): void; + cancel(): void; + enabled: boolean; +} + +export class MasksHandlerImpl implements MasksHandler { + private onDrawDone: ( + data: object | null, + duration?: number, + continueDraw?: boolean, + prevDrawData?: DrawData, + ) => void; + private onDrawRepeat: (data: DrawData) => void; + private onEditStart: (state: any) => void; + private onEditDone: (state: any, points: number[]) => void; + private vectorDrawHandler: DrawHandler; + + private redraw: number | null; + private isDrawing: boolean; + private isEditing: boolean; + private isInsertion: boolean; + private isMouseDown: boolean; + private isBrushSizeChanging: boolean; + private resizeBrushToolLatestX: number; + private brushMarker: fabric.Rect | fabric.Circle | null; + private drawablePolygon: null | fabric.Polygon; + private isPolygonDrawing: boolean; + private drawnObjects: (fabric.Polygon | fabric.Circle | fabric.Rect | fabric.Line | fabric.Image)[]; + + private tool: DrawData['brushTool'] | null; + private drawData: DrawData | null; + private canvas: fabric.Canvas; + + private editData: MasksEditData | null; + + private colorBy: ColorBy; + private latestMousePos: { x: number; y: number; }; + private startTimestamp: number; + private geometry: Geometry; + private drawingOpacity: number; + + private keepDrawnPolygon(): void { + const canvasWrapper = this.canvas.getElement().parentElement; + canvasWrapper.style.pointerEvents = ''; + canvasWrapper.style.zIndex = ''; + this.isPolygonDrawing = false; + this.vectorDrawHandler.draw({ enabled: false }, this.geometry); + } + + private removeBrushMarker(): void { + if (this.brushMarker) { + this.canvas.remove(this.brushMarker); + this.brushMarker = null; + this.canvas.renderAll(); + } + } + + private setupBrushMarker(): void { + if (['brush', 'eraser'].includes(this.tool.type)) { + const common = { + evented: false, + selectable: false, + opacity: 0.75, + left: this.latestMousePos.x - this.tool.size / 2, + top: this.latestMousePos.y - this.tool.size / 2, + stroke: 'white', + strokeWidth: 1, + }; + this.brushMarker = this.tool.form === 'circle' ? new fabric.Circle({ + ...common, + radius: this.tool.size / 2, + }) : new fabric.Rect({ + ...common, + width: this.tool.size, + height: this.tool.size, + }); + + this.canvas.defaultCursor = 'none'; + this.canvas.add(this.brushMarker); + } else { + this.canvas.defaultCursor = 'inherit'; + } + } + + private releaseCanvasWrapperCSS(): void { + const canvasWrapper = this.canvas.getElement().parentElement; + canvasWrapper.style.pointerEvents = ''; + canvasWrapper.style.zIndex = ''; + canvasWrapper.style.display = ''; + } + + private releasePaste(): void { + this.releaseCanvasWrapperCSS(); + this.canvas.clear(); + this.canvas.renderAll(); + this.isInsertion = false; + this.drawnObjects = []; + this.onDrawDone(null); + } + + private releaseDraw(): void { + this.removeBrushMarker(); + this.releaseCanvasWrapperCSS(); + if (this.isPolygonDrawing) { + this.isPolygonDrawing = false; + this.vectorDrawHandler.cancel(); + } + this.canvas.clear(); + this.canvas.renderAll(); + this.isDrawing = false; + this.isInsertion = false; + this.redraw = null; + this.drawnObjects = []; + this.onDrawDone(null); + } + + private releaseEdit(): void { + this.removeBrushMarker(); + this.releaseCanvasWrapperCSS(); + if (this.isPolygonDrawing) { + this.isPolygonDrawing = false; + this.vectorDrawHandler.cancel(); + } + this.canvas.clear(); + this.canvas.renderAll(); + this.isEditing = false; + this.drawnObjects = []; + this.onEditDone(null, null); + } + + private getStateColor(state: any): string { + if (this.colorBy === ColorBy.INSTANCE) { + return state.color; + } + + if (this.colorBy === ColorBy.LABEL) { + return state.label.color; + } + + return state.group.color; + } + + private getDrawnObjectsWrappingBox(): WrappingBBox { + type BoundingRect = ReturnType>; + type TwoCornerBox = Pick & { right: number; bottom: number }; + const { width, height } = this.geometry.image; + const wrappingBbox = this.drawnObjects + .map((obj) => { + if (obj instanceof fabric.Polygon) { + const bbox = computeWrappingBox(obj.points + .reduce(((acc, val) => { + acc.push(val.x, val.y); + return acc; + }), [])); + + return { + left: bbox.xtl, + top: bbox.ytl, + width: bbox.width, + height: bbox.height, + }; + } + + if (obj instanceof fabric.Image) { + return { + left: obj.left, + top: obj.top, + width: obj.width, + height: obj.height, + }; + } + + return obj.getBoundingRect(); + }) + .reduce((acc: TwoCornerBox, rect: BoundingRect) => { + acc.top = Math.floor(Math.max(0, Math.min(rect.top, acc.top))); + acc.left = Math.floor(Math.max(0, Math.min(rect.left, acc.left))); + acc.bottom = Math.floor(Math.min(height, Math.max(rect.top + rect.height, acc.bottom))); + acc.right = Math.floor(Math.min(width, Math.max(rect.left + rect.width, acc.right))); + return acc; + }, { + left: Number.MAX_SAFE_INTEGER, + top: Number.MAX_SAFE_INTEGER, + right: Number.MIN_SAFE_INTEGER, + bottom: Number.MIN_SAFE_INTEGER, + }); + + return wrappingBbox; + } + + private imageDataFromCanvas(wrappingBBox: WrappingBBox): Uint8ClampedArray { + const imageData = this.canvas.toCanvasElement() + .getContext('2d').getImageData( + wrappingBBox.left, wrappingBBox.top, + wrappingBBox.right - wrappingBBox.left + 1, wrappingBBox.bottom - wrappingBBox.top + 1, + ).data; + return imageData; + } + + private updateBrushTools(brushTool?: BrushTool, opts: Partial = {}): void { + if (this.isPolygonDrawing) { + // tool was switched from polygon to brush for example + this.keepDrawnPolygon(); + } + + this.removeBrushMarker(); + if (brushTool) { + if (brushTool.color && this.tool?.color !== brushTool.color) { + const color = fabric.Color.fromHex(brushTool.color); + for (const object of this.drawnObjects) { + if (object instanceof fabric.Line) { + const alpha = +object.stroke.split(',')[3].slice(0, -1); + color.setAlpha(alpha); + object.set({ stroke: color.toRgba() }); + } else if (!(object instanceof fabric.Image)) { + const alpha = +(object.fill as string).split(',')[3].slice(0, -1); + color.setAlpha(alpha); + object.set({ fill: color.toRgba() }); + } + } + this.canvas.renderAll(); + } + + this.tool = { ...brushTool, ...opts }; + if (this.isDrawing || this.isEditing) { + this.setupBrushMarker(); + } + } + + if (this.tool?.type?.startsWith('polygon-')) { + this.isPolygonDrawing = true; + this.vectorDrawHandler.draw({ + enabled: true, + shapeType: 'polygon', + onDrawDone: (data: { points: number[] } | null) => { + if (!data) return; + const points = data.points.reduce((acc: fabric.Point[], _: number, idx: number) => { + if (idx % 2) { + acc.push(new fabric.Point(data.points[idx - 1], data.points[idx])); + } + + return acc; + }, []); + + const color = fabric.Color.fromHex(this.tool.color); + color.setAlpha(this.tool.type === 'polygon-minus' ? 1 : this.drawingOpacity); + const polygon = new fabric.Polygon(points, { + fill: color.toRgba(), + selectable: false, + objectCaching: false, + absolutePositioned: true, + globalCompositeOperation: this.tool.type === 'polygon-minus' ? 'destination-out' : 'xor', + }); + + this.canvas.add(polygon); + this.drawnObjects.push(polygon); + this.canvas.renderAll(); + }, + }, this.geometry); + + const canvasWrapper = this.canvas.getElement().parentElement as HTMLDivElement; + canvasWrapper.style.pointerEvents = 'none'; + canvasWrapper.style.zIndex = '0'; + } + } + + public constructor( + onDrawDone: ( + data: object | null, + duration?: number, + continueDraw?: boolean, + prevDrawData?: DrawData, + ) => void, + onDrawRepeat: (data: DrawData) => void, + onEditStart: (state: any) => void, + onEditDone: (state: any, points: number[]) => void, + vectorDrawHandler: DrawHandler, + canvas: HTMLCanvasElement, + ) { + this.redraw = null; + this.isDrawing = false; + this.isEditing = false; + this.isMouseDown = false; + this.isBrushSizeChanging = false; + this.isPolygonDrawing = false; + this.drawData = null; + this.editData = null; + this.drawnObjects = []; + this.drawingOpacity = 0.5; + this.brushMarker = null; + this.colorBy = ColorBy.LABEL; + this.onDrawDone = onDrawDone; + this.onDrawRepeat = onDrawRepeat; + this.onEditDone = onEditDone; + this.onEditStart = onEditStart; + this.vectorDrawHandler = vectorDrawHandler; + this.canvas = new fabric.Canvas(canvas, { + containerClass: 'cvat_masks_canvas_wrapper', + fireRightClick: true, + selection: false, + defaultCursor: 'inherit', + }); + this.canvas.imageSmoothingEnabled = false; + + this.canvas.getElement().parentElement.addEventListener('contextmenu', (e: MouseEvent) => e.preventDefault()); + this.latestMousePos = { x: -1, y: -1 }; + window.document.addEventListener('mouseup', () => { + this.isMouseDown = false; + this.isBrushSizeChanging = false; + }); + + this.canvas.on('mouse:down', (options: fabric.IEvent) => { + const { isDrawing, isEditing, isInsertion } = this; + this.isMouseDown = (isDrawing || isEditing) && options.e.button === 0 && !options.e.altKey; + this.isBrushSizeChanging = (isDrawing || isEditing) && options.e.button === 2 && options.e.altKey; + + if (isInsertion) { + const continueInserting = options.e.ctrlKey; + const wrappingBbox = this.getDrawnObjectsWrappingBox(); + const imageData = this.imageDataFromCanvas(wrappingBbox); + const alpha = alphaChannelOnly(imageData); + alpha.push(wrappingBbox.left, wrappingBbox.top, wrappingBbox.right, wrappingBbox.bottom); + + this.onDrawDone({ + shapeType: this.drawData.shapeType, + points: alpha, + }, Date.now() - this.startTimestamp, continueInserting, this.drawData); + + if (!continueInserting) { + this.releasePaste(); + } + } + }); + + this.canvas.on('mouse:move', (e: fabric.IEvent) => { + const { image: { width: imageWidth, height: imageHeight } } = this.geometry; + const { angle } = this.geometry; + let [x, y] = [e.pointer.x, e.pointer.y]; + if (angle === 180) { + [x, y] = [imageWidth - x, imageHeight - y]; + } else if (angle === 270) { + [x, y] = [imageWidth - (y / imageHeight) * imageWidth, (x / imageWidth) * imageHeight]; + } else if (angle === 90) { + [x, y] = [(y / imageHeight) * imageWidth, imageHeight - (x / imageWidth) * imageHeight]; + } + + const position = { x, y }; + const { + tool, isMouseDown, isInsertion, isBrushSizeChanging, + } = this; + + if (isInsertion) { + const [object] = this.drawnObjects; + if (object && object instanceof fabric.Image) { + object.left = position.x - object.width / 2; + object.top = position.y - object.height / 2; + this.canvas.renderAll(); + } + } + + if (isBrushSizeChanging && ['brush', 'eraser'].includes(tool?.type)) { + const xDiff = e.pointer.x - this.resizeBrushToolLatestX; + let onUpdateConfiguration = null; + if (this.isDrawing) { + onUpdateConfiguration = this.drawData.onUpdateConfiguration; + } else if (this.isEditing) { + onUpdateConfiguration = this.editData.onUpdateConfiguration; + } + if (onUpdateConfiguration) { + onUpdateConfiguration({ + brushTool: { + size: Math.trunc(Math.max(1, this.tool.size + xDiff)), + }, + }); + } + + this.resizeBrushToolLatestX = e.pointer.x; + e.e.stopPropagation(); + return; + } + + if (this.brushMarker) { + this.brushMarker.left = position.x - tool.size / 2; + this.brushMarker.top = position.y - tool.size / 2; + this.canvas.bringToFront(this.brushMarker); + this.canvas.renderAll(); + } + + if (isMouseDown && !isBrushSizeChanging && ['brush', 'eraser'].includes(tool?.type)) { + const color = fabric.Color.fromHex(tool.color); + color.setAlpha(tool.type === 'eraser' ? 1 : 0.5); + + const commonProperties = { + selectable: false, + evented: false, + globalCompositeOperation: tool.type === 'eraser' ? 'destination-out' : 'xor', + }; + + const shapeProperties = { + ...commonProperties, + fill: color.toRgba(), + left: position.x - tool.size / 2, + top: position.y - tool.size / 2, + }; + + let shape: fabric.Circle | fabric.Rect | null = null; + if (tool.form === 'circle') { + shape = new fabric.Circle({ + ...shapeProperties, + radius: tool.size / 2, + }); + } else if (tool.form === 'square') { + shape = new fabric.Rect({ + ...shapeProperties, + width: tool.size, + height: tool.size, + }); + } + + this.canvas.add(shape); + if (tool.type === 'brush') { + this.drawnObjects.push(shape); + } + + // add line to smooth the mask + if (this.latestMousePos.x !== -1 && this.latestMousePos.y !== -1) { + const dx = position.x - this.latestMousePos.x; + const dy = position.y - this.latestMousePos.y; + if (Math.sqrt(dx ** 2 + dy ** 2) > tool.size / 2) { + const line = new fabric.Line([ + this.latestMousePos.x - tool.size / 2, + this.latestMousePos.y - tool.size / 2, + position.x - tool.size / 2, + position.y - tool.size / 2, + ], { + ...commonProperties, + stroke: color.toRgba(), + strokeWidth: tool.size, + strokeLineCap: tool.form === 'circle' ? 'round' : 'square', + }); + + this.canvas.add(line); + if (tool.type === 'brush') { + this.drawnObjects.push(line); + } + } + } + this.canvas.renderAll(); + } else if (tool?.type.startsWith('polygon-') && this.drawablePolygon) { + // update the polygon position + const points = this.drawablePolygon.get('points'); + if (points.length) { + points[points.length - 1].setX(e.e.offsetX); + points[points.length - 1].setY(e.e.offsetY); + } + this.canvas.renderAll(); + } + + this.latestMousePos.x = position.x; + this.latestMousePos.y = position.y; + this.resizeBrushToolLatestX = position.x; + }); + } + + public configurate(configuration: Configuration): void { + this.colorBy = configuration.colorBy; + } + + public transform(geometry: Geometry): void { + this.geometry = geometry; + const { + scale, angle, image: { width, height }, top, left, + } = geometry; + + const topCanvas = this.canvas.getElement().parentElement as HTMLDivElement; + if (this.canvas.width !== width || this.canvas.height !== height) { + this.canvas.setHeight(height); + this.canvas.setWidth(width); + this.canvas.setDimensions({ width, height }); + } + + topCanvas.style.top = `${top}px`; + topCanvas.style.left = `${left}px`; + topCanvas.style.transform = `scale(${scale}) rotate(${angle}deg)`; + + if (this.drawablePolygon) { + this.drawablePolygon.set('strokeWidth', consts.BASE_STROKE_WIDTH / scale); + this.canvas.renderAll(); + } + } + + public draw(drawData: DrawData): void { + if (drawData.enabled && drawData.shapeType === 'mask') { + if (!this.isInsertion && drawData.initialState?.shapeType === 'mask') { + // initialize inserting pipeline if not started + const { points } = drawData.initialState; + const color = fabric.Color.fromHex(this.getStateColor(drawData.initialState)).getSource(); + const [left, top, right, bottom] = points.slice(-4); + const imageBitmap = expandChannels(color[0], color[1], color[2], points, 4); + imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1, + (dataURL: string) => new Promise((resolve) => { + fabric.Image.fromURL(dataURL, (image: fabric.Image) => { + try { + image.selectable = false; + image.evented = false; + image.globalCompositeOperation = 'xor'; + image.opacity = 0.5; + this.canvas.add(image); + this.drawnObjects.push(image); + this.canvas.renderAll(); + } finally { + resolve(); + } + }, { left, top }); + })); + + this.isInsertion = true; + } else if (!this.isDrawing) { + // initialize drawing pipeline if not started + this.isDrawing = true; + this.redraw = drawData.redraw || null; + } + + this.canvas.getElement().parentElement.style.display = 'block'; + this.startTimestamp = Date.now(); + } + + this.updateBrushTools(drawData.brushTool); + + if (!drawData.enabled && this.isDrawing) { + try { + if (this.drawnObjects.length) { + const wrappingBbox = this.getDrawnObjectsWrappingBox(); + const imageData = this.imageDataFromCanvas(wrappingBbox); + const alpha = alphaChannelOnly(imageData); + alpha.push(wrappingBbox.left, wrappingBbox.top, wrappingBbox.right, wrappingBbox.bottom); + this.onDrawDone({ + shapeType: this.drawData.shapeType, + points: alpha, + ...(Number.isInteger(this.redraw) ? { clientID: this.redraw } : {}), + }, Date.now() - this.startTimestamp, drawData.continue, this.drawData); + } + } finally { + this.releaseDraw(); + } + + if (drawData.continue) { + const newDrawData = { + ...this.drawData, + brushTool: { ...this.tool }, + ...drawData, + enabled: true, + shapeType: 'mask', + }; + this.onDrawRepeat(newDrawData); + return; + } + } + + this.drawData = drawData; + } + + public edit(editData: MasksEditData): void { + if (editData.enabled && editData.state.shapeType === 'mask') { + if (!this.isEditing) { + // start editing pipeline if not started yet + this.canvas.getElement().parentElement.style.display = 'block'; + const { points } = editData.state; + const color = fabric.Color.fromHex(this.getStateColor(editData.state)).getSource(); + const [left, top, right, bottom] = points.slice(-4); + const imageBitmap = expandChannels(color[0], color[1], color[2], points, 4); + imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1, + (dataURL: string) => new Promise((resolve) => { + fabric.Image.fromURL(dataURL, (image: fabric.Image) => { + try { + image.selectable = false; + image.evented = false; + image.globalCompositeOperation = 'xor'; + image.opacity = 0.5; + this.canvas.add(image); + this.drawnObjects.push(image); + this.canvas.renderAll(); + } finally { + resolve(); + } + }, { left, top }); + })); + + this.isEditing = true; + this.startTimestamp = Date.now(); + this.onEditStart(editData.state); + } + } + + this.updateBrushTools( + editData.brushTool, + editData.state ? { color: this.getStateColor(editData.state) } : {}, + ); + + if (!editData.enabled && this.isEditing) { + try { + if (this.drawnObjects.length) { + const wrappingBbox = this.getDrawnObjectsWrappingBox(); + const imageData = this.imageDataFromCanvas(wrappingBbox); + const alpha = alphaChannelOnly(imageData); + alpha.push(wrappingBbox.left, wrappingBbox.top, wrappingBbox.right, wrappingBbox.bottom); + this.onEditDone(this.editData.state, alpha); + } + } finally { + this.releaseEdit(); + } + } + this.editData = editData; + } + + get enabled(): boolean { + return this.isDrawing || this.isEditing || this.isInsertion; + } + + public cancel(): void { + if (this.isDrawing || this.isInsertion) { + this.releaseDraw(); + } + + if (this.isEditing) { + this.releaseEdit(); + } + } +} diff --git a/cvat-canvas/src/typescript/mergeHandler.ts b/cvat-canvas/src/typescript/mergeHandler.ts index b5d0f459229..c7c7005e8f0 100644 --- a/cvat-canvas/src/typescript/mergeHandler.ts +++ b/cvat-canvas/src/typescript/mergeHandler.ts @@ -103,6 +103,10 @@ export class MergeHandlerImpl implements MergeHandler { } public select(objectState: any): void { + if (objectState.shapeType === 'mask') { + // masks can not be merged + return; + } const stateIndexes = this.statesToBeMerged.map((state): number => state.clientID); const stateFrames = this.statesToBeMerged.map((state): number => state.frame); const includes = stateIndexes.indexOf(objectState.clientID); diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts index b8274108072..b288d56d2a7 100644 --- a/cvat-canvas/src/typescript/shared.ts +++ b/cvat-canvas/src/typescript/shared.ts @@ -361,4 +361,46 @@ export function setupSkeletonEdges(skeleton: SVG.G, referenceSVG: SVG.G): void { } } +export function imageDataToDataURL( + imageBitmap: Uint8ClampedArray, + width: number, + height: number, + handleResult: (dataURL: string) => Promise, +): void { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + + canvas.getContext('2d').putImageData( + new ImageData(imageBitmap, width, height), 0, 0, + ); + + canvas.toBlob((blob) => { + const dataURL = URL.createObjectURL(blob); + handleResult(dataURL).finally(() => { + URL.revokeObjectURL(dataURL); + }); + }, 'image/png'); +} + +export function alphaChannelOnly(imageData: Uint8ClampedArray): number[] { + const alpha = new Array(imageData.length / 4); + for (let i = 3; i < imageData.length; i += 4) { + alpha[Math.floor(i / 4)] = imageData[i] > 0 ? 1 : 0; + } + return alpha; +} + +export function expandChannels(r: number, g: number, b: number, alpha: number[], endOffset = 0): Uint8ClampedArray { + const imageBitmap = new Uint8ClampedArray((alpha.length - endOffset) * 4); + for (let i = 0; i < alpha.length - endOffset; i++) { + const val = alpha[i] ? 1 : 0; + imageBitmap[i * 4] = r; + imageBitmap[i * 4 + 1] = g; + imageBitmap[i * 4 + 2] = b; + imageBitmap[i * 4 + 3] = val * 255; + } + return imageBitmap; +} + export type PropType = T[Prop]; diff --git a/cvat-canvas/tsconfig.json b/cvat-canvas/tsconfig.json index 700dfe36889..eed9cbafe0d 100644 --- a/cvat-canvas/tsconfig.json +++ b/cvat-canvas/tsconfig.json @@ -3,7 +3,7 @@ "baseUrl": ".", "emitDeclarationOnly": true, "module": "es6", - "target": "es2016", + "target": "es2019", "noImplicitAny": true, "preserveConstEnums": true, "declaration": true, diff --git a/cvat-core/package.json b/cvat-core/package.json index 8871826b2c5..0e0b7ee4b85 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "7.0.3", + "version": "7.1.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", "scripts": { diff --git a/cvat-core/src/annotations-collection.ts b/cvat-core/src/annotations-collection.ts index 5787b9942f3..118b8b0fc32 100644 --- a/cvat-core/src/annotations-collection.ts +++ b/cvat-core/src/annotations-collection.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corp +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -13,10 +13,12 @@ } = require('./annotations-objects'); const AnnotationsFilter = require('./annotations-filter').default; const { checkObjectType } = require('./common'); - const Statistics = require('./statistics'); + const Statistics = require('./statistics').default; const { Label } = require('./labels'); const { ArgumentError, ScriptingError } = require('./exceptions'); const ObjectState = require('./object-state').default; + const { mask2Rle, truncateMask } = require('./object-utils'); + const config = require('./config').default; const { HistoryActions, ShapeType, ObjectType, colors, Source, @@ -55,6 +57,8 @@ history: this.history, nextClientID: () => ++this.count, groupColors: {}, + getMasksOnFrame: (frame: number) => this.shapes[frame] + .filter((object) => object.objectShape === ObjectType.MASK), }; } @@ -93,9 +97,8 @@ // In this case a corresponded message will be sent to the console if (trackModel) { this.tracks.push(trackModel); - this.objects[clientID] = trackModel; - result.tracks.push(trackModel); + this.objects[clientID] = trackModel; } } @@ -106,15 +109,15 @@ const data = { tracks: this.tracks.filter((track) => !track.removed).map((track) => track.toJSON()), shapes: Object.values(this.shapes) - .reduce((accumulator, value) => { - accumulator.push(...value); + .reduce((accumulator, frameShapes) => { + accumulator.push(...frameShapes); return accumulator; }, []) .filter((shape) => !shape.removed) .map((shape) => shape.toJSON()), tags: Object.values(this.tags) - .reduce((accumulator, value) => { - accumulator.push(...value); + .reduce((accumulator, frameTags) => { + accumulator.push(...frameTags); return accumulator; }, []) .filter((tag) => !tag.removed) @@ -356,6 +359,12 @@ 'The object is not in collection yet. Call ObjectState.put([state]) before you can merge it', ); } + + if (state.shapeType === ShapeType.MASK) { + throw new ArgumentError( + 'Merging for masks is not supported', + ); + } return object; }); @@ -601,6 +610,7 @@ [val]: { shape: 0, track: 0 }, }), {})), + mask: { shape: 0 }, tag: 0, manually: 0, interpolated: 0, @@ -787,7 +797,14 @@ group: 0, label_id: state.label.id, occluded: state.occluded || false, - points: [...state.points], + points: state.shapeType === 'mask' ? (() => { + const { width, height } = this.frameMeta[state.frame]; + const points = truncateMask(state.points, 0, width, height); + const [left, top, right, bottom] = points.splice(-4); + const rlePoints = mask2Rle(points); + rlePoints.push(left, top, right, bottom); + return rlePoints; + })() : state.points, rotation: state.rotation || 0, type: state.shapeType, z_order: state.zOrder, @@ -865,6 +882,11 @@ // eslint-disable-next-line no-unsanitized/method const imported = this.import(constructed); const importedArray = imported.tags.concat(imported.tracks).concat(imported.shapes); + for (const object of importedArray) { + if (object.shapeType === ShapeType.MASK && config.removeUnderlyingMaskPixels) { + object.removeUnderlyingPixels(object.frame); + } + } if (objectStates.length) { this.history.do( diff --git a/cvat-core/src/annotations-filter.ts b/cvat-core/src/annotations-filter.ts index 72e21de2bdb..63c748cdd0e 100644 --- a/cvat-core/src/annotations-filter.ts +++ b/cvat-core/src/annotations-filter.ts @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT import jsonLogic from 'json-logic-js'; -import { AttributeType, ObjectType } from './enums'; +import { AttributeType, ObjectType, ShapeType } from './enums'; function adjustName(name): string { return name.replace(/\./g, '\u2219'); @@ -17,29 +17,35 @@ export default class AnnotationsFilter { return acc; }, {}); - let xtl = Number.MAX_SAFE_INTEGER; - let xbr = Number.MIN_SAFE_INTEGER; - let ytl = Number.MAX_SAFE_INTEGER; - let ybr = Number.MIN_SAFE_INTEGER; let [width, height] = [null, null]; if (state.objectType !== ObjectType.TAG) { - const points = state.points || state.elements.reduce((acc, val) => { - acc.push(val.points); - return acc; - }, []).flat(); - points.forEach((coord, idx) => { - if (idx % 2) { - // y - ytl = Math.min(ytl, coord); - ybr = Math.max(ybr, coord); - } else { - // x - xtl = Math.min(xtl, coord); - xbr = Math.max(xbr, coord); - } - }); - [width, height] = [xbr - xtl, ybr - ytl]; + if (state.shapeType === ShapeType.MASK) { + const [xtl, ytl, xbr, ybr] = state.points.slice(-4); + [width, height] = [xbr - xtl + 1, ybr - ytl + 1]; + } else { + let xtl = Number.MAX_SAFE_INTEGER; + let xbr = Number.MIN_SAFE_INTEGER; + let ytl = Number.MAX_SAFE_INTEGER; + let ybr = Number.MIN_SAFE_INTEGER; + + const points = state.points || state.elements.reduce((acc, val) => { + acc.push(val.points); + return acc; + }, []).flat(); + points.forEach((coord, idx) => { + if (idx % 2) { + // y + ytl = Math.min(ytl, coord); + ybr = Math.max(ybr, coord); + } else { + // x + xtl = Math.min(xtl, coord); + xbr = Math.max(xbr, coord); + } + }); + [width, height] = [xbr - xtl, ybr - ytl]; + } } const attributes = {}; diff --git a/cvat-core/src/annotations-objects.ts b/cvat-core/src/annotations-objects.ts index bd8b9f03970..bca6a83d192 100644 --- a/cvat-core/src/annotations-objects.ts +++ b/cvat-core/src/annotations-objects.ts @@ -1,172 +1,24 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corp +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -import ObjectState from './object-state'; +import config from './config'; +import ObjectState, { SerializedData } from './object-state'; import { checkObjectType, clamp } from './common'; import { DataError, ArgumentError, ScriptingError } from './exceptions'; -import { Label, Attribute } from './labels'; +import { Label } from './labels'; import { - colors, Source, ShapeType, ObjectType, AttributeType, HistoryActions, + colors, Source, ShapeType, ObjectType, HistoryActions, } from './enums'; import AnnotationHistory from './annotations-history'; +import { + checkNumberOfPoints, attrsAsAnObject, checkShapeArea, mask2Rle, rle2Mask, + computeWrappingBox, findAngleDiff, rotatePoint, validateAttributeValue, truncateMask, +} from './object-utils'; const defaultGroupColor = '#E0E0E0'; -function checkNumberOfPoints(shapeType: ShapeType, points: number[]): void { - if (shapeType === ShapeType.RECTANGLE) { - if (points.length / 2 !== 2) { - throw new DataError(`Rectangle must have 2 points, but got ${points.length / 2}`); - } - } else if (shapeType === ShapeType.POLYGON) { - if (points.length / 2 < 3) { - throw new DataError(`Polygon must have at least 3 points, but got ${points.length / 2}`); - } - } else if (shapeType === ShapeType.POLYLINE) { - if (points.length / 2 < 2) { - throw new DataError(`Polyline must have at least 2 points, but got ${points.length / 2}`); - } - } else if (shapeType === ShapeType.POINTS) { - if (points.length / 2 < 1) { - throw new DataError(`Points must have at least 1 points, but got ${points.length / 2}`); - } - } else if (shapeType === ShapeType.CUBOID) { - if (points.length / 2 !== 8) { - throw new DataError(`Cuboid must have 8 points, but got ${points.length / 2}`); - } - } else if (shapeType === ShapeType.ELLIPSE) { - if (points.length / 2 !== 2) { - throw new DataError(`Ellipse must have 1 point, rx and ry but got ${points.toString()}`); - } - } else { - throw new ArgumentError(`Unknown value of shapeType has been received ${shapeType}`); - } -} - -function attrsAsAnObject(attributes: Attribute[]): Record { - return attributes.reduce((accumulator, value) => { - accumulator[value.id] = value; - return accumulator; - }, {}); -} - -function findAngleDiff(rightAngle: number, leftAngle: number): number { - let angleDiff = rightAngle - leftAngle; - angleDiff = ((angleDiff + 180) % 360) - 180; - if (Math.abs(angleDiff) >= 180) { - // if the main arc is bigger than 180, go another arc - // to find it, just substract absolute value from 360 and inverse sign - angleDiff = 360 - Math.abs(angleDiff) * Math.sign(angleDiff) * -1; - } - return angleDiff; -} - -function checkShapeArea(shapeType: ShapeType, points: number[]): boolean { - const MIN_SHAPE_LENGTH = 3; - const MIN_SHAPE_AREA = 9; - - if (shapeType === ShapeType.POINTS) { - return true; - } - - if (shapeType === ShapeType.ELLIPSE) { - const [cx, cy, rightX, topY] = points; - const [rx, ry] = [rightX - cx, cy - topY]; - return rx * ry * Math.PI > MIN_SHAPE_AREA; - } - - let xmin = Number.MAX_SAFE_INTEGER; - let xmax = Number.MIN_SAFE_INTEGER; - let ymin = Number.MAX_SAFE_INTEGER; - let ymax = Number.MIN_SAFE_INTEGER; - - for (let i = 0; i < points.length - 1; i += 2) { - xmin = Math.min(xmin, points[i]); - xmax = Math.max(xmax, points[i]); - ymin = Math.min(ymin, points[i + 1]); - ymax = Math.max(ymax, points[i + 1]); - } - - if (shapeType === ShapeType.POLYLINE) { - const length = Math.max(xmax - xmin, ymax - ymin); - return length >= MIN_SHAPE_LENGTH; - } - - const area = (xmax - xmin) * (ymax - ymin); - return area >= MIN_SHAPE_AREA; -} - -function rotatePoint(x: number, y: number, angle: number, cx = 0, cy = 0): number[] { - const sin = Math.sin((angle * Math.PI) / 180); - const cos = Math.cos((angle * Math.PI) / 180); - const rotX = (x - cx) * cos - (y - cy) * sin + cx; - const rotY = (y - cy) * cos + (x - cx) * sin + cy; - return [rotX, rotY]; -} - -function computeWrappingBox(points: number[], margin = 0): { - xtl: number; - ytl: number; - xbr: number; - ybr: number; - x: number; - y: number; - width: number; - height: number; -} { - let xtl = Number.MAX_SAFE_INTEGER; - let ytl = Number.MAX_SAFE_INTEGER; - let xbr = Number.MIN_SAFE_INTEGER; - let ybr = Number.MIN_SAFE_INTEGER; - - for (let i = 0; i < points.length; i += 2) { - const [x, y] = [points[i], points[i + 1]]; - xtl = Math.min(xtl, x); - ytl = Math.min(ytl, y); - xbr = Math.max(xbr, x); - ybr = Math.max(ybr, y); - } - - const box = { - xtl: xtl - margin, - ytl: ytl - margin, - xbr: xbr + margin, - ybr: ybr + margin, - }; - - return { - ...box, - x: box.xtl, - y: box.ytl, - width: box.xbr - box.xtl, - height: box.ybr - box.ytl, - }; -} - -function validateAttributeValue(value: string, attr: Attribute): boolean { - const { values } = attr; - const type = attr.inputType; - - if (typeof value !== 'string') { - throw new ArgumentError(`Attribute value is expected to be string, but got ${typeof value}`); - } - - if (type === AttributeType.NUMBER) { - return +value >= +values[0] && +value <= +values[1]; - } - - if (type === AttributeType.CHECKBOX) { - return ['true', 'false'].includes(value.toLowerCase()); - } - - if (type === AttributeType.TEXT) { - return true; - } - - return values.includes(value); -} - function copyShape(state: TrackedShape, data: Partial = {}): TrackedShape { return { rotation: state.rotation, @@ -190,6 +42,7 @@ interface AnnotationInjection { parentID?: number; readOnlyFields?: string[]; nextClientID: () => number; + getMasksOnFrame: (frame: number) => MaskShape[]; } class Annotation { @@ -203,7 +56,7 @@ class Annotation { public label: Label; protected frame: number; private _removed: boolean; - protected lock: boolean; + public lock: boolean; protected readOnlyFields: string[]; protected color: string; protected source: Source; @@ -260,16 +113,21 @@ class Annotation { injection.groups.max = Math.max(injection.groups.max, this.group); } - _withContext(frame: number) { + protected withContext(frame: number): { + __internal: { + save: (data: ObjectState) => ObjectState; + delete: Annotation['delete']; + }; + } { return { __internal: { - save: this.save.bind(this, frame), + save: (this as any).save.bind(this, frame), delete: this.delete.bind(this), }, }; } - _saveLock(lock: boolean, frame: number): void { + protected saveLock(lock: boolean, frame: number): void { const undoLock = this.lock; const redoLock = lock; @@ -290,7 +148,7 @@ class Annotation { this.lock = lock; } - _saveColor(color: string, frame: number): void { + protected saveColor(color: string, frame: number): void { const undoColor = this.color; const redoColor = color; @@ -311,7 +169,7 @@ class Annotation { this.color = color; } - _saveLabel(label: Label, frame: number): void { + protected saveLabel(label: Label, frame: number): void { const undoLabel = this.label; const redoLabel = label; const undoAttributes = { ...this.attributes }; @@ -349,7 +207,7 @@ class Annotation { ); } - _saveAttributes(attributes: Record, frame: number): void { + protected saveAttributes(attributes: Record, frame: number): void { const undoAttributes = { ...this.attributes }; for (const attrID of Object.keys(attributes)) { @@ -373,7 +231,7 @@ class Annotation { ); } - _validateStateBeforeSave(data: ObjectState, updated: ObjectState['updateFlags']): void { + protected validateStateBeforeSave(data: ObjectState, updated: ObjectState['updateFlags']): void { if (updated.label) { checkObjectType('label', data.label, null, Label); } @@ -446,15 +304,15 @@ class Annotation { } } - clearServerID(): void { + public clearServerID(): void { this.serverID = undefined; } - updateServerID(body: any): void { + public updateServerID(body: any): void { this.serverID = body.id; } - appendDefaultAttributes(label: Label): void { + protected appendDefaultAttributes(label: Label): void { const labelAttributes = label.attributes; for (const attribute of labelAttributes) { if (!(attribute.id in this.attributes)) { @@ -463,14 +321,14 @@ class Annotation { } } - updateTimestamp(updated: ObjectState['updateFlags']): void { + protected updateTimestamp(updated: ObjectState['updateFlags']): void { const anyChanges = Object.keys(updated).some((key) => !!updated[key]); if (anyChanges) { this.updated = Date.now(); } } - delete(frame: number, force: boolean): boolean { + public delete(frame: number, force: boolean): boolean { if (!this.lock || force) { this.removed = true; this.history.do( @@ -491,18 +349,6 @@ class Annotation { return this.removed; } - save(): void { - throw new ScriptingError('Is not implemented'); - } - - get(): void { - throw new ScriptingError('Is not implemented'); - } - - toJSON(): void { - throw new ScriptingError('Is not implemented'); - } - public get removed(): boolean { return this._removed; } @@ -518,7 +364,7 @@ class Annotation { class Drawn extends Annotation { protected frameMeta: AnnotationInjection['frameMeta']; protected descriptions: string[]; - protected hidden: boolean; + public hidden: boolean; protected pinned: boolean; protected shapeType: ShapeType; @@ -531,11 +377,11 @@ class Drawn extends Annotation { this.shapeType = null; } - _saveDescriptions(descriptions: string[]): void { + protected saveDescriptions(descriptions: string[]): void { this.descriptions = [...descriptions]; } - _savePinned(pinned: boolean, frame: number): void { + protected savePinned(pinned: boolean, frame: number): void { const undoPinned = this.pinned; const redoPinned = pinned; @@ -556,7 +402,7 @@ class Drawn extends Annotation { this.pinned = pinned; } - _saveHidden(hidden: boolean, frame: number): void { + protected saveHidden(hidden: boolean, frame: number): void { const undoHidden = this.hidden; const redoHidden = hidden; @@ -577,7 +423,7 @@ class Drawn extends Annotation { this.hidden = hidden; } - _fitPoints(points: number[], rotation: number, maxX: number, maxY: number): number[] { + private fitPoints(points: number[], rotation: number, maxX: number, maxY: number): number[] { const { shapeType, parentID } = this; checkObjectType('rotation', rotation, 'number', null); points.forEach((coordinate) => checkObjectType('coordinate', coordinate, 'number', null)); @@ -601,17 +447,16 @@ class Drawn extends Annotation { return fittedPoints; } - protected _validateStateBeforeSave(frame: number, data: ObjectState, updated: ObjectState['updateFlags']): number[] { - /* eslint-disable-next-line no-underscore-dangle */ - Annotation.prototype._validateStateBeforeSave.call(this, data, updated); + protected validateStateBeforeSave(data: ObjectState, updated: ObjectState['updateFlags'], frame?: number): number[] { + Annotation.prototype.validateStateBeforeSave.call(this, data, updated); let fittedPoints = []; - if (updated.points) { + if (updated.points && Number.isInteger(frame)) { checkObjectType('points', data.points, null, Array); checkNumberOfPoints(this.shapeType, data.points); // cut points const { width, height, filename } = this.frameMeta[frame]; - fittedPoints = this._fitPoints(data.points, data.rotation, width, height); + fittedPoints = this.fitPoints(data.points, data.rotation, width, height); let check = true; if (filename && filename.slice(filename.length - 3) === 'pcd') { check = false; @@ -653,10 +498,10 @@ interface RawShapeData { } export class Shape extends Drawn { - protected points: number[]; + public points: number[]; + public occluded: boolean; + public outside: boolean; protected rotation: number; - protected occluded: boolean; - protected outside: boolean; protected zOrder: number; constructor(data: RawShapeData, clientID: number, color: string, injection: AnnotationInjection) { @@ -669,13 +514,13 @@ export class Shape extends Drawn { } // Method is used to export data to the server - toJSON(): RawShapeData { + public toJSON(): RawShapeData { const result: RawShapeData = { type: this.shapeType, clientID: this.clientID, occluded: this.occluded, z_order: this.zOrder, - points: [...this.points], + points: this.points.slice(), rotation: this.rotation, attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => { attributeAccumulator.push({ @@ -703,12 +548,12 @@ export class Shape extends Drawn { return result; } - get(frame) { + public get(frame): { outside?: boolean } & Omit, 'keyframe' | 'keyframes' | 'elements' | 'outside'> { if (frame !== this.frame) { throw new ScriptingError('Received frame is not equal to the frame of the shape'); } - const result = { + const result: ReturnType = { objectType: ObjectType.SHAPE, shapeType: this.shapeType, clientID: this.clientID, @@ -717,7 +562,7 @@ export class Shape extends Drawn { occluded: this.occluded, lock: this.lock, zOrder: this.zOrder, - points: [...this.points], + points: this.points.slice(), rotation: this.rotation, attributes: { ...this.attributes }, descriptions: [...this.descriptions], @@ -729,7 +574,7 @@ export class Shape extends Drawn { pinned: this.pinned, frame, source: this.source, - ...this._withContext(frame), + ...this.withContext(frame), }; if (typeof this.outside !== 'undefined') { @@ -739,7 +584,7 @@ export class Shape extends Drawn { return result; } - _saveRotation(rotation: number, frame: number): void { + protected saveRotation(rotation: number, frame: number): void { const undoRotation = this.rotation; const redoRotation = rotation; const undoSource = this.source; @@ -765,7 +610,7 @@ export class Shape extends Drawn { this.rotation = redoRotation; } - _savePoints(points: number[], frame: number): void { + protected savePoints(points: number[], frame: number): void { const undoPoints = this.points; const redoPoints = points; const undoSource = this.source; @@ -791,7 +636,7 @@ export class Shape extends Drawn { this.points = redoPoints; } - _saveOccluded(occluded: boolean, frame: number): void { + protected saveOccluded(occluded: boolean, frame: number): void { const undoOccluded = this.occluded; const redoOccluded = occluded; const undoSource = this.source; @@ -817,7 +662,7 @@ export class Shape extends Drawn { this.occluded = redoOccluded; } - _saveOutside(outside: boolean, frame: number): void { + protected saveOutside(outside: boolean, frame: number): void { const undoOutside = this.outside; const redoOutside = outside; const undoSource = this.source; @@ -843,7 +688,7 @@ export class Shape extends Drawn { this.outside = redoOutside; } - _saveZOrder(zOrder: number, frame: number): void { + protected saveZOrder(zOrder: number, frame: number): void { const undoZOrder = this.zOrder; const redoZOrder = zOrder; const undoSource = this.source; @@ -869,7 +714,7 @@ export class Shape extends Drawn { this.zOrder = redoZOrder; } - save(frame: number, data: ObjectState): ObjectState { + public save(frame: number, data: ObjectState): ObjectState { if (frame !== this.frame) { throw new ScriptingError('Received frame is not equal to the frame of the shape'); } @@ -883,56 +728,56 @@ export class Shape extends Drawn { updated[readOnlyField] = false; } - const fittedPoints = this._validateStateBeforeSave(frame, data, updated); + const fittedPoints = this.validateStateBeforeSave(data, updated, frame); const { rotation } = data; // Now when all fields are validated, we can apply them if (updated.label) { - this._saveLabel(data.label, frame); + this.saveLabel(data.label, frame); } if (updated.attributes) { - this._saveAttributes(data.attributes, frame); + this.saveAttributes(data.attributes, frame); } if (updated.descriptions) { - this._saveDescriptions(data.descriptions); + this.saveDescriptions(data.descriptions); } if (updated.rotation) { - this._saveRotation(rotation, frame); + this.saveRotation(rotation, frame); } if (updated.points && fittedPoints.length) { - this._savePoints(fittedPoints, frame); + this.savePoints(fittedPoints, frame); } if (updated.occluded) { - this._saveOccluded(data.occluded, frame); + this.saveOccluded(data.occluded, frame); } if (updated.outside) { - this._saveOutside(data.outside, frame); + this.saveOutside(data.outside, frame); } if (updated.zOrder) { - this._saveZOrder(data.zOrder, frame); + this.saveZOrder(data.zOrder, frame); } if (updated.lock) { - this._saveLock(data.lock, frame); + this.saveLock(data.lock, frame); } if (updated.pinned) { - this._savePinned(data.pinned, frame); + this.savePinned(data.pinned, frame); } if (updated.color) { - this._saveColor(data.color, frame); + this.saveColor(data.color, frame); } if (updated.hidden) { - this._saveHidden(data.hidden, frame); + this.saveHidden(data.hidden, frame); } this.updateTimestamp(updated); @@ -974,6 +819,14 @@ interface TrackedShape { attributes: Record; } +export interface InterpolatedPosition { + points: number[]; + rotation: number; + occluded: boolean; + outside: boolean; + zOrder: number; +} + export class Track extends Drawn { public shapes: Record; constructor(data: RawTrackData, clientID: number, color: string, injection: AnnotationInjection) { @@ -997,7 +850,7 @@ export class Track extends Drawn { } // Method is used to export data to the server - toJSON(): RawTrackData { + public toJSON(): RawTrackData { const labelAttributes = attrsAsAnObject(this.label.attributes); const result: RawTrackData = { @@ -1056,7 +909,7 @@ export class Track extends Drawn { return result; } - get(frame: number) { + public get(frame: number): Omit, 'elements'> { const { prev, next, first, last, } = this.boundedKeyframes(frame); @@ -1085,11 +938,11 @@ export class Track extends Drawn { }, frame, source: this.source, - ...this._withContext(frame), + ...this.withContext(frame), }; } - boundedKeyframes(targetFrame: number): ObjectState['keyframes'] { + public boundedKeyframes(targetFrame: number): ObjectState['keyframes'] { const frames = Object.keys(this.shapes).map((frame) => +frame); let lDiff = Number.MAX_SAFE_INTEGER; let rDiff = Number.MAX_SAFE_INTEGER; @@ -1128,7 +981,7 @@ export class Track extends Drawn { }; } - getAttributes(targetFrame: number): Record { + protected getAttributes(targetFrame: number): Record { const result = {}; // First of all copy all unmutable attributes @@ -1155,21 +1008,21 @@ export class Track extends Drawn { return result; } - updateServerID(body: RawTrackData): void { + public updateServerID(body: RawTrackData): void { this.serverID = body.id; for (const shape of body.shapes) { this.shapes[shape.frame].serverID = shape.id; } } - clearServerID(): void { + public clearServerID(): void { Drawn.prototype.clearServerID.call(this); for (const keyframe of Object.keys(this.shapes)) { this.shapes[keyframe].serverID = undefined; } } - _saveLabel(label: Label, frame: number): void { + protected saveLabel(label: Label, frame: number): void { const undoLabel = this.label; const redoLabel = label; const undoAttributes = { @@ -1218,7 +1071,7 @@ export class Track extends Drawn { ); } - _saveAttributes(attributes: Record, frame: number): void { + protected saveAttributes(attributes: Record, frame: number): void { const current = this.get(frame); const labelAttributes = attrsAsAnObject(this.label.attributes); @@ -1296,7 +1149,7 @@ export class Track extends Drawn { ); } - _appendShapeActionToHistory(actionType, frame, undoShape, redoShape, undoSource, redoSource) { + protected appendShapeActionToHistory(actionType, frame, undoShape, redoShape, undoSource, redoSource): void { this.history.do( actionType, () => { @@ -1322,7 +1175,7 @@ export class Track extends Drawn { ); } - _saveRotation(rotation: number, frame: number): void { + protected saveRotation(rotation: number, frame: number): void { const wasKeyframe = frame in this.shapes; const undoSource = this.source; const redoSource = this.readOnlyFields.includes('source') ? this.source : Source.MANUAL; @@ -1332,7 +1185,7 @@ export class Track extends Drawn { this.shapes[frame] = redoShape; this.source = redoSource; - this._appendShapeActionToHistory( + this.appendShapeActionToHistory( HistoryActions.CHANGED_ROTATION, frame, undoShape, @@ -1342,7 +1195,7 @@ export class Track extends Drawn { ); } - _savePoints(points: number[], frame: number): void { + protected savePoints(points: number[], frame: number): void { const wasKeyframe = frame in this.shapes; const undoSource = this.source; const redoSource = this.readOnlyFields.includes('source') ? this.source : Source.MANUAL; @@ -1352,7 +1205,7 @@ export class Track extends Drawn { this.shapes[frame] = redoShape; this.source = redoSource; - this._appendShapeActionToHistory( + this.appendShapeActionToHistory( HistoryActions.CHANGED_POINTS, frame, undoShape, @@ -1362,7 +1215,7 @@ export class Track extends Drawn { ); } - _saveOutside(frame: number, outside: boolean): void { + protected saveOutside(frame: number, outside: boolean): void { const wasKeyframe = frame in this.shapes; const undoSource = this.source; const redoSource = this.readOnlyFields.includes('source') ? this.source : Source.MANUAL; @@ -1373,7 +1226,7 @@ export class Track extends Drawn { this.shapes[frame] = redoShape; this.source = redoSource; - this._appendShapeActionToHistory( + this.appendShapeActionToHistory( HistoryActions.CHANGED_OUTSIDE, frame, undoShape, @@ -1383,7 +1236,7 @@ export class Track extends Drawn { ); } - _saveOccluded(occluded: boolean, frame: number): void { + protected saveOccluded(occluded: boolean, frame: number): void { const wasKeyframe = frame in this.shapes; const undoSource = this.source; const redoSource = this.readOnlyFields.includes('source') ? this.source : Source.MANUAL; @@ -1394,7 +1247,7 @@ export class Track extends Drawn { this.shapes[frame] = redoShape; this.source = redoSource; - this._appendShapeActionToHistory( + this.appendShapeActionToHistory( HistoryActions.CHANGED_OCCLUDED, frame, undoShape, @@ -1404,7 +1257,7 @@ export class Track extends Drawn { ); } - _saveZOrder(zOrder: number, frame: number): void { + protected saveZOrder(zOrder: number, frame: number): void { const wasKeyframe = frame in this.shapes; const undoSource = this.source; const redoSource = this.readOnlyFields.includes('source') ? this.source : Source.MANUAL; @@ -1415,7 +1268,7 @@ export class Track extends Drawn { this.shapes[frame] = redoShape; this.source = redoSource; - this._appendShapeActionToHistory( + this.appendShapeActionToHistory( HistoryActions.CHANGED_ZORDER, frame, undoShape, @@ -1425,7 +1278,7 @@ export class Track extends Drawn { ); } - _saveKeyframe(frame: number, keyframe: boolean): void { + protected saveKeyframe(frame: number, keyframe: boolean): void { const wasKeyframe = frame in this.shapes; if ((keyframe && wasKeyframe) || (!keyframe && !wasKeyframe)) { @@ -1444,7 +1297,7 @@ export class Track extends Drawn { delete this.shapes[frame]; } - this._appendShapeActionToHistory( + this.appendShapeActionToHistory( HistoryActions.CHANGED_KEYFRAME, frame, undoShape, @@ -1454,7 +1307,7 @@ export class Track extends Drawn { ); } - save(frame, data) { + public save(frame: number, data: ObjectState): ObjectState { if (this.lock && data.lock) { return new ObjectState(this.get(frame)); } @@ -1464,59 +1317,59 @@ export class Track extends Drawn { updated[readOnlyField] = false; } - const fittedPoints = this._validateStateBeforeSave(frame, data, updated); + const fittedPoints = this.validateStateBeforeSave(data, updated, frame); const { rotation } = data; if (updated.label) { - this._saveLabel(data.label, frame); + this.saveLabel(data.label, frame); } if (updated.lock) { - this._saveLock(data.lock, frame); + this.saveLock(data.lock, frame); } if (updated.pinned) { - this._savePinned(data.pinned, frame); + this.savePinned(data.pinned, frame); } if (updated.color) { - this._saveColor(data.color, frame); + this.saveColor(data.color, frame); } if (updated.hidden) { - this._saveHidden(data.hidden, frame); + this.saveHidden(data.hidden, frame); } if (updated.points && fittedPoints.length) { - this._savePoints(fittedPoints, frame); + this.savePoints(fittedPoints, frame); } if (updated.rotation) { - this._saveRotation(rotation, frame); + this.saveRotation(rotation, frame); } if (updated.outside) { - this._saveOutside(frame, data.outside); + this.saveOutside(frame, data.outside); } if (updated.occluded) { - this._saveOccluded(data.occluded, frame); + this.saveOccluded(data.occluded, frame); } if (updated.zOrder) { - this._saveZOrder(data.zOrder, frame); + this.saveZOrder(data.zOrder, frame); } if (updated.attributes) { - this._saveAttributes(data.attributes, frame); + this.saveAttributes(data.attributes, frame); } if (updated.descriptions) { - this._saveDescriptions(data.descriptions); + this.saveDescriptions(data.descriptions); } if (updated.keyframe) { - this._saveKeyframe(frame, data.keyframe); + this.saveKeyframe(frame, data.keyframe); } this.updateTimestamp(updated); @@ -1525,18 +1378,16 @@ export class Track extends Drawn { return new ObjectState(this.get(frame)); } - interpolatePosition(): {} { - throw new ScriptingError('Not implemented'); - } - - getPosition(targetFrame: number, leftKeyframe: number | null, rightFrame: number | null) { + protected getPosition( + targetFrame: number, leftKeyframe: number | null, rightFrame: number | null, + ): InterpolatedPosition & { keyframe: boolean } { const leftFrame = targetFrame in this.shapes ? targetFrame : leftKeyframe; const rightPosition = Number.isInteger(rightFrame) ? this.shapes[rightFrame] : null; const leftPosition = Number.isInteger(leftFrame) ? this.shapes[leftFrame] : null; if (leftPosition && rightPosition) { return { - ...this.interpolatePosition( + ...(this as any).interpolatePosition( leftPosition, rightPosition, (targetFrame - leftFrame) / (rightFrame - leftFrame), @@ -1576,7 +1427,7 @@ interface RawTagData { export class Tag extends Annotation { // Method is used to export data to the server - toJSON(): RawTagData { + public toJSON(): RawTagData { const result: RawTagData = { clientID: this.clientID, frame: this.frame, @@ -1600,7 +1451,11 @@ export class Tag extends Annotation { return result; } - get(frame: number) { + public get(frame: number): Omit, + 'elements' | 'occluded' | 'outside' | 'rotation' | 'zOrder' | + 'points' | 'hidden' | 'pinned' | 'keyframe' | 'shapeType' | + 'parentID' | 'descriptions' | 'keyframes' + > { if (frame !== this.frame) { throw new ScriptingError('Received frame is not equal to the frame of the shape'); } @@ -1617,11 +1472,11 @@ export class Tag extends Annotation { updated: this.updated, frame, source: this.source, - ...this._withContext(frame), + ...this.withContext(frame), }; } - save(frame: number, data: ObjectState): ObjectState { + public save(frame: number, data: ObjectState): ObjectState { if (frame !== this.frame) { throw new ScriptingError('Received frame is not equal to the frame of the tag'); } @@ -1635,23 +1490,23 @@ export class Tag extends Annotation { updated[readOnlyField] = false; } - this._validateStateBeforeSave(data, updated); + this.validateStateBeforeSave(data, updated); // Now when all fields are validated, we can apply them if (updated.label) { - this._saveLabel(data.label, frame); + this.saveLabel(data.label, frame); } if (updated.attributes) { - this._saveAttributes(data.attributes, frame); + this.saveAttributes(data.attributes, frame); } if (updated.lock) { - this._saveLock(data.lock, frame); + this.saveLock(data.lock, frame); } if (updated.color) { - this._saveColor(data.color, frame); + this.saveColor(data.color, frame); } this.updateTimestamp(updated); @@ -1698,7 +1553,7 @@ export class EllipseShape extends Shape { const [rx, ry] = [rightX - cx, cy - topY]; const [rotX, rotY] = rotatePoint(x, y, -angle, cx, cy); // https://math.stackexchange.com/questions/76457/check-if-a-point-is-within-an-ellipse - const pointWithinEllipse = (_x, _y) => ( + const pointWithinEllipse = (_x: number, _y: number): boolean => ( ((_x - cx) ** 2) / rx ** 2) + (((_y - cy) ** 2) / ry ** 2 ) <= 1; @@ -1719,8 +1574,8 @@ export class EllipseShape extends Shape { // we have one point inside the ellipse, let's build two lines (horizontal and vertical) through the point // and find their interception with ellipse - const x2Equation = (_y) => (((rx * ry) ** 2) - ((_y * rx) ** 2)) / (ry ** 2); - const y2Equation = (_x) => (((rx * ry) ** 2) - ((_x * ry) ** 2)) / (rx ** 2); + const x2Equation = (_y: number): number => (((rx * ry) ** 2) - ((_y * rx) ** 2)) / (ry ** 2); + const y2Equation = (_x: number): number => (((rx * ry) ** 2) - ((_x * ry) ** 2)) / (rx ** 2); // shift x,y to the ellipse coordinate system to compute equation correctly // y axis is inverted @@ -1883,6 +1738,11 @@ export class PointsShape extends PolyShape { } } +interface Point2D { + x: number; + y: number; +} + export class CuboidShape extends Shape { constructor(data: RawShapeData, clientID: number, color: string, injection: AnnotationInjection) { super(data, clientID, color, injection); @@ -1892,9 +1752,9 @@ export class CuboidShape extends Shape { checkNumberOfPoints(this.shapeType, this.points); } - static makeHull(geoPoints) { + static makeHull(geoPoints: Point2D[]): Point2D[] { // Returns the convex hull, assuming that each points[i] <= points[i + 1]. - function makeHullPresorted(points) { + function makeHullPresorted(points: Point2D[]): Point2D[] { if (points.length <= 1) return points.slice(); // Andrew's monotone chain algorithm. Positive y coordinates correspond to 'up' @@ -1936,7 +1796,7 @@ export class CuboidShape extends Shape { return upperHull.concat(lowerHull); } - function POINT_COMPARATOR(a, b) { + function pointsComparator(a, b): number { if (a.x < b.x) return -1; if (a.x > b.x) return +1; if (a.y < b.y) return -1; @@ -1945,14 +1805,15 @@ export class CuboidShape extends Shape { } const newPoints = geoPoints.slice(); - newPoints.sort(POINT_COMPARATOR); + newPoints.sort(pointsComparator); return makeHullPresorted(newPoints); } - static contain(shapePoints, x, y) { - function isLeft(P0, P1, P2) { + static contain(shapePoints, x, y): boolean { + function isLeft(P0, P1, P2): number { return (P1.x - P0.x) * (P2.y - P0.y) - (P2.x - P0.x) * (P1.y - P0.y); } + const points = CuboidShape.makeHull(shapePoints); let wn = 0; for (let i = 0; i < points.length; i += 1) { @@ -2067,7 +1928,7 @@ export class SkeletonShape extends Shape { } // Method is used to export data to the server - toJSON(): RawShapeData { + public toJSON(): RawShapeData { const elements = this.elements.map((element) => ({ ...element.toJSON(), outside: element.outside, @@ -2108,7 +1969,7 @@ export class SkeletonShape extends Shape { return result; } - get(frame) { + public get(frame): Omit, 'parentID' | 'keyframe' | 'keyframes'> { if (frame !== this.frame) { throw new ScriptingError('Received frame is not equal to the frame of the shape'); } @@ -2143,11 +2004,11 @@ export class SkeletonShape extends Shape { hidden: elements.every((el) => el.hidden), frame, source: this.source, - ...this._withContext(frame), + ...this.withContext(frame), }; } - updateServerID(body: RawShapeData): void { + public updateServerID(body: RawShapeData): void { Shape.prototype.updateServerID.call(this, body); for (const element of body.elements) { const thisElement = this.elements.find((_element: Shape) => _element.label.id === element.label_id); @@ -2155,14 +2016,14 @@ export class SkeletonShape extends Shape { } } - clearServerID(): void { + public clearServerID(): void { Shape.prototype.clearServerID.call(this); for (const element of this.elements) { element.clearServerID(); } } - _saveRotation(rotation, frame) { + protected saveRotation(rotation, frame): void { const undoSkeletonPoints = this.elements.map((element) => element.points); const undoSource = this.source; const redoSource = this.readOnlyFields.includes('source') ? this.source : Source.MANUAL; @@ -2205,7 +2066,7 @@ export class SkeletonShape extends Shape { ); } - save(frame, data) { + public save(frame: number, data: ObjectState): ObjectState { if (this.lock && data.lock) { return new ObjectState(this.get(frame)); } @@ -2255,7 +2116,7 @@ export class SkeletonShape extends Shape { const updatedHidden = data.elements.filter((el) => el.updateFlags.hidden); const updatedLock = data.elements.filter((el) => el.updateFlags.lock); - updatedOccluded.forEach((el) => { el.updateFlags.oсcluded = false; }); + updatedOccluded.forEach((el) => { el.updateFlags.occluded = false; }); updatedHidden.forEach((el) => { el.updateFlags.hidden = false; }); updatedLock.forEach((el) => { el.updateFlags.lock = false; }); @@ -2264,7 +2125,7 @@ export class SkeletonShape extends Shape { } if (updatedOccluded.length) { - updatedOccluded.forEach((el) => { el.updateFlags.oсcluded = true; }); + updatedOccluded.forEach((el) => { el.updateFlags.occluded = true; }); updateElements(updatedOccluded, HistoryActions.CHANGED_OCCLUDED, 'occluded'); } @@ -2282,7 +2143,7 @@ export class SkeletonShape extends Shape { return result; } - get occluded() { + get occluded(): boolean { return this.elements.every((element) => element.occluded); } @@ -2290,7 +2151,7 @@ export class SkeletonShape extends Shape { // stub } - get lock() { + get lock(): boolean { return this.elements.every((element) => element.lock); } @@ -2299,6 +2160,153 @@ export class SkeletonShape extends Shape { } } +export class MaskShape extends Shape { + private left: number; + private top: number; + private right: number; + private bottom: number; + private getMasksOnFrame: AnnotationInjection['getMasksOnFrame']; + + constructor(data, clientID, color, injection) { + super(data, clientID, color, injection); + [this.left, this.top, this.right, this.bottom] = this.points.splice(-4, 4); + this.getMasksOnFrame = injection.getMasksOnFrame; + this.pinned = true; + this.shapeType = ShapeType.MASK; + } + + protected validateStateBeforeSave(data: ObjectState, updated: ObjectState['updateFlags'], frame?: number): number[] { + Annotation.prototype.validateStateBeforeSave.call(this, data, updated); + if (updated.points) { + const { width, height } = this.frameMeta[frame]; + const fittedPoints = truncateMask(data.points, 0, width, height); + return fittedPoints; + } + + return []; + } + + protected removeUnderlyingPixels(frame: number): void { + if (frame !== this.frame) { + throw new ArgumentError( + `Wrong "frame" attribute: is not equal to the shape frame (${frame} vs ${this.frame})`, + ); + } + + const others = this.getMasksOnFrame(frame) + .filter((mask: MaskShape) => mask.clientID !== this.clientID && !mask.removed); + const width = this.right - this.left + 1; + const height = this.bottom - this.top + 1; + const updatedObjects: Record = {}; + + const masks = {}; + const currentMask = rle2Mask(this.points, width, height); + for (let i = 0; i < currentMask.length; i++) { + if (currentMask[i]) { + const imageX = (i % width) + this.left; + const imageY = Math.trunc(i / width) + this.top; + for (const other of others) { + const box = { + left: other.left, + top: other.top, + right: other.right, + bottom: other.bottom, + }; + const translatedX = imageX - box.left; + const translatedY = imageY - box.top; + const [otherWidth, otherHeight] = [box.right - box.left + 1, box.bottom - box.top + 1]; + if (translatedX >= 0 && translatedX < otherWidth && + translatedY >= 0 && translatedY < otherHeight) { + masks[other.clientID] = masks[other.clientID] || + rle2Mask(other.points, otherWidth, otherHeight); + const j = translatedY * otherWidth + translatedX; + masks[other.clientID][j] = 0; + updatedObjects[other.clientID] = other; + } + } + } + } + + for (const object of Object.values(updatedObjects)) { + object.points = mask2Rle(masks[object.clientID]); + object.updated = Date.now(); + } + } + + protected savePoints(maskPoints: number[], frame: number): void { + const undoPoints = this.points; + const undoLeft = this.left; + const undoRight = this.right; + const undoTop = this.top; + const undoBottom = this.bottom; + const undoSource = this.source; + + const [redoLeft, redoTop, redoRight, redoBottom] = maskPoints.splice(-4); + const points = mask2Rle(maskPoints); + + const redoPoints = points; + const redoSource = Source.MANUAL; + + const undo = (): void => { + this.points = undoPoints; + this.source = undoSource; + this.left = undoLeft; + this.top = undoTop; + this.right = undoRight; + this.bottom = undoBottom; + this.updated = Date.now(); + }; + + const redo = (): void => { + this.points = redoPoints; + this.source = redoSource; + this.left = redoLeft; + this.top = redoTop; + this.right = redoRight; + this.bottom = redoBottom; + this.updated = Date.now(); + }; + + this.history.do( + HistoryActions.CHANGED_POINTS, + undo, redo, [this.clientID], frame, + ); + + redo(); + + if (config.removeUnderlyingMaskPixels) { + this.removeUnderlyingPixels(frame); + } + } + + static distance(points: number[], x: number, y: number): null | number { + const [left, top, right, bottom] = points.slice(-4); + const [width, height] = [right - left + 1, bottom - top + 1]; + const [translatedX, translatedY] = [x - left, y - top]; + if (translatedX < 0 || translatedX >= width || translatedY < 0 || translatedY >= height) { + return null; + } + const offset = Math.floor(translatedY) * width + Math.floor(translatedX); + + if (points[offset]) return 1; + return null; + } +} + +MaskShape.prototype.toJSON = function () { + const result = Shape.prototype.toJSON.call(this); + result.points = this.points.slice(); + result.points.push(this.left, this.top, this.right, this.bottom); + return result; +}; + +MaskShape.prototype.get = function (frame) { + const result = Shape.prototype.get.call(this, frame); + result.points = rle2Mask(this.points, this.right - this.left + 1, this.bottom - this.top + 1); + result.points.push(this.left, this.top, this.right, this.bottom); + return result; +}; + export class RectangleTrack extends Track { constructor(data: RawTrackData, clientID: number, color: string, injection: AnnotationInjection) { super(data, clientID, color, injection); @@ -2309,7 +2317,7 @@ export class RectangleTrack extends Track { } } - interpolatePosition(leftPosition, rightPosition, offset) { + protected interpolatePosition(leftPosition, rightPosition, offset): InterpolatedPosition { const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point); return { points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset), @@ -2334,7 +2342,7 @@ export class EllipseTrack extends Track { } } - interpolatePosition(leftPosition, rightPosition, offset) { + interpolatePosition(leftPosition, rightPosition, offset): InterpolatedPosition { const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point); return { @@ -2358,7 +2366,7 @@ class PolyTrack extends Track { } } - interpolatePosition(leftPosition, rightPosition, offset) { + protected interpolatePosition(leftPosition, rightPosition, offset): InterpolatedPosition { if (offset === 0) { return { points: [...leftPosition.points], @@ -2369,14 +2377,14 @@ class PolyTrack extends Track { }; } - function toArray(points) { + function toArray(points: { x: number; y: number; }[]): number[] { return points.reduce((acc, val) => { acc.push(val.x, val.y); return acc; }, []); } - function toPoints(array) { + function toPoints(array: number[]): { x: number; y: number; }[] { return array.reduce((acc, _, index) => { if (index % 2) { acc.push({ @@ -2389,7 +2397,7 @@ class PolyTrack extends Track { }, []); } - function curveLength(points) { + function curveLength(points: { x: number; y: number; }[]): number { return points.slice(1).reduce((acc, _, index) => { const dx = points[index + 1].x - points[index].x; const dy = points[index + 1].y - points[index].y; @@ -2397,7 +2405,7 @@ class PolyTrack extends Track { }, 0); } - function curveToOffsetVec(points, length) { + function curveToOffsetVec(points: { x: number; y: number; }[], length: number): number[] { const offsetVector = [0]; // with initial value let accumulatedLength = 0; @@ -2411,7 +2419,7 @@ class PolyTrack extends Track { return offsetVector; } - function findNearestPair(value, curve) { + function findNearestPair(value: number, curve: number[]): number { let minimum = [0, Math.abs(value - curve[0])]; for (let i = 1; i < curve.length; i++) { const distance = Math.abs(value - curve[i]); @@ -2423,7 +2431,7 @@ class PolyTrack extends Track { return minimum[0]; } - function matchLeftRight(leftCurve, rightCurve) { + function matchLeftRight(leftCurve: number[], rightCurve: number[]): Record { const matching = {}; for (let i = 0; i < leftCurve.length; i++) { matching[i] = [findNearestPair(leftCurve[i], rightCurve)]; @@ -2432,7 +2440,11 @@ class PolyTrack extends Track { return matching; } - function matchRightLeft(leftCurve, rightCurve, leftRightMatching) { + function matchRightLeft( + leftCurve: number[], + rightCurve: number[], + leftRightMatching: Record, + ): Record { const matchedRightPoints = Object.values(leftRightMatching).flat(); const unmatchedRightPoints = rightCurve .map((_, index) => index) @@ -2453,7 +2465,7 @@ class PolyTrack extends Track { } function reduceInterpolation(interpolatedPoints, matching, leftPoints, rightPoints) { - function averagePoint(points) { + function averagePoint(points: Point2D[]): Point2D { let sumX = 0; let sumY = 0; for (const point of points) { @@ -2467,11 +2479,11 @@ class PolyTrack extends Track { }; } - function computeDistance(point1, point2) { + function computeDistance(point1: Point2D, point2: Point2D): number { return Math.sqrt((point1.x - point2.x) ** 2 + (point1.y - point2.y) ** 2); } - function minimizeSegment(baseLength, N, startInterpolated, stopInterpolated) { + function minimizeSegment(baseLength: number, N: number, startInterpolated, stopInterpolated) { const threshold = baseLength / (2 * N); const minimized = [interpolatedPoints[startInterpolated]]; let latestPushed = startInterpolated; @@ -2626,7 +2638,7 @@ export class PolygonTrack extends PolyTrack { } } - interpolatePosition(leftPosition, rightPosition, offset) { + protected interpolatePosition(leftPosition, rightPosition, offset): InterpolatedPosition { const copyLeft = { ...leftPosition, points: [...leftPosition.points, leftPosition.points[0], leftPosition.points[1]], @@ -2665,7 +2677,7 @@ export class PointsTrack extends PolyTrack { } } - interpolatePosition(leftPosition, rightPosition, offset) { + protected interpolatePosition(leftPosition, rightPosition, offset): InterpolatedPosition { // interpolate only when one point in both left and right positions if (leftPosition.points.length === 2 && rightPosition.points.length === 2) { return { @@ -2700,7 +2712,7 @@ export class CuboidTrack extends Track { } } - interpolatePosition(leftPosition, rightPosition, offset) { + protected interpolatePosition(leftPosition, rightPosition, offset): InterpolatedPosition { const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point); return { @@ -2742,7 +2754,7 @@ export class SkeletonTrack extends Track { )).sort((a: Annotation, b: Annotation) => a.label.id - b.label.id) as any as Track[]; } - updateServerID(body: RawTrackData): void { + public updateServerID(body: RawTrackData): void { Track.prototype.updateServerID.call(this, body); for (const element of body.elements) { const thisElement = this.elements.find((_element: Track) => _element.label.id === element.label_id); @@ -2750,14 +2762,14 @@ export class SkeletonTrack extends Track { } } - clearServerID(): void { + public clearServerID(): void { Track.prototype.clearServerID.call(this); for (const element of this.elements) { element.clearServerID(); } } - _saveRotation(rotation: number, frame: number): void { + protected saveRotation(rotation: number, frame: number): void { const undoSkeletonShapes = this.elements.map((element) => element.shapes[frame]); const undoSource = this.source; const redoSource = this.readOnlyFields.includes('source') ? this.source : Source.MANUAL; @@ -2822,13 +2834,13 @@ export class SkeletonTrack extends Track { } // Method is used to export data to the server - toJSON(): RawTrackData { + public toJSON(): RawTrackData { const result: RawTrackData = Track.prototype.toJSON.call(this); result.elements = this.elements.map((el) => el.toJSON()); return result; } - get(frame: number) { + public get(frame: number): Omit, 'parentID'> { const { prev, next } = this.boundedKeyframes(frame); const position = this.getPosition(frame, prev, next); const elements = this.elements.map((element) => ({ @@ -2861,12 +2873,12 @@ export class SkeletonTrack extends Track { occluded: elements.every((el) => el.occluded), lock: elements.every((el) => el.lock), hidden: elements.every((el) => el.hidden), - ...this._withContext(frame), + ...this.withContext(frame), }; } // finds keyframes considering keyframes of nested elements - deepBoundedKeyframes(targetFrame: number): ObjectState['keyframes'] { + private deepBoundedKeyframes(targetFrame: number): ObjectState['keyframes'] { const boundedKeyframes = Track.prototype.boundedKeyframes.call(this, targetFrame); for (const element of this.elements) { @@ -2896,7 +2908,7 @@ export class SkeletonTrack extends Track { return boundedKeyframes; } - save(frame: number, data: ObjectState): ObjectState { + public save(frame: number, data: ObjectState): ObjectState { if (this.lock && data.lock) { return new ObjectState(this.get(frame)); } @@ -2994,8 +3006,8 @@ export class SkeletonTrack extends Track { if (updatedKeyframe.length) { updatedKeyframe.forEach((el) => { el.updateFlags.keyframe = true; }); // todo: fix extra undo/redo change - this._validateStateBeforeSave(frame, data, data.updateFlags); - this._saveKeyframe(frame, data.keyframe); + this.validateStateBeforeSave(data, data.updateFlags, frame); + this.saveKeyframe(frame, data.keyframe); data.updateFlags.keyframe = false; updateElements(updatedKeyframe, HistoryActions.CHANGED_KEYFRAME); } @@ -3014,7 +3026,9 @@ export class SkeletonTrack extends Track { return result; } - getPosition(targetFrame: number, leftKeyframe: number | null, rightKeyframe: number | null) { + protected getPosition( + targetFrame: number, leftKeyframe: number | null, rightKeyframe: number | null, + ): Omit & { keyframe: boolean } { const leftFrame = targetFrame in this.shapes ? targetFrame : leftKeyframe; const rightPosition = Number.isInteger(rightKeyframe) ? this.shapes[rightKeyframe] : null; const leftPosition = Number.isInteger(leftFrame) ? this.shapes[leftFrame] : null; @@ -3032,7 +3046,6 @@ export class SkeletonTrack extends Track { const singlePosition = leftPosition || rightPosition; if (singlePosition) { return { - points: undefined, rotation: 0, occluded: singlePosition.occluded, zOrder: singlePosition.zOrder, @@ -3080,6 +3093,9 @@ export function shapeFactory(data: RawShapeData, clientID: number, injection: An case ShapeType.CUBOID: shapeModel = new CuboidShape(data, clientID, color, injection); break; + case ShapeType.MASK: + shapeModel = new MaskShape(data, clientID, color, injection); + break; case ShapeType.SKELETON: shapeModel = new SkeletonShape(data, clientID, color, injection); break; diff --git a/cvat-core/src/annotations.ts b/cvat-core/src/annotations.ts index 046ae0a1ece..33b5b058231 100644 --- a/cvat-core/src/annotations.ts +++ b/cvat-core/src/annotations.ts @@ -62,7 +62,7 @@ async function getAnnotationsFromServer(session) { } } -export async function closeSession(session) { +export async function clearCache(session) { const sessionType = session instanceof Task ? 'task' : 'job'; const cache = getCache(sessionType); @@ -284,29 +284,57 @@ export function importDataset( useDefaultSettings: boolean, sourceStorage: Storage, file: File | string, - updateStatusCallback = () => {}, -) { + options: { + convMaskToPoly?: boolean, + updateStatusCallback?: (s: string, n: number) => void, + } = {}, +): Promise { + const updateStatusCallback = options.updateStatusCallback || (() => {}); + const convMaskToPoly = 'convMaskToPoly' in options ? options.convMaskToPoly : true; + const adjustedOptions = { + updateStatusCallback, + convMaskToPoly, + }; + if (!(instance instanceof Project || instance instanceof Task || instance instanceof Job)) { - throw new ArgumentError('Instance should be a Project || Task || Job instance'); + throw new ArgumentError('Instance must be a Project || Task || Job instance'); } if (!(typeof updateStatusCallback === 'function')) { - throw new ArgumentError('Callback should be a function'); + throw new ArgumentError('Callback must be a function'); + } + if (!(typeof convMaskToPoly === 'boolean')) { + throw new ArgumentError('Option "convMaskToPoly" must be a boolean'); } if (typeof file === 'string' && !file.toLowerCase().endsWith('.zip')) { - throw new ArgumentError('File should be file instance with ZIP extension'); + throw new ArgumentError('File must be file instance with ZIP extension'); } if (file instanceof File && !(['application/zip', 'application/x-zip-compressed'].includes(file.type))) { - throw new ArgumentError('File should be file instance with ZIP extension'); + throw new ArgumentError('File must be file instance with ZIP extension'); } if (instance instanceof Project) { return serverProxy.projects - .importDataset(instance.id, format, useDefaultSettings, sourceStorage, file, updateStatusCallback); + .importDataset( + instance.id, + format, + useDefaultSettings, + sourceStorage, + file, + adjustedOptions, + ); } const instanceType = instance instanceof Task ? 'task' : 'job'; return serverProxy.annotations - .uploadAnnotations(instanceType, instance.id, format, useDefaultSettings, sourceStorage, file); + .uploadAnnotations( + instanceType, + instance.id, + format, + useDefaultSettings, + sourceStorage, + file, + adjustedOptions, + ); } export function getHistory(session) { diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 5f6dd839da8..7f6a0e5db3d 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -8,7 +8,7 @@ const config = require('./config').default; (() => { const PluginRegistry = require('./plugins').default; const serverProxy = require('./server-proxy').default; - const lambdaManager = require('./lambda-manager'); + const lambdaManager = require('./lambda-manager').default; const { isBoolean, isInteger, diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index b9d58d3ff37..ccd216ae207 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -13,14 +13,14 @@ function build() { const loggerStorage = require('./logger-storage').default; const { Log } = require('./log'); const ObjectState = require('./object-state').default; - const Statistics = require('./statistics'); + const Statistics = require('./statistics').default; const Comment = require('./comment').default; const Issue = require('./issue').default; const { Job, Task } = require('./session'); const Project = require('./project').default; const implementProject = require('./project-implementation').default; const { Attribute, Label } = require('./labels'); - const MLModel = require('./ml-model'); + const MLModel = require('./ml-model').default; const { FrameData } = require('./frames'); const CloudStorage = require('./cloud-storage').default; const Organization = require('./organization').default; @@ -703,6 +703,9 @@ function build() { * @memberof module:API.cvat.config * @property {number} uploadChunkSize max size of one data request in mb * @memberof module:API.cvat.config + * @property {number} removeUnderlyingMaskPixels defines if after adding/changing + * a mask it should remove overlapped pixels from other objects + * @memberof module:API.cvat.config */ get backendAPI() { return config.backendAPI; @@ -728,6 +731,12 @@ function build() { set uploadChunkSize(value) { config.uploadChunkSize = value; }, + get removeUnderlyingMaskPixels(): boolean { + return config.removeUnderlyingMaskPixels; + }, + set removeUnderlyingMaskPixels(value: boolean) { + config.removeUnderlyingMaskPixels = value; + }, }, /** * Namespace contains some library information e.g. api version diff --git a/cvat-core/src/config.ts b/cvat-core/src/config.ts index 70dfbaac813..b44d136295b 100644 --- a/cvat-core/src/config.ts +++ b/cvat-core/src/config.ts @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -8,6 +9,7 @@ const config = { organizationID: null, origin: '', uploadChunkSize: 100, + removeUnderlyingMaskPixels: false, }; export default config; diff --git a/cvat-core/src/enums.ts b/cvat-core/src/enums.ts index a2a4647100e..bfa114ab1ee 100644 --- a/cvat-core/src/enums.ts +++ b/cvat-core/src/enums.ts @@ -172,6 +172,7 @@ export enum ShapeType { ELLIPSE = 'ellipse', CUBOID = 'cuboid', SKELETON = 'skeleton', + MASK = 'mask', } /** diff --git a/cvat-core/src/exceptions.ts b/cvat-core/src/exceptions.ts index 88eaee74e91..ba40b0a8e6f 100644 --- a/cvat-core/src/exceptions.ts +++ b/cvat-core/src/exceptions.ts @@ -1,10 +1,10 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import Platform from 'platform'; import ErrorStackParser from 'error-stack-parser'; -// import config from './config'; /** * Base exception class diff --git a/cvat-core/src/frames.ts b/cvat-core/src/frames.ts index 048c1fe29ba..0a4f6954063 100644 --- a/cvat-core/src/frames.ts +++ b/cvat-core/src/frames.ts @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -740,19 +741,19 @@ return frameDataCache[jobID].frameBuffer.require(frame, jobID, isPlaying, step); } - async function getDeletedFrames(sessionType, id) { - if (sessionType === 'job') { + async function getDeletedFrames(instanceType, id) { + if (instanceType === 'job') { const { meta } = frameDataCache[id]; return meta.deleted_frames; } - if (sessionType === 'task') { + if (instanceType === 'task') { const meta = await serverProxy.frames.getMeta('job', id); meta.deleted_frames = Object.fromEntries(meta.deleted_frames.map((_frame) => [_frame, true])); return meta; } - throw Exception('getDeletedFrames is not implemented for tasks'); + throw new Exception(`getDeletedFrames is not implemented for ${instanceType}`); } function deleteFrame(jobID, frame) { diff --git a/cvat-core/src/labels.ts b/cvat-core/src/labels.ts index 5ffe7a1809f..1e609f760ed 100644 --- a/cvat-core/src/labels.ts +++ b/cvat-core/src/labels.ts @@ -139,7 +139,7 @@ export class Attribute { } } -type LabelType = 'rectangle' | 'polygon' | 'polyline' | 'points' | 'ellipse' | 'cuboid' | 'skeleton' | 'any'; +type LabelType = 'rectangle' | 'polygon' | 'polyline' | 'points' | 'ellipse' | 'cuboid' | 'skeleton' | 'mask' | 'tag' | 'any'; export interface RawLabel { id?: number; name: string; diff --git a/cvat-core/src/lambda-manager.ts b/cvat-core/src/lambda-manager.ts index 51a8571078c..184723ab08f 100644 --- a/cvat-core/src/lambda-manager.ts +++ b/cvat-core/src/lambda-manager.ts @@ -1,19 +1,23 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -const serverProxy = require('./server-proxy').default; -const { ArgumentError } = require('./exceptions'); -const MLModel = require('./ml-model'); -const { RQStatus } = require('./enums'); +import serverProxy from './server-proxy'; +import { ArgumentError } from './exceptions'; +import MLModel from './ml-model'; +import { RQStatus } from './enums'; class LambdaManager { + private listening: any; + private cachedList: any; + constructor() { this.listening = {}; this.cachedList = null; } - async list() { + async list(): Promise { if (Array.isArray(this.cachedList)) { return [...this.cachedList]; } @@ -34,7 +38,7 @@ class LambdaManager { return models; } - async run(taskID, model, args) { + async run(taskID: number, model: MLModel, args: any) { if (!Number.isInteger(taskID) || taskID < 0) { throw new ArgumentError(`Argument taskID must be a positive integer. Got "${taskID}"`); } @@ -78,7 +82,7 @@ class LambdaManager { return result.filter((request) => ['queued', 'started'].includes(request.status)); } - async cancel(requestID) { + async cancel(requestID): Promise { if (typeof requestID !== 'string') { throw new ArgumentError(`Request id argument is required to be a string. But got ${requestID}`); } @@ -90,8 +94,8 @@ class LambdaManager { await serverProxy.lambda.cancel(requestID); } - async listen(requestID, onUpdate) { - const timeoutCallback = async () => { + async listen(requestID, onUpdate): Promise { + const timeoutCallback = async (): Promise => { try { this.listening[requestID].timeout = null; const response = await serverProxy.lambda.status(requestID); @@ -124,4 +128,4 @@ class LambdaManager { } } -module.exports = new LambdaManager(); +export default new LambdaManager(); diff --git a/cvat-core/src/ml-model.ts b/cvat-core/src/ml-model.ts index 68e29bc4a3a..13cdd102bc7 100644 --- a/cvat-core/src/ml-model.ts +++ b/cvat-core/src/ml-model.ts @@ -1,134 +1,111 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -/** - * Class representing a serverless function - * @memberof module:API.cvat.classes - */ -class MLModel { - constructor(data) { - this._id = data.id; - this._name = data.name; - this._labels = data.labels; - this._attributes = data.attributes || []; - 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, - minNegVertices: data.min_neg_points, - startWithBox: data.startswith_box, - }, - }; +import { ModelType } from './enums'; + +interface ModelAttribute { + name: string; + values: string[]; + input_type: 'select' | 'number' | 'checkbox' | 'radio' | 'text'; +} + +interface ModelParams { + canvas: { + minPosVertices?: number; + minNegVertices?: number; + startWithBox?: boolean; + onChangeToolsBlockerState?: (event: string) => void; + }; +} + +interface ModelTip { + message: string; + gif: string; +} + +interface SerializedModel { + id: string; + name: string; + labels: string[]; + version: number; + attributes: Record; + framework: string; + description: string; + type: ModelType; + help_message?: string; + animated_gif?: string; + min_pos_points?: number; + min_neg_points?: number; + startswith_box?: boolean; +} + +export default class MLModel { + private serialized: SerializedModel; + private changeToolsBlockerStateCallback?: (event: string) => void; + + constructor(serialized: SerializedModel) { + this.serialized = { ...serialized }; } - /** - * @type {string} - * @readonly - */ - get id() { - return this._id; + public get id(): string { + return this.serialized.id; } - /** - * @type {string} - * @readonly - */ - get name() { - return this._name; + public get name(): string { + return this.serialized.name; } - /** - * @description labels supported by the model - * @type {string[]} - * @readonly - */ - get labels() { - if (Array.isArray(this._labels)) { - return [...this._labels]; - } + public get labels(): string[] { + return Array.isArray(this.serialized.labels) ? [...this.serialized.labels] : []; + } - return []; + public get version(): number { + return this.serialized.version; } - /** - * @typedef ModelAttribute - * @property {string} name - * @property {string[]} values - * @property {'select'|'number'|'checkbox'|'radio'|'text'} input_type - */ - /** - * @type {Object} - * @readonly - */ - get attributes() { - return { ...this._attributes }; + public get attributes(): Record { + return this.serialized.attributes || {}; } - /** - * @type {string} - * @readonly - */ - get framework() { - return this._framework; + public get framework(): string { + return this.serialized.framework; } - /** - * @type {string} - * @readonly - */ - get description() { - return this._description; + public get description(): string { + return this.serialized.description; } - /** - * @type {module:API.cvat.enums.ModelType} - * @readonly - */ - get type() { - return this._type; + public get type(): ModelType { + return this.serialized.type; } - /** - * @type {object} - * @readonly - */ - get params() { - return { - canvas: { ...this._params.canvas }, + public get params(): ModelParams { + const result: ModelParams = { + canvas: { + minPosVertices: this.serialized.min_pos_points, + minNegVertices: this.serialized.min_neg_points, + startWithBox: this.serialized.startswith_box, + }, }; + + if (this.changeToolsBlockerStateCallback) { + result.canvas.onChangeToolsBlockerState = this.changeToolsBlockerStateCallback; + } + + return result; } - /** - * @type {MlModelTip} - * @property {string} message A short message for a user about the model - * @property {string} gif A gif URL to be shown to a user as an example - * @readonly - */ - get tip() { - return { ...this._tip }; + public get tip(): ModelTip { + return { + message: this.serialized.help_message, + gif: this.serialized.animated_gif, + }; } - /** - * @typedef onRequestStatusChange - * @param {string} event - * @global - */ - /** - * @param {onRequestStatusChange} onRequestStatusChange - * @instance - * @description Used to set a callback when the tool is blocked in UI - * @returns {void} - */ - set onChangeToolsBlockerState(onChangeToolsBlockerState) { - this._params.canvas.onChangeToolsBlockerState = onChangeToolsBlockerState; + // Used to set a callback when the tool is blocked in UI + public set onChangeToolsBlockerState(onChangeToolsBlockerState: (event: string) => void) { + this.changeToolsBlockerStateCallback = onChangeToolsBlockerState; } } - -module.exports = MLModel; diff --git a/cvat-core/src/object-state.ts b/cvat-core/src/object-state.ts index 0814c8d9573..958c4103b34 100644 --- a/cvat-core/src/object-state.ts +++ b/cvat-core/src/object-state.ts @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -26,7 +27,7 @@ interface UpdateFlags { reset: () => void; } -interface SerializedData { +export interface SerializedData { objectType: ObjectType; label: Label; frame: number; @@ -333,7 +334,7 @@ export default class ObjectState { } if (Array.isArray(data.points)) { - return [...data.points]; + return data.points; } return []; @@ -365,7 +366,7 @@ export default class ObjectState { data.updateFlags.points = true; } - data.points = [...points]; + data.points = points.slice(); }, }, rotation: { diff --git a/cvat-core/src/object-utils.ts b/cvat-core/src/object-utils.ts new file mode 100644 index 00000000000..46ee961b4f6 --- /dev/null +++ b/cvat-core/src/object-utils.ts @@ -0,0 +1,279 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { DataError, ArgumentError } from './exceptions'; +import { Attribute } from './labels'; +import { ShapeType, AttributeType } from './enums'; + +export function checkNumberOfPoints(shapeType: ShapeType, points: number[]): void { + if (shapeType === ShapeType.RECTANGLE) { + if (points.length / 2 !== 2) { + throw new DataError(`Rectangle must have 2 points, but got ${points.length / 2}`); + } + } else if (shapeType === ShapeType.POLYGON) { + if (points.length / 2 < 3) { + throw new DataError(`Polygon must have at least 3 points, but got ${points.length / 2}`); + } + } else if (shapeType === ShapeType.POLYLINE) { + if (points.length / 2 < 2) { + throw new DataError(`Polyline must have at least 2 points, but got ${points.length / 2}`); + } + } else if (shapeType === ShapeType.POINTS) { + if (points.length / 2 < 1) { + throw new DataError(`Points must have at least 1 points, but got ${points.length / 2}`); + } + } else if (shapeType === ShapeType.CUBOID) { + if (points.length / 2 !== 8) { + throw new DataError(`Cuboid must have 8 points, but got ${points.length / 2}`); + } + } else if (shapeType === ShapeType.ELLIPSE) { + if (points.length / 2 !== 2) { + throw new DataError(`Ellipse must have 1 point, rx and ry but got ${points.toString()}`); + } + } else if (shapeType === ShapeType.MASK) { + const [left, top, right, bottom] = points.slice(-4); + const [width, height] = [right - left, bottom - top]; + if (width < 0 || !Number.isInteger(width) || height < 0 || !Number.isInteger(height)) { + throw new DataError(`Mask width, height must be positive integers, but got ${width}x${height}`); + } + + if (points.length !== width * height + 4) { + throw new DataError(`Points array must have length ${width}x${height} + 4, got ${points.length}`); + } + } else { + throw new ArgumentError(`Unknown value of shapeType has been received ${shapeType}`); + } +} + +export function attrsAsAnObject(attributes: Attribute[]): Record { + return attributes.reduce((accumulator, value) => { + accumulator[value.id] = value; + return accumulator; + }, {}); +} + +export function findAngleDiff(rightAngle: number, leftAngle: number): number { + let angleDiff = rightAngle - leftAngle; + angleDiff = ((angleDiff + 180) % 360) - 180; + if (Math.abs(angleDiff) >= 180) { + // if the main arc is bigger than 180, go another arc + // to find it, just substract absolute value from 360 and inverse sign + angleDiff = 360 - Math.abs(angleDiff) * Math.sign(angleDiff) * -1; + } + return angleDiff; +} + +export function checkShapeArea(shapeType: ShapeType, points: number[]): boolean { + const MIN_SHAPE_LENGTH = 3; + const MIN_SHAPE_AREA = 9; + const MIN_MASK_SHAPE_AREA = 1; + + if (shapeType === ShapeType.POINTS) { + return true; + } + + if (shapeType === ShapeType.MASK) { + const [left, top, right, bottom] = points.slice(-4); + const area = (right - left + 1) * (bottom - top + 1); + return area >= MIN_MASK_SHAPE_AREA; + } + + if (shapeType === ShapeType.ELLIPSE) { + const [cx, cy, rightX, topY] = points; + const [rx, ry] = [rightX - cx, cy - topY]; + return rx * ry * Math.PI > MIN_SHAPE_AREA; + } + + let xmin = Number.MAX_SAFE_INTEGER; + let xmax = Number.MIN_SAFE_INTEGER; + let ymin = Number.MAX_SAFE_INTEGER; + let ymax = Number.MIN_SAFE_INTEGER; + + for (let i = 0; i < points.length - 1; i += 2) { + xmin = Math.min(xmin, points[i]); + xmax = Math.max(xmax, points[i]); + ymin = Math.min(ymin, points[i + 1]); + ymax = Math.max(ymax, points[i + 1]); + } + + if (shapeType === ShapeType.POLYLINE) { + const length = Math.max(xmax - xmin, ymax - ymin); + return length >= MIN_SHAPE_LENGTH; + } + + const area = (xmax - xmin) * (ymax - ymin); + return area >= MIN_SHAPE_AREA; +} + +export function rotatePoint(x: number, y: number, angle: number, cx = 0, cy = 0): number[] { + const sin = Math.sin((angle * Math.PI) / 180); + const cos = Math.cos((angle * Math.PI) / 180); + const rotX = (x - cx) * cos - (y - cy) * sin + cx; + const rotY = (y - cy) * cos + (x - cx) * sin + cy; + return [rotX, rotY]; +} + +export function computeWrappingBox(points: number[], margin = 0): { + xtl: number; + ytl: number; + xbr: number; + ybr: number; + x: number; + y: number; + width: number; + height: number; +} { + let xtl = Number.MAX_SAFE_INTEGER; + let ytl = Number.MAX_SAFE_INTEGER; + let xbr = Number.MIN_SAFE_INTEGER; + let ybr = Number.MIN_SAFE_INTEGER; + + for (let i = 0; i < points.length; i += 2) { + const [x, y] = [points[i], points[i + 1]]; + xtl = Math.min(xtl, x); + ytl = Math.min(ytl, y); + xbr = Math.max(xbr, x); + ybr = Math.max(ybr, y); + } + + const box = { + xtl: xtl - margin, + ytl: ytl - margin, + xbr: xbr + margin, + ybr: ybr + margin, + }; + + return { + ...box, + x: box.xtl, + y: box.ytl, + width: box.xbr - box.xtl, + height: box.ybr - box.ytl, + }; +} + +export function validateAttributeValue(value: string, attr: Attribute): boolean { + const { values } = attr; + const type = attr.inputType; + + if (typeof value !== 'string') { + throw new ArgumentError(`Attribute value is expected to be string, but got ${typeof value}`); + } + + if (type === AttributeType.NUMBER) { + return +value >= +values[0] && +value <= +values[1]; + } + + if (type === AttributeType.CHECKBOX) { + return ['true', 'false'].includes(value.toLowerCase()); + } + + if (type === AttributeType.TEXT) { + return true; + } + + return values.includes(value); +} + +export function truncateMask(points: number[], _: number, width: number, height: number): number[] { + const [currentLeft, currentTop, currentRight, currentBottom] = points.slice(-4); + const [currentWidth, currentHeight] = [currentRight - currentLeft + 1, currentBottom - currentTop + 1]; + + let left = width; + let right = 0; + let top = height; + let bottom = 0; + let atLeastOnePixel = false; + const truncatedPoints = []; + + for (let y = 0; y < currentHeight; y++) { + const absY = y + currentTop; + + for (let x = 0; x < currentWidth; x++) { + const absX = x + currentLeft; + const offset = y * currentWidth + x; + + if (absX >= width || absY >= height || absX < 0 || absY < 0) { + points[offset] = 0; + } + + if (points[offset]) { + atLeastOnePixel = true; + left = Math.min(left, absX); + top = Math.min(top, absY); + right = Math.max(right, absX); + bottom = Math.max(bottom, absY); + } + } + } + + if (!atLeastOnePixel) { + // if mask is empty, set its size as 0 + left = 0; + top = 0; + } + + // TODO: check corner case when right = left = 0 + const [newWidth, newHeight] = [right - left + 1, bottom - top + 1]; + for (let y = 0; y < newHeight; y++) { + for (let x = 0; x < newWidth; x++) { + const leftDiff = left - currentLeft; + const topDiff = top - currentTop; + const offset = (y + topDiff) * currentWidth + (x + leftDiff); + truncatedPoints.push(points[offset]); + } + } + + truncatedPoints.push(left, top, right, bottom); + if (!checkShapeArea(ShapeType.MASK, truncatedPoints)) { + return []; + } + + return truncatedPoints; +} + +export function mask2Rle(mask: number[]): number[] { + return mask.reduce((acc, val, idx, arr) => { + if (idx > 0) { + if (arr[idx - 1] === val) { + acc[acc.length - 1] += 1; + } else { + acc.push(1); + } + + return acc; + } + + if (val > 0) { + // 0, 0, 0, 1 => [3, 1] + // 1, 1, 0, 0 => [0, 2, 2] + acc.push(0, 1); + } else { + acc.push(1); + } + + return acc; + }, []); +} + +export function rle2Mask(rle: number[], width: number, height: number): number[] { + const decoded = Array(width * height).fill(0); + const { length } = rle; + let decodedIdx = 0; + let value = 0; + let i = 0; + + while (i < length) { + let count = rle[i]; + while (count > 0) { + decoded[decodedIdx] = value; + decodedIdx++; + count--; + } + i++; + value = Math.abs(value - 1); + } + + return decoded; +} diff --git a/cvat-core/src/project-implementation.ts b/cvat-core/src/project-implementation.ts index efef65a74f3..bfdbdbe85f9 100644 --- a/cvat-core/src/project-implementation.ts +++ b/cvat-core/src/project-implementation.ts @@ -85,9 +85,12 @@ export default function implementProject(projectClass) { useDefaultSettings: boolean, sourceStorage: Storage, file: File | string, - updateStatusCallback, + options?: { + convMaskToPoly?: boolean, + updateStatusCallback?: (s: string, n: number) => void, + }, ) { - return importDataset(this, format, useDefaultSettings, sourceStorage, file, updateStatusCallback); + return importDataset(this, format, useDefaultSettings, sourceStorage, file, options); }; projectClass.prototype.backup.implementation = async function ( diff --git a/cvat-core/src/project.ts b/cvat-core/src/project.ts index f50ddbe8e6f..de10e7c4f0d 100644 --- a/cvat-core/src/project.ts +++ b/cvat-core/src/project.ts @@ -409,7 +409,10 @@ Object.defineProperties( useDefaultSettings: boolean, sourceStorage: Storage, file: File | string, - updateStatusCallback = null, + options?: { + convMaskToPoly?: boolean, + updateStatusCallback?: (s: string, n: number) => void, + }, ) { const result = await PluginRegistry.apiWrapper.call( this, @@ -418,7 +421,7 @@ Object.defineProperties( useDefaultSettings, sourceStorage, file, - updateStatusCallback, + options, ); return result; }, diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index aef9961f89f..5e163160b92 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -684,14 +684,18 @@ class ServerProxy { useDefaultLocation: boolean, sourceStorage: Storage, file: File | string, - onUpdate, - ) { + options: { + convMaskToPoly: boolean, + updateStatusCallback: (s: string, n: number) => void, + }, + ): Promise { const { backendAPI, origin } = config; - const params: Params = { + const params: Params & { conv_mask_to_poly: boolean } = { ...enableOrganization(), ...configureStorage(sourceStorage, useDefaultLocation), format, filename: typeof file === 'string' ? file : file.name, + conv_mask_to_poly: options.convMaskToPoly, }; const url = `${backendAPI}/projects/${id}/dataset`; @@ -705,8 +709,8 @@ class ServerProxy { proxy: config.proxy, }); if (response.status === 202) { - if (onUpdate && response.data.message) { - onUpdate(response.data.message, response.data.progress || 0); + if (response.data.message) { + options.updateStatusCallback(response.data.message, response.data.progress || 0); } setTimeout(requestStatus, 3000); } else if (response.status === 201) { @@ -740,7 +744,7 @@ class ServerProxy { totalSentSize: 0, totalSize: (file as File).size, onUpdate: (percentage) => { - onUpdate('The dataset is being uploaded to the server', percentage); + options.updateStatusCallback('The dataset is being uploaded to the server', percentage); }, }; @@ -1449,13 +1453,15 @@ class ServerProxy { useDefaultLocation: boolean, sourceStorage: Storage, file: File | string, - ) { + options: { convMaskToPoly: boolean }, + ): Promise { const { backendAPI, origin } = config; - const params: Params = { + const params: Params & { conv_mask_to_poly: boolean } = { ...enableOrganization(), ...configureStorage(sourceStorage, useDefaultLocation), format, filename: typeof file === 'string' ? file : file.name, + conv_mask_to_poly: options.convMaskToPoly, }; const url = `${backendAPI}/${session}s/${id}/annotations`; diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index 262fd8476b1..8c7362ac0f8 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -34,7 +34,13 @@ function buildDuplicatedAPI(prototype) { Object.defineProperties(prototype, { annotations: Object.freeze({ value: { - async upload(format: string, useDefaultLocation: boolean, sourceStorage: Storage, file: File | string) { + async upload( + format: string, + useDefaultLocation: boolean, + sourceStorage: Storage, + file: File | string, + options?: { convMaskToPoly?: boolean }, + ) { const result = await PluginRegistry.apiWrapper.call( this, prototype.annotations.upload, @@ -42,6 +48,7 @@ function buildDuplicatedAPI(prototype) { useDefaultLocation, sourceStorage, file, + options, ); return result; }, @@ -1939,819 +1946,811 @@ export class Task extends Session { } } -const { - getAnnotations, - putAnnotations, - saveAnnotations, - hasUnsavedChanges, - searchAnnotations, - searchEmptyFrame, - mergeAnnotations, - splitAnnotations, - groupAnnotations, - clearAnnotations, - selectObject, - annotationsStatistics, - importCollection, - exportCollection, - importDataset, - exportDataset, - undoActions, - redoActions, - freezeHistory, - clearActions, - getActions, - closeSession, - getHistory, -} = require('./annotations'); - buildDuplicatedAPI(Job.prototype); buildDuplicatedAPI(Task.prototype); -Job.prototype.save.implementation = async function () { - if (this.id) { - const jobData = this._updateTrigger.getUpdated(this); - if (jobData.assignee) { - jobData.assignee = jobData.assignee.id; +(async () => { + const annotations = await import('./annotations'); + const { + getAnnotations, putAnnotations, saveAnnotations, + hasUnsavedChanges, searchAnnotations, searchEmptyFrame, + mergeAnnotations, splitAnnotations, groupAnnotations, + clearAnnotations, selectObject, annotationsStatistics, + importCollection, exportCollection, importDataset, + exportDataset, undoActions, redoActions, + freezeHistory, clearActions, getActions, + clearCache, getHistory, + } = annotations; + + + Job.prototype.save.implementation = async function () { + if (this.id) { + const jobData = this._updateTrigger.getUpdated(this); + if (jobData.assignee) { + jobData.assignee = jobData.assignee.id; + } + + const data = await serverProxy.jobs.save(this.id, jobData); + this._updateTrigger.reset(); + return new Job(data); } - const data = await serverProxy.jobs.save(this.id, jobData); - this._updateTrigger.reset(); - return new Job(data); - } + throw new ArgumentError('Could not save job without id'); + }; - throw new ArgumentError('Could not save job without id'); -}; + Job.prototype.issues.implementation = async function () { + const result = await serverProxy.issues.get(this.id); + return result.map((issue) => new Issue(issue)); + }; -Job.prototype.issues.implementation = async function () { - const result = await serverProxy.issues.get(this.id); - return result.map((issue) => new Issue(issue)); -}; + Job.prototype.openIssue.implementation = async function (issue, message) { + checkObjectType('issue', issue, null, Issue); + checkObjectType('message', message, 'string'); + const result = await serverProxy.issues.create({ + ...issue.serialize(), + message, + }); + return new Issue(result); + }; -Job.prototype.openIssue.implementation = async function (issue, message) { - checkObjectType('issue', issue, null, Issue); - checkObjectType('message', message, 'string'); - const result = await serverProxy.issues.create({ - ...issue.serialize(), - message, - }); - return new Issue(result); -}; + Job.prototype.frames.get.implementation = async function (frame, isPlaying, step) { + if (!Number.isInteger(frame) || frame < 0) { + throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); + } + + if (frame < this.startFrame || frame > this.stopFrame) { + throw new ArgumentError(`The frame with number ${frame} is out of the job`); + } + + const frameData = await getFrame( + this.id, + this.dataChunkSize, + this.dataChunkType, + this.mode, + frame, + this.startFrame, + this.stopFrame, + isPlaying, + step, + this.dimension, + ); + return frameData; + }; -Job.prototype.frames.get.implementation = async function (frame, isPlaying, step) { - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); + // must be called with task/job context + async function deleteFrameWrapper(jobID, frame) { + const history = getHistory(this); + const redo = async () => { + deleteFrame(jobID, frame); + }; + + await redo(); + history.do(HistoryActions.REMOVED_FRAME, async () => { + restoreFrame(jobID, frame); + }, redo, [], frame); } - if (frame < this.startFrame || frame > this.stopFrame) { - throw new ArgumentError(`The frame with number ${frame} is out of the job`); + async function restoreFrameWrapper(jobID, frame) { + const history = getHistory(this); + const redo = async () => { + restoreFrame(jobID, frame); + }; + + await redo(); + history.do(HistoryActions.RESTORED_FRAME, async () => { + deleteFrame(jobID, frame); + }, redo, [], frame); } - const frameData = await getFrame( - this.id, - this.dataChunkSize, - this.dataChunkType, - this.mode, - frame, - this.startFrame, - this.stopFrame, - isPlaying, - step, - this.dimension, - ); - return frameData; -}; - -// must be called with task/job context -async function deleteFrameWrapper(jobID, frame) { - const history = getHistory(this); - const redo = async () => { - deleteFrame(jobID, frame); - }; - - await redo(); - history.do(HistoryActions.REMOVED_FRAME, async () => { - restoreFrame(jobID, frame); - }, redo, [], frame); -} + Job.prototype.frames.delete.implementation = async function (frame) { + if (!Number.isInteger(frame)) { + throw new Error(`Frame must be an integer. Got: "${frame}"`); + } -async function restoreFrameWrapper(jobID, frame) { - const history = getHistory(this); - const redo = async () => { - restoreFrame(jobID, frame); + if (frame < this.startFrame || frame > this.stopFrame) { + throw new Error('The frame is out of the job'); + } + + await deleteFrameWrapper.call(this, this.id, frame); }; - await redo(); - history.do(HistoryActions.RESTORED_FRAME, async () => { - deleteFrame(jobID, frame); - }, redo, [], frame); -} + Job.prototype.frames.restore.implementation = async function (frame) { + if (!Number.isInteger(frame)) { + throw new Error(`Frame must be an integer. Got: "${frame}"`); + } -Job.prototype.frames.delete.implementation = async function (frame) { - if (!Number.isInteger(frame)) { - throw new Error(`Frame must be an integer. Got: "${frame}"`); - } + if (frame < this.startFrame || frame > this.stopFrame) { + throw new Error('The frame is out of the job'); + } - if (frame < this.startFrame || frame > this.stopFrame) { - throw new Error('The frame is out of the job'); - } + await restoreFrameWrapper.call(this, this.id, frame); + }; - await deleteFrameWrapper.call(this, this.id, frame); -}; + Job.prototype.frames.save.implementation = async function () { + const result = await patchMeta(this.id); + return result; + }; -Job.prototype.frames.restore.implementation = async function (frame) { - if (!Number.isInteger(frame)) { - throw new Error(`Frame must be an integer. Got: "${frame}"`); - } + Job.prototype.frames.ranges.implementation = async function () { + const rangesData = await getRanges(this.id); + return rangesData; + }; - if (frame < this.startFrame || frame > this.stopFrame) { - throw new Error('The frame is out of the job'); - } + Job.prototype.frames.preview.implementation = async function () { + if (this.id === null || this.taskId === null) { + return ''; + } - await restoreFrameWrapper.call(this, this.id, frame); -}; + const frameData = await getPreview(this.taskId, this.id); + return frameData; + }; -Job.prototype.frames.save.implementation = async function () { - const result = await patchMeta(this.id); - return result; -}; + Job.prototype.frames.contextImage.implementation = async function (frameId) { + const result = await getContextImage(this.id, frameId); + return result; + }; -Job.prototype.frames.ranges.implementation = async function () { - const rangesData = await getRanges(this.id); - return rangesData; -}; + Job.prototype.frames.search.implementation = async function (filters, frameFrom, frameTo) { + if (typeof filters !== 'object') { + throw new ArgumentError('Filters should be an object'); + } -Job.prototype.frames.preview.implementation = async function () { - if (this.id === null || this.taskId === null) { - return ''; - } + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } - const frameData = await getPreview(this.taskId, this.id); - return frameData; -}; + if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { + throw new ArgumentError('The start frame is out of the job'); + } -Job.prototype.frames.contextImage.implementation = async function (frameId) { - const result = await getContextImage(this.id, frameId); - return result; -}; + if (frameTo < this.startFrame || frameTo > this.stopFrame) { + throw new ArgumentError('The stop frame is out of the job'); + } + if (filters.notDeleted) { + return findNotDeletedFrame(this.id, frameFrom, frameTo, filters.offset || 1); + } + return null; + }; -Job.prototype.frames.search.implementation = async function (filters, frameFrom, frameTo) { - if (typeof filters !== 'object') { - throw new ArgumentError('Filters should be an object'); - } + // TODO: Check filter for annotations + Job.prototype.annotations.get.implementation = async function (frame, allTracks, filters) { + if (!Array.isArray(filters)) { + throw new ArgumentError('Filters must be an array'); + } - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } + if (!Number.isInteger(frame)) { + throw new ArgumentError('The frame argument must be an integer'); + } - if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { - throw new ArgumentError('The start frame is out of the job'); - } + if (frame < this.startFrame || frame > this.stopFrame) { + throw new ArgumentError(`Frame ${frame} does not exist in the job`); + } - if (frameTo < this.startFrame || frameTo > this.stopFrame) { - throw new ArgumentError('The stop frame is out of the job'); - } - if (filters.notDeleted) { - return findNotDeletedFrame(this.id, frameFrom, frameTo, filters.offset || 1); - } - return null; -}; + const annotationsData = await getAnnotations(this, frame, allTracks, filters); + const deletedFrames = await getDeletedFrames('job', this.id); + if (frame in deletedFrames) { + return []; + } -// TODO: Check filter for annotations -Job.prototype.annotations.get.implementation = async function (frame, allTracks, filters) { - if (!Array.isArray(filters)) { - throw new ArgumentError('Filters must be an array'); - } + return annotationsData; + }; - if (!Number.isInteger(frame)) { - throw new ArgumentError('The frame argument must be an integer'); - } + Job.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) { + if (!Array.isArray(filters)) { + throw new ArgumentError('Filters must be an array'); + } - if (frame < this.startFrame || frame > this.stopFrame) { - throw new ArgumentError(`Frame ${frame} does not exist in the job`); - } + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } - const annotationsData = await getAnnotations(this, frame, allTracks, filters); - const deletedFrames = await getDeletedFrames('job', this.id); - if (frame in deletedFrames) { - return []; - } + if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { + throw new ArgumentError('The start frame is out of the job'); + } - return annotationsData; -}; + if (frameTo < this.startFrame || frameTo > this.stopFrame) { + throw new ArgumentError('The stop frame is out of the job'); + } -Job.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) { - if (!Array.isArray(filters)) { - throw new ArgumentError('Filters must be an array'); - } + const result = searchAnnotations(this, filters, frameFrom, frameTo); + return result; + }; - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } + Job.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) { + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } - if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { - throw new ArgumentError('The start frame is out of the job'); - } + if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { + throw new ArgumentError('The start frame is out of the job'); + } - if (frameTo < this.startFrame || frameTo > this.stopFrame) { - throw new ArgumentError('The stop frame is out of the job'); - } + if (frameTo < this.startFrame || frameTo > this.stopFrame) { + throw new ArgumentError('The stop frame is out of the job'); + } - const result = searchAnnotations(this, filters, frameFrom, frameTo); - return result; -}; + const result = searchEmptyFrame(this, frameFrom, frameTo); + return result; + }; -Job.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) { - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } + Job.prototype.annotations.save.implementation = async function (onUpdate) { + const result = await saveAnnotations(this, onUpdate); + return result; + }; - if (frameFrom < this.startFrame || frameFrom > this.stopFrame) { - throw new ArgumentError('The start frame is out of the job'); - } + Job.prototype.annotations.merge.implementation = async function (objectStates) { + const result = await mergeAnnotations(this, objectStates); + return result; + }; - if (frameTo < this.startFrame || frameTo > this.stopFrame) { - throw new ArgumentError('The stop frame is out of the job'); - } + Job.prototype.annotations.split.implementation = async function (objectState, frame) { + const result = await splitAnnotations(this, objectState, frame); + return result; + }; - const result = searchEmptyFrame(this, frameFrom, frameTo); - return result; -}; - -Job.prototype.annotations.save.implementation = async function (onUpdate) { - const result = await saveAnnotations(this, onUpdate); - return result; -}; - -Job.prototype.annotations.merge.implementation = async function (objectStates) { - const result = await mergeAnnotations(this, objectStates); - return result; -}; - -Job.prototype.annotations.split.implementation = async function (objectState, frame) { - const result = await splitAnnotations(this, objectState, frame); - return result; -}; - -Job.prototype.annotations.group.implementation = async function (objectStates, reset) { - const result = await groupAnnotations(this, objectStates, reset); - return result; -}; - -Job.prototype.annotations.hasUnsavedChanges.implementation = function () { - const result = hasUnsavedChanges(this); - return result; -}; - -Job.prototype.annotations.clear.implementation = async function ( - reload, startframe, endframe, delTrackKeyframesOnly, -) { - const result = await clearAnnotations(this, reload, startframe, endframe, delTrackKeyframesOnly); - return result; -}; - -Job.prototype.annotations.select.implementation = function (frame, x, y) { - const result = selectObject(this, frame, x, y); - return result; -}; - -Job.prototype.annotations.statistics.implementation = function () { - const result = annotationsStatistics(this); - return result; -}; - -Job.prototype.annotations.put.implementation = function (objectStates) { - const result = putAnnotations(this, objectStates); - return result; -}; - -Job.prototype.annotations.upload.implementation = async function ( - format: string, - useDefaultLocation: boolean, - sourceStorage: Storage, - file: File | string, -) { - const result = await importDataset(this, format, useDefaultLocation, sourceStorage, file); - return result; -}; - -Job.prototype.annotations.import.implementation = function (data) { - const result = importCollection(this, data); - return result; -}; - -Job.prototype.annotations.export.implementation = function () { - const result = exportCollection(this); - return result; -}; - -Job.prototype.annotations.exportDataset.implementation = async function ( - format: string, - saveImages: boolean, - useDefaultSettings: boolean, - targetStorage: Storage, - customName?: string, -) { - const result = await exportDataset(this, format, saveImages, useDefaultSettings, targetStorage, customName); - return result; -}; - -Job.prototype.actions.undo.implementation = async function (count) { - const result = await undoActions(this, count); - return result; -}; - -Job.prototype.actions.redo.implementation = async function (count) { - const result = await redoActions(this, count); - return result; -}; - -Job.prototype.actions.freeze.implementation = function (frozen) { - const result = freezeHistory(this, frozen); - return result; -}; - -Job.prototype.actions.clear.implementation = function () { - const result = clearActions(this); - return result; -}; - -Job.prototype.actions.get.implementation = function () { - const result = getActions(this); - return result; -}; - -Job.prototype.logger.log.implementation = async function (logType, payload, wait) { - const result = await loggerStorage.log(logType, { ...payload, task_id: this.taskId, job_id: this.id }, wait); - return result; -}; - -Job.prototype.predictor.status.implementation = async function () { - if (!Number.isInteger(this.projectId)) { - throw new DataError('The job must belong to a project to use the feature'); - } + Job.prototype.annotations.group.implementation = async function (objectStates, reset) { + const result = await groupAnnotations(this, objectStates, reset); + return result; + }; - const result = await serverProxy.predictor.status(this.projectId); - return { - message: result.message, - progress: result.progress, - projectScore: result.score, - timeRemaining: result.time_remaining, - mediaAmount: result.media_amount, - annotationAmount: result.annotation_amount, + Job.prototype.annotations.hasUnsavedChanges.implementation = function () { + const result = hasUnsavedChanges(this); + return result; }; -}; -Job.prototype.predictor.predict.implementation = async function (frame) { - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); - } + Job.prototype.annotations.clear.implementation = async function ( + reload, startframe, endframe, delTrackKeyframesOnly, + ) { + const result = await clearAnnotations(this, reload, startframe, endframe, delTrackKeyframesOnly); + return result; + }; - if (frame < this.startFrame || frame > this.stopFrame) { - throw new ArgumentError(`The frame with number ${frame} is out of the job`); - } + Job.prototype.annotations.select.implementation = function (frame, x, y) { + const result = selectObject(this, frame, x, y); + return result; + }; - if (!Number.isInteger(this.projectId)) { - throw new DataError('The job must belong to a project to use the feature'); - } + Job.prototype.annotations.statistics.implementation = function () { + const result = annotationsStatistics(this); + return result; + }; - const result = await serverProxy.predictor.predict(this.taskId, frame); - return result; -}; + Job.prototype.annotations.put.implementation = function (objectStates) { + const result = putAnnotations(this, objectStates); + return result; + }; -Job.prototype.close.implementation = function closeTask() { - clearFrames(this.id); - closeSession(this); - return this; -}; + Job.prototype.annotations.upload.implementation = async function ( + format: string, + useDefaultLocation: boolean, + sourceStorage: Storage, + file: File | string, + options?: { convMaskToPoly?: boolean }, + ) { + const result = await importDataset(this, format, useDefaultLocation, sourceStorage, file, options); + return result; + }; -Task.prototype.close.implementation = function closeTask() { - for (const job of this.jobs) { - clearFrames(job.id); - closeSession(job); - } + Job.prototype.annotations.import.implementation = function (data) { + const result = importCollection(this, data); + return result; + }; - closeSession(this); - return this; -}; - -Task.prototype.save.implementation = async function (onUpdate) { - // TODO: Add ability to change an owner and an assignee - if (typeof this.id !== 'undefined') { - // If the task has been already created, we update it - const taskData = this._updateTrigger.getUpdated(this, { - bugTracker: 'bug_tracker', - projectId: 'project_id', - assignee: 'assignee_id', - }); - if (taskData.assignee_id) { - taskData.assignee_id = taskData.assignee_id.id; + Job.prototype.annotations.export.implementation = function () { + const result = exportCollection(this); + return result; + }; + + Job.prototype.annotations.exportDataset.implementation = async function ( + format: string, + saveImages: boolean, + useDefaultSettings: boolean, + targetStorage: Storage, + customName?: string, + ) { + const result = await exportDataset(this, format, saveImages, useDefaultSettings, targetStorage, customName); + return result; + }; + + Job.prototype.actions.undo.implementation = async function (count) { + const result = await undoActions(this, count); + return result; + }; + + Job.prototype.actions.redo.implementation = async function (count) { + const result = await redoActions(this, count); + return result; + }; + + Job.prototype.actions.freeze.implementation = function (frozen) { + const result = freezeHistory(this, frozen); + return result; + }; + + Job.prototype.actions.clear.implementation = function () { + const result = clearActions(this); + return result; + }; + + Job.prototype.actions.get.implementation = function () { + const result = getActions(this); + return result; + }; + + Job.prototype.logger.log.implementation = async function (logType, payload, wait) { + const result = await loggerStorage.log(logType, { ...payload, task_id: this.taskId, job_id: this.id }, wait); + return result; + }; + + Job.prototype.predictor.status.implementation = async function () { + if (!Number.isInteger(this.projectId)) { + throw new DataError('The job must belong to a project to use the feature'); } - if (taskData.labels) { - taskData.labels = this._internalData.labels; - taskData.labels = taskData.labels.map((el) => el.toJSON()); + + const result = await serverProxy.predictor.status(this.projectId); + return { + message: result.message, + progress: result.progress, + projectScore: result.score, + timeRemaining: result.time_remaining, + mediaAmount: result.media_amount, + annotationAmount: result.annotation_amount, + }; + }; + + Job.prototype.predictor.predict.implementation = async function (frame) { + if (!Number.isInteger(frame) || frame < 0) { + throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); } - const data = await serverProxy.tasks.save(this.id, taskData); - this._updateTrigger.reset(); - return new Task(data); - } + if (frame < this.startFrame || frame > this.stopFrame) { + throw new ArgumentError(`The frame with number ${frame} is out of the job`); + } - const taskSpec: any = { - name: this.name, - labels: this.labels.map((el) => el.toJSON()), + if (!Number.isInteger(this.projectId)) { + throw new DataError('The job must belong to a project to use the feature'); + } + + const result = await serverProxy.predictor.predict(this.taskId, frame); + return result; }; - if (typeof this.bugTracker !== 'undefined') { - taskSpec.bug_tracker = this.bugTracker; - } - if (typeof this.segmentSize !== 'undefined') { - taskSpec.segment_size = this.segmentSize; - } - if (typeof this.overlap !== 'undefined') { - taskSpec.overlap = this.overlap; - } - if (typeof this.projectId !== 'undefined') { - taskSpec.project_id = this.projectId; - } - if (typeof this.subset !== 'undefined') { - taskSpec.subset = this.subset; - } + Job.prototype.close.implementation = function closeTask() { + clearFrames(this.id); + clearCache(this); + return this; + }; - if (this.targetStorage) { - taskSpec.target_storage = this.targetStorage.toJSON(); - } + Task.prototype.close.implementation = function closeTask() { + for (const job of this.jobs) { + clearFrames(job.id); + clearCache(job); + } - if (this.sourceStorage) { - taskSpec.source_storage = this.sourceStorage.toJSON(); - } + clearCache(this); + return this; + }; + + Task.prototype.save.implementation = async function (onUpdate) { + // TODO: Add ability to change an owner and an assignee + if (typeof this.id !== 'undefined') { + // If the task has been already created, we update it + const taskData = this._updateTrigger.getUpdated(this, { + bugTracker: 'bug_tracker', + projectId: 'project_id', + assignee: 'assignee_id', + }); + if (taskData.assignee_id) { + taskData.assignee_id = taskData.assignee_id.id; + } + if (taskData.labels) { + taskData.labels = this._internalData.labels; + taskData.labels = taskData.labels.map((el) => el.toJSON()); + } + + const data = await serverProxy.tasks.save(this.id, taskData); + this._updateTrigger.reset(); + return new Task(data); + } + + const taskSpec: any = { + name: this.name, + labels: this.labels.map((el) => el.toJSON()), + }; + + if (typeof this.bugTracker !== 'undefined') { + taskSpec.bug_tracker = this.bugTracker; + } + if (typeof this.segmentSize !== 'undefined') { + taskSpec.segment_size = this.segmentSize; + } + if (typeof this.overlap !== 'undefined') { + taskSpec.overlap = this.overlap; + } + if (typeof this.projectId !== 'undefined') { + taskSpec.project_id = this.projectId; + } + if (typeof this.subset !== 'undefined') { + taskSpec.subset = this.subset; + } + + if (this.targetStorage) { + taskSpec.target_storage = this.targetStorage.toJSON(); + } - const taskDataSpec = { - client_files: this.clientFiles, - server_files: this.serverFiles, - remote_files: this.remoteFiles, - image_quality: this.imageQuality, - use_zip_chunks: this.useZipChunks, - use_cache: this.useCache, - sorting_method: this.sortingMethod, + if (this.sourceStorage) { + taskSpec.source_storage = this.sourceStorage.toJSON(); + } + + const taskDataSpec = { + client_files: this.clientFiles, + server_files: this.serverFiles, + remote_files: this.remoteFiles, + image_quality: this.imageQuality, + use_zip_chunks: this.useZipChunks, + use_cache: this.useCache, + sorting_method: this.sortingMethod, + }; + + if (typeof this.startFrame !== 'undefined') { + taskDataSpec.start_frame = this.startFrame; + } + if (typeof this.stopFrame !== 'undefined') { + taskDataSpec.stop_frame = this.stopFrame; + } + if (typeof this.frameFilter !== 'undefined') { + taskDataSpec.frame_filter = this.frameFilter; + } + if (typeof this.dataChunkSize !== 'undefined') { + taskDataSpec.chunk_size = this.dataChunkSize; + } + if (typeof this.copyData !== 'undefined') { + taskDataSpec.copy_data = this.copyData; + } + if (typeof this.cloudStorageId !== 'undefined') { + taskDataSpec.cloud_storage_id = this.cloudStorageId; + } + + const task = await serverProxy.tasks.create(taskSpec, taskDataSpec, onUpdate); + return new Task(task); }; - if (typeof this.startFrame !== 'undefined') { - taskDataSpec.start_frame = this.startFrame; - } - if (typeof this.stopFrame !== 'undefined') { - taskDataSpec.stop_frame = this.stopFrame; - } - if (typeof this.frameFilter !== 'undefined') { - taskDataSpec.frame_filter = this.frameFilter; - } - if (typeof this.dataChunkSize !== 'undefined') { - taskDataSpec.chunk_size = this.dataChunkSize; - } - if (typeof this.copyData !== 'undefined') { - taskDataSpec.copy_data = this.copyData; - } - if (typeof this.cloudStorageId !== 'undefined') { - taskDataSpec.cloud_storage_id = this.cloudStorageId; - } + Task.prototype.delete.implementation = async function () { + const result = await serverProxy.tasks.delete(this.id); + return result; + }; - const task = await serverProxy.tasks.create(taskSpec, taskDataSpec, onUpdate); - return new Task(task); -}; - -Task.prototype.delete.implementation = async function () { - const result = await serverProxy.tasks.delete(this.id); - return result; -}; - -Task.prototype.backup.implementation = async function ( - targetStorage: Storage, - useDefaultSettings: boolean, - fileName?: string, -) { - const result = await serverProxy.tasks.backup(this.id, targetStorage, useDefaultSettings, fileName); - return result; -}; - -Task.restore.implementation = async function (storage: Storage, file: File | string) { - // eslint-disable-next-line no-unsanitized/method - const result = await serverProxy.tasks.restore(storage, file); - return result; -}; - -Task.prototype.frames.get.implementation = async function (frame, isPlaying, step) { - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); - } + Task.prototype.backup.implementation = async function ( + targetStorage: Storage, + useDefaultSettings: boolean, + fileName?: string, + ) { + const result = await serverProxy.tasks.backup(this.id, targetStorage, useDefaultSettings, fileName); + return result; + }; - if (frame >= this.size) { - throw new ArgumentError(`The frame with number ${frame} is out of the task`); - } + Task.restore.implementation = async function (storage: Storage, file: File | string) { + // eslint-disable-next-line no-unsanitized/method + const result = await serverProxy.tasks.restore(storage, file); + return result; + }; - const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0]; - - const result = await getFrame( - job.id, - this.dataChunkSize, - this.dataChunkType, - this.mode, - frame, - job.startFrame, - job.stopFrame, - isPlaying, - step, - ); - return result; -}; - -Task.prototype.frames.ranges.implementation = async function () { - const rangesData = { - decoded: [], - buffered: [], - }; - for (const job of this.jobs) { - const { decoded, buffered } = await getRanges(job.id); - rangesData.decoded.push(decoded); - rangesData.buffered.push(buffered); - } - return rangesData; -}; + Task.prototype.frames.get.implementation = async function (frame, isPlaying, step) { + if (!Number.isInteger(frame) || frame < 0) { + throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); + } -Task.prototype.frames.preview.implementation = async function () { - if (this.id === null) { - return ''; - } + if (frame >= this.size) { + throw new ArgumentError(`The frame with number ${frame} is out of the task`); + } - const frameData = await getPreview(this.id); - return frameData; -}; + const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0]; + + const result = await getFrame( + job.id, + this.dataChunkSize, + this.dataChunkType, + this.mode, + frame, + job.startFrame, + job.stopFrame, + isPlaying, + step, + ); + return result; + }; -Task.prototype.frames.delete.implementation = async function (frame) { - if (!Number.isInteger(frame)) { - throw new Error(`Frame must be an integer. Got: "${frame}"`); - } + Task.prototype.frames.ranges.implementation = async function () { + const rangesData = { + decoded: [], + buffered: [], + }; + for (const job of this.jobs) { + const { decoded, buffered } = await getRanges(job.id); + rangesData.decoded.push(decoded); + rangesData.buffered.push(buffered); + } + return rangesData; + }; - if (frame < 0 || frame >= this.size) { - throw new Error('The frame is out of the task'); - } + Task.prototype.frames.preview.implementation = async function () { + if (this.id === null) { + return ''; + } - const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0]; - if (job) { - await deleteFrameWrapper.call(this, job.id, frame); - } -}; + const frameData = await getPreview(this.id); + return frameData; + }; -Task.prototype.frames.restore.implementation = async function (frame) { - if (!Number.isInteger(frame)) { - throw new Error(`Frame must be an integer. Got: "${frame}"`); - } + Task.prototype.frames.delete.implementation = async function (frame) { + if (!Number.isInteger(frame)) { + throw new Error(`Frame must be an integer. Got: "${frame}"`); + } - if (frame < 0 || frame >= this.size) { - throw new Error('The frame is out of the task'); - } + if (frame < 0 || frame >= this.size) { + throw new Error('The frame is out of the task'); + } - const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0]; - if (job) { - await restoreFrameWrapper.call(this, job.id, frame); - } -}; + const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0]; + if (job) { + await deleteFrameWrapper.call(this, job.id, frame); + } + }; -Task.prototype.frames.save.implementation = async function () { - return Promise.all(this.jobs.map((job) => patchMeta(job.id))); -}; + Task.prototype.frames.restore.implementation = async function (frame) { + if (!Number.isInteger(frame)) { + throw new Error(`Frame must be an integer. Got: "${frame}"`); + } -Task.prototype.frames.search.implementation = async function (filters, frameFrom, frameTo) { - if (typeof filters !== 'object') { - throw new ArgumentError('Filters should be an object'); - } + if (frame < 0 || frame >= this.size) { + throw new Error('The frame is out of the task'); + } - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } + const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0]; + if (job) { + await restoreFrameWrapper.call(this, job.id, frame); + } + }; - if (frameFrom < 0 || frameFrom > this.size) { - throw new ArgumentError('The start frame is out of the task'); - } + Task.prototype.frames.save.implementation = async function () { + return Promise.all(this.jobs.map((job) => patchMeta(job.id))); + }; - if (frameTo < 0 || frameTo > this.size) { - throw new ArgumentError('The stop frame is out of the task'); - } + Task.prototype.frames.search.implementation = async function (filters, frameFrom, frameTo) { + if (typeof filters !== 'object') { + throw new ArgumentError('Filters should be an object'); + } - const jobs = this.jobs.filter((_job) => ( - (frameFrom >= _job.startFrame && frameFrom <= _job.stopFrame) || - (frameTo >= _job.startFrame && frameTo <= _job.stopFrame) || - (frameFrom < _job.startFrame && frameTo > _job.stopFrame) - )); + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } - if (filters.notDeleted) { - for (const job of jobs) { - const result = await findNotDeletedFrame( - job.id, Math.max(frameFrom, job.startFrame), Math.min(frameTo, job.stopFrame), 1, - ); + if (frameFrom < 0 || frameFrom > this.size) { + throw new ArgumentError('The start frame is out of the task'); + } - if (result !== null) return result; + if (frameTo < 0 || frameTo > this.size) { + throw new ArgumentError('The stop frame is out of the task'); } - } - return null; -}; + const jobs = this.jobs.filter((_job) => ( + (frameFrom >= _job.startFrame && frameFrom <= _job.stopFrame) || + (frameTo >= _job.startFrame && frameTo <= _job.stopFrame) || + (frameFrom < _job.startFrame && frameTo > _job.stopFrame) + )); -// TODO: Check filter for annotations -Task.prototype.annotations.get.implementation = async function (frame, allTracks, filters) { - if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) { - throw new ArgumentError('The filters argument must be an array of strings'); - } + if (filters.notDeleted) { + for (const job of jobs) { + const result = await findNotDeletedFrame( + job.id, Math.max(frameFrom, job.startFrame), Math.min(frameTo, job.stopFrame), 1, + ); - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); - } + if (result !== null) return result; + } + } - if (frame >= this.size) { - throw new ArgumentError(`Frame ${frame} does not exist in the task`); - } + return null; + }; - const result = await getAnnotations(this, frame, allTracks, filters); - const deletedFrames = await getDeletedFrames('task', this.id); - if (frame in deletedFrames) { - return []; - } + // TODO: Check filter for annotations + Task.prototype.annotations.get.implementation = async function (frame, allTracks, filters) { + if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) { + throw new ArgumentError('The filters argument must be an array of strings'); + } - return result; -}; + if (!Number.isInteger(frame) || frame < 0) { + throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); + } -Task.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) { - if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) { - throw new ArgumentError('The filters argument must be an array of strings'); - } + if (frame >= this.size) { + throw new ArgumentError(`Frame ${frame} does not exist in the task`); + } - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } + const result = await getAnnotations(this, frame, allTracks, filters); + const deletedFrames = await getDeletedFrames('task', this.id); + if (frame in deletedFrames) { + return []; + } - if (frameFrom < 0 || frameFrom >= this.size) { - throw new ArgumentError('The start frame is out of the task'); - } + return result; + }; - if (frameTo < 0 || frameTo >= this.size) { - throw new ArgumentError('The stop frame is out of the task'); - } + Task.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) { + if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) { + throw new ArgumentError('The filters argument must be an array of strings'); + } - const result = searchAnnotations(this, filters, frameFrom, frameTo); - return result; -}; + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } -Task.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) { - if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { - throw new ArgumentError('The start and end frames both must be an integer'); - } + if (frameFrom < 0 || frameFrom >= this.size) { + throw new ArgumentError('The start frame is out of the task'); + } - if (frameFrom < 0 || frameFrom >= this.size) { - throw new ArgumentError('The start frame is out of the task'); - } + if (frameTo < 0 || frameTo >= this.size) { + throw new ArgumentError('The stop frame is out of the task'); + } - if (frameTo < 0 || frameTo >= this.size) { - throw new ArgumentError('The stop frame is out of the task'); - } + const result = searchAnnotations(this, filters, frameFrom, frameTo); + return result; + }; - const result = searchEmptyFrame(this, frameFrom, frameTo); - return result; -}; - -Task.prototype.annotations.save.implementation = async function (onUpdate) { - const result = await saveAnnotations(this, onUpdate); - return result; -}; - -Task.prototype.annotations.merge.implementation = async function (objectStates) { - const result = await mergeAnnotations(this, objectStates); - return result; -}; - -Task.prototype.annotations.split.implementation = async function (objectState, frame) { - const result = await splitAnnotations(this, objectState, frame); - return result; -}; - -Task.prototype.annotations.group.implementation = async function (objectStates, reset) { - const result = await groupAnnotations(this, objectStates, reset); - return result; -}; - -Task.prototype.annotations.hasUnsavedChanges.implementation = function () { - const result = hasUnsavedChanges(this); - return result; -}; - -Task.prototype.annotations.clear.implementation = async function (reload) { - const result = await clearAnnotations(this, reload); - return result; -}; - -Task.prototype.annotations.select.implementation = function (frame, x, y) { - const result = selectObject(this, frame, x, y); - return result; -}; - -Task.prototype.annotations.statistics.implementation = function () { - const result = annotationsStatistics(this); - return result; -}; - -Task.prototype.annotations.put.implementation = function (objectStates) { - const result = putAnnotations(this, objectStates); - return result; -}; - -Task.prototype.annotations.upload.implementation = async function ( - format: string, - useDefaultLocation: boolean, - sourceStorage: Storage, - file: File | string, -) { - const result = await importDataset(this, format, useDefaultLocation, sourceStorage, file); - return result; -}; - -Task.prototype.annotations.import.implementation = function (data) { - const result = importCollection(this, data); - return result; -}; - -Task.prototype.annotations.export.implementation = function () { - const result = exportCollection(this); - return result; -}; - -Task.prototype.annotations.exportDataset.implementation = async function ( - format: string, - saveImages: boolean, - useDefaultSettings: boolean, - targetStorage: Storage, - customName?: string, -) { - const result = await exportDataset(this, format, saveImages, useDefaultSettings, targetStorage, customName); - return result; -}; - -Task.prototype.actions.undo.implementation = function (count) { - const result = undoActions(this, count); - return result; -}; - -Task.prototype.actions.redo.implementation = function (count) { - const result = redoActions(this, count); - return result; -}; - -Task.prototype.actions.freeze.implementation = function (frozen) { - const result = freezeHistory(this, frozen); - return result; -}; - -Task.prototype.actions.clear.implementation = function () { - const result = clearActions(this); - return result; -}; - -Task.prototype.actions.get.implementation = function () { - const result = getActions(this); - return result; -}; - -Task.prototype.logger.log.implementation = async function (logType, payload, wait) { - const result = await loggerStorage.log(logType, { ...payload, task_id: this.id }, wait); - return result; -}; - -Task.prototype.predictor.status.implementation = async function () { - if (!Number.isInteger(this.projectId)) { - throw new DataError('The task must belong to a project to use the feature'); - } + Task.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) { + if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { + throw new ArgumentError('The start and end frames both must be an integer'); + } - const result = await serverProxy.predictor.status(this.projectId); - return { - message: result.message, - progress: result.progress, - projectScore: result.score, - timeRemaining: result.time_remaining, - mediaAmount: result.media_amount, - annotationAmount: result.annotation_amount, + if (frameFrom < 0 || frameFrom >= this.size) { + throw new ArgumentError('The start frame is out of the task'); + } + + if (frameTo < 0 || frameTo >= this.size) { + throw new ArgumentError('The stop frame is out of the task'); + } + + const result = searchEmptyFrame(this, frameFrom, frameTo); + return result; }; -}; -Task.prototype.predictor.predict.implementation = async function (frame) { - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); - } + Task.prototype.annotations.save.implementation = async function (onUpdate) { + const result = await saveAnnotations(this, onUpdate); + return result; + }; - if (frame >= this.size) { - throw new ArgumentError(`The frame with number ${frame} is out of the task`); - } + Task.prototype.annotations.merge.implementation = async function (objectStates) { + const result = await mergeAnnotations(this, objectStates); + return result; + }; - if (!Number.isInteger(this.projectId)) { - throw new DataError('The task must belong to a project to use the feature'); - } + Task.prototype.annotations.split.implementation = async function (objectState, frame) { + const result = await splitAnnotations(this, objectState, frame); + return result; + }; + + Task.prototype.annotations.group.implementation = async function (objectStates, reset) { + const result = await groupAnnotations(this, objectStates, reset); + return result; + }; + + Task.prototype.annotations.hasUnsavedChanges.implementation = function () { + const result = hasUnsavedChanges(this); + return result; + }; + + Task.prototype.annotations.clear.implementation = async function (reload) { + const result = await clearAnnotations(this, reload); + return result; + }; + + Task.prototype.annotations.select.implementation = function (frame, x, y) { + const result = selectObject(this, frame, x, y); + return result; + }; + + Task.prototype.annotations.statistics.implementation = function () { + const result = annotationsStatistics(this); + return result; + }; + + Task.prototype.annotations.put.implementation = function (objectStates) { + const result = putAnnotations(this, objectStates); + return result; + }; + + Task.prototype.annotations.upload.implementation = async function ( + format: string, + useDefaultLocation: boolean, + sourceStorage: Storage, + file: File | string, + options?: { convMaskToPoly?: boolean }, + ) { + const result = await importDataset(this, format, useDefaultLocation, sourceStorage, file, options); + return result; + }; + + Task.prototype.annotations.import.implementation = function (data) { + const result = importCollection(this, data); + return result; + }; + + Task.prototype.annotations.export.implementation = function () { + const result = exportCollection(this); + return result; + }; + + Task.prototype.annotations.exportDataset.implementation = async function ( + format: string, + saveImages: boolean, + useDefaultSettings: boolean, + targetStorage: Storage, + customName?: string, + ) { + const result = await exportDataset(this, format, saveImages, useDefaultSettings, targetStorage, customName); + return result; + }; + + Task.prototype.actions.undo.implementation = function (count) { + const result = undoActions(this, count); + return result; + }; + + Task.prototype.actions.redo.implementation = function (count) { + const result = redoActions(this, count); + return result; + }; + + Task.prototype.actions.freeze.implementation = function (frozen) { + const result = freezeHistory(this, frozen); + return result; + }; + + Task.prototype.actions.clear.implementation = function () { + const result = clearActions(this); + return result; + }; + + Task.prototype.actions.get.implementation = function () { + const result = getActions(this); + return result; + }; + + Task.prototype.logger.log.implementation = async function (logType, payload, wait) { + const result = await loggerStorage.log(logType, { ...payload, task_id: this.id }, wait); + return result; + }; + + Task.prototype.predictor.status.implementation = async function () { + if (!Number.isInteger(this.projectId)) { + throw new DataError('The task must belong to a project to use the feature'); + } + + const result = await serverProxy.predictor.status(this.projectId); + return { + message: result.message, + progress: result.progress, + projectScore: result.score, + timeRemaining: result.time_remaining, + mediaAmount: result.media_amount, + annotationAmount: result.annotation_amount, + }; + }; + + Task.prototype.predictor.predict.implementation = async function (frame) { + if (!Number.isInteger(frame) || frame < 0) { + throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); + } + + if (frame >= this.size) { + throw new ArgumentError(`The frame with number ${frame} is out of the task`); + } + + if (!Number.isInteger(this.projectId)) { + throw new DataError('The task must belong to a project to use the feature'); + } + + const result = await serverProxy.predictor.predict(this.id, frame); + return result; + }; - const result = await serverProxy.predictor.predict(this.id, frame); - return result; -}; +})(); diff --git a/cvat-core/src/statistics.ts b/cvat-core/src/statistics.ts index 50ff8d76120..d30baf83dc8 100644 --- a/cvat-core/src/statistics.ts +++ b/cvat-core/src/statistics.ts @@ -1,116 +1,44 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -(() => { - /** - * Class representing collection statistics - * @memberof module:API.cvat.classes - * @hideconstructor - */ - class Statistics { - constructor(label, total) { - Object.defineProperties( - this, - Object.freeze({ - /** - * Statistics collected by labels, has the following structure: - * @example - * { - * label: { - * rectangle: { - * track: 10, - * shape: 11, - * }, - * polygon: { - * track: 13, - * shape: 14, - * }, - * polyline: { - * track: 16, - * shape: 17, - * }, - * points: { - * track: 19, - * shape: 20, - * }, - * ellipse: { - * track: 13, - * shape: 15, - * }, - * cuboid: { - * track: 21, - * shape: 22, - * }, - * skeleton: { - * track: 21, - * shape: 22, - * }, - * tag: 66, - * manually: 207, - * interpolated: 500, - * total: 630, - * } - * } - * @name label - * @type {Object} - * @memberof module:API.cvat.classes.Statistics - * @readonly - * @instance - */ - label: { - get: () => JSON.parse(JSON.stringify(label)), - }, - /** - * Total objects statistics (within all the labels), has the following structure: - * @example - * { - * rectangle: { - * tracks: 10, - * shapes: 11, - * }, - * polygon: { - * tracks: 13, - * shapes: 14, - * }, - * polyline: { - * tracks: 16, - * shapes: 17, - * }, - * point: { - * tracks: 19, - * shapes: 20, - * }, - * ellipse: { - * tracks: 13, - * shapes: 15, - * }, - * cuboid: { - * tracks: 21, - * shapes: 22, - * }, - * skeleton: { - * tracks: 21, - * shapes: 22, - * }, - * tag: 66, - * manually: 186, - * interpolated: 500, - * total: 608, - * } - * @name total - * @type {Object} - * @memberof module:API.cvat.classes.Statistics - * @readonly - * @instance - */ - total: { - get: () => JSON.parse(JSON.stringify(total)), - }, - }), - ); - } +interface ObjectStatistics { + track: number; + shape: number; +} + +interface StatisticsBody { + rectangle: ObjectStatistics; + polygon: ObjectStatistics; + polyline: ObjectStatistics; + points: ObjectStatistics; + ellipse: ObjectStatistics; + cuboid: ObjectStatistics; + skeleton: ObjectStatistics; + mask: { + shape: number; + }; + tag: number; + manually: number; + interpolated: number; + total: number; +} + +export default class Statistics { + private labelData: Record; + private totalData: StatisticsBody; + + constructor(label: Statistics['labelData'], total: Statistics['totalData']) { + this.labelData = label; + this.totalData = total; } - module.exports = Statistics; -})(); + public get label(): Record { + return JSON.parse(JSON.stringify(this.labelData)); + } + + public get total(): StatisticsBody { + return JSON.parse(JSON.stringify(this.totalData)); + } +} diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 09e27893ba4..5bb695061a9 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.42.5", + "version": "1.43.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 3246751b2f1..fa5106aeaef 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -208,6 +208,7 @@ export enum AnnotationActionTypes { RESTORE_FRAME = 'RESTORE_FRAME', RESTORE_FRAME_SUCCESS = 'RESTORE_FRAME_SUCCESS', RESTORE_FRAME_FAILED = 'RESTORE_FRAME_FAILED', + UPDATE_BRUSH_TOOLS_CONFIG = 'UPDATE_BRUSH_TOOLS_CONFIG', } export function saveLogsAsync(): ThunkAction { @@ -319,6 +320,15 @@ export function updateCanvasContextMenu( }; } +export function updateCanvasBrushTools(config: { + visible?: boolean, left?: number, top?: number +}): AnyAction { + return { + type: AnnotationActionTypes.UPDATE_BRUSH_TOOLS_CONFIG, + payload: config, + }; +} + export function removeAnnotationsAsync( startFrame: number, endFrame: number, delTrackKeyframesOnly: boolean, ): ThunkAction { @@ -1212,8 +1222,9 @@ export function updateAnnotationsAsync(statesToUpdate: any[]): ThunkAction { const promises = statesToUpdate.map((objectState: any): Promise => objectState.save()); const states = await Promise.all(promises); - const withSkeletonElements = states.some((state: any) => state.parentID !== null); - if (withSkeletonElements) { + const needToUpdateAll = states + .some((state: any) => [ShapeType.MASK, ShapeType.SKELETON].includes(state.shapeType)); + if (needToUpdateAll) { dispatch(fetchAnnotationsAsync()); return; } @@ -1458,6 +1469,7 @@ const ShapeTypeToControl: Record = { [ShapeType.CUBOID]: ActiveControl.DRAW_CUBOID, [ShapeType.ELLIPSE]: ActiveControl.DRAW_ELLIPSE, [ShapeType.SKELETON]: ActiveControl.DRAW_SKELETON, + [ShapeType.MASK]: ActiveControl.DRAW_MASK, }; export function pasteShapeAsync(): ThunkAction { diff --git a/cvat-ui/src/actions/import-actions.ts b/cvat-ui/src/actions/import-actions.ts index 6372850b31a..7e40afbf709 100644 --- a/cvat-ui/src/actions/import-actions.ts +++ b/cvat-ui/src/actions/import-actions.ts @@ -70,6 +70,7 @@ export const importDatasetAsync = ( useDefaultSettings: boolean, sourceStorage: Storage, file: File | string, + convMaskToPoly: boolean, ): ThunkAction => ( async (dispatch, getState) => { const resource = instance instanceof core.classes.Project ? 'dataset' : 'annotation'; @@ -83,18 +84,20 @@ export const importDatasetAsync = ( } dispatch(importActions.importDataset(instance, format)); await instance.annotations - .importDataset(format, useDefaultSettings, sourceStorage, file, - (message: string, progress: number) => ( + .importDataset(format, useDefaultSettings, sourceStorage, file, { + convMaskToPoly, + updateStatusCallback: (message: string, progress: number) => ( dispatch(importActions.importDatasetUpdateStatus( instance, Math.floor(progress * 100), message, )) - )); + ), + }); } else if (instance instanceof core.classes.Task) { if (state.import.tasks.dataset.current?.[instance.id]) { throw Error('Only one importing of annotation/dataset allowed at the same time'); } dispatch(importActions.importDataset(instance, format)); - await instance.annotations.upload(format, useDefaultSettings, sourceStorage, file); + await instance.annotations.upload(format, useDefaultSettings, sourceStorage, file, { convMaskToPoly }); } else { // job if (state.import.tasks.dataset.current?.[instance.taskId]) { throw Error('Annotations is being uploaded for the task'); @@ -107,7 +110,7 @@ export const importDatasetAsync = ( dispatch(importActions.importDataset(instance, format)); const frame = state.annotation.player.frame.number; - await instance.annotations.upload(format, useDefaultSettings, sourceStorage, file); + await instance.annotations.upload(format, useDefaultSettings, sourceStorage, file, { convMaskToPoly }); await instance.logger.log(LogType.uploadAnnotations, { ...(await jobInfoGenerator(instance)), diff --git a/cvat-ui/src/assets/brush-icon.svg b/cvat-ui/src/assets/brush-icon.svg new file mode 100644 index 00000000000..b57b27e3557 --- /dev/null +++ b/cvat-ui/src/assets/brush-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/cvat-ui/src/assets/check-icon.svg b/cvat-ui/src/assets/check-icon.svg new file mode 100644 index 00000000000..f099a0f237b --- /dev/null +++ b/cvat-ui/src/assets/check-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/cvat-ui/src/assets/eraser-icon.svg b/cvat-ui/src/assets/eraser-icon.svg new file mode 100644 index 00000000000..99175e72a00 --- /dev/null +++ b/cvat-ui/src/assets/eraser-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/cvat-ui/src/assets/move-icon.svg b/cvat-ui/src/assets/move-icon.svg index e6c169b41e8..ff6d45f7246 100644 --- a/cvat-ui/src/assets/move-icon.svg +++ b/cvat-ui/src/assets/move-icon.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/cvat-ui/src/assets/opencv.svg b/cvat-ui/src/assets/opencv.svg index 7608729daf8..b185a2eb622 100644 --- a/cvat-ui/src/assets/opencv.svg +++ b/cvat-ui/src/assets/opencv.svg @@ -4,7 +4,7 @@ The file has been modified --> - + diff --git a/cvat-ui/src/assets/plus-icon.svg b/cvat-ui/src/assets/plus-icon.svg new file mode 100644 index 00000000000..353102d3b81 --- /dev/null +++ b/cvat-ui/src/assets/plus-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/cvat-ui/src/assets/polygon-minus.svg b/cvat-ui/src/assets/polygon-minus.svg new file mode 100644 index 00000000000..e8edd1f6a51 --- /dev/null +++ b/cvat-ui/src/assets/polygon-minus.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/cvat-ui/src/assets/polygon-plus.svg b/cvat-ui/src/assets/polygon-plus.svg new file mode 100644 index 00000000000..dd2f59b161b --- /dev/null +++ b/cvat-ui/src/assets/polygon-plus.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/cvat-ui/src/components/annotation-page/canvas/brush-toolbox-styles.scss b/cvat-ui/src/components/annotation-page/canvas/brush-toolbox-styles.scss new file mode 100644 index 00000000000..31060b9afd2 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/canvas/brush-toolbox-styles.scss @@ -0,0 +1,57 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +@import '../../../base.scss'; + +.cvat-brush-tools-toolbox { + position: absolute; + margin: $grid-unit-size; + padding: 0 $grid-unit-size; + border-radius: 4px; + background: $background-color-2; + display: flex; + align-items: center; + z-index: 100; + box-shadow: $box-shadow-base; + + > hr { + width: 1px; + height: $grid-unit-size * 4; + background: $border-color-1; + } + + > * { + margin-right: $grid-unit-size; + } + + > button { + font-size: 20px; + + > span.anticon { + font-size: inherit; + } + } + + .cvat-brush-tools-brush, + .cvat-brush-tools-eraser { + svg { + width: 24px; + height: 25px; + } + } + + .cvat-brush-tools-draggable-area { + display: flex; + font-size: 20px; + + svg { + width: 24px; + height: 25px; + } + } + + .cvat-brush-tools-active-tool { + background: $header-color; + } +} diff --git a/cvat-ui/src/components/annotation-page/canvas/brush-tools.tsx b/cvat-ui/src/components/annotation-page/canvas/brush-tools.tsx new file mode 100644 index 00000000000..cee63311571 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/canvas/brush-tools.tsx @@ -0,0 +1,275 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './brush-toolbox-styles.scss'; + +import React, { useEffect, useState } from 'react'; +import ReactDOM from 'react-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import Button from 'antd/lib/button'; +import Icon, { VerticalAlignBottomOutlined } from '@ant-design/icons'; +import InputNumber from 'antd/lib/input-number'; +import Select from 'antd/lib/select'; + +import { getCore } from 'cvat-core-wrapper'; +import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; +import { + BrushIcon, EraserIcon, PolygonMinusIcon, PolygonPlusIcon, + PlusIcon, CheckIcon, MoveIcon, +} from 'icons'; +import CVATTooltip from 'components/common/cvat-tooltip'; +import { CombinedState, ObjectType, ShapeType } from 'reducers'; +import LabelSelector from 'components/label-selector/label-selector'; +import { rememberObject, updateCanvasBrushTools } from 'actions/annotation-actions'; +import useDraggable from './draggable-hoc'; + +const DraggableArea = ( +
+ +
+); + +const MIN_BRUSH_SIZE = 1; +function BrushTools(): React.ReactPortal | null { + const dispatch = useDispatch(); + const defaultLabelID = useSelector((state: CombinedState) => state.annotation.drawing.activeLabelID); + const config = useSelector((state: CombinedState) => state.annotation.canvas.brushTools); + const canvasInstance = useSelector((state: CombinedState) => state.annotation.canvas.instance); + const labels = useSelector((state: CombinedState) => state.annotation.job.labels); + const { visible } = config; + + const [editableState, setEditableState] = useState(null); + const [currentTool, setCurrentTool] = useState<'brush' | 'eraser' | 'polygon-plus' | 'polygon-minus'>('brush'); + const [brushForm, setBrushForm] = useState<'circle' | 'square'>('circle'); + const [[top, left], setTopLeft] = useState([0, 0]); + const [brushSize, setBrushSize] = useState(10); + + const [removeUnderlyingPixels, setRemoveUnderlyingPixels] = useState(false); + const dragBar = useDraggable( + (): number[] => { + const [element] = window.document.getElementsByClassName('cvat-brush-tools-toolbox'); + if (element) { + const { offsetTop, offsetLeft } = element as HTMLDivElement; + return [offsetTop, offsetLeft]; + } + + return [0, 0]; + }, + (newTop, newLeft) => setTopLeft([newTop, newLeft]), + DraggableArea, + ); + + useEffect(() => { + const label = labels.find((_label: any) => _label.id === defaultLabelID); + getCore().config.removeUnderlyingMaskPixels = removeUnderlyingPixels; + if (visible && label && canvasInstance instanceof Canvas) { + const onUpdateConfiguration = ({ brushTool }: any): void => { + if (brushTool?.size) { + setBrushSize(Math.max(MIN_BRUSH_SIZE, brushTool.size)); + } + }; + + if (canvasInstance.mode() === CanvasMode.DRAW) { + canvasInstance.draw({ + enabled: true, + shapeType: ShapeType.MASK, + crosshair: false, + brushTool: { + type: currentTool, + size: brushSize, + form: brushForm, + color: label.color, + }, + onUpdateConfiguration, + }); + } else if (canvasInstance.mode() === CanvasMode.EDIT && editableState) { + canvasInstance.edit({ + enabled: true, + state: editableState, + brushTool: { + type: currentTool, + size: brushSize, + form: brushForm, + color: label.color, + }, + onUpdateConfiguration, + }); + } + } + }, [currentTool, brushSize, brushForm, visible, defaultLabelID, editableState]); + + useEffect(() => { + const canvasContainer = window.document.getElementsByClassName('cvat-canvas-container')[0]; + if (canvasContainer) { + const { offsetTop, offsetLeft } = canvasContainer.parentElement as HTMLElement; + setTopLeft([offsetTop, offsetLeft]); + } + }, []); + + useEffect(() => { + const hideToolset = (): void => { + if (visible) { + dispatch(updateCanvasBrushTools({ visible: false })); + } + }; + + const showToolset = (e: Event): void => { + const evt = e as CustomEvent; + if (evt.detail?.state?.shapeType === ShapeType.MASK || + (evt.detail?.drawData?.shapeType === ShapeType.MASK && !evt.detail?.drawData?.initialState)) { + dispatch(updateCanvasBrushTools({ visible: true })); + } + }; + + const updateEditableState = (e: Event): void => { + const evt = e as CustomEvent; + if (evt.type === 'canvas.editstart' && evt.detail.state) { + setEditableState(evt.detail.state); + } else if (editableState) { + setEditableState(null); + } + }; + + if (canvasInstance instanceof Canvas) { + canvasInstance.html().addEventListener('canvas.drawn', hideToolset); + canvasInstance.html().addEventListener('canvas.canceled', hideToolset); + canvasInstance.html().addEventListener('canvas.canceled', updateEditableState); + canvasInstance.html().addEventListener('canvas.drawstart', showToolset); + canvasInstance.html().addEventListener('canvas.editstart', showToolset); + canvasInstance.html().addEventListener('canvas.editstart', updateEditableState); + canvasInstance.html().addEventListener('canvas.editdone', updateEditableState); + } + + return () => { + if (canvasInstance instanceof Canvas) { + canvasInstance.html().removeEventListener('canvas.drawn', hideToolset); + canvasInstance.html().removeEventListener('canvas.canceled', hideToolset); + canvasInstance.html().removeEventListener('canvas.canceled', updateEditableState); + canvasInstance.html().removeEventListener('canvas.drawstart', showToolset); + canvasInstance.html().removeEventListener('canvas.editstart', showToolset); + canvasInstance.html().removeEventListener('canvas.editstart', updateEditableState); + canvasInstance.html().removeEventListener('canvas.editdone', updateEditableState); + } + }; + }, [visible, editableState]); + + if (!labels.length) { + return null; + } + + return ReactDOM.createPortal(( +
+
+ ), window.document.body); +} + +export default React.memo(BrushTools); diff --git a/cvat-ui/src/components/annotation-page/canvas/canvas-context-menu.tsx b/cvat-ui/src/components/annotation-page/canvas/canvas-context-menu.tsx index 3e8ea4b9b11..fae0b614410 100644 --- a/cvat-ui/src/components/annotation-page/canvas/canvas-context-menu.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/canvas-context-menu.tsx @@ -11,7 +11,7 @@ import { MenuInfo } from 'rc-menu/lib/interface'; import ObjectItemElementComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item-element'; import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item'; -import { Workspace } from 'reducers'; +import { ShapeType, Workspace } from 'reducers'; import { rotatePoint } from 'utils/math'; import consts from 'consts'; @@ -24,9 +24,9 @@ interface Props { visible: boolean; left: number; top: number; + latestComments: string[]; onStartIssue(position: number[]): void; openIssue(position: number[], message: string): void; - latestComments: string[]; } interface ReviewContextMenuProps { @@ -109,7 +109,7 @@ export default function CanvasContextMenu(props: Props): JSX.Element | null { const state = objectStates.find((_state: any): boolean => _state.clientID === contextMenuClientID); if (state) { let { points } = state; - if (['ellipse', 'rectangle'].includes(state.shapeType)) { + if ([ShapeType.ELLIPSE, ShapeType.RECTANGLE].includes(state.shapeType)) { const [cx, cy] = state.shapeType === 'ellipse' ? state.points : [ (state.points[0] + state.points[2]) / 2, (state.points[1] + state.points[3]) / 2, @@ -128,6 +128,14 @@ export default function CanvasContextMenu(props: Props): JSX.Element | null { [points[2], points[3]], [points[0], points[3]], ].map(([x, y]: number[]) => rotatePoint(x, y, state.rotation, cx, cy)).flat(); + } else if (state.shapeType === ShapeType.MASK) { + points = state.points.slice(-4); + points = [ + points[0], points[1], + points[2], points[1], + points[2], points[3], + points[0], points[3], + ]; } if (param.key === ReviewContextMenuKeys.OPEN_ISSUE) { 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 f49b8249b57..58eda99dbfc 100644 --- a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -20,6 +21,7 @@ import consts from 'consts'; import CVATTooltip from 'components/common/cvat-tooltip'; import FrameTags from 'components/annotation-page/tag-annotation-workspace/frame-tags'; import ImageSetupsContent from './image-setups-content'; +import BrushTools from './brush-tools'; import ContextImage from '../standard-workspace/context-image/context-image'; const cvat = getCore(); @@ -243,10 +245,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { if (prevProps.activatedStateID !== null && prevProps.activatedStateID !== activatedStateID) { canvasInstance.activate(null); - const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`); - if (el) { - (el as any).instance.fill({ opacity }); - } } if (gridSize !== prevProps.gridSize) { @@ -342,7 +340,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().removeEventListener('mousedown', this.onCanvasMouseDown); canvasInstance.html().removeEventListener('click', this.onCanvasClicked); - canvasInstance.html().removeEventListener('contextmenu', this.onCanvasContextMenu); canvasInstance.html().removeEventListener('canvas.editstart', this.onCanvasEditStart); canvasInstance.html().removeEventListener('canvas.edited', this.onCanvasEditDone); canvasInstance.html().removeEventListener('canvas.dragstart', this.onCanvasDragStart); @@ -367,7 +364,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().removeEventListener('canvas.regionselected', this.onCanvasPositionSelected); canvasInstance.html().removeEventListener('canvas.splitted', this.onCanvasTrackSplitted); - canvasInstance.html().removeEventListener('canvas.contextmenu', this.onCanvasPointContextMenu); canvasInstance.html().removeEventListener('canvas.error', this.onCanvasErrorOccurrence); window.removeEventListener('resize', this.fitCanvas); @@ -480,22 +476,12 @@ export default class CanvasWrapperComponent extends React.PureComponent { }; private onCanvasClicked = (): void => { - const { onUpdateContextMenu } = this.props; const { canvasInstance } = this.props as { canvasInstance: Canvas }; - onUpdateContextMenu(false, 0, 0, ContextMenuType.CANVAS_SHAPE); if (!canvasInstance.html().contains(document.activeElement) && document.activeElement instanceof HTMLElement) { document.activeElement.blur(); } }; - private onCanvasContextMenu = (e: MouseEvent): void => { - const { activatedStateID, onUpdateContextMenu } = this.props; - - if (e.target && !(e.target as HTMLElement).classList.contains('svg_select_points')) { - onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY, ContextMenuType.CANVAS_SHAPE); - } - }; - private onCanvasShapeDragged = (e: any): void => { const { jobInstance } = this.props; const { id } = e.detail; @@ -653,7 +639,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { const { activatedStateID, activatedAttributeID, - selectedOpacity, aamZoomMargin, workspace, annotations, @@ -672,10 +657,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { if (activatedState && activatedState.objectType !== ObjectType.TAG) { canvasInstance.activate(activatedStateID, activatedAttributeID); } - const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`); - if (el) { - ((el as any) as SVGElement).setAttribute('fill-opacity', `${selectedOpacity}`); - } } } @@ -746,7 +727,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().addEventListener('mousedown', this.onCanvasMouseDown); canvasInstance.html().addEventListener('click', this.onCanvasClicked); - canvasInstance.html().addEventListener('contextmenu', this.onCanvasContextMenu); canvasInstance.html().addEventListener('canvas.editstart', this.onCanvasEditStart); canvasInstance.html().addEventListener('canvas.edited', this.onCanvasEditDone); canvasInstance.html().addEventListener('canvas.dragstart', this.onCanvasDragStart); @@ -771,7 +751,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().addEventListener('canvas.regionselected', this.onCanvasPositionSelected); canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted); - canvasInstance.html().addEventListener('canvas.contextmenu', this.onCanvasPointContextMenu); canvasInstance.html().addEventListener('canvas.error', this.onCanvasErrorOccurrence); } @@ -826,6 +805,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { /> + }> diff --git a/cvat-ui/src/components/annotation-page/canvas/draggable-hoc.tsx b/cvat-ui/src/components/annotation-page/canvas/draggable-hoc.tsx new file mode 100644 index 00000000000..59cc85a4714 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/canvas/draggable-hoc.tsx @@ -0,0 +1,58 @@ +// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useEffect, useRef } from 'react'; + +export default function useDraggable( + getPosition: () => number[], + onDrag: (diffX: number, diffY: number) => void, + component: JSX.Element, +): JSX.Element { + const ref = useRef(null); + useEffect(() => { + if (!ref.current) return () => {}; + const click = [0, 0]; + const position = getPosition(); + + const mouseMoveListener = (event: MouseEvent): void => { + const dy = event.clientY - click[0]; + const dx = event.clientX - click[1]; + onDrag(position[0] + dy, position[1] + dx); + event.stopPropagation(); + event.preventDefault(); + }; + + const mouseDownListener = (event: MouseEvent): void => { + const [initialTop, initialLeft] = getPosition(); + position[0] = initialTop; + position[1] = initialLeft; + click[0] = event.clientY; + click[1] = event.clientX; + window.addEventListener('mousemove', mouseMoveListener); + event.stopPropagation(); + event.preventDefault(); + }; + + const mouseUpListener = (): void => { + window.removeEventListener('mousemove', mouseMoveListener); + }; + + window.document.addEventListener('mouseup', mouseUpListener); + ref.current.addEventListener('mousedown', mouseDownListener); + + return () => { + window.document.removeEventListener('mouseup', mouseUpListener); + if (ref.current) { + ref.current.removeEventListener('mousedown', mouseDownListener); + } + }; + }, [ref.current]); + + return ( +
+ {component} +
+ ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/control-visibility-observer.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/control-visibility-observer.tsx index fd6bb5891a3..28f96b44e26 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/control-visibility-observer.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/control-visibility-observer.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -8,6 +9,7 @@ import { SmallDashOutlined } from '@ant-design/icons'; import Popover from 'antd/lib/popover'; import React, { useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; +import { ConnectedComponent } from 'react-redux'; const extraControlsContentClassName = 'cvat-extra-controls-control-content'; @@ -48,7 +50,7 @@ export function ExtraControlsControl(): JSX.Element { } export default function ControlVisibilityObserver

( - ControlComponent: React.FunctionComponent

, + ControlComponent: React.FunctionComponent

| ConnectedComponent, ): React.FunctionComponent

{ let visibilityHeightThreshold = 0; // minimum value of height when element can be pushed to main panel diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx index 44ca08c2fe0..6fa50213a3e 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -9,8 +10,8 @@ import { ActiveControl, ObjectType, Rotation, ShapeType, } from 'reducers'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; -import { Canvas } from 'cvat-canvas-wrapper'; -import { Label } from 'components/labels-editor/common'; +import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; +import { LabelOptColor } from 'components/labels-editor/common'; import ControlVisibilityObserver, { ExtraControlsControl } from './control-visibility-observer'; import RotateControl, { Props as RotateControlProps } from './rotate-control'; @@ -26,6 +27,7 @@ import DrawPolylineControl, { Props as DrawPolylineControlProps } from './draw-p import DrawPointsControl, { Props as DrawPointsControlProps } from './draw-points-control'; import DrawEllipseControl, { Props as DrawEllipseControlProps } from './draw-ellipse-control'; import DrawCuboidControl, { Props as DrawCuboidControlProps } from './draw-cuboid-control'; +import DrawMaskControl, { Props as DrawMaskControlProps } from './draw-mask-control'; import DrawSkeletonControl, { Props as DrawSkeletonControlProps } from './draw-skeleton-control'; import SetupTagControl, { Props as SetupTagControlProps } from './setup-tag-control'; import MergeControl, { Props as MergeControlProps } from './merge-control'; @@ -65,6 +67,7 @@ const ObservedDrawPolylineControl = ControlVisibilityObserver(DrawPointsControl); const ObservedDrawEllipseControl = ControlVisibilityObserver(DrawEllipseControl); const ObservedDrawCuboidControl = ControlVisibilityObserver(DrawCuboidControl); +const ObservedDrawMaskControl = ControlVisibilityObserver(DrawMaskControl); const ObservedDrawSkeletonControl = ControlVisibilityObserver(DrawSkeletonControl); const ObservedSetupTagControl = ControlVisibilityObserver(SetupTagControl); const ObservedMergeControl = ControlVisibilityObserver(MergeControl); @@ -97,15 +100,17 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { let pointsControlVisible = withUnspecifiedType; let ellipseControlVisible = withUnspecifiedType; let cuboidControlVisible = withUnspecifiedType; + let maskControlVisible = withUnspecifiedType; let tagControlVisible = withUnspecifiedType; - const skeletonControlVisible = labels.some((label: Label) => label.type === 'skeleton'); - labels.forEach((label: Label) => { + const skeletonControlVisible = labels.some((label: LabelOptColor) => label.type === 'skeleton'); + labels.forEach((label: LabelOptColor) => { rectangleControlVisible = rectangleControlVisible || label.type === ShapeType.RECTANGLE; polygonControlVisible = polygonControlVisible || label.type === ShapeType.POLYGON; polylineControlVisible = polylineControlVisible || label.type === ShapeType.POLYLINE; pointsControlVisible = pointsControlVisible || label.type === ShapeType.POINTS; ellipseControlVisible = ellipseControlVisible || label.type === ShapeType.ELLIPSE; cuboidControlVisible = cuboidControlVisible || label.type === ShapeType.CUBOID; + maskControlVisible = maskControlVisible || label.type === ShapeType.MASK; tagControlVisible = tagControlVisible || label.type === ObjectType.TAG; }); @@ -156,11 +161,20 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { ActiveControl.DRAW_CUBOID, ActiveControl.DRAW_ELLIPSE, ActiveControl.DRAW_SKELETON, + ActiveControl.DRAW_MASK, ActiveControl.AI_TOOLS, ActiveControl.OPENCV_TOOLS, ].includes(activeControl); + const editing = canvasInstance.mode() === CanvasMode.EDIT; if (!drawing) { + if (editing) { + // users probably will press N as they are used to do when they want to finish editing + // in this case, if a mask is being edited we probably want to finish editing first + canvasInstance.edit({ enabled: false }); + return; + } + canvasInstance.cancel(); // repeateDrawShapes gets all the latest parameters // and calls canvasInstance.draw() with them @@ -306,6 +320,15 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { /> ) } + { + maskControlVisible && ( + + ) + } { skeletonControlVisible && ( { + canvasInstance.draw({ enabled: false }); + }, + } : { + className: 'cvat-draw-mask-control', + }; + + return disabled ? ( + + ) : ( + } + > + + + ); +} + +export default React.memo(DrawPointsControl); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-mask-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-mask-popover.tsx new file mode 100644 index 00000000000..504f236fc3f --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-mask-popover.tsx @@ -0,0 +1,61 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { Row, Col } from 'antd/lib/grid'; +import Button from 'antd/lib/button'; +import Text from 'antd/lib/typography/Text'; + +import LabelSelector from 'components/label-selector/label-selector'; +import CVATTooltip from 'components/common/cvat-tooltip'; + +interface Props { + labels: any[]; + selectedLabelID: number; + repeatShapeShortcut: string; + onChangeLabel(value: string): void; + onDraw(labelID: number): void; +} + +function DrawMaskPopover(props: Props): JSX.Element { + const { + labels, selectedLabelID, repeatShapeShortcut, onChangeLabel, onDraw, + } = props; + + return ( +

+ + + + Draw new mask + + + + + + Label + + + + + + + + + + + + + + +
+ ); +} + +export default React.memo(DrawMaskPopover); 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 7587e69f31c..6258b0dc116 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 @@ -21,11 +21,12 @@ import { Row, Col } from 'antd/lib/grid'; import notification from 'antd/lib/notification'; import message from 'antd/lib/message'; import Dropdown from 'antd/lib/dropdown'; +import Switch from 'antd/lib/switch'; import lodash from 'lodash'; import { AIToolsIcon } from 'icons'; import { Canvas, convertShapesForInteractor } from 'cvat-canvas-wrapper'; -import { getCore } from 'cvat-core-wrapper'; +import { getCore, Attribute, Label } from 'cvat-core-wrapper'; import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper'; import { CombinedState, ActiveControl, Model, ObjectType, ShapeType, ToolsBlockerState, ModelAttribute, @@ -40,7 +41,6 @@ import { import DetectorRunner, { DetectorRequestBody } from 'components/model-runner-modal/detector-runner'; import LabelSelector from 'components/label-selector/label-selector'; import CVATTooltip from 'components/common/cvat-tooltip'; -import { Attribute, Label } from 'components/labels-editor/common'; import ApproximationAccuracy, { thresholdFromAccuracy, @@ -75,6 +75,7 @@ interface DispatchToProps { switchNavigationBlocked(navigationBlocked: boolean): void; } +const MIN_SUPPORTED_INTERACTOR_VERSION = 2; const core = getCore(); const CustomPopover = withVisibilityHandling(Popover, 'tools-control'); @@ -139,6 +140,7 @@ interface State { activeInteractor: Model | null; activeLabelID: number; activeTracker: Model | null; + convertMasksToPolygons: boolean; trackedShapes: TrackedShape[]; fetching: boolean; pointsRecieved: boolean; @@ -203,8 +205,11 @@ export class ToolsControlComponent extends React.PureComponent { private interaction: { id: string | null; isAborted: boolean; - latestResponse: number[][]; - latestResult: number[][]; + latestResponse: { + mask: number[][], + points: number[][], + }; + lastestApproximatedPoints: number[][]; latestRequest: null | { interactor: Model; data: { @@ -219,6 +224,7 @@ export class ToolsControlComponent extends React.PureComponent { public constructor(props: Props) { super(props); this.state = { + convertMasksToPolygons: false, 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, @@ -233,8 +239,11 @@ export class ToolsControlComponent extends React.PureComponent { this.interaction = { id: null, isAborted: false, - latestResponse: [], - latestResult: [], + latestResponse: { + mask: [], + points: [], + }, + lastestApproximatedPoints: [], latestRequest: null, hideMessage: null, }; @@ -277,8 +286,8 @@ export class ToolsControlComponent extends React.PureComponent { this.interaction = { id: null, isAborted: false, - latestResponse: [], - latestResult: [], + latestResponse: { mask: [], points: [] }, + lastestApproximatedPoints: [], latestRequest: null, hideMessage: null, }; @@ -291,18 +300,19 @@ export class ToolsControlComponent extends React.PureComponent { } if (prevState.approxPolyAccuracy !== approxPolyAccuracy) { - 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.interaction.latestResult.flat(), - }, - onChangeToolsBlockerState: this.onChangeToolsBlockerState, + if (isActivated && mode === 'interaction' && this.interaction.latestResponse.points.length) { + this.approximateResponsePoints(this.interaction.latestResponse.points) + .then((points: number[][]) => { + this.interaction.lastestApproximatedPoints = points; + canvasInstance.interact({ + enabled: true, + intermediateShape: { + shapeType: ShapeType.POLYGON, + points: this.interaction.lastestApproximatedPoints.flat(), + }, + onChangeToolsBlockerState: this.onChangeToolsBlockerState, + }); }); - }); } } @@ -337,7 +347,7 @@ export class ToolsControlComponent extends React.PureComponent { private runInteractionRequest = async (interactionId: string): Promise => { const { jobInstance, canvasInstance } = this.props; - const { activeInteractor, fetching } = this.state; + const { activeInteractor, fetching, convertMasksToPolygons } = this.state; const { id, latestRequest } = this.interaction; if (id !== interactionId || !latestRequest || fetching) { @@ -360,18 +370,22 @@ export class ToolsControlComponent extends React.PureComponent { // run server request this.setState({ fetching: true }); const response = await core.lambda.call(jobInstance.taskId, interactor, data); + // approximation with cv.approxPolyDP - const approximated = await this.approximateResponsePoints(response); + const approximated = await this.approximateResponsePoints(response.points); if (this.interaction.id !== interactionId || this.interaction.isAborted) { // new interaction session or the session is aborted return; } - this.interaction.latestResponse = response; - this.interaction.latestResult = approximated; + this.interaction.latestResponse = { + mask: response.mask, + points: response.points, + }; + this.interaction.lastestApproximatedPoints = approximated; - this.setState({ pointsRecieved: !!response.length }); + this.setState({ pointsRecieved: !!response.points.length }); } finally { if (this.interaction.id === interactionId && this.interaction.hideMessage) { this.interaction.hideMessage(); @@ -381,12 +395,17 @@ export class ToolsControlComponent extends React.PureComponent { this.setState({ fetching: false }); } - if (this.interaction.latestResult.length) { + if (this.interaction.lastestApproximatedPoints.length) { + const height = this.interaction.latestResponse.mask.length; + const width = this.interaction.latestResponse.mask[0].length; + const maskPoints = this.interaction.latestResponse.mask.flat(); + maskPoints.push(0, 0, width - 1, height - 1); canvasInstance.interact({ enabled: true, intermediateShape: { - shapeType: ShapeType.POLYGON, - points: this.interaction.latestResult.flat(), + shapeType: convertMasksToPolygons ? ShapeType.POLYGON : ShapeType.MASK, + points: convertMasksToPolygons ? this.interaction.lastestApproximatedPoints.flat() : + maskPoints, }, onChangeToolsBlockerState: this.onChangeToolsBlockerState, }); @@ -420,8 +439,8 @@ export class ToolsControlComponent extends React.PureComponent { // prevent future requests if possible this.interaction.isAborted = true; this.interaction.latestRequest = null; - if (this.interaction.latestResult.length) { - this.constructFromPoints(this.interaction.latestResult); + if (this.interaction.lastestApproximatedPoints.length) { + this.constructFromPoints(this.interaction.lastestApproximatedPoints); } } else if (shapesUpdated) { const interactor = activeInteractor as Model; @@ -507,8 +526,17 @@ export class ToolsControlComponent extends React.PureComponent { private setActiveInteractor = (value: string): void => { const { interactors } = this.props; + const [interactor] = interactors.filter((_interactor: Model) => _interactor.id === value); + + if (interactor.version < MIN_SUPPORTED_INTERACTOR_VERSION) { + notification.warning({ + message: 'Interactor API is outdated', + description: 'Probably, you should consider updating the serverless function', + }); + } + this.setState({ - activeInteractor: interactors.filter((interactor: Model) => interactor.id === value)[0], + activeInteractor: interactor, }); }; @@ -756,7 +784,7 @@ export class ToolsControlComponent extends React.PureComponent { }); // eslint-disable-next-line no-await-in-loop const response = await core.lambda.call(jobInstance.taskId, tracker, { - frame: frame , + frame, shapes: trackableObjects.shapes, states: trackableObjects.states, }); @@ -797,21 +825,40 @@ export class ToolsControlComponent extends React.PureComponent { } private constructFromPoints(points: number[][]): void { + const { convertMasksToPolygons } = this.state; 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, - }); + if (convertMasksToPolygons) { + 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]); + } else { + const height = this.interaction.latestResponse.mask.length; + const width = this.interaction.latestResponse.mask[0].length; + const maskPoints = this.interaction.latestResponse.mask.flat(); + maskPoints.push(0, 0, width - 1, height - 1); + const object = new core.classes.ObjectState({ + frame, + objectType: ObjectType.SHAPE, + label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null, + shapeType: ShapeType.MASK, + points: maskPoints, + occluded: false, + zOrder: curZOrder, + }); - createAnnotations(jobInstance, frame, [object]); + createAnnotations(jobInstance, frame, [object]); + } } private async approximateResponsePoints(points: number[][]): Promise { @@ -833,6 +880,21 @@ export class ToolsControlComponent extends React.PureComponent { return points; } + private renderMasksConvertingBlock(): JSX.Element { + const { convertMasksToPolygons } = this.state; + return ( + + { + this.setState({ convertMasksToPolygons: checked }); + }} + /> + Convert masks to polygons + + ); + } + private renderLabelBlock(): JSX.Element { const { labels } = this.props; const { activeLabelID } = this.state; @@ -932,7 +994,9 @@ export class ToolsControlComponent extends React.PureComponent { private renderInteractorBlock(): JSX.Element { const { interactors, canvasInstance, onInteractionStart } = this.props; - const { activeInteractor, activeLabelID, fetching } = this.state; + const { + activeInteractor, activeLabelID, fetching, + } = this.state; if (!interactors.length) { return ( @@ -995,7 +1059,9 @@ export class ToolsControlComponent extends React.PureComponent { type='primary' loading={fetching} className='cvat-tools-interact-button' - disabled={!activeInteractor || fetching} + disabled={!activeInteractor || + fetching || + activeInteractor.version < MIN_SUPPORTED_INTERACTOR_VERSION} onClick={() => { this.setState({ mode: 'interaction' }); @@ -1074,7 +1140,7 @@ export class ToolsControlComponent extends React.PureComponent { case 'number': return dbAttribute.values.includes(value) || inputType === 'text'; case 'text': - return ['select', 'radio'].includes(dbAttribute.input_type) && dbAttribute.values.includes(value); + return ['select', 'radio'].includes(dbAttribute.inputType) && dbAttribute.values.includes(value); case 'select': return (inputType === 'radio' && dbAttribute.values.includes(value)) || inputType === 'text'; case 'radio': @@ -1105,10 +1171,8 @@ export class ToolsControlComponent extends React.PureComponent { if (!jobLabel || !modelLabel) return null; - return new core.classes.ObjectState({ - shapeType: data.type, + const objectData = { label: jobLabel, - points: data.points, objectType: ObjectType.SHAPE, frame, occluded: false, @@ -1118,7 +1182,8 @@ export class ToolsControlComponent extends React.PureComponent { const [modelAttr] = Object.entries(body.mapping[modelLabel].attributes) .find((value: string[]) => value[1] === attr.name) || []; const areCompatible = checkAttributesCompatibility( - model.attributes[modelLabel].find((mAttr) => mAttr.name === modelAttr), + model.attributes[modelLabel] + .find((mAttr) => mAttr.name === modelAttr), jobLabel.attributes.find((jobAttr: Attribute) => ( jobAttr.name === attr.name )), @@ -1132,6 +1197,28 @@ export class ToolsControlComponent extends React.PureComponent { return acc; }, {} as Record), zOrder: curZOrder, + }; + + if (data.type === 'mask' && data.points && body.convMaskToPoly) { + return new core.classes.ObjectState({ + ...objectData, + shapeType: 'polygon', + points: data.points, + }); + } + + if (data.type === 'mask') { + return new core.classes.ObjectState({ + ...objectData, + shapeType: data.type, + points: data.mask, + }); + } + + return new core.classes.ObjectState({ + ...objectData, + shapeType: data.type, + points: data.points, }); }, ).filter((state: any) => state); @@ -1164,6 +1251,7 @@ export class ToolsControlComponent extends React.PureComponent { + {this.renderMasksConvertingBlock()} {this.renderLabelBlock()} {this.renderInteractorBlock()} @@ -1184,7 +1272,7 @@ export class ToolsControlComponent extends React.PureComponent { interactors, detectors, trackers, isActivated, canvasInstance, labels, frameIsDeleted, } = this.props; const { - fetching, approxPolyAccuracy, pointsRecieved, mode, portals, + fetching, approxPolyAccuracy, pointsRecieved, mode, portals, convertMasksToPolygons, } = this.state; if (![...interactors, ...detectors, ...trackers].length) return null; @@ -1209,7 +1297,7 @@ export class ToolsControlComponent extends React.PureComponent { }; const showAnyContent = labels.length && !frameIsDeleted; - const showInteractionContent = isActivated && mode === 'interaction' && pointsRecieved; + const showInteractionContent = isActivated && mode === 'interaction' && pointsRecieved && convertMasksToPolygons; const showDetectionContent = fetching && mode === 'detection'; const interactionContent: JSX.Element | null = showInteractionContent ? ( diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-basics.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-basics.tsx index 2231748c140..12c63eadd5c 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-basics.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-basics.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -43,6 +44,7 @@ interface Props { toBackground(): void; toForeground(): void; resetCuboidPerspective(): void; + edit(): void; } function ItemTopComponent(props: Props): JSX.Element { @@ -75,6 +77,7 @@ function ItemTopComponent(props: Props): JSX.Element { toBackground, toForeground, resetCuboidPerspective, + edit, jobInstance, } = props; @@ -150,6 +153,7 @@ function ItemTopComponent(props: Props): JSX.Element { toForeground, resetCuboidPerspective, changeColorPickerVisible, + edit, })} > diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx index ecf12028b40..4a8eca564c6 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -6,7 +7,7 @@ import React from 'react'; import Menu from 'antd/lib/menu'; import Button from 'antd/lib/button'; import Icon, { - LinkOutlined, CopyOutlined, BlockOutlined, RetweetOutlined, DeleteOutlined, + LinkOutlined, CopyOutlined, BlockOutlined, RetweetOutlined, DeleteOutlined, EditOutlined, } from '@ant-design/icons'; import { @@ -44,6 +45,7 @@ interface Props { toForeground(): void; resetCuboidPerspective(): void; changeColorPickerVisible(visible: boolean): void; + edit(): void; jobInstance: any; } @@ -77,6 +79,20 @@ function MakeCopyItem(props: ItemProps): JSX.Element { ); } +function EditMaskItem(props: ItemProps): JSX.Element { + const { toolProps, ...rest } = props; + const { edit } = toolProps; + return ( + + + + + + ); +} + function PropagateItem(props: ItemProps): JSX.Element { const { toolProps, ...rest } = props; const { propagateShortcut, propagate } = toolProps; @@ -209,6 +225,7 @@ export default function ItemMenu(props: Props): JSX.Element { TO_FOREGROUND = 'to_foreground', SWITCH_COLOR = 'switch_color', REMOVE_ITEM = 'remove_item', + EDIT_MASK = 'edit_mask', } const is2D = jobInstance.dimension === DimensionType.DIM_2D; @@ -219,6 +236,7 @@ export default function ItemMenu(props: Props): JSX.Element { {!readonly && objectType !== ObjectType.TAG && ( )} + {!readonly && } {!readonly && } {is2D && !readonly && [ShapeType.POLYGON, ShapeType.POLYLINE, ShapeType.CUBOID].includes(shapeType) && ( diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index a6c58b53da9..b5909e8de46 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -40,6 +41,7 @@ interface Props { changeLabel(label: any): void; changeColor(color: string): void; resetCuboidPerspective(): void; + edit(): void; } function ObjectItemComponent(props: Props): JSX.Element { @@ -69,6 +71,7 @@ function ObjectItemComponent(props: Props): JSX.Element { changeLabel, changeColor, resetCuboidPerspective, + edit, jobInstance, } = props; @@ -124,6 +127,7 @@ function ObjectItemComponent(props: Props): JSX.Element { toBackground={toBackground} toForeground={toForeground} resetCuboidPerspective={resetCuboidPerspective} + edit={edit} /> {!!attributes.length && ( 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 aed8b52f49d..a6c23fccb0c 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss @@ -115,6 +115,7 @@ .cvat-draw-polyline-control, .cvat-draw-points-control, .cvat-draw-ellipse-control, +.cvat-draw-mask-control, .cvat-draw-cuboid-control, .cvat-draw-skeleton-control, .cvat-setup-tag-control, @@ -191,7 +192,7 @@ .cvat-tools-track-button, .cvat-tools-interact-button { width: 100%; - margin-top: 10px; + margin-top: $grid-unit-size; } .cvat-interactors-tips-icon-container { @@ -199,6 +200,15 @@ font-size: 20px; } +.cvat-interactors-setups-container { + margin-top: $grid-unit-size; + margin-bottom: $grid-unit-size; + + > button { + margin-right: $grid-unit-size; + } +} + .cvat-interactor-tip-container { background: $background-color-2; padding: $grid-unit-size; diff --git a/cvat-ui/src/components/annotation-page/top-bar/filters-modal.tsx b/cvat-ui/src/components/annotation-page/top-bar/filters-modal.tsx index 6573b0f2174..ee550bc53ef 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/filters-modal.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/filters-modal.tsx @@ -112,6 +112,7 @@ function FiltersModalComponent(): JSX.Element { { value: 'cuboid', title: 'Cuboid' }, { value: 'ellipse', title: 'Ellipse' }, { value: 'skeleton', title: 'Skeleton' }, + { value: 'mask', title: 'Mask' }, ], }, }, diff --git a/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx b/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx index ec59c114d99..4b7a803dc46 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { Row, Col } from 'antd/lib/grid'; -import { ArrowRightOutlined, QuestionCircleOutlined, RightOutlined } from '@ant-design/icons'; +import { QuestionCircleOutlined } from '@ant-design/icons'; import Table from 'antd/lib/table'; import Modal from 'antd/lib/modal'; import Spin from 'antd/lib/spin'; @@ -114,6 +114,7 @@ function StatisticsModalComponent(props: StateToProps & DispatchToProps): JSX.El ellipse: `${data.label[key].ellipse.shape} / ${data.label[key].ellipse.track}`, cuboid: `${data.label[key].cuboid.shape} / ${data.label[key].cuboid.track}`, skeleton: `${data.label[key].skeleton.shape} / ${data.label[key].skeleton.track}`, + mask: `${data.label[key].mask.shape}`, tag: data.label[key].tag, manually: data.label[key].manually, interpolated: data.label[key].interpolated, @@ -130,6 +131,7 @@ function StatisticsModalComponent(props: StateToProps & DispatchToProps): JSX.El ellipse: `${data.total.ellipse.shape} / ${data.total.ellipse.track}`, cuboid: `${data.total.cuboid.shape} / ${data.total.cuboid.track}`, skeleton: `${data.total.skeleton.shape} / ${data.total.skeleton.track}`, + mask: `${data.total.mask.shape}`, tag: data.total.tag, manually: data.total.manually, interpolated: data.total.interpolated, @@ -137,7 +139,7 @@ function StatisticsModalComponent(props: StateToProps & DispatchToProps): JSX.El }); const makeShapesTracksTitle = (title: string): JSX.Element => ( - + {title} @@ -210,6 +212,12 @@ function StatisticsModalComponent(props: StateToProps & DispatchToProps): JSX.El key: 'skeleton', width: 100, }, + { + title: makeShapesTracksTitle('Mask'), + dataIndex: 'mask', + key: 'mask', + width: 100, + }, { title: Tag , dataIndex: 'tag', diff --git a/cvat-ui/src/components/import-dataset/import-dataset-modal.tsx b/cvat-ui/src/components/import-dataset/import-dataset-modal.tsx index fc6c426ed6c..f97fdd4552a 100644 --- a/cvat-ui/src/components/import-dataset/import-dataset-modal.tsx +++ b/cvat-ui/src/components/import-dataset/import-dataset-modal.tsx @@ -49,6 +49,7 @@ const initialValues: FormValues = { interface UploadParams { resource: 'annotation' | 'dataset'; + convMaskToPoly: boolean; useDefaultSettings: boolean; sourceStorage: Storage; selectedFormat: string | null; @@ -75,6 +76,7 @@ function ImportDatasetModal(props: StateToProps): JSX.Element { const [helpMessage, setHelpMessage] = useState(''); const [selectedSourceStorageLocation, setSelectedSourceStorageLocation] = useState(StorageLocation.LOCAL); const [uploadParams, setUploadParams] = useState({ + convMaskToPoly: true, useDefaultSettings: true, } as UploadParams); const [resource, setResource] = useState(''); @@ -242,7 +244,8 @@ function ImportDatasetModal(props: StateToProps): JSX.Element { instance, uploadParams.selectedFormat as string, uploadParams.useDefaultSettings, uploadParams.sourceStorage, uploadParams.file || uploadParams.fileName as string, - )); + uploadParams.convMaskToPoly, + ) as any); const resToPrint = uploadParams.resource.charAt(0).toUpperCase() + uploadParams.resource.slice(1); Notification.info({ message: `${resToPrint} import started`, @@ -314,11 +317,15 @@ function ImportDatasetModal(props: StateToProps): JSX.Element { onCancel={closeModal} onOk={() => form.submit()} className='cvat-modal-import-dataset' + destroyOnClose >
@@ -371,7 +378,27 @@ function ImportDatasetModal(props: StateToProps): JSX.Element { )} - + + + { + setUploadParams({ + ...uploadParams, + convMaskToPoly: value, + } as UploadParams); + }} + /> + + Convert masks to polygons + + + + + ; export interface DetectorRequestBody { mapping: MappedLabelsList; cleanup: boolean; + convMaskToPoly: boolean; } interface Match { @@ -57,6 +58,7 @@ function DetectorRunner(props: Props): JSX.Element { const [threshold, setThreshold] = useState(0.5); const [distance, setDistance] = useState(50); const [cleanup, setCleanup] = useState(false); + const [convertMasksToPolygons, setConvertMasksToPolygons] = useState(false); const [match, setMatch] = useState({ model: null, task: null }); const [attrMatches, setAttrMatch] = useState>({}); @@ -352,14 +354,24 @@ function DetectorRunner(props: Props): JSX.Element { ) : null} + {isDetector && ( +
+ { + setConvertMasksToPolygons(checked); + }} + /> + Convert masks to polygons +
+ )} {isDetector && withCleanup ? ( -
- + setCleanup(e.target.checked)} - > - Clean old annotations - + onChange={(checked: boolean): void => setCleanup(checked)} + /> + Clean previous annotations
) : null} {isReId ? ( @@ -414,6 +426,7 @@ function DetectorRunner(props: Props): JSX.Element { const detectorRequestBody: DetectorRequestBody = { mapping, cleanup, + convMaskToPoly: convertMasksToPolygons, }; runInference( diff --git a/cvat-ui/src/components/model-runner-modal/styles.scss b/cvat-ui/src/components/model-runner-modal/styles.scss index ff6409400f7..273bb6d96ab 100644 --- a/cvat-ui/src/components/model-runner-modal/styles.scss +++ b/cvat-ui/src/components/model-runner-modal/styles.scss @@ -15,3 +15,10 @@ .cvat-run-model-label-attribute-block { padding-left: $grid-unit-size * 4; } + +.detector-runner-clean-previous-annotations-wrapper, +.detector-runner-convert-masks-to-polygons-wrapper { + span { + margin-left: $grid-unit-size; + } +} diff --git a/cvat-ui/src/containers/annotation-page/canvas/canvas-context-menu.tsx b/cvat-ui/src/containers/annotation-page/canvas/canvas-context-menu.tsx index c51baaff988..748b75c6f10 100644 --- a/cvat-ui/src/containers/annotation-page/canvas/canvas-context-menu.tsx +++ b/cvat-ui/src/containers/annotation-page/canvas/canvas-context-menu.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -6,12 +7,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { CombinedState, ContextMenuType, Workspace } from 'reducers'; +import { + CombinedState, ContextMenuType, ShapeType, Workspace, +} from 'reducers'; import CanvasContextMenuComponent from 'components/annotation-page/canvas/canvas-context-menu'; import { updateCanvasContextMenu } from 'actions/annotation-actions'; import { reviewActions, finishIssueAsync } from 'actions/review-actions'; import { ThunkDispatch } from 'utils/redux'; +import { Canvas } from 'cvat-canvas-wrapper'; import { ObjectState } from 'cvat-core-wrapper'; interface OwnProps { @@ -21,6 +25,7 @@ interface OwnProps { interface StateToProps { contextMenuParentID: number | null; contextMenuClientID: number | null; + canvasInstance: Canvas | null; objectStates: any[]; visible: boolean; top: number; @@ -29,9 +34,14 @@ interface StateToProps { collapsed: boolean | undefined; workspace: Workspace; latestComments: string[]; + activatedStateID: number | null; } interface DispatchToProps { + onUpdateContextMenu( + visible: boolean, left: number, top: number, + pointID: number | null, type?: ContextMenuType, + ): void; onStartIssue(position: number[]): void; openIssue(position: number[], message: string): void; } @@ -39,8 +49,9 @@ interface DispatchToProps { function mapStateToProps(state: CombinedState): StateToProps { const { annotation: { - annotations: { collapsed, states: objectStates }, + annotations: { collapsed, states: objectStates, activatedStateID }, canvas: { + instance, contextMenu: { visible, top, left, type, clientID, parentID, }, @@ -63,7 +74,9 @@ function mapStateToProps(state: CombinedState): StateToProps { contextMenuClientID: clientID, contextMenuParentID: parentID, collapsed: clientID !== null ? collapsed[clientID] : undefined, + activatedStateID, objectStates, + canvasInstance: instance instanceof Canvas ? instance : null, visible: clientID !== null && visible && @@ -79,6 +92,12 @@ function mapStateToProps(state: CombinedState): StateToProps { function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps { return { + onUpdateContextMenu( + visible: boolean, left: number, top: number, + pointID: number | null, type?: ContextMenuType, + ): void { + dispatch(updateCanvasContextMenu(visible, left, top, pointID, type)); + }, onStartIssue(position: number[]): void { dispatch(reviewActions.startIssue(position)); dispatch(updateCanvasContextMenu(false, 0, 0)); @@ -144,8 +163,15 @@ class CanvasContextMenuContainer extends React.PureComponent { } public componentDidMount(): void { + const { canvasInstance } = this.props; this.updatePositionIfOutOfScreen(); + window.addEventListener('mousemove', this.moveContextMenu); + if (canvasInstance) { + canvasInstance.html().addEventListener('canvas.clicked', this.onClickCanvas); + canvasInstance.html().addEventListener('contextmenu', this.onOpenCanvasContextMenu); + canvasInstance.html().addEventListener('canvas.contextmenu', this.onCanvasPointContextMenu); + } } public componentDidUpdate(prevProps: Props): void { @@ -180,9 +206,46 @@ class CanvasContextMenuContainer extends React.PureComponent { } public componentWillUnmount(): void { + const { canvasInstance } = this.props; window.removeEventListener('mousemove', this.moveContextMenu); + if (canvasInstance) { + canvasInstance.html().removeEventListener('canvas.clicked', this.onClickCanvas); + canvasInstance.html().removeEventListener('contextmenu', this.onOpenCanvasContextMenu); + canvasInstance.html().removeEventListener('canvas.contextmenu', this.onCanvasPointContextMenu); + } } + private onClickCanvas = (): void => { + const { visible, onUpdateContextMenu } = this.props; + if (visible) { + onUpdateContextMenu(false, 0, 0, null, ContextMenuType.CANVAS_SHAPE); + } + }; + + private onOpenCanvasContextMenu = (e: MouseEvent): void => { + const { activatedStateID, onUpdateContextMenu } = this.props; + if (e.target && !(e.target as HTMLElement).classList.contains('svg_select_points')) { + onUpdateContextMenu( + activatedStateID !== null, e.clientX, e.clientY, null, ContextMenuType.CANVAS_SHAPE, + ); + } + }; + + private onCanvasPointContextMenu = (e: any): void => { + const { objectStates, activatedStateID, onUpdateContextMenu } = this.props; + + const [state] = objectStates.filter((el: any) => el.clientID === activatedStateID); + if (![ShapeType.CUBOID, ShapeType.RECTANGLE, ShapeType.MASK].includes(state.shapeType)) { + onUpdateContextMenu( + activatedStateID !== null, + e.detail.mouseEvent.clientX, + e.detail.mouseEvent.clientY, + e.detail.pointID, + ContextMenuType.CANVAS_SHAPE_POINT, + ); + } + }; + private moveContextMenu = (e: MouseEvent): void => { if (this.dragging) { this.setState((state) => { @@ -219,7 +282,7 @@ class CanvasContextMenuContainer extends React.PureComponent { } } - public render(): JSX.Element { + public render(): JSX.Element | null { const { left, top } = this.state; const { visible, @@ -235,23 +298,21 @@ class CanvasContextMenuContainer extends React.PureComponent { } = this.props; return ( - <> - {type === ContextMenuType.CANVAS_SHAPE && ( - - )} - + type === ContextMenuType.CANVAS_SHAPE ? ( + + ) : null ); } } 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 afb6d81c618..eac4d37a48b 100644 --- a/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx +++ b/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx @@ -232,6 +232,7 @@ function mapStateToProps(state: CombinedState): StateToProps { switchableAutomaticBordering: activeControl === ActiveControl.DRAW_POLYGON || activeControl === ActiveControl.DRAW_POLYLINE || + activeControl === ActiveControl.DRAW_MASK || activeControl === ActiveControl.EDIT, }; } diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-mask-popover.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-mask-popover.tsx new file mode 100644 index 00000000000..5f15588c6dc --- /dev/null +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-mask-popover.tsx @@ -0,0 +1,108 @@ +// Copyright (C) 2022 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { connect } from 'react-redux'; + +import DrawMaskPopoverComponent from 'components/annotation-page/standard-workspace/controls-side-bar/draw-mask-popover'; +import { rememberObject } from 'actions/annotation-actions'; +import { CombinedState, ShapeType, ObjectType } from 'reducers'; +import { Canvas } from 'cvat-canvas-wrapper'; + +interface DispatchToProps { + onDrawStart( + shapeType: ShapeType, + labelID: number, + objectType: ObjectType, + ): void; +} + +interface StateToProps { + normalizedKeyMap: Record; + canvasInstance: Canvas; + labels: any[]; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + onDrawStart( + shapeType: ShapeType, + labelID: number, + objectType: ObjectType, + ): void { + dispatch( + rememberObject({ + activeObjectType: objectType, + activeShapeType: shapeType, + activeLabelID: labelID, + }), + ); + }, + }; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { + annotation: { + canvas: { instance: canvasInstance }, + job: { labels }, + }, + shortcuts: { normalizedKeyMap }, + } = state; + + return { + canvasInstance: canvasInstance as Canvas, + normalizedKeyMap, + labels, + }; +} + +type Props = StateToProps & DispatchToProps; + +interface State { + selectedLabelID: number; +} + +class DrawShapePopoverContainer extends React.PureComponent { + constructor(props: Props) { + super(props); + const defaultLabelID = props.labels.length ? props.labels[0].id : null; + this.state = { selectedLabelID: defaultLabelID }; + } + + private onDraw = (): void => { + const { canvasInstance, onDrawStart } = this.props; + const { selectedLabelID } = this.state; + + canvasInstance.cancel(); + canvasInstance.draw({ + enabled: true, + shapeType: ShapeType.MASK, + crosshair: false, + }); + + onDrawStart(ShapeType.MASK, selectedLabelID, ObjectType.SHAPE); + }; + + private onChangeLabel = (value: any): void => { + this.setState({ selectedLabelID: value.id }); + }; + + public render(): JSX.Element { + const { selectedLabelID } = this.state; + const { normalizedKeyMap, labels } = this.props; + + return ( + + ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(DrawShapePopoverContainer); diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index 40088f08cb9..8be016b83a7 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -23,7 +24,7 @@ import ObjectStateItemComponent from 'components/annotation-page/standard-worksp import { getColor } from 'components/annotation-page/standard-workspace/objects-side-bar/shared'; import { shift } from 'utils/math'; import { Label } from 'cvat-core-wrapper'; -import { Canvas } from 'cvat-canvas-wrapper'; +import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; import { Canvas3d } from 'cvat-canvas3d-wrapper'; interface OwnProps { @@ -142,6 +143,18 @@ class ObjectItemContainer extends React.PureComponent { } }; + private edit = (): void => { + const { objectState, readonly, canvasInstance } = this.props; + + if (!readonly && canvasInstance instanceof Canvas) { + if (canvasInstance.mode() !== CanvasMode.IDLE) { + canvasInstance.cancel(); + } + + canvasInstance.edit({ enabled: true, state: objectState }); + } + } + private remove = (): void => { const { objectState, readonly, removeObject, @@ -342,6 +355,7 @@ class ObjectItemContainer extends React.PureComponent { toForeground={this.toForeground} changeColor={this.changeColor} changeLabel={this.changeLabel} + edit={this.edit} resetCuboidPerspective={() => this.resetCuboidPerspective()} /> ); diff --git a/cvat-ui/src/icons.tsx b/cvat-ui/src/icons.tsx index 2f5b0921163..247f72131e8 100644 --- a/cvat-ui/src/icons.tsx +++ b/cvat-ui/src/icons.tsx @@ -53,7 +53,13 @@ import SVGCVATAzureProvider from './assets/vscode-icons_file-type-azure.svg'; import SVGCVATS3Provider from './assets/S3.svg'; import SVGCVATGoogleCloudProvider from './assets/google-cloud.svg'; import SVGRestoreIcon from './assets/restore-icon.svg'; +import SVGBrushIcon from './assets/brush-icon.svg'; +import SVGEraserIcon from './assets/eraser-icon.svg'; +import SVGPolygonPlusIcon from './assets/polygon-plus.svg'; +import SVGPolygonMinusIcon from './assets/polygon-minus.svg'; import SVGMultiPlusIcon from './assets/multi-plus-icon.svg'; +import SVGPlusIcon from './assets/plus-icon.svg'; +import SVGCheckIcon from './assets/check-icon.svg'; export const CVATLogo = React.memo((): JSX.Element => ); export const CursorIcon = React.memo((): JSX.Element => ); @@ -103,4 +109,10 @@ export const AzureProvider = React.memo((): JSX.Element => ); export const GoogleCloudProvider = React.memo((): JSX.Element => ); export const RestoreIcon = React.memo((): JSX.Element => ); +export const BrushIcon = React.memo((): JSX.Element => ); +export const EraserIcon = React.memo((): JSX.Element => ); +export const PolygonPlusIcon = React.memo((): JSX.Element => ); +export const PolygonMinusIcon = React.memo((): JSX.Element => ); export const MutliPlusIcon = React.memo((): JSX.Element => ); +export const PlusIcon = React.memo((): JSX.Element => ); +export const CheckIcon = React.memo((): JSX.Element => ); diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 7147c2b6aed..15bd9d55fd1 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -42,6 +42,11 @@ const defaultState: AnnotationState = { clientID: null, parentID: null, }, + brushTools: { + visible: false, + top: 0, + left: 0, + }, instance: null, ready: false, activeControl: ActiveControl.CURSOR, @@ -484,22 +489,21 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { const { payload } = action; let { activeControl } = state.canvas; - if (payload.activeShapeType === ShapeType.RECTANGLE) { - activeControl = ActiveControl.DRAW_RECTANGLE; - } else if (payload.activeShapeType === ShapeType.POLYGON) { - activeControl = ActiveControl.DRAW_POLYGON; - } else if (payload.activeShapeType === ShapeType.POLYLINE) { - activeControl = ActiveControl.DRAW_POLYLINE; - } else if (payload.activeShapeType === ShapeType.POINTS) { - activeControl = ActiveControl.DRAW_POINTS; - } else if (payload.activeShapeType === ShapeType.ELLIPSE) { - activeControl = ActiveControl.DRAW_ELLIPSE; - } else if (payload.activeShapeType === ShapeType.CUBOID) { - activeControl = ActiveControl.DRAW_CUBOID; - } else if (payload.activeShapeType === ShapeType.SKELETON) { - activeControl = ActiveControl.DRAW_SKELETON; - } else if (payload.activeObjectType === ObjectType.TAG) { + if ('activeObjectType' in payload && payload.activeObjectType === ObjectType.TAG) { activeControl = ActiveControl.CURSOR; + } else if ('activeShapeType' in payload) { + const controlMapping = { + [ShapeType.RECTANGLE]: ActiveControl.DRAW_RECTANGLE, + [ShapeType.POLYGON]: ActiveControl.DRAW_POLYGON, + [ShapeType.POLYLINE]: ActiveControl.DRAW_POLYLINE, + [ShapeType.POINTS]: ActiveControl.DRAW_POINTS, + [ShapeType.ELLIPSE]: ActiveControl.DRAW_ELLIPSE, + [ShapeType.CUBOID]: ActiveControl.DRAW_CUBOID, + [ShapeType.SKELETON]: ActiveControl.DRAW_SKELETON, + [ShapeType.MASK]: ActiveControl.DRAW_MASK, + }; + + activeControl = controlMapping[payload.activeShapeType as ShapeType]; } return { @@ -793,6 +797,9 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { canvas: { ...state.canvas, activeControl, + contextMenu: { + ...defaultState.canvas.contextMenu, + }, }, annotations: { ...state.annotations, @@ -991,6 +998,18 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }, }; } + case AnnotationActionTypes.UPDATE_BRUSH_TOOLS_CONFIG: { + return { + ...state, + canvas: { + ...state.canvas, + brushTools: { + ...state.canvas.brushTools, + ...action.payload, + }, + }, + }; + } case AnnotationActionTypes.REDO_ACTION_SUCCESS: case AnnotationActionTypes.UNDO_ACTION_SUCCESS: { const { activatedStateID } = state.annotations; diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index 5a1d12e48c3..5c7c5c1d5e8 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -317,6 +317,7 @@ export interface Model { id: string; name: string; labels: string[]; + version: number; attributes: Record; framework: string; description: string; @@ -558,6 +559,7 @@ export enum ActiveControl { DRAW_POLYLINE = 'draw_polyline', DRAW_POINTS = 'draw_points', DRAW_ELLIPSE = 'draw_ellipse', + DRAW_MASK = 'draw_mask', DRAW_CUBOID = 'draw_cuboid', DRAW_SKELETON = 'draw_skeleton', MERGE = 'merge', @@ -577,6 +579,7 @@ export enum ShapeType { POINTS = 'points', ELLIPSE = 'ellipse', CUBOID = 'cuboid', + MASK = 'mask', SKELETON = 'skeleton', } @@ -633,6 +636,11 @@ export interface AnnotationState { parentID: number | null; clientID: number | null; }; + brushTools: { + visible: boolean; + top: number; + left: number; + }; instance: Canvas | Canvas3d | null; ready: boolean; activeControl: ActiveControl; diff --git a/cvat-ui/src/reducers/settings-reducer.ts b/cvat-ui/src/reducers/settings-reducer.ts index 532abf0c3a8..4105c9e6f79 100644 --- a/cvat-ui/src/reducers/settings-reducer.ts +++ b/cvat-ui/src/reducers/settings-reducer.ts @@ -8,10 +8,10 @@ import { BoundariesActionTypes } from 'actions/boundaries-actions'; import { AuthActionTypes } from 'actions/auth-actions'; import { SettingsActionTypes } from 'actions/settings-actions'; import { AnnotationActionTypes } from 'actions/annotation-actions'; - import { SettingsState, GridColor, FrameSpeed, ColorBy, DimensionType, -} from '.'; +} from 'reducers'; +import { ObjectState, ShapeType } from 'cvat-core-wrapper'; const defaultState: SettingsState = { shapes: { @@ -383,6 +383,27 @@ export default (state = defaultState, action: AnyAction): SettingsState => { }, }; } + case AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS: + case AnnotationActionTypes.CREATE_ANNOTATIONS_SUCCESS: + case AnnotationActionTypes.CHANGE_FRAME_SUCCESS: { + const { states } = action.payload; + if (states.some((_state: ObjectState): boolean => _state.shapeType === ShapeType.MASK)) { + const MIN_OPACITY = 30; + const { shapes: { opacity } } = state; + if (opacity < MIN_OPACITY) { + return { + ...state, + shapes: { + ...state.shapes, + opacity: MIN_OPACITY, + selectedOpacity: MIN_OPACITY * 2, + }, + }; + } + } + + return state; + } case BoundariesActionTypes.RESET_AFTER_ERROR: case AnnotationActionTypes.GET_JOB_SUCCESS: { const { job } = action.payload; diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index f24cc0d397b..82ef2ac4c2a 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -4,9 +4,11 @@ # # SPDX-License-Identifier: MIT +from functools import reduce import os.path as osp import re import sys +import numpy as np from collections import namedtuple from pathlib import Path from types import SimpleNamespace @@ -26,7 +28,7 @@ from cvat.apps.engine.models import Label, LabelType, Project, ShapeType, Task from .annotation import AnnotationIR, AnnotationManager, TrackManager -from .formats.transformations import EllipsesToMasks +from .formats.transformations import EllipsesToMasks, CVATRleToCOCORle CVAT_INTERNAL_ATTRIBUTES = {'occluded', 'outside', 'keyframe', 'track_id', 'rotation'} @@ -1568,6 +1570,15 @@ def convert_attrs(label, cvat_attrs): "group": anno_group, "attributes": anno_attr, }), cvat_frame_anno.height, cvat_frame_anno.width) + elif shape_obj.type == ShapeType.MASK: + anno = CVATRleToCOCORle.convert_mask(SimpleNamespace(**{ + "points": shape_obj.points, + "label": anno_label, + "z_order": shape_obj.z_order, + "rotation": shape_obj.rotation, + "group": anno_group, + "attributes": anno_attr, + }), cvat_frame_anno.height, cvat_frame_anno.width) elif shape_obj.type == ShapeType.POLYLINE: anno = dm.PolyLine(anno_points, label=anno_label, attributes=anno_attr, group=anno_group, @@ -1671,7 +1682,8 @@ def import_dm_annotations(dm_dataset: dm.Dataset, instance_data: Union[ProjectDa dm.AnnotationType.polyline: ShapeType.POLYLINE, dm.AnnotationType.points: ShapeType.POINTS, dm.AnnotationType.cuboid_3d: ShapeType.CUBOID, - dm.AnnotationType.skeleton: ShapeType.SKELETON + dm.AnnotationType.skeleton: ShapeType.SKELETON, + dm.AnnotationType.mask: ShapeType.MASK } label_cat = dm_dataset.categories()[dm.AnnotationType.label] @@ -1715,6 +1727,23 @@ def import_dm_annotations(dm_dataset: dm.Dataset, instance_data: Union[ProjectDa points = [] if ann.type == dm.AnnotationType.cuboid_3d: points = [*ann.position, *ann.rotation, *ann.scale, 0, 0, 0, 0, 0, 0, 0] + elif ann.type == dm.AnnotationType.mask: + istrue = np.argwhere(ann.image == 1).transpose() + top = int(istrue[0].min()) + left = int(istrue[1].min()) + bottom = int(istrue[0].max()) + right = int(istrue[1].max()) + points = ann.image[top:bottom + 1, left:right + 1] + + def reduce_fn(acc, v): + if v == acc['val']: + acc['res'][-1] += 1 + else: + acc['val'] = v + acc['res'].append(1) + return acc + points = reduce(reduce_fn, points.reshape(np.prod(points.shape)), { 'res': [0], 'val': False })['res'] + points.extend([int(left), int(top), int(right), int(bottom)]) elif ann.type != dm.AnnotationType.skeleton: points = ann.points diff --git a/cvat/apps/dataset_manager/formats/camvid.py b/cvat/apps/dataset_manager/formats/camvid.py index 6d9735c4e72..b6476e53391 100644 --- a/cvat/apps/dataset_manager/formats/camvid.py +++ b/cvat/apps/dataset_manager/formats/camvid.py @@ -11,7 +11,7 @@ import_dm_annotations) from cvat.apps.dataset_manager.util import make_zip_archive -from .transformations import RotatedBoxesToPolygons +from .transformations import MaskToPolygonTransformation, RotatedBoxesToPolygons from .registry import dm_env, exporter, importer from .utils import make_colormap @@ -33,12 +33,12 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='CamVid', ext='ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'camvid', env=dm_env) - dataset.transform('masks_to_polygons') + dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs) if load_data_callback is not None: load_data_callback(dataset, instance_data) import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/cityscapes.py b/cvat/apps/dataset_manager/formats/cityscapes.py index a7315688355..c660ad8d6de 100644 --- a/cvat/apps/dataset_manager/formats/cityscapes.py +++ b/cvat/apps/dataset_manager/formats/cityscapes.py @@ -13,7 +13,7 @@ import_dm_annotations) from cvat.apps.dataset_manager.util import make_zip_archive -from .transformations import RotatedBoxesToPolygons +from .transformations import MaskToPolygonTransformation, RotatedBoxesToPolygons from .registry import dm_env, exporter, importer from .utils import make_colormap @@ -34,7 +34,7 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='Cityscapes', ext='ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) @@ -45,7 +45,7 @@ def _import(src_file, instance_data, load_data_callback=None): write_label_map(labelmap_file, colormap) dataset = Dataset.import_from(tmp_dir, 'cityscapes', env=dm_env) - dataset.transform('masks_to_polygons') + dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs) if load_data_callback is not None: load_data_callback(dataset, instance_data) import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/coco.py b/cvat/apps/dataset_manager/formats/coco.py index fba7c3c474f..0674556c41a 100644 --- a/cvat/apps/dataset_manager/formats/coco.py +++ b/cvat/apps/dataset_manager/formats/coco.py @@ -25,7 +25,7 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='COCO', ext='JSON, ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): if zipfile.is_zipfile(src_file): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) @@ -49,7 +49,7 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='COCO Keypoints', ext='JSON, ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): def remove_extra_annotations(dataset): for item in dataset: annotations = [ann for ann in item.annotations diff --git a/cvat/apps/dataset_manager/formats/cvat.py b/cvat/apps/dataset_manager/formats/cvat.py index 5d286e22752..dc3e8f00c33 100644 --- a/cvat/apps/dataset_manager/formats/cvat.py +++ b/cvat/apps/dataset_manager/formats/cvat.py @@ -611,6 +611,11 @@ def open_points(self, points): self.xmlgen.startElement("points", points) self._level += 1 + def open_mask(self, points): + self._indent() + self.xmlgen.startElement("mask", points) + self._level += 1 + def open_cuboid(self, cuboid): self._indent() self.xmlgen.startElement("cuboid", cuboid) @@ -657,6 +662,11 @@ def close_points(self): self._indent() self.xmlgen.endElement("points") + def close_mask(self): + self._level -= 1 + self._indent() + self.xmlgen.endElement("mask") + def close_cuboid(self): self._level -= 1 self._indent() @@ -775,7 +785,14 @@ def dump_labeled_shapes(shapes, is_skeleton=False): ("points", ''), ("rotation", "{:.2f}".format(shape.rotation)) ])) - + elif shape.type == "mask": + dump_data.update(OrderedDict([ + ("rle", f"{list(int (v) for v in shape.points[:-4])}"[1:-1]), + ("left", f"{int(shape.points[-4])}"), + ("top", f"{int(shape.points[-3])}"), + ("width", f"{int(shape.points[-2] - shape.points[-4])}"), + ("height", f"{int(shape.points[-1] - shape.points[-3])}"), + ])) else: dump_data.update(OrderedDict([ ("points", ';'.join(( @@ -800,6 +817,8 @@ def dump_labeled_shapes(shapes, is_skeleton=False): dumper.open_polyline(dump_data) elif shape.type == "points": dumper.open_points(dump_data) + elif shape.type == "mask": + dumper.open_mask(dump_data) elif shape.type == "cuboid": dumper.open_cuboid(dump_data) elif shape.type == "skeleton": @@ -826,6 +845,8 @@ def dump_labeled_shapes(shapes, is_skeleton=False): dumper.close_points() elif shape.type == "cuboid": dumper.close_cuboid() + elif shape.type == "mask": + dumper.close_mask() elif shape.type == "skeleton": dumper.close_skeleton() else: @@ -907,6 +928,14 @@ def dump_track(idx, track): dump_data.update(OrderedDict([ ("rotation", "{:.2f}".format(shape.rotation)) ])) + elif shape.type == "mask": + dump_data.update(OrderedDict([ + ("rle", f"{list(int (v) for v in shape.points[:-4])}"[1:-1]), + ("left", f"{int(shape.points[-4])}"), + ("top", f"{int(shape.points[-3])}"), + ("width", f"{int(shape.points[-2] - shape.points[-4])}"), + ("height", f"{int(shape.points[-1] - shape.points[-3])}"), + ])) elif shape.type == "cuboid": dump_data.update(OrderedDict([ ("xtl1", "{:.2f}".format(shape.points[0])), @@ -944,6 +973,8 @@ def dump_track(idx, track): dumper.open_polyline(dump_data) elif shape.type == "points": dumper.open_points(dump_data) + elif shape.type == 'mask': + dumper.open_mask(dump_data) elif shape.type == "cuboid": dumper.open_cuboid(dump_data) elif shape.type == 'skeleton': @@ -967,6 +998,8 @@ def dump_track(idx, track): dumper.close_polyline() elif shape.type == "points": dumper.close_points() + elif shape.type == 'mask': + dumper.close_mask() elif shape.type == "cuboid": dumper.close_cuboid() elif shape.type == "skeleton": @@ -1063,7 +1096,7 @@ def dump_track(idx, track): dumper.close_root() def load_anno(file_object, annotations): - supported_shapes = ('box', 'ellipse', 'polygon', 'polyline', 'points', 'cuboid', 'skeleton') + supported_shapes = ('box', 'ellipse', 'polygon', 'polyline', 'points', 'cuboid', 'skeleton', 'mask') context = ElementTree.iterparse(file_object, events=("start", "end")) context = iter(context) next(context) @@ -1211,6 +1244,12 @@ def load_anno(file_object, annotations): shape['points'].append(el.attrib['cy']) shape['points'].append("{:.2f}".format(float(el.attrib['cx']) + float(el.attrib['rx']))) shape['points'].append("{:.2f}".format(float(el.attrib['cy']) - float(el.attrib['ry']))) + elif el.tag == 'mask': + shape['points'] = el.attrib['rle'].split(',') + shape['points'].append(el.attrib['left']) + shape['points'].append(el.attrib['top']) + shape['points'].append("{}".format(int(el.attrib['left']) + int(el.attrib['width']))) + shape['points'].append("{}".format(int(el.attrib['top']) + int(el.attrib['height']))) elif el.tag == 'cuboid': shape['points'].append(el.attrib['xtl1']) shape['points'].append(el.attrib['ytl1']) @@ -1249,7 +1288,23 @@ def load_anno(file_object, annotations): track.elements.append(track_element) track_element = None else: - annotations.add_track(track) + if track.shapes[0].type == 'mask': + # convert mask tracks to shapes + # because mask track are not supported + annotations.add_shape(annotations.LabeledShape(**{ + 'attributes': track.shapes[0].attributes, + 'points': track.shapes[0].points, + 'type': track.shapes[0].type, + 'occluded': track.shapes[0].occluded, + 'frame': track.shapes[0].frame, + 'source': track.shapes[0].source, + 'rotation': track.shapes[0].rotation, + 'z_order': track.shapes[0].z_order, + 'group': track.shapes[0].group, + 'label': track.label, + })) + else: + annotations.add_track(track) track = None elif el.tag == 'image': image_is_opened = False @@ -1334,7 +1389,7 @@ def _export_images(dst_file, instance_data, save_images=False): anno_callback=dump_as_cvat_annotation, save_images=save_images) @importer(name='CVAT', ext='XML, ZIP', version='1.1') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): is_zip = zipfile.is_zipfile(src_file) src_file.seek(0) if is_zip: diff --git a/cvat/apps/dataset_manager/formats/datumaro.py b/cvat/apps/dataset_manager/formats/datumaro.py index b90bc5beac2..3ef8482433e 100644 --- a/cvat/apps/dataset_manager/formats/datumaro.py +++ b/cvat/apps/dataset_manager/formats/datumaro.py @@ -36,7 +36,7 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(tmp_dir, dst_file) @importer(name="Datumaro", ext="ZIP", version="1.0") -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) @@ -60,7 +60,7 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(tmp_dir, dst_file) @importer(name="Datumaro 3D", ext="ZIP", version="1.0", dimension=DimensionType.DIM_3D) -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) diff --git a/cvat/apps/dataset_manager/formats/icdar.py b/cvat/apps/dataset_manager/formats/icdar.py index 013ccf0a3ad..7e4048237e2 100644 --- a/cvat/apps/dataset_manager/formats/icdar.py +++ b/cvat/apps/dataset_manager/formats/icdar.py @@ -15,7 +15,7 @@ import_dm_annotations) from cvat.apps.dataset_manager.util import make_zip_archive -from .transformations import RotatedBoxesToPolygons +from .transformations import MaskToPolygonTransformation, RotatedBoxesToPolygons from .registry import dm_env, exporter, importer @@ -87,7 +87,7 @@ def _export_recognition(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='ICDAR Recognition', ext='ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'icdar_word_recognition', env=dm_env) @@ -106,7 +106,7 @@ def _export_localization(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='ICDAR Localization', ext='ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) @@ -130,12 +130,12 @@ def _export_segmentation(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='ICDAR Segmentation', ext='ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'icdar_text_segmentation', env=dm_env) dataset.transform(AddLabelToAnns, label='icdar') - dataset.transform('masks_to_polygons') + dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs) if load_data_callback is not None: load_data_callback(dataset, instance_data) import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/imagenet.py b/cvat/apps/dataset_manager/formats/imagenet.py index 37ffca23ed2..51cb2ee1447 100644 --- a/cvat/apps/dataset_manager/formats/imagenet.py +++ b/cvat/apps/dataset_manager/formats/imagenet.py @@ -29,7 +29,7 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='ImageNet', ext='ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) if glob(osp.join(tmp_dir, '*.txt')): diff --git a/cvat/apps/dataset_manager/formats/kitti.py b/cvat/apps/dataset_manager/formats/kitti.py index a380d76adad..57d46a94928 100644 --- a/cvat/apps/dataset_manager/formats/kitti.py +++ b/cvat/apps/dataset_manager/formats/kitti.py @@ -13,7 +13,7 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations) from cvat.apps.dataset_manager.util import make_zip_archive -from .transformations import RotatedBoxesToPolygons +from .transformations import MaskToPolygonTransformation, RotatedBoxesToPolygons from .registry import dm_env, exporter, importer from .utils import make_colormap @@ -35,7 +35,7 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(tmp_dir, dst_file) @importer(name='KITTI', ext='ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) @@ -49,7 +49,7 @@ def _import(src_file, instance_data, load_data_callback=None): if 'background' not in [label['name'] for _, label in labels_meta]: dataset.filter('/item/annotation[label != "background"]', filter_annotations=True) - dataset.transform('masks_to_polygons') + dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs) if load_data_callback is not None: load_data_callback(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/labelme.py b/cvat/apps/dataset_manager/formats/labelme.py index e46a778cc01..8a4753633e2 100644 --- a/cvat/apps/dataset_manager/formats/labelme.py +++ b/cvat/apps/dataset_manager/formats/labelme.py @@ -9,6 +9,7 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations) +from cvat.apps.dataset_manager.formats.transformations import MaskToPolygonTransformation from cvat.apps.dataset_manager.util import make_zip_archive from .registry import dm_env, exporter, importer @@ -24,12 +25,12 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='LabelMe', ext='ZIP', version='3.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'label_me', env=dm_env) - dataset.transform('masks_to_polygons') + dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs) if load_data_callback is not None: load_data_callback(dataset, instance_data) import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/lfw.py b/cvat/apps/dataset_manager/formats/lfw.py index 09444f0ee24..d1b5138cb63 100644 --- a/cvat/apps/dataset_manager/formats/lfw.py +++ b/cvat/apps/dataset_manager/formats/lfw.py @@ -14,7 +14,7 @@ @importer(name='LFW', ext='ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) diff --git a/cvat/apps/dataset_manager/formats/market1501.py b/cvat/apps/dataset_manager/formats/market1501.py index b5902002d46..07bdf21b29f 100644 --- a/cvat/apps/dataset_manager/formats/market1501.py +++ b/cvat/apps/dataset_manager/formats/market1501.py @@ -71,7 +71,7 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='Market-1501', ext='ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) diff --git a/cvat/apps/dataset_manager/formats/mask.py b/cvat/apps/dataset_manager/formats/mask.py index 710203f9985..fb84ffab80e 100644 --- a/cvat/apps/dataset_manager/formats/mask.py +++ b/cvat/apps/dataset_manager/formats/mask.py @@ -11,7 +11,7 @@ import_dm_annotations) from cvat.apps.dataset_manager.util import make_zip_archive -from .transformations import RotatedBoxesToPolygons +from .transformations import MaskToPolygonTransformation, RotatedBoxesToPolygons from .registry import dm_env, exporter, importer from .utils import make_colormap @@ -30,12 +30,12 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='Segmentation mask', ext='ZIP', version='1.1') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'voc', env=dm_env) - dataset.transform('masks_to_polygons') + dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs) if load_data_callback is not None: load_data_callback(dataset, instance_data) import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/mot.py b/cvat/apps/dataset_manager/formats/mot.py index 031aa3a5457..b0e392036be 100644 --- a/cvat/apps/dataset_manager/formats/mot.py +++ b/cvat/apps/dataset_manager/formats/mot.py @@ -99,7 +99,7 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='MOT', ext='ZIP', version='1.1') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) diff --git a/cvat/apps/dataset_manager/formats/mots.py b/cvat/apps/dataset_manager/formats/mots.py index 90f17184b45..b602f0a0539 100644 --- a/cvat/apps/dataset_manager/formats/mots.py +++ b/cvat/apps/dataset_manager/formats/mots.py @@ -14,7 +14,7 @@ find_dataset_root, match_dm_item) from cvat.apps.dataset_manager.util import make_zip_archive -from .transformations import RotatedBoxesToPolygons +from .transformations import MaskToPolygonTransformation, RotatedBoxesToPolygons from .registry import dm_env, exporter, importer @@ -109,12 +109,12 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='MOTS PNG', ext='ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) dataset = Dataset.import_from(tmp_dir, 'mots', env=dm_env) - dataset.transform('masks_to_polygons') + dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs) if load_data_callback is not None: load_data_callback(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/openimages.py b/cvat/apps/dataset_manager/formats/openimages.py index 94b80e7fcf8..21526c398ca 100644 --- a/cvat/apps/dataset_manager/formats/openimages.py +++ b/cvat/apps/dataset_manager/formats/openimages.py @@ -15,7 +15,7 @@ find_dataset_root, import_dm_annotations, match_dm_item) from cvat.apps.dataset_manager.util import make_zip_archive -from .transformations import RotatedBoxesToPolygons +from .transformations import MaskToPolygonTransformation, RotatedBoxesToPolygons from .registry import dm_env, exporter, importer @@ -51,7 +51,7 @@ def _export(dst_file, task_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='Open Images V6', ext='ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) @@ -79,7 +79,7 @@ def _import(src_file, instance_data, load_data_callback=None): dataset = Dataset.import_from(tmp_dir, 'open_images', image_meta=image_meta, env=dm_env) - dataset.transform('masks_to_polygons') + dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs) if load_data_callback is not None: load_data_callback(dataset, instance_data) import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/pascal_voc.py b/cvat/apps/dataset_manager/formats/pascal_voc.py index d965e25a5e4..bd297e62f94 100644 --- a/cvat/apps/dataset_manager/formats/pascal_voc.py +++ b/cvat/apps/dataset_manager/formats/pascal_voc.py @@ -13,6 +13,7 @@ from pyunpack import Archive from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations) +from cvat.apps.dataset_manager.formats.transformations import MaskToPolygonTransformation from cvat.apps.dataset_manager.util import make_zip_archive from .registry import dm_env, exporter, importer @@ -29,7 +30,7 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='PASCAL VOC', ext='ZIP', version='1.1') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) @@ -56,7 +57,7 @@ def _import(src_file, instance_data, load_data_callback=None): shutil.move(f, anno_dir) dataset = Dataset.import_from(tmp_dir, 'voc', env=dm_env) - dataset.transform('masks_to_polygons') + dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs) if load_data_callback is not None: load_data_callback(dataset, instance_data) import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/formats/pointcloud.py b/cvat/apps/dataset_manager/formats/pointcloud.py index 0d85f3d5d84..f92c036d8fa 100644 --- a/cvat/apps/dataset_manager/formats/pointcloud.py +++ b/cvat/apps/dataset_manager/formats/pointcloud.py @@ -28,7 +28,7 @@ def _export_images(dst_file, task_data, save_images=False): @importer(name='Sly Point Cloud Format', ext='ZIP', version='1.0', dimension=DimensionType.DIM_3D) -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: if zipfile.is_zipfile(src_file): diff --git a/cvat/apps/dataset_manager/formats/tfrecord.py b/cvat/apps/dataset_manager/formats/tfrecord.py index ec19071a2bd..f0b15f4a349 100644 --- a/cvat/apps/dataset_manager/formats/tfrecord.py +++ b/cvat/apps/dataset_manager/formats/tfrecord.py @@ -32,7 +32,7 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='TFRecord', ext='ZIP', version='1.0', enabled=tf_available) -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) diff --git a/cvat/apps/dataset_manager/formats/transformations.py b/cvat/apps/dataset_manager/formats/transformations.py index ecce6474a65..7258c9c7b21 100644 --- a/cvat/apps/dataset_manager/formats/transformations.py +++ b/cvat/apps/dataset_manager/formats/transformations.py @@ -36,6 +36,28 @@ def transform_item(self, item): return item.wrap(annotations=annotations) +class CVATRleToCOCORle(ItemTransform): + @staticmethod + def convert_mask(shape, img_h, img_w): + rle = shape.points[:-4] + left, top, right = list(math.trunc(v) for v in shape.points[-4:-1]) + mat = np.zeros((img_h, img_w), dtype=np.uint8) + width = right - left + 1 + value = 0 + offset = 0 + for rleCount in rle: + rleCount = math.trunc(rleCount) + while rleCount > 0: + x, y = offset % width, offset // width + mat[y + top][x + left] = value + rleCount -= 1 + offset += 1 + value = abs(value - 1) + + rle = mask_utils.encode(np.asfortranarray(mat)) + return dm.RleMask(rle=rle, label=shape.label, z_order=shape.z_order, + attributes=shape.attributes, group=shape.group) + class EllipsesToMasks: @staticmethod def convert_ellipse(ellipse, img_h, img_w): @@ -50,3 +72,19 @@ def convert_ellipse(ellipse, img_h, img_w): rle = mask_utils.encode(np.asfortranarray(mat)) return dm.RleMask(rle=rle, label=ellipse.label, z_order=ellipse.z_order, attributes=ellipse.attributes, group=ellipse.group) + +class MaskToPolygonTransformation: + """ + Manages common logic for mask to polygons conversion in dataset import. + This usecase is supposed for backward compatibility for the transition period. + """ + + @classmethod + def declare_arg_names(cls): + return ['conv_mask_to_poly'] + + @classmethod + def convert_dataset(cls, dataset, **kwargs): + if kwargs.get('conv_mask_to_poly', True): + dataset.transform('masks_to_polygons') + return dataset diff --git a/cvat/apps/dataset_manager/formats/velodynepoint.py b/cvat/apps/dataset_manager/formats/velodynepoint.py index a2e1ac68a37..887a4056bd5 100644 --- a/cvat/apps/dataset_manager/formats/velodynepoint.py +++ b/cvat/apps/dataset_manager/formats/velodynepoint.py @@ -29,7 +29,7 @@ def _export_images(dst_file, task_data, save_images=False): @importer(name='Kitti Raw Format', ext='ZIP', version='1.0', dimension=DimensionType.DIM_3D) -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: if zipfile.is_zipfile(src_file): zipfile.ZipFile(src_file).extractall(tmp_dir) diff --git a/cvat/apps/dataset_manager/formats/vggface2.py b/cvat/apps/dataset_manager/formats/vggface2.py index b01797994a4..bc296ca1049 100644 --- a/cvat/apps/dataset_manager/formats/vggface2.py +++ b/cvat/apps/dataset_manager/formats/vggface2.py @@ -25,7 +25,7 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='VGGFace2', ext='ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) diff --git a/cvat/apps/dataset_manager/formats/widerface.py b/cvat/apps/dataset_manager/formats/widerface.py index ce29cb93749..afa3cdac857 100644 --- a/cvat/apps/dataset_manager/formats/widerface.py +++ b/cvat/apps/dataset_manager/formats/widerface.py @@ -24,7 +24,7 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='WiderFace', ext='ZIP', version='1.0') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: zipfile.ZipFile(src_file).extractall(tmp_dir) diff --git a/cvat/apps/dataset_manager/formats/yolo.py b/cvat/apps/dataset_manager/formats/yolo.py index d1ac0e350a2..f72bc9803b5 100644 --- a/cvat/apps/dataset_manager/formats/yolo.py +++ b/cvat/apps/dataset_manager/formats/yolo.py @@ -28,7 +28,7 @@ def _export(dst_file, instance_data, save_images=False): make_zip_archive(temp_dir, dst_file) @importer(name='YOLO', ext='ZIP', version='1.1') -def _import(src_file, instance_data, load_data_callback=None): +def _import(src_file, instance_data, load_data_callback=None, **kwargs): with TemporaryDirectory() as tmp_dir: Archive(src_file.name).extractall(tmp_dir) diff --git a/cvat/apps/dataset_manager/project.py b/cvat/apps/dataset_manager/project.py index 524046200e6..4858429f7f5 100644 --- a/cvat/apps/dataset_manager/project.py +++ b/cvat/apps/dataset_manager/project.py @@ -132,7 +132,7 @@ def export(self, dst_file: str, exporter: Callable, host: str='', **options): def load_dataset_data(self, *args, **kwargs): load_dataset_data(self, *args, **kwargs) - def import_dataset(self, dataset_file, importer): + def import_dataset(self, dataset_file, importer, **options): project_data = ProjectData( annotation_irs=self.annotation_irs, db_project=self.db_project, @@ -141,7 +141,7 @@ def import_dataset(self, dataset_file, importer): ) project_data.soft_attribute_import = True - importer(dataset_file, project_data, self.load_dataset_data) + importer(dataset_file, project_data, self.load_dataset_data, **options) self.create({tid: ir.serialize() for tid, ir in self.annotation_irs.items() if tid in project_data.new_tasks}) @@ -150,7 +150,7 @@ def data(self) -> dict: raise NotImplementedError() @transaction.atomic -def import_dataset_as_project(project_id, dataset_file, format_name): +def import_dataset_as_project(project_id, dataset_file, format_name, conv_mask_to_poly): rq_job = rq.get_current_job() rq_job.meta['status'] = 'Dataset import has been started...' rq_job.meta['progress'] = 0. @@ -161,4 +161,4 @@ def import_dataset_as_project(project_id, dataset_file, format_name): importer = make_importer(format_name) with open(dataset_file, 'rb') as f: - project.import_dataset(f, importer) + project.import_dataset(f, importer, conv_mask_to_poly=conv_mask_to_poly) diff --git a/cvat/apps/dataset_manager/task.py b/cvat/apps/dataset_manager/task.py index 8210e331b78..d1d6af6727f 100644 --- a/cvat/apps/dataset_manager/task.py +++ b/cvat/apps/dataset_manager/task.py @@ -560,7 +560,7 @@ def export(self, dst_file, exporter, host='', **options): ) exporter(dst_file, job_data, **options) - def import_annotations(self, src_file, importer): + def import_annotations(self, src_file, importer, **options): job_data = JobData( annotation_ir=AnnotationIR(), db_job=self.db_job, @@ -568,7 +568,7 @@ def import_annotations(self, src_file, importer): ) self.delete() - importer(src_file, job_data) + importer(src_file, job_data, **options) self.create(job_data.data.slice(self.start_frame, self.stop_frame).serialize()) @@ -766,19 +766,19 @@ def export_task(task_id, dst_file, format_name, task.export(f, exporter, host=server_url, save_images=save_images) @transaction.atomic -def import_task_annotations(task_id, src_file, format_name): +def import_task_annotations(task_id, src_file, format_name, conv_mask_to_poly): task = TaskAnnotation(task_id) task.init_from_db() importer = make_importer(format_name) with open(src_file, 'rb') as f: - task.import_annotations(f, importer) + task.import_annotations(f, importer, conv_mask_to_poly=conv_mask_to_poly) @transaction.atomic -def import_job_annotations(job_id, src_file, format_name): +def import_job_annotations(job_id, src_file, format_name, conv_mask_to_poly): job = JobAnnotation(job_id) job.init_from_db() importer = make_importer(format_name) with open(src_file, 'rb') as f: - job.import_annotations(f, importer) + job.import_annotations(f, importer, conv_mask_to_poly=conv_mask_to_poly) diff --git a/cvat/apps/dataset_manager/tests/test_formats.py b/cvat/apps/dataset_manager/tests/test_formats.py index ba8b090fb7d..302991ef0a9 100644 --- a/cvat/apps/dataset_manager/tests/test_formats.py +++ b/cvat/apps/dataset_manager/tests/test_formats.py @@ -910,7 +910,7 @@ def _test_can_import_annotations(self, task, import_format): expected_ann.init_from_db() dm.task.import_task_annotations(task["id"], - file_path, import_format) + file_path, import_format, True) actual_ann = TaskAnnotation(task["id"]) actual_ann.init_from_db() @@ -962,6 +962,6 @@ def test_can_import_mots_annotations_with_splited_masks(self): task.update() task = self._create_task(task, images) - dm.task.import_task_annotations(task['id'], dataset_path, format_name) + dm.task.import_task_annotations(task['id'], dataset_path, format_name, True) self._test_can_import_annotations(task, format_name) diff --git a/cvat/apps/engine/mixins.py b/cvat/apps/engine/mixins.py index 5a4d03d0fd0..8dd38a62661 100644 --- a/cvat/apps/engine/mixins.py +++ b/cvat/apps/engine/mixins.py @@ -287,6 +287,7 @@ def import_annotations(self, request, pk, db_obj, import_func, rq_func, rq_id): return self.init_tus_upload(request) use_default_location = request.query_params.get('use_default_location', True) + conv_mask_to_poly = strtobool(request.query_params.get('conv_mask_to_poly', 'True')) use_settings = strtobool(str(use_default_location)) obj = db_obj if use_settings else request.query_params location_conf = get_location_configuration( @@ -307,6 +308,7 @@ def import_annotations(self, request, pk, db_obj, import_func, rq_func, rq_id): format_name=format_name, location_conf=location_conf, filename=file_name, + conv_mask_to_poly=conv_mask_to_poly, ) return self.upload_data(request) diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 93798fe793a..d12f5200351 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -568,6 +568,7 @@ class ShapeType(str, Enum): POINTS = 'points' # (x0, y0, ..., xn, yn) ELLIPSE = 'ellipse' # (cx, cy, rx, ty) CUBOID = 'cuboid' # (x0, y0, ..., x7, y7) + MASK = 'mask' # (rle mask, left, top, right, bottom) SKELETON = 'skeleton' @classmethod diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index f81991e361d..e30dac1df2c 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -8,6 +8,7 @@ import shutil from tempfile import NamedTemporaryFile +from typing import OrderedDict from rest_framework import serializers, exceptions from django.contrib.auth.models import User, Group @@ -919,14 +920,37 @@ class LabeledImageSerializer(AnnotationSerializer): attributes = AttributeValSerializer(many=True, source="labeledimageattributeval_set", default=[]) +class OptimizedFloatListField(serializers.ListField): + '''Default ListField is extremely slow when try to process long lists of points''' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs, child=serializers.FloatField()) + + def to_internal_value(self, data): + return self.run_child_validation(data) + + def to_representation(self, data): + return data + + def run_child_validation(self, data): + errors = OrderedDict() + for idx, item in enumerate(data): + if type(item) not in [int, float]: + errors[idx] = exceptions.ValidationError('Value must be a float or an integer') + + if not errors: + return data + + raise exceptions.ValidationError(errors) + + class ShapeSerializer(serializers.Serializer): type = serializers.ChoiceField(choices=models.ShapeType.choices()) occluded = serializers.BooleanField(default=False) outside = serializers.BooleanField(default=False, required=False) z_order = serializers.IntegerField(default=0) rotation = serializers.FloatField(default=0, min_value=0, max_value=360) - points = serializers.ListField( - child=serializers.FloatField(), + points = OptimizedFloatListField( allow_empty=True, required=False ) diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 7ac366e1abb..53b4c763541 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -389,7 +389,6 @@ def dataset(self, request, pk): self._object = self.get_object() # force to call check_object_permissions if request.method in {'POST', 'OPTIONS'}: - return self.import_annotations( request=request, pk=pk, @@ -460,6 +459,7 @@ def upload_finished(self, request): if self.action == 'dataset': format_name = request.query_params.get("format", "") filename = request.query_params.get("filename", "") + conv_mask_to_poly = strtobool(request.query_params.get('conv_mask_to_poly', 'True')) tmp_dir = self._object.get_tmp_dirname() uploaded_file = None if os.path.isfile(os.path.join(tmp_dir, filename)): @@ -471,6 +471,7 @@ def upload_finished(self, request): rq_func=dm.project.import_dataset_as_project, pk=self._object.pk, format_name=format_name, + conv_mask_to_poly=conv_mask_to_poly ) elif self.action == 'import_backup': filename = request.query_params.get("filename", "") @@ -865,6 +866,7 @@ def upload_finished(self, request): if self.action == 'annotations': format_name = request.query_params.get("format", "") filename = request.query_params.get("filename", "") + conv_mask_to_poly = strtobool(request.query_params.get('conv_mask_to_poly', 'True')) tmp_dir = self._object.get_tmp_dirname() if os.path.isfile(os.path.join(tmp_dir, filename)): annotation_file = os.path.join(tmp_dir, filename) @@ -875,6 +877,7 @@ def upload_finished(self, request): rq_func=dm.task.import_task_annotations, pk=self._object.pk, format_name=format_name, + conv_mask_to_poly=conv_mask_to_poly, ) else: return Response(data='No such file were uploaded', @@ -1099,6 +1102,7 @@ def annotations(self, request, pk): format_name = request.query_params.get('format') if format_name: use_settings = strtobool(str(request.query_params.get('use_default_location', True))) + conv_mask_to_poly = strtobool(request.query_params.get('conv_mask_to_poly', 'True')) obj = self._object if use_settings else request.query_params location_conf = get_location_configuration( obj=obj, use_settings=use_settings, field_name=StorageType.SOURCE @@ -1109,7 +1113,8 @@ def annotations(self, request, pk): rq_func=dm.task.import_task_annotations, pk=pk, format_name=format_name, - location_conf=location_conf + location_conf=location_conf, + conv_mask_to_poly=conv_mask_to_poly ) else: serializer = LabeledDataSerializer(data=request.data) @@ -1324,6 +1329,7 @@ def upload_finished(self, request): if self.action == 'annotations': format_name = request.query_params.get("format", "") filename = request.query_params.get("filename", "") + conv_mask_to_poly = strtobool(request.query_params.get('conv_mask_to_poly', 'True')) tmp_dir = task.get_tmp_dirname() if os.path.isfile(os.path.join(tmp_dir, filename)): annotation_file = os.path.join(tmp_dir, filename) @@ -1334,6 +1340,7 @@ def upload_finished(self, request): rq_func=dm.task.import_job_annotations, pk=self._object.pk, format_name=format_name, + conv_mask_to_poly=conv_mask_to_poly, ) else: return Response(data='No such file were uploaded', @@ -1449,6 +1456,7 @@ def annotations(self, request, pk): format_name = request.query_params.get('format', '') if format_name: use_settings = strtobool(str(request.query_params.get('use_default_location', True))) + conv_mask_to_poly = strtobool(request.query_params.get('conv_mask_to_poly', 'True')) obj = self._object.segment.task if use_settings else request.query_params location_conf = get_location_configuration( obj=obj, use_settings=use_settings, field_name=StorageType.SOURCE @@ -1459,7 +1467,8 @@ def annotations(self, request, pk): rq_func=dm.task.import_job_annotations, pk=pk, format_name=format_name, - location_conf=location_conf + location_conf=location_conf, + conv_mask_to_poly=conv_mask_to_poly ) else: serializer = LabeledDataSerializer(data=request.data) @@ -2164,7 +2173,7 @@ def _download_file_from_bucket(db_storage, filename, key): f.write(data.getbuffer()) def _import_annotations(request, rq_id, rq_func, pk, format_name, - filename=None, location_conf=None): + filename=None, location_conf=None, conv_mask_to_poly=True): format_desc = {f.DISPLAY_NAME: f for f in dm.views.get_import_formats()}.get(format_name) if format_desc is None: @@ -2211,7 +2220,7 @@ def _import_annotations(request, rq_id, rq_func, pk, format_name, av_scan_paths(filename) rq_job = queue.enqueue_call( func=rq_func, - args=(pk, filename, format_name), + args=(pk, filename, format_name, conv_mask_to_poly), job_id=rq_id, depends_on=dependent_job ) @@ -2331,7 +2340,7 @@ def _export_annotations(db_instance, rq_id, request, format_name, action, callba result_ttl=ttl, failure_ttl=ttl) return Response(status=status.HTTP_202_ACCEPTED) -def _import_project_dataset(request, rq_id, rq_func, pk, format_name, filename=None, location_conf=None): +def _import_project_dataset(request, rq_id, rq_func, pk, format_name, filename=None, conv_mask_to_poly=True, location_conf=None): format_desc = {f.DISPLAY_NAME: f for f in dm.views.get_import_formats()}.get(format_name) if format_desc is None: @@ -2372,7 +2381,7 @@ def _import_project_dataset(request, rq_id, rq_func, pk, format_name, filename=N rq_job = queue.enqueue_call( func=rq_func, - args=(pk, filename, format_name), + args=(pk, filename, format_name, conv_mask_to_poly), job_id=rq_id, meta={ 'tmp_file': filename, diff --git a/cvat/apps/lambda_manager/views.py b/cvat/apps/lambda_manager/views.py index 2a1d3f663e2..9a9209311f2 100644 --- a/cvat/apps/lambda_manager/views.py +++ b/cvat/apps/lambda_manager/views.py @@ -13,6 +13,7 @@ import requests import rq import os + from django.conf import settings from django.core.exceptions import ObjectDoesNotExist, ValidationError from rest_framework import status, viewsets @@ -139,6 +140,7 @@ def __init__(self, gateway, data): 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.version = int(meta_anno.get('version', '1')) self.help_message = meta_anno.get('help_message', '') self.gateway = gateway @@ -149,7 +151,8 @@ def to_dict(self): 'labels': self.labels, 'description': self.description, 'framework': self.framework, - 'name': self.name + 'name': self.name, + 'version': self.version } if self.kind is LambdaType.INTERACTOR: @@ -358,7 +361,7 @@ def get_jobs(self): return [LambdaJob(job) for job in jobs if job.meta.get("lambda")] - def enqueue(self, lambda_func, threshold, task, quality, mapping, cleanup, max_distance): + def enqueue(self, lambda_func, threshold, task, quality, mapping, cleanup, conv_mask_to_poly, max_distance): jobs = self.get_jobs() # It is still possible to run several concurrent jobs for the same task. # But the race isn't critical. The filtration is just a light-weight @@ -381,6 +384,7 @@ def enqueue(self, lambda_func, threshold, task, quality, mapping, cleanup, max_d "task": task, "quality": quality, "cleanup": cleanup, + "conv_mask_to_poly": conv_mask_to_poly, "mapping": mapping, "max_distance": max_distance }) @@ -453,7 +457,7 @@ def delete(self): self.job.delete() @staticmethod - def _call_detector(function, db_task, labels, quality, threshold, mapping): + def _call_detector(function, db_task, labels, quality, threshold, mapping, conv_mask_to_poly): class Results: def __init__(self, task_id): self.task_id = task_id @@ -509,22 +513,41 @@ def reset(self): "group": None, }) else: - results.append_shape({ + shape = { "frame": frame, "label_id": label['id'], "type": anno["type"], "occluded": False, - "points": anno["points"], + "points": anno["mask"] if anno["type"] == "mask" else anno["points"], "z_order": 0, "group": anno["group_id"] if "group_id" in anno else None, "attributes": attrs, "source": "auto" - }) + } + + if anno["type"] == "mask" and "points" in anno and conv_mask_to_poly: + shape["type"] = "polygon" + shape["points"] = anno["points"] + elif anno["type"] == "mask": + [xtl, ytl, xbr, ybr] = shape["points"][-4:] + cut_points = shape["points"][:-4] + rle = [0] + prev = shape["points"][0] + for val in cut_points: + if val == prev: + rle[-1] += 1 + else: + rle.append(1) + prev = val + rle.extend([xtl, ytl, xbr, ybr]) + shape["points"] = rle + + results.append_shape(shape) # Accumulate data during 100 frames before sumbitting results. # It is optimization to make fewer calls to our server. Also # it isn't possible to keep all results in memory. - if frame % 100 == 0: + if frame and frame % 100 == 0: results.submit() results.submit() @@ -636,7 +659,7 @@ def __call__(function, task, quality, cleanup, **kwargs): if function.kind == LambdaType.DETECTOR: LambdaJob._call_detector(function, db_task, labels, quality, - kwargs.get("threshold"), kwargs.get("mapping")) + kwargs.get("threshold"), kwargs.get("mapping"), kwargs.get("conv_mask_to_poly")) elif function.kind == LambdaType.REID: LambdaJob._call_reid(function, db_task, quality, kwargs.get("threshold"), kwargs.get("max_distance")) @@ -755,6 +778,7 @@ def create(self, request): task = request.data['task'] quality = request.data.get("quality") cleanup = request.data.get('cleanup', False) + conv_mask_to_poly = request.data.get('convMaskToPoly', False) mapping = request.data.get('mapping') max_distance = request.data.get('max_distance') except KeyError as err: @@ -767,7 +791,7 @@ def create(self, request): queue = LambdaQueue() lambda_func = gateway.get(function) job = queue.enqueue(lambda_func, threshold, task, quality, - mapping, cleanup, max_distance) + mapping, cleanup, conv_mask_to_poly, max_distance) return job.to_dict() diff --git a/serverless/common/openvino/shared.py b/serverless/common/openvino/shared.py new file mode 100644 index 00000000000..c12e1a7230e --- /dev/null +++ b/serverless/common/openvino/shared.py @@ -0,0 +1,9 @@ +# Copyright (C) 2022 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +def to_cvat_mask(box: list, mask): + xtl, ytl, xbr, ybr = box + flattened = mask[ytl:ybr + 1, xtl:xbr + 1].flat[:].tolist() + flattened.extend([xtl, ytl, xbr, ybr]) + return flattened diff --git a/serverless/openvino/dextr/nuclio/function.yaml b/serverless/openvino/dextr/nuclio/function.yaml index 16d22ff2990..0adeb064e34 100644 --- a/serverless/openvino/dextr/nuclio/function.yaml +++ b/serverless/openvino/dextr/nuclio/function.yaml @@ -3,6 +3,7 @@ metadata: namespace: cvat annotations: name: DEXTR + version: 2 type: interactor spec: framework: openvino diff --git a/serverless/openvino/dextr/nuclio/main.py b/serverless/openvino/dextr/nuclio/main.py index 5242b334fc8..69886045102 100644 --- a/serverless/openvino/dextr/nuclio/main.py +++ b/serverless/openvino/dextr/nuclio/main.py @@ -19,8 +19,12 @@ def handler(context, event): buf = io.BytesIO(base64.b64decode(data["image"])) image = Image.open(buf) - polygon = context.user_data.model.handle(image, points) - return context.Response(body=json.dumps(polygon), - headers={}, - content_type='application/json', - status_code=200) + mask, polygon = context.user_data.model.handle(image, points) + return context.Response(body=json.dumps({ + 'points': polygon, + 'mask': mask.tolist(), + }), + headers={}, + content_type='application/json', + status_code=200 + ) diff --git a/serverless/openvino/dextr/nuclio/model_handler.py b/serverless/openvino/dextr/nuclio/model_handler.py index 13449820980..40491f8ccb7 100644 --- a/serverless/openvino/dextr/nuclio/model_handler.py +++ b/serverless/openvino/dextr/nuclio/model_handler.py @@ -19,6 +19,7 @@ def __init__(self): # points: [[x1,y1], [x2,y2], [x3,y3], [x4,y4], ...] # Output: # polygon: [[x1,y1], [x2,y2], [x3,y3], [x4,y4], ...] + # mask: [[a, a, a, a, a, ...], [a, a, a, a, ...], ...] def handle(self, image, points): DEXTR_PADDING = 50 DEXTR_TRESHOLD = 0.8 @@ -76,8 +77,8 @@ def handle(self, image, points): if contours.size < 3 * 2: raise Exception('Less then three point have been detected. Can not build a polygon.') - result = [] + polygon = [] for point in contours: - result.append([int(point[0]), int(point[1])]) + polygon.append([int(point[0]), int(point[1])]) - return result + return result, polygon diff --git a/serverless/openvino/omz/intel/semantic-segmentation-adas-0001/nuclio/model_handler.py b/serverless/openvino/omz/intel/semantic-segmentation-adas-0001/nuclio/model_handler.py index 1315088a753..06b43f617a8 100644 --- a/serverless/openvino/omz/intel/semantic-segmentation-adas-0001/nuclio/model_handler.py +++ b/serverless/openvino/omz/intel/semantic-segmentation-adas-0001/nuclio/model_handler.py @@ -1,12 +1,15 @@ # Copyright (C) 2020-2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT import os import cv2 import numpy as np -from skimage.measure import approximate_polygon, find_contours from model_loader import ModelLoader +from shared import to_cvat_mask +from skimage.measure import approximate_polygon, find_contours + class ModelHandler: def __init__(self, labels): @@ -27,24 +30,33 @@ def infer(self, image, threshold): for i in range(len(self.labels)): mask_by_label = np.zeros((width, height), dtype=np.uint8) - mask_by_label = ((mask == float(i)) * 255).astype(np.float32) + mask_by_label = ((mask == float(i))).astype(np.uint8) mask_by_label = cv2.resize(mask_by_label, dsize=(image.width, image.height), interpolation=cv2.INTER_CUBIC) + cv2.normalize(mask_by_label, mask_by_label, 0, 255, cv2.NORM_MINMAX) contours = find_contours(mask_by_label, 0.8) for contour in contours: contour = np.flip(contour, axis=1) contour = approximate_polygon(contour, tolerance=2.5) + + x_min = max(0, int(np.min(contour[:,0]))) + x_max = max(0, int(np.max(contour[:,0]))) + y_min = max(0, int(np.min(contour[:,1]))) + y_max = max(0, int(np.max(contour[:,1]))) if len(contour) < 3: continue + cvat_mask = to_cvat_mask((x_min, y_min, x_max, y_max), mask_by_label) + results.append({ "confidence": None, "label": self.labels.get(i, "unknown"), "points": contour.ravel().tolist(), - "type": "polygon", + "mask": cvat_mask, + "type": "mask", }) - return results \ No newline at end of file + return results diff --git a/serverless/openvino/omz/intel/text-detection-0004/nuclio/model_handler.py b/serverless/openvino/omz/intel/text-detection-0004/nuclio/model_handler.py index 0bdcb989f37..ffcb1308986 100644 --- a/serverless/openvino/omz/intel/text-detection-0004/nuclio/model_handler.py +++ b/serverless/openvino/omz/intel/text-detection-0004/nuclio/model_handler.py @@ -1,4 +1,5 @@ # Copyright (C) 2020-2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT @@ -6,6 +7,8 @@ import cv2 import numpy as np from model_loader import ModelLoader +from shared import to_cvat_mask + class PixelLinkDecoder(): def __init__(self, pixel_threshold, link_threshold): @@ -207,11 +210,24 @@ def infer(self, image, pixel_threshold, link_threshold): pcd.decode(image.height, image.width, output_layer) for box in pcd.bboxes: + mask = pcd.pixel_mask + mask = np.array(mask, dtype=np.uint8) + mask = cv2.resize(mask, dsize=(image.width, image.height), interpolation=cv2.INTER_CUBIC) + cv2.normalize(mask, mask, 0, 255, cv2.NORM_MINMAX) + + box = box.ravel().tolist() + x_min = min(box[::2]) + x_max = max(box[::2]) + y_min = min(box[1::2]) + y_max = max(box[1::2]) + cvat_mask = to_cvat_mask((x_min, y_min, x_max, y_max), mask) + results.append({ "confidence": None, "label": self.labels.get(obj_class, "unknown"), - "points": box.ravel().tolist(), - "type": "polygon", + "points": box, + "mask": cvat_mask, + "type": "mask", }) - return results \ No newline at end of file + return results diff --git a/serverless/openvino/omz/public/mask_rcnn_inception_resnet_v2_atrous_coco/nuclio/model_handler.py b/serverless/openvino/omz/public/mask_rcnn_inception_resnet_v2_atrous_coco/nuclio/model_handler.py index 8805b16074c..a0b76d90bbf 100644 --- a/serverless/openvino/omz/public/mask_rcnn_inception_resnet_v2_atrous_coco/nuclio/model_handler.py +++ b/serverless/openvino/omz/public/mask_rcnn_inception_resnet_v2_atrous_coco/nuclio/model_handler.py @@ -1,32 +1,30 @@ # Copyright (C) 2020-2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT +import math import os import cv2 import numpy as np from model_loader import ModelLoader +from shared import to_cvat_mask from skimage.measure import approximate_polygon, find_contours - MASK_THRESHOLD = 0.5 # Ref: https://software.intel.com/en-us/forums/computer-vision/topic/804895 def segm_postprocess(box: list, raw_cls_mask, im_h, im_w): - ymin, xmin, ymax, xmax = box + xmin, ymin, xmax, ymax = box - width = int(abs(xmax - xmin)) - height = int(abs(ymax - ymin)) + width = xmax - xmin + 1 + height = ymax - ymin + 1 result = np.zeros((im_h, im_w), dtype=np.uint8) - resized_mask = cv2.resize(raw_cls_mask, dsize=(height, width), interpolation=cv2.INTER_CUBIC) + resized_mask = cv2.resize(raw_cls_mask, dsize=(width, height), interpolation=cv2.INTER_CUBIC) # extract the ROI of the image - ymin = int(round(ymin)) - xmin = int(round(xmin)) - ymax = ymin + height - xmax = xmin + width - result[xmin:xmax, ymin:ymax] = (resized_mask>MASK_THRESHOLD).astype(np.uint8) * 255 + result[ymin:ymax + 1, xmin:xmax + 1] = (resized_mask > MASK_THRESHOLD).astype(np.uint8) * 255 return result @@ -51,19 +49,20 @@ def infer(self, image, threshold): obj_value = box[2] obj_label = self.labels.get(obj_class, "unknown") if obj_value >= threshold: - xtl = box[3] * image.width - ytl = box[4] * image.height - xbr = box[5] * image.width - ybr = box[6] * image.height + xtl = math.trunc(box[3] * image.width) + ytl = math.trunc(box[4] * image.height) + xbr = math.trunc(box[5] * image.width) + ybr = math.trunc(box[6] * image.height) mask = masks[index][obj_class - 1] - mask = segm_postprocess((xtl, ytl, xbr, ybr), - mask, image.height, image.width) + mask = segm_postprocess((xtl, ytl, xbr, ybr), mask, image.height, image.width) + cvat_mask = to_cvat_mask((xtl, ytl, xbr, ybr), mask) contours = find_contours(mask, MASK_THRESHOLD) contour = contours[0] contour = np.flip(contour, axis=1) contour = approximate_polygon(contour, tolerance=2.5) + if len(contour) < 3: continue @@ -71,7 +70,8 @@ def infer(self, image, threshold): "confidence": str(obj_value), "label": obj_label, "points": contour.ravel().tolist(), - "type": "polygon", + "mask": cvat_mask, + "type": "mask", }) - return results \ No newline at end of file + return results diff --git a/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml b/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml index 6b11b2ff988..aaa973c9631 100644 --- a/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml +++ b/serverless/pytorch/saic-vul/fbrs/nuclio/function.yaml @@ -3,6 +3,7 @@ metadata: namespace: cvat annotations: name: f-BRS + version: 2 type: interactor spec: framework: pytorch diff --git a/serverless/pytorch/saic-vul/fbrs/nuclio/main.py b/serverless/pytorch/saic-vul/fbrs/nuclio/main.py index 36516cb2312..de6fab77ab8 100644 --- a/serverless/pytorch/saic-vul/fbrs/nuclio/main.py +++ b/serverless/pytorch/saic-vul/fbrs/nuclio/main.py @@ -1,4 +1,5 @@ # Copyright (C) 2020-2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT @@ -11,7 +12,7 @@ def init_context(context): context.logger.info("Init context... 0%") - model = ModelHandler() + model = ModelHandler() # pylint: disable=no-value-for-parameter context.user_data.model = model context.logger.info("Init context...100%") @@ -25,9 +26,12 @@ def handler(context, event): buf = io.BytesIO(base64.b64decode(data["image"])) image = Image.open(buf) - polygon = context.user_data.model.handle(image, pos_points, + mask, polygon = context.user_data.model.handle(image, pos_points, neg_points, threshold) - return context.Response(body=json.dumps(polygon), - headers={}, - content_type='application/json', - status_code=200) + return context.Response(body=json.dumps({ + 'points': polygon, + 'mask': mask.tolist(), + }), + headers={}, + content_type='application/json', + status_code=200) diff --git a/serverless/pytorch/saic-vul/fbrs/nuclio/model_handler.py b/serverless/pytorch/saic-vul/fbrs/nuclio/model_handler.py index 2667f49d300..8cb3d1034f9 100644 --- a/serverless/pytorch/saic-vul/fbrs/nuclio/model_handler.py +++ b/serverless/pytorch/saic-vul/fbrs/nuclio/model_handler.py @@ -1,4 +1,5 @@ # Copyright (C) 2020-2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT @@ -13,8 +14,6 @@ from isegm.inference.clicker import Clicker, Click def convert_mask_to_polygon(mask): - mask = np.array(mask, dtype=np.uint8) - cv2.normalize(mask, mask, 0, 255, cv2.NORM_MINMAX) contours = None if int(cv2.__version__.split('.')[0]) > 3: contours = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_KCOS)[0] @@ -84,8 +83,8 @@ def handle(self, image, pos_points, neg_points, threshold): if self.device == 'cuda': torch.cuda.empty_cache() object_mask = object_prob > threshold + object_mask = np.array(object_mask, dtype=np.uint8) + cv2.normalize(object_mask, object_mask, 0, 255, cv2.NORM_MINMAX) polygon = convert_mask_to_polygon(object_mask) - return polygon - - + return object_mask, polygon diff --git a/serverless/pytorch/saic-vul/hrnet/nuclio/function-gpu.yaml b/serverless/pytorch/saic-vul/hrnet/nuclio/function-gpu.yaml index d8471a3fa4b..939b472ffd9 100644 --- a/serverless/pytorch/saic-vul/hrnet/nuclio/function-gpu.yaml +++ b/serverless/pytorch/saic-vul/hrnet/nuclio/function-gpu.yaml @@ -3,6 +3,7 @@ metadata: namespace: cvat annotations: name: HRNET + version: 2 type: interactor spec: framework: pytorch diff --git a/serverless/pytorch/saic-vul/hrnet/nuclio/main.py b/serverless/pytorch/saic-vul/hrnet/nuclio/main.py index 62da161ad75..3ac93ae54a8 100644 --- a/serverless/pytorch/saic-vul/hrnet/nuclio/main.py +++ b/serverless/pytorch/saic-vul/hrnet/nuclio/main.py @@ -1,4 +1,5 @@ # Copyright (C) 2021-2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT @@ -11,7 +12,7 @@ def init_context(context): context.logger.info("Init context... 0%") - model = ModelHandler() + model = ModelHandler() # pylint: disable=no-value-for-parameter context.user_data.model = model context.logger.info("Init context...100%") @@ -25,9 +26,12 @@ def handler(context, event): buf = io.BytesIO(base64.b64decode(data["image"])) image = Image.open(buf) - polygon = context.user_data.model.handle(image, pos_points, + mask, polygon = context.user_data.model.handle(image, pos_points, neg_points, threshold) - return context.Response(body=json.dumps(polygon), - headers={}, - content_type='application/json', - status_code=200) + return context.Response(body=json.dumps({ + 'points': polygon, + 'mask': mask.tolist(), + }), + headers={}, + content_type='application/json', + status_code=200) diff --git a/serverless/pytorch/saic-vul/hrnet/nuclio/model_handler.py b/serverless/pytorch/saic-vul/hrnet/nuclio/model_handler.py index b43dbb936a5..c8b0373697a 100644 --- a/serverless/pytorch/saic-vul/hrnet/nuclio/model_handler.py +++ b/serverless/pytorch/saic-vul/hrnet/nuclio/model_handler.py @@ -1,4 +1,5 @@ # Copyright (C) 2021-2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT @@ -12,8 +13,6 @@ from isegm.inference.clicker import Clicker, Click def convert_mask_to_polygon(mask): - mask = np.array(mask, dtype=np.uint8) - cv2.normalize(mask, mask, 0, 255, cv2.NORM_MINMAX) contours = None if int(cv2.__version__.split('.')[0]) > 3: contours = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_KCOS)[0] @@ -63,6 +62,8 @@ def handle(self, image, pos_points, neg_points, threshold): if self.device == 'cuda': torch.cuda.empty_cache() object_mask = object_prob > threshold + object_mask = np.array(object_mask, dtype=np.uint8) + cv2.normalize(object_mask, object_mask, 0, 255, cv2.NORM_MINMAX) polygon = convert_mask_to_polygon(object_mask) - return polygon + return object_mask, polygon diff --git a/serverless/pytorch/shiyinzhang/iog/nuclio/function.yaml b/serverless/pytorch/shiyinzhang/iog/nuclio/function.yaml index 20e10ab195e..ece7de17ee5 100644 --- a/serverless/pytorch/shiyinzhang/iog/nuclio/function.yaml +++ b/serverless/pytorch/shiyinzhang/iog/nuclio/function.yaml @@ -3,6 +3,7 @@ metadata: namespace: cvat annotations: name: IOG + version: 2 type: interactor spec: framework: pytorch diff --git a/serverless/pytorch/shiyinzhang/iog/nuclio/main.py b/serverless/pytorch/shiyinzhang/iog/nuclio/main.py index 9df1f2e0b2b..6898cee9d5a 100644 --- a/serverless/pytorch/shiyinzhang/iog/nuclio/main.py +++ b/serverless/pytorch/shiyinzhang/iog/nuclio/main.py @@ -1,4 +1,5 @@ # Copyright (C) 2020-2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT @@ -12,7 +13,7 @@ def init_context(context): context.logger.info("Init context... 0%") - model = ModelHandler() + model = ModelHandler() # pylint: disable=no-value-for-parameter context.user_data.model = model context.logger.info("Init context...100%") @@ -32,9 +33,13 @@ def handler(context, event): obj_bbox = [np.min(x), np.min(y), np.max(x), np.max(y)] neg_points = [] - polygon = context.user_data.model.handle(image, obj_bbox, + mask, polygon = context.user_data.model.handle(image, obj_bbox, pos_points, neg_points, threshold) - return context.Response(body=json.dumps(polygon), - headers={}, - content_type='application/json', - status_code=200) + return context.Response(body=json.dumps({ + 'points': polygon, + 'mask': mask.tolist(), + }), + headers={}, + content_type='application/json', + status_code=200 + ) diff --git a/serverless/pytorch/shiyinzhang/iog/nuclio/model_handler.py b/serverless/pytorch/shiyinzhang/iog/nuclio/model_handler.py index bf687f2a6d7..e3bb1192d05 100644 --- a/serverless/pytorch/shiyinzhang/iog/nuclio/model_handler.py +++ b/serverless/pytorch/shiyinzhang/iog/nuclio/model_handler.py @@ -1,4 +1,5 @@ # Copyright (C) 2020-2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT @@ -10,8 +11,6 @@ from dataloaders import helpers def convert_mask_to_polygon(mask): - mask = np.array(mask, dtype=np.uint8) - cv2.normalize(mask, mask, 0, 255, cv2.NORM_MINMAX) contours = None if int(cv2.__version__.split('.')[0]) > 3: contours = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_KCOS)[0] @@ -108,16 +107,16 @@ def translate_points_to_crop(points): pred = np.squeeze(pred) # Convert a mask to a polygon - polygon = convert_mask_to_polygon(pred) - def translate_points_to_image(points): - points = [ - (p[0] / crop_scale[0] + crop_bbox[0], # x - p[1] / crop_scale[1] + crop_bbox[1]) # y - for p in points] - - return points + pred = np.array(pred, dtype=np.uint8) + pred = cv2.resize(pred, dsize=(crop_shape[0], crop_shape[1]), + interpolation=cv2.INTER_CUBIC) + cv2.normalize(pred, pred, 0, 255, cv2.NORM_MINMAX) - polygon = translate_points_to_image(polygon) + mask = np.zeros((image.height, image.width), dtype=np.uint8) + x = int(crop_bbox[0]) + y = int(crop_bbox[1]) + mask[y : y + crop_shape[1], x : x + crop_shape[0]] = pred - return polygon + polygon = convert_mask_to_polygon(mask) + return mask, polygon diff --git a/serverless/tensorflow/matterport/mask_rcnn/nuclio/main.py b/serverless/tensorflow/matterport/mask_rcnn/nuclio/main.py index 75829539333..99c533b0eee 100644 --- a/serverless/tensorflow/matterport/mask_rcnn/nuclio/main.py +++ b/serverless/tensorflow/matterport/mask_rcnn/nuclio/main.py @@ -15,7 +15,7 @@ def init_context(context): labels_spec = functionconfig['metadata']['annotations']['spec'] labels = {item['id']: item['name'] for item in json.loads(labels_spec)} - model_handler = ModelLoader(labels) + model_handler = ModelLoader(labels) # pylint: disable=no-value-for-parameter context.user_data.model_handler = model_handler context.logger.info("Init context...100%") @@ -30,4 +30,4 @@ def handler(context, event): results = context.user_data.model_handler.infer(np.array(image), threshold) return context.Response(body=json.dumps(results), headers={}, - content_type='application/json', status_code=200) \ No newline at end of file + content_type='application/json', status_code=200) diff --git a/serverless/tensorflow/matterport/mask_rcnn/nuclio/model_loader.py b/serverless/tensorflow/matterport/mask_rcnn/nuclio/model_loader.py index ecb7d74b014..196cab8663d 100644 --- a/serverless/tensorflow/matterport/mask_rcnn/nuclio/model_loader.py +++ b/serverless/tensorflow/matterport/mask_rcnn/nuclio/model_loader.py @@ -13,6 +13,11 @@ from mrcnn import model as modellib from mrcnn.config import Config +def to_cvat_mask(box: list, mask): + xtl, ytl, xbr, ybr = box + flattened = mask[ytl:ybr + 1, xtl:xbr + 1].flat[:].tolist() + flattened.extend([xtl, ytl, xbr, ybr]) + return flattened class ModelLoader: def __init__(self, labels): @@ -65,11 +70,18 @@ def infer(self, image, threshold): continue label = self.labels[class_id] + Xmin = int(np.min(contour[:,0])) + Xmax = int(np.max(contour[:,0])) + Ymin = int(np.min(contour[:,1])) + Ymax = int(np.max(contour[:,1])) + cvat_mask = to_cvat_mask((Xmin, Ymin, Xmax, Ymax), mask) + results.append({ "confidence": str(score), "label": label, "points": contour.ravel().tolist(), - "type": "polygon", + "mask": cvat_mask, + "type": "mask", }) - return results \ No newline at end of file + return results diff --git a/tests/cypress/integration/actions_objects/case_34_drawing_with_predefined_number_points.js b/tests/cypress/integration/actions_objects/case_34_drawing_with_predefined_number_points.js index ce61672d882..17c7076a54e 100644 --- a/tests/cypress/integration/actions_objects/case_34_drawing_with_predefined_number_points.js +++ b/tests/cypress/integration/actions_objects/case_34_drawing_with_predefined_number_points.js @@ -44,9 +44,9 @@ context('Drawing with predefined number of points.', () => { }); function tryDrawObjectPredefinedNumberPoints(object, pointsCount) { - cy.get(`.cvat-draw-${object}-control`).click().wait(500); - cy.get('.cvat-draw-shape-popover') - .not('.ant-popover-hidden') + cy.get(`.cvat-draw-${object}-control`).click(); + cy.get(`.cvat-draw-${object}-popover`) + .should('be.visible') .within(() => { cy.get('.cvat-draw-shape-popover-points-selector') .type(`${pointsCount - 1}`) @@ -58,7 +58,7 @@ context('Drawing with predefined number of points.', () => { function tryDeletePoint() { const svgJsCircleId = []; - cy.get('#cvat_canvas_shape_1').trigger('mousemove', { force: true }).should('have.attr', 'fill-opacity', 0.3); + cy.get('#cvat_canvas_shape_1').trigger('mousemove', { force: true }).should('have.class', 'cvat_canvas_shape_activated'); cy.get('circle').then((circle) => { for (let i = 0; i < circle.length; i++) { if (circle[i].id.match(/^SvgjsCircle\d+$/)) { diff --git a/tests/cypress/integration/actions_objects2/case_108_rotated_bounding_boxes.js b/tests/cypress/integration/actions_objects2/case_108_rotated_bounding_boxes.js index 301d61b28d3..7757a85d33f 100644 --- a/tests/cypress/integration/actions_objects2/case_108_rotated_bounding_boxes.js +++ b/tests/cypress/integration/actions_objects2/case_108_rotated_bounding_boxes.js @@ -117,7 +117,7 @@ context('Rotated bounding boxes.', () => { cy.goCheckFrameNumber(3); // Split tracks - cy.get('.cvat-split-track-control').click(); + cy.pressSplitControl(); // A single click does not reproduce the split a track scenario in cypress test. cy.get('#cvat_canvas_shape_2').click().click(); diff --git a/tests/cypress/integration/actions_objects2/case_13_merge_split_features.js b/tests/cypress/integration/actions_objects2/case_13_merge_split_features.js index 57db8cc8871..cb70ffba8d0 100644 --- a/tests/cypress/integration/actions_objects2/case_13_merge_split_features.js +++ b/tests/cypress/integration/actions_objects2/case_13_merge_split_features.js @@ -11,7 +11,7 @@ context('Merge/split features', () => { const createRectangleShape2Points = { points: 'By 2 Points', type: 'Shape', - labelName: labelName, + labelName, firstX: 250, firstY: 350, secondX: 350, @@ -20,7 +20,7 @@ context('Merge/split features', () => { const createRectangleShape2PointsSecond = { points: 'By 2 Points', type: 'Shape', - labelName: labelName, + labelName, firstX: createRectangleShape2Points.firstX + 300, firstY: createRectangleShape2Points.firstY, secondX: createRectangleShape2Points.secondX + 300, @@ -133,7 +133,7 @@ context('Merge/split features', () => { cy.get('#cvat_canvas_shape_3').should('exist').and('be.visible'); }); it('Split a track with "split" button. Previous track became invisible (has "outside" flag). One more track and it is visible.', () => { - cy.get('.cvat-split-track-control').click(); + cy.pressSplitControl(); // A single click does not reproduce the split a track scenario in cypress test. cy.get('#cvat_canvas_shape_3').click().click(); cy.get('#cvat_canvas_shape_4').should('exist').and('be.hidden'); diff --git a/tests/cypress/integration/actions_tasks3/case_48_issue_2663_annotations_statistics.js b/tests/cypress/integration/actions_tasks3/case_48_issue_2663_annotations_statistics.js index 336b0d200c8..bb6ad4acdee 100644 --- a/tests/cypress/integration/actions_tasks3/case_48_issue_2663_annotations_statistics.js +++ b/tests/cypress/integration/actions_tasks3/case_48_issue_2663_annotations_statistics.js @@ -152,7 +152,7 @@ context('Annotations statistics.', () => { it(`Check issue ${issueId}`, () => { // Issue 2663: "Cuboids are missed in annotations statistics" - const objectTypes = ['Rectangle', 'Polygon', 'Polyline', 'Points', 'Ellipse', 'Cuboid', 'Skeleton', 'Tag']; + const objectTypes = ['Rectangle', 'Polygon', 'Polyline', 'Points', 'Ellipse', 'Cuboid', 'Skeleton', 'Mask', 'Tag']; cy.get('.cvat-job-info-statistics') .find('table') .first() @@ -186,10 +186,10 @@ context('Annotations statistics.', () => { for (let i = 1; i < 7; i++) { expect(elTextContent[i]).to.be.equal('1 / 1'); // Rectangle, Polygon, Polyline, Points, Cuboids, Ellipses } - expect(elTextContent[8]).to.be.equal('1'); // Tags - expect(elTextContent[9]).to.be.equal('13'); // Manually - expect(elTextContent[10]).to.be.equal('39'); // Interpolated - expect(elTextContent[11]).to.be.equal('52'); // Total + expect(elTextContent[9]).to.be.equal('1'); // Tags + expect(elTextContent[10]).to.be.equal('13'); // Manually + expect(elTextContent[11]).to.be.equal('39'); // Interpolated + expect(elTextContent[12]).to.be.equal('52'); // Total }); }); cy.contains('[type="button"]', 'OK').click(); diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 2facf07d40f..616cb422e11 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -266,7 +266,7 @@ Cypress.Commands.add('openJob', (jobID = 0, removeAnnotations = true, expectedFa if (expectedFail) { cy.get('.cvat-canvas-container').should('not.exist'); } else { - cy.get('.cvat-canvas-container').should('exist'); + cy.get('.cvat-canvas-container').should('exist').and('be.visible'); } if (removeAnnotations) { cy.document().then((doc) => { @@ -279,6 +279,24 @@ Cypress.Commands.add('openJob', (jobID = 0, removeAnnotations = true, expectedFa } }); +Cypress.Commands.add('pressSplitControl', () => { + cy.document().then((doc) => { + const [el] = doc.getElementsByClassName('cvat-extra-controls-control'); + if (el) { + el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); + } + }); + + cy.get('.cvat-split-track-control').click(); + + cy.document().then((doc) => { + const [el] = doc.getElementsByClassName('cvat-extra-controls-control'); + if (el) { + el.dispatchEvent(new MouseEvent('mouseout', { bubbles: true })); + } + }); +}); + Cypress.Commands.add('openTaskJob', (taskName, jobID = 0, removeAnnotations = true, expectedFail = false) => { cy.openTask(taskName); cy.openJob(jobID, removeAnnotations, expectedFail); diff --git a/yarn.lock b/yarn.lock index 588e2d3a30e..151a1c4d7eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -75,37 +75,37 @@ "@babel/highlight" "^7.18.6" "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8": - version "7.18.8" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d" - integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ== + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.13.tgz#6aff7b350a1e8c3e40b029e46cbe78e24a913483" + integrity sha512-5yUzC5LqyTFp2HLmDoxGQelcdYgSpP9xsnMWBphAscOdFrHSAVbLNzWiy32sVNDqJRDiJK6klfDnAgu6PAGSHw== "@babel/core@^7.1.0", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.17.9", "@babel/core@^7.4.5", "@babel/core@^7.6.0", "@babel/core@^7.7.5": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.10.tgz#39ad504991d77f1f3da91be0b8b949a5bc466fb8" - integrity sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw== + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.13.tgz#9be8c44512751b05094a4d3ab05fc53a47ce00ac" + integrity sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A== dependencies: "@ampproject/remapping" "^2.1.0" "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.18.10" + "@babel/generator" "^7.18.13" "@babel/helper-compilation-targets" "^7.18.9" "@babel/helper-module-transforms" "^7.18.9" "@babel/helpers" "^7.18.9" - "@babel/parser" "^7.18.10" + "@babel/parser" "^7.18.13" "@babel/template" "^7.18.10" - "@babel/traverse" "^7.18.10" - "@babel/types" "^7.18.10" + "@babel/traverse" "^7.18.13" + "@babel/types" "^7.18.13" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.1" semver "^6.3.0" -"@babel/generator@^7.18.10", "@babel/generator@^7.7.2": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.10.tgz#794f328bfabdcbaf0ebf9bf91b5b57b61fa77a2a" - integrity sha512-0+sW7e3HjQbiHbj1NeU/vN8ornohYlacAfZIaXhdoGweQqgcNy69COVciYYqEXJ/v+9OBA7Frxm4CVAuNqKeNA== +"@babel/generator@^7.18.13", "@babel/generator@^7.7.2": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.13.tgz#59550cbb9ae79b8def15587bdfbaa388c4abf212" + integrity sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ== dependencies: - "@babel/types" "^7.18.10" + "@babel/types" "^7.18.13" "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" @@ -135,9 +135,9 @@ semver "^6.3.0" "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.9.tgz#d802ee16a64a9e824fcbf0a2ffc92f19d58550ce" - integrity sha512-WvypNAYaVh23QcjpMR24CwZY2Nz6hqdOcFdPbNpV56hL5H6KiFheO7Xm1aPdlLQ7d5emYZX7VZwPp9x3z+2opw== + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.13.tgz#63e771187bd06d234f95fdf8bd5f8b6429de6298" + integrity sha512-hDvXp+QYxSRL+23mpAlSGxHMDyIGChm0/AwTfTAAK5Ufe40nCsyNdaYCGuK91phn/fVu9kqayImRDkvNAgdrsA== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/helper-environment-visitor" "^7.18.9" @@ -319,10 +319,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.18.11", "@babel/parser@^7.7.0", "@babel/parser@^7.9.4": - version "7.18.11" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9" - integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.18.13", "@babel/parser@^7.7.0", "@babel/parser@^7.9.4": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4" + integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -654,9 +654,9 @@ "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-transform-destructuring@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.9.tgz#68906549c021cb231bee1db21d3b5b095f8ee292" - integrity sha512-p5VCYNddPLkZTq4XymQIaIfZNJwT9YsjkPOhkVEqt6QIpQFZVM9IltqqYpOEkJoN1DPznmxUDyZ5CTZs/ZCuHA== + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.13.tgz#9e03bc4a94475d62b7f4114938e6c5c33372cbf5" + integrity sha512-TodpQ29XekIsex2A+YJPj5ax2plkGa8YYY6mFjCohk/IG9IY42Rtuj1FuDeemfg2ipxIFLzPeA83SIBnlhSIow== dependencies: "@babel/helper-plugin-utils" "^7.18.9" @@ -873,9 +873,9 @@ "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-transform-typescript@^7.18.6": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.10.tgz#b23401b32f1f079396bcaed01667a54ebe4f9f85" - integrity sha512-j2HQCJuMbi88QftIb5zlRu3c7PU+sXNnscqsrjqegoGiCgXR569pEdben9vly5QHKL2ilYkfnSwu64zsZo/VYQ== + version "7.18.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.12.tgz#712e9a71b9e00fde9f8c0238e0cceee86ab2f8fd" + integrity sha512-2vjjam0cum0miPkenUbQswKowuxs/NjMwIKEq0zwegRxXk12C9YOF9STXnaUptITOtOJHKHpzvvWYOjbm6tc0w== dependencies: "@babel/helper-create-class-features-plugin" "^7.18.9" "@babel/helper-plugin-utils" "^7.18.9" @@ -1033,26 +1033,26 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.18.10", "@babel/traverse@^7.18.11", "@babel/traverse@^7.18.9", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.2": - version "7.18.11" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.11.tgz#3d51f2afbd83ecf9912bcbb5c4d94e3d2ddaa16f" - integrity sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.18.11", "@babel/traverse@^7.18.13", "@babel/traverse@^7.18.9", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.2": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.13.tgz#5ab59ef51a997b3f10c4587d648b9696b6cb1a68" + integrity sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA== dependencies: "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.18.10" + "@babel/generator" "^7.18.13" "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-function-name" "^7.18.9" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.18.11" - "@babel/types" "^7.18.10" + "@babel/parser" "^7.18.13" + "@babel/types" "^7.18.13" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.10.tgz#4908e81b6b339ca7c6b7a555a5fc29446f26dde6" - integrity sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ== +"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.13", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a" + integrity sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ== dependencies: "@babel/helper-string-parser" "^7.18.10" "@babel/helper-validator-identifier" "^7.18.6" @@ -1500,10 +1500,10 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.8", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.14" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" - integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ== +"@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.8", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.15" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" + integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== dependencies: "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" @@ -1513,6 +1513,21 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@mapbox/node-pre-gyp@^1.0.0": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc" + integrity sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": version "2.1.8-no-fsevents.3" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" @@ -1540,9 +1555,9 @@ fastq "^1.6.0" "@sinclair/typebox@^0.24.1": - version "0.24.27" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.27.tgz#d55643516a1546174e10da681a8aaa81e757452d" - integrity sha512-K7C7IlQ3zLePEZleUN21ceBA2aLcMnLHTLph8QWk1JK37L90obdpY+QGY8bXMKxf1ht1Z0MNewvXxWv0oGDYFg== + version "0.24.28" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.28.tgz#15aa0b416f82c268b1573ab653e4413c965fe794" + integrity sha512-dgJd3HLOkLmz4Bw50eZx/zJwtBq65nms3N9VBYu5LTjJ883oBFkTyXRlCB/ZGGwqYpJJHA5zW2Ibhl5ngITfow== "@sindresorhus/is@^0.14.0": version "0.14.0" @@ -1597,6 +1612,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14", "@types/babel__core@^7.1.7": version "7.1.19" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" @@ -1674,9 +1694,9 @@ "@types/estree" "*" "@types/eslint@*": - version "8.4.5" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.5.tgz#acdfb7dd36b91cc5d812d7c093811a8f3d9b31e4" - integrity sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ== + version "8.4.6" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.6.tgz#7976f054c1bccfcf514bff0564c0c41df5c08207" + integrity sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g== dependencies: "@types/estree" "*" "@types/json-schema" "*" @@ -1710,6 +1730,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/fabric@^4.5.7": + version "4.5.11" + resolved "https://registry.yarnpkg.com/@types/fabric/-/fabric-4.5.11.tgz#ca7be016c6d803bf1066ce7072836ac1e7f2350d" + integrity sha512-JgOnbIm03EDYI+X5/hjstMZUbQ9W3704BQ4Dlu7t9JwWzNSdJE4cTV/4XtIHFCCfiFpcL756re+qCazTA84InA== + "@types/graceful-fs@^4.1.2", "@types/graceful-fs@^4.1.3": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -1785,9 +1810,9 @@ integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA== "@types/lodash@^4.14.172": - version "4.14.182" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" - integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== + version "4.14.184" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.184.tgz#23f96cd2a21a28e106dc24d825d4aa966de7a9fe" + integrity sha512-RoZphVtHbxPZizt4IcILciSWiC6dcn+eZ8oX9IWEYfDMcocdd42f7NPI6fQj+6zI8y4E0L7gu2pcZKLGTRaV9Q== "@types/markdown-it@^12.2.3": version "12.2.3" @@ -1825,9 +1850,9 @@ integrity sha512-HUAiN65VsRXyFCTicolwb5+I7FM6f72zjMWr+ajGk+YTvzBgXqa2A5U7d+rtsouAkunJ5U4Sb5lNJjo9w+nmXg== "@types/node@*", "@types/node@^18.0.3": - version "18.6.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.4.tgz#fd26723a8a3f8f46729812a7f9b4fc2d1608ed39" - integrity sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg== + version "18.7.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.11.tgz#486e72cfccde88da24e1f23ff1b7d8bfb64e6250" + integrity sha512-KZhFpSLlmK/sdocfSAjqPETTMd0ug6HIMIAwkwUpU79olnZdQtMxpQP+G1wDzCH7na+FltSIhbaZuKdwZ8RDrw== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -2044,13 +2069,13 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^5.30.5": - version "5.32.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.32.0.tgz#e27e38cffa4a61226327c874a7be965e9a861624" - integrity sha512-CHLuz5Uz7bHP2WgVlvoZGhf0BvFakBJKAD/43Ty0emn4wXWv5k01ND0C0fHcl/Im8Td2y/7h44E9pca9qAu2ew== + version "5.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.34.0.tgz#d690f60e335596f38b01792e8f4b361d9bd0cb35" + integrity sha512-eRfPPcasO39iwjlUAMtjeueRGuIrW3TQ9WseIDl7i5UWuFbf83yYaU7YPs4j8+4CxUMIsj1k+4kV+E+G+6ypDQ== dependencies: - "@typescript-eslint/scope-manager" "5.32.0" - "@typescript-eslint/type-utils" "5.32.0" - "@typescript-eslint/utils" "5.32.0" + "@typescript-eslint/scope-manager" "5.34.0" + "@typescript-eslint/type-utils" "5.34.0" + "@typescript-eslint/utils" "5.34.0" debug "^4.3.4" functional-red-black-tree "^1.0.1" ignore "^5.2.0" @@ -2069,13 +2094,13 @@ debug "^4.3.1" "@typescript-eslint/parser@^5.30.5": - version "5.32.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.32.0.tgz#1de243443bc6186fb153b9e395b842e46877ca5d" - integrity sha512-IxRtsehdGV9GFQ35IGm5oKKR2OGcazUoiNBxhRV160iF9FoyuXxjY+rIqs1gfnd+4eL98OjeGnMpE7RF/NBb3A== + version "5.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.34.0.tgz#ca710858ea85dbfd30c9b416a335dc49e82dbc07" + integrity sha512-SZ3NEnK4usd2CXkoV3jPa/vo1mWX1fqRyIVUQZR4As1vyp4fneknBNJj+OFtV8WAVgGf+rOHMSqQbs2Qn3nFZQ== dependencies: - "@typescript-eslint/scope-manager" "5.32.0" - "@typescript-eslint/types" "5.32.0" - "@typescript-eslint/typescript-estree" "5.32.0" + "@typescript-eslint/scope-manager" "5.34.0" + "@typescript-eslint/types" "5.34.0" + "@typescript-eslint/typescript-estree" "5.34.0" debug "^4.3.4" "@typescript-eslint/scope-manager@4.33.0": @@ -2086,20 +2111,20 @@ "@typescript-eslint/types" "4.33.0" "@typescript-eslint/visitor-keys" "4.33.0" -"@typescript-eslint/scope-manager@5.32.0": - version "5.32.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.32.0.tgz#763386e963a8def470580cc36cf9228864190b95" - integrity sha512-KyAE+tUON0D7tNz92p1uetRqVJiiAkeluvwvZOqBmW9z2XApmk5WSMV9FrzOroAcVxJZB3GfUwVKr98Dr/OjOg== +"@typescript-eslint/scope-manager@5.34.0": + version "5.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.34.0.tgz#14efd13dc57602937e25f188fd911f118781e527" + integrity sha512-HNvASMQlah5RsBW6L6c7IJ0vsm+8Sope/wu5sEAf7joJYWNb1LDbJipzmdhdUOnfrDFE6LR1j57x1EYVxrY4ow== dependencies: - "@typescript-eslint/types" "5.32.0" - "@typescript-eslint/visitor-keys" "5.32.0" + "@typescript-eslint/types" "5.34.0" + "@typescript-eslint/visitor-keys" "5.34.0" -"@typescript-eslint/type-utils@5.32.0": - version "5.32.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.32.0.tgz#45a14506fe3fb908600b4cef2f70778f7b5cdc79" - integrity sha512-0gSsIhFDduBz3QcHJIp3qRCvVYbqzHg8D6bHFsDMrm0rURYDj+skBK2zmYebdCp+4nrd9VWd13egvhYFJj/wZg== +"@typescript-eslint/type-utils@5.34.0": + version "5.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.34.0.tgz#7a324ab9ddd102cd5e1beefc94eea6f3eb32d32d" + integrity sha512-Pxlno9bjsQ7hs1pdWRUv9aJijGYPYsHpwMeCQ/Inavhym3/XaKt1ZKAA8FIw4odTBfowBdZJDMxf2aavyMDkLg== dependencies: - "@typescript-eslint/utils" "5.32.0" + "@typescript-eslint/utils" "5.34.0" debug "^4.3.4" tsutils "^3.21.0" @@ -2108,10 +2133,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== -"@typescript-eslint/types@5.32.0": - version "5.32.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.32.0.tgz#484273021eeeae87ddb288f39586ef5efeb6dcd8" - integrity sha512-EBUKs68DOcT/EjGfzywp+f8wG9Zw6gj6BjWu7KV/IYllqKJFPlZlLSYw/PTvVyiRw50t6wVbgv4p9uE2h6sZrQ== +"@typescript-eslint/types@5.34.0": + version "5.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.34.0.tgz#217bf08049e9e7b86694d982e88a2c1566330c78" + integrity sha512-49fm3xbbUPuzBIOcy2CDpYWqy/X7VBkxVN+DC21e0zIm3+61Z0NZi6J9mqPmSW1BDVk9FIOvuCFyUPjXz93sjA== "@typescript-eslint/typescript-estree@4.33.0": version "4.33.0" @@ -2126,28 +2151,28 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.32.0": - version "5.32.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.32.0.tgz#282943f34babf07a4afa7b0ff347a8e7b6030d12" - integrity sha512-ZVAUkvPk3ITGtCLU5J4atCw9RTxK+SRc6hXqLtllC2sGSeMFWN+YwbiJR9CFrSFJ3w4SJfcWtDwNb/DmUIHdhg== +"@typescript-eslint/typescript-estree@5.34.0": + version "5.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.34.0.tgz#ba7b83f4bf8ccbabf074bbf1baca7a58de3ccb9a" + integrity sha512-mXHAqapJJDVzxauEkfJI96j3D10sd567LlqroyCeJaHnu42sDbjxotGb3XFtGPYKPD9IyLjhsoULML1oI3M86A== dependencies: - "@typescript-eslint/types" "5.32.0" - "@typescript-eslint/visitor-keys" "5.32.0" + "@typescript-eslint/types" "5.34.0" + "@typescript-eslint/visitor-keys" "5.34.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.32.0", "@typescript-eslint/utils@^5.10.0": - version "5.32.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.32.0.tgz#eccb6b672b94516f1afc6508d05173c45924840c" - integrity sha512-W7lYIAI5Zlc5K082dGR27Fczjb3Q57ECcXefKU/f0ajM5ToM0P+N9NmJWip8GmGu/g6QISNT+K6KYB+iSHjXCQ== +"@typescript-eslint/utils@5.34.0", "@typescript-eslint/utils@^5.10.0": + version "5.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.34.0.tgz#0cae98f48d8f9e292e5caa9343611b6faf49e743" + integrity sha512-kWRYybU4Rn++7lm9yu8pbuydRyQsHRoBDIo11k7eqBWTldN4xUdVUMCsHBiE7aoEkFzrUEaZy3iH477vr4xHAQ== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.32.0" - "@typescript-eslint/types" "5.32.0" - "@typescript-eslint/typescript-estree" "5.32.0" + "@typescript-eslint/scope-manager" "5.34.0" + "@typescript-eslint/types" "5.34.0" + "@typescript-eslint/typescript-estree" "5.34.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" @@ -2159,12 +2184,12 @@ "@typescript-eslint/types" "4.33.0" eslint-visitor-keys "^2.0.0" -"@typescript-eslint/visitor-keys@5.32.0": - version "5.32.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.32.0.tgz#b9715d0b11fdb5dd10fd0c42ff13987470525394" - integrity sha512-S54xOHZgfThiZ38/ZGTgB2rqx51CMJ5MCfVT2IplK4Q7hgzGfe0nLzLCcenDnc/cSjP568hdeKfeDcBgqNHD/g== +"@typescript-eslint/visitor-keys@5.34.0": + version "5.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.34.0.tgz#d0fb3e31033e82ddd5de048371ad39eb342b2d40" + integrity sha512-O1moYjOSrab0a2fUvFpsJe0QHtvTC+cR+ovYpgKrAVXzqQyc74mv76TgY6z+aEtjQE2vgZux3CQVtGryqdcOAw== dependencies: - "@typescript-eslint/types" "5.32.0" + "@typescript-eslint/types" "5.34.0" eslint-visitor-keys "^3.3.0" "@webassemblyjs/ast@1.11.1": @@ -2315,7 +2340,7 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -abab@^2.0.3, abab@^2.0.5: +abab@^2.0.3, abab@^2.0.5, abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== @@ -2574,11 +2599,24 @@ append-transform@^2.0.0: dependencies: default-require-extensions "^3.0.0" +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + archy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -3331,14 +3369,23 @@ camelcase@^6.0.0, camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== camera-controls@^1.25.3: - version "1.36.0" - resolved "https://registry.yarnpkg.com/camera-controls/-/camera-controls-1.36.0.tgz#740e6952b6effff69c979fbde3f3c0919a655fbb" - integrity sha512-EFRS/PGyaqwc+mousHbLF9nFJQbcIcYIpzkddxFltiAtYcwyR017t1HUh51c75YqqfTKPGerIlITGRu3a+8j6g== + version "1.36.1" + resolved "https://registry.yarnpkg.com/camera-controls/-/camera-controls-1.36.1.tgz#90e19123c49fd7b379d6d25783eb0870e91264e8" + integrity sha512-muYZT4bgA9iR/lfZwM1TLM/GPttAlRM+fDCjg9qsmH4omDMefGF8/Q5QFkadRGCeCOTFESGYXcOhqV/tt4z6fQ== caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001370: - version "1.0.30001374" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001374.tgz#3dab138e3f5485ba2e74bd13eca7fe1037ce6f57" - integrity sha512-mWvzatRx3w+j5wx/mpFN5v5twlPrabG8NqX2c6e45LCpymdoGqNvRkRutFUqpRTXKFQFNQJasvK0YT7suW6/Hw== + version "1.0.30001382" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001382.tgz#4d37f0d0b6fffb826c8e5e1c0f4bf8ce592db949" + integrity sha512-2rtJwDmSZ716Pxm1wCtbPvHtbDWAreTPxXbkc5RkKglow3Ig/4GNGazDI9/BVnXbG/wnv6r3B5FEbkfg9OcTGg== + +canvas@^2.8.0: + version "2.9.3" + resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.9.3.tgz#8723c4f970442d4cdcedba5221579f9660a58bdb" + integrity sha512-WOUM7ghii5TV2rbhaZkh1youv/vW1/Canev6Yx6BG2W+1S07w8jKZqKkPnbiPpQEDsnJdN8ouDd7OvQEGXDcUw== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.0" + nan "^2.15.0" + simple-get "^3.0.3" capture-exit@^2.0.0: version "2.0.0" @@ -3445,6 +3492,11 @@ check-links@^1.1.8: optionalDependencies: fsevents "~2.3.2" +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + chrome-trace-event@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" @@ -3632,6 +3684,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.16, colorette@^2.0.17: version "2.0.19" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" @@ -3737,6 +3794,11 @@ connect-history-api-fallback@^2.0.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== +console-control-strings@^1.0.0, console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -3995,6 +4057,11 @@ cssom@^0.4.4: resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== +cssom@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" + integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== + cssom@~0.3.6: version "0.3.8" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" @@ -4079,15 +4146,24 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +data-urls@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" + integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== + dependencies: + abab "^2.0.6" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + date-fns@2.x: - version "2.29.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.1.tgz#9667c2615525e552b5135a3116b95b1961456e60" - integrity sha512-dlLD5rKaKxpFdnjrs+5azHDFOPEu4ANy/LTh04A1DTzMM7qoajmKCBc8pkKRFT41CNzw+4gQh79X5C+Jq27HAw== + version "2.29.2" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.2.tgz#0d4b3d0f3dff0f920820a070920f0d9662c51931" + integrity sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA== dayjs@1.x: - version "1.11.4" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.4.tgz#3b3c10ca378140d8917e06ebc13a4922af4f433e" - integrity sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g== + version "1.11.5" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93" + integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA== debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" @@ -4123,10 +4199,10 @@ decamelize@^1.1.0, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== -decimal.js@^10.2.1: - version "10.3.1" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" - integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== +decimal.js@^10.2.1, decimal.js@^10.3.1: + version "10.4.0" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.0.tgz#97a7448873b01e92e5ff9117d89a7bca8e63e0fe" + integrity sha512-Nv6ENEzyPQ6AItkGwLE2PGKinZZ9g59vSh2BeH6NqPu0OTKZ5ruJsVqh/orbAnqXc9pBbgXAIrc2EyaCj8NpGg== decode-uri-component@^0.2.0: version "0.2.0" @@ -4140,6 +4216,13 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -4219,6 +4302,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -4246,6 +4334,11 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" +detect-libc@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -4352,6 +4445,13 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" +domexception@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" + integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== + dependencies: + webidl-conversions "^7.0.0" + domhandler@^2.3.0: version "2.4.2" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" @@ -4398,10 +4498,10 @@ dotenv-defaults@^2.0.2: dependencies: dotenv "^8.2.0" -dotenv-webpack@^7.1.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/dotenv-webpack/-/dotenv-webpack-7.1.1.tgz#ee8a699e1d736fd8eb9363fbc7054cfff1bd9dbf" - integrity sha512-xw/19VqHDkXALtBOJAnnrSU/AZDIQRXczAmJyp0lZv6SH2aBLzUTl96W1MVryJZ7okZ+djZS4Gj4KlZ0xP7deA== +dotenv-webpack@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/dotenv-webpack/-/dotenv-webpack-8.0.1.tgz#6656550460a8076fab20e5ac2eac867e72478645" + integrity sha512-CdrgfhZOnx4uB18SgaoP9XHRN2v48BbjuXQsZY5ixs5A8579NxQkmMxRtI7aTwSiSQcM2ao12Fdu+L3ZS3bG4w== dependencies: dotenv-defaults "^2.0.2" @@ -4442,9 +4542,9 @@ ee-first@1.1.1: integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== electron-to-chromium@^1.4.202: - version "1.4.211" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.211.tgz#afaa8b58313807501312d598d99b953568d60f91" - integrity sha512-BZSbMpyFQU0KBJ1JG26XGeFI3i4op+qOYGxftmZXFZoHkhLgsSv4DHDJfl8ogII3hIuzGt51PaZ195OVu0yJ9A== + version "1.4.227" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.227.tgz#28e46e2a701fed3188db3ca7bf0a3a475e484046" + integrity sha512-I9VVajA3oswIJOUFg2PSBqrHLF5Y+ahIfjOV9+v6uYyBqFZutmPxA6fxocDUUmgwYevRWFu1VjLyVG3w45qa/g== emittery@^0.10.2: version "0.10.2" @@ -4665,12 +4765,11 @@ eslint-import-resolver-node@^0.3.6: resolve "^1.20.0" eslint-module-utils@^2.7.3: - version "2.7.3" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz#ad7e3a10552fdd0642e1e55292781bd6e34876ee" - integrity sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ== + version "2.7.4" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" + integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== dependencies: debug "^3.2.7" - find-up "^2.1.0" eslint-plugin-cypress@^2.11.2: version "2.12.1" @@ -4699,9 +4798,9 @@ eslint-plugin-import@^2.22.1: tsconfig-paths "^3.14.1" eslint-plugin-jest@^26.5.3: - version "26.7.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-26.7.0.tgz#41d405ac9143e1284a3401282db47ed459436778" - integrity sha512-/YNitdfG3o3cC6juZziAdkk6nfJt01jXVfj4AgaYVLs7bupHzRDL5K+eipdzhDXtQsiqaX1TzfwSuRlEgeln1A== + version "26.8.7" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-26.8.7.tgz#f38f067d0a69483d64578eb43508ca7b29c8a4b7" + integrity sha512-nJJVv3VY6ZZvJGDMC8h1jN/TIGT4We1JkNn1lvstPURicr/eZPVnlFULQ4W2qL9ByCuCr1hPmlBOc2aZ1ktw4Q== dependencies: "@typescript-eslint/utils" "^5.10.0" @@ -5094,6 +5193,14 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== +fabric@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/fabric/-/fabric-5.2.1.tgz#40cdc8712d939f3b6bded55ac40e716f2a67e013" + integrity sha512-Irltx4i+aLccWgdQj2Uvrwh/XulDAqqYMZ1bI13fAtmlxl4ggobo0t7VVYy3Ob4YEB0sCeJZKE8ExZgGo/amkw== + optionalDependencies: + canvas "^2.8.0" + jsdom "^19.0.0" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -5211,13 +5318,6 @@ find-cache-dir@^3.2.0, find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ== - dependencies: - locate-path "^2.0.0" - find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -5242,9 +5342,9 @@ flat-cache@^3.0.4: rimraf "^3.0.2" flatted@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.6.tgz#022e9218c637f9f3fc9c35ab9c9193f05add60b2" - integrity sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ== + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== flatten@^1.0.2: version "1.0.3" @@ -5328,6 +5428,13 @@ fromentries@^1.2.0: resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.3.2.tgz#e4bca6808816bf8f93b52750f1127f5a6fd86e3a" integrity sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg== +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs-monkey@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" @@ -5373,6 +5480,21 @@ functions-have-names@^1.2.2: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -5631,6 +5753,11 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -5738,6 +5865,13 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== + dependencies: + whatwg-encoding "^2.0.0" + html-entities@^2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46" @@ -5844,6 +5978,15 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + http-proxy-middleware@^2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" @@ -5908,6 +6051,13 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + icss-utils@^4.0.0, icss-utils@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" @@ -7326,6 +7476,39 @@ jsdom@^16.4.0: ws "^7.4.6" xml-name-validator "^3.0.0" +jsdom@^19.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-19.0.0.tgz#93e67c149fe26816d38a849ea30ac93677e16b6a" + integrity sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A== + dependencies: + abab "^2.0.5" + acorn "^8.5.0" + acorn-globals "^6.0.0" + cssom "^0.5.0" + cssstyle "^2.3.0" + data-urls "^3.0.1" + decimal.js "^10.3.1" + domexception "^4.0.0" + escodegen "^2.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.0" + parse5 "6.0.1" + saxes "^5.0.1" + symbol-tree "^3.2.4" + tough-cookie "^4.0.0" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^3.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^10.0.0" + ws "^8.2.3" + xml-name-validator "^4.0.0" + jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" @@ -7428,12 +7611,12 @@ jsprim@^1.2.2: verror "1.10.0" "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.2.tgz#afe5efe4332cd3515c065072bd4d6b0aa22152bd" - integrity sha512-4ZCADZHRkno244xlNnn4AOG6sRQ7iBZ5BbgZ4vW4y5IZw7cVUD1PPeblm1xx/nfmMxPdt/LHsXZW8z/j58+l9Q== + version "3.3.3" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea" + integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== dependencies: array-includes "^3.1.5" - object.assign "^4.1.2" + object.assign "^4.1.3" jszip@3.10.1: version "3.10.1" @@ -7633,14 +7816,6 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA== - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -7890,9 +8065,9 @@ markdown-table@^2.0.0: repeat-string "^1.0.0" marked@^4.0.10: - version "4.0.18" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.18.tgz#cd0ac54b2e5610cfb90e8fd46ccaa8292c9ed569" - integrity sha512-wbLDJ7Zh0sqA0Vdg6aqlbT+yPxqLblpAZh1mK2+AO2twQkPywvvqQNfEPVwSSRjZ7dZcdeVBIAgiO7MMp3Dszw== + version "4.0.19" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.19.tgz#d36198d1ac1255525153c351c68c75bc1d7aee46" + integrity sha512-rgQF/OxOiLcvgUAj1Q1tAf4Bgxn5h5JZTp04Fx4XUkVhs7B+7YA9JEWJhJpoO8eJt8MkZMwqLCNeNqj1bCREZQ== material-colors@^1.2.1: version "1.2.6" @@ -8208,6 +8383,11 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +mimic-response@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" @@ -8247,6 +8427,21 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minipass@^3.0.0: + version "3.3.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.4.tgz#ca99f95dd77c43c7a76bf51e6d200025eee0ffae" + integrity sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw== + dependencies: + yallist "^4.0.0" + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -8255,7 +8450,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@1.x, mkdirp@^1.0.4: +mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -8300,6 +8495,11 @@ multicast-dns@^7.2.5: dns-packet "^5.2.2" thunky "^1.0.2" +nan@^2.15.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916" + integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -8345,6 +8545,13 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +node-fetch@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + node-forge@^1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" @@ -8395,6 +8602,13 @@ nodemon@^2.0.7: touch "^3.1.0" undefsafe "^2.0.5" +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + nopt@~1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" @@ -8470,6 +8684,16 @@ npm-run-path@^5.1.0: dependencies: path-key "^4.0.0" +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + nth-check@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" @@ -8563,14 +8787,14 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.1.0, object.assign@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== +object.assign@^4.1.0, object.assign@^4.1.2, object.assign@^4.1.3: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" object-keys "^1.1.1" object.entries@^1.1.2, object.entries@^1.1.5: @@ -8745,13 +8969,6 @@ p-is-promise@^2.0.0: resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -8766,13 +8983,6 @@ p-limit@^3.1.0: dependencies: yocto-queue "^0.1.0" -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg== - dependencies: - p-limit "^1.1.0" - p-locate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" @@ -8837,11 +9047,6 @@ p-timeout@^3.0.0: dependencies: p-finally "^1.0.0" -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww== - p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -10254,7 +10459,7 @@ readable-stream@^2.0.1, readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1: +readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -11312,7 +11517,7 @@ safe-regex@^2.1.1: dependencies: regexp-tree "~0.1.1" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -11344,9 +11549,9 @@ sass-loader@^10.0.0: semver "^7.3.2" sass@^1.42.1: - version "1.54.3" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.3.tgz#37baa2652f7f1fdadb73240ee9a2b9b81fabb5c4" - integrity sha512-fLodey5Qd41Pxp/Tk7Al97sViYwF/TazRc5t6E65O7JOk4XF8pzwIW7CvCxYVOfJFFI/1x5+elDyBIixrp+zrw== + version "1.54.5" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.5.tgz#93708f5560784f6ff2eab8542ade021a4a947b3a" + integrity sha512-p7DTOzxkUPa/63FU0R3KApkRHwcVZYC0PLnLm5iyZACyp15qSi32x7zVUhRdABAATmkALqgGrjCJAcWvobmhHw== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -11584,6 +11789,20 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55" + integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA== + dependencies: + decompress-response "^4.2.0" + once "^1.3.1" + simple-concat "^1.0.0" + simple-update-notifier@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz#7edf75c5bdd04f88828d632f762b2bc32996a9cc" @@ -11779,9 +11998,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.11" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95" - integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g== + version "3.0.12" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz#69077835abe2710b65f03969898b6637b505a779" + integrity sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA== spdy-transport@^3.0.0: version "3.0.0" @@ -11901,7 +12120,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12253,6 +12472,18 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +tar@^6.1.11: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" @@ -12262,20 +12493,20 @@ terminal-link@^2.0.0: supports-hyperlinks "^2.0.0" terser-webpack-plugin@^5.1.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz#8033db876dd5875487213e87c627bca323e5ed90" - integrity sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ== + version "5.3.5" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.5.tgz#f7d82286031f915a4f8fb81af4bd35d2e3c011bc" + integrity sha512-AOEDLDxD2zylUGf/wxHxklEkOe2/r+seuyOWujejFrIxHf11brA1/dWQNIgXa1c6/Wkxgu7zvv0JhOWfc2ELEA== dependencies: - "@jridgewell/trace-mapping" "^0.3.7" + "@jridgewell/trace-mapping" "^0.3.14" jest-worker "^27.4.5" schema-utils "^3.1.1" serialize-javascript "^6.0.0" - terser "^5.7.2" + terser "^5.14.1" -terser@^5.10.0, terser@^5.7.2: - version "5.14.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" - integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA== +terser@^5.10.0, terser@^5.14.1: + version "5.15.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.0.tgz#e16967894eeba6e1091509ec83f0c60e179f2425" + integrity sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA== dependencies: "@jridgewell/source-map" "^0.3.2" acorn "^8.5.0" @@ -12414,13 +12645,14 @@ touch@^3.1.0: nopt "~1.0.10" tough-cookie@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" - integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== + version "4.1.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.0.tgz#039b203b2ad95cd9d2a2aae07b238cb83adc46c7" + integrity sha512-IVX6AagLelGwl6F0E+hoRpXzuD192cZhAcmT7/eoLr0PnsB1wv2E5c+A2O+V8xth9FlL2p0OstFsWn0bZpVn4w== dependencies: psl "^1.1.33" punycode "^2.1.1" - universalify "^0.1.2" + universalify "^0.2.0" + url-parse "^1.5.3" tough-cookie@~2.5.0: version "2.5.0" @@ -12437,6 +12669,18 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + trim-newlines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" @@ -12796,10 +13040,10 @@ universal-cookie@^4.0.0: "@types/cookie" "^0.3.3" cookie "^0.4.0" -universalify@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" @@ -12846,7 +13090,7 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.5.7: +url-parse@^1.5.3, url-parse@^1.5.7: version "1.5.10" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== @@ -13004,6 +13248,13 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" +w3c-xmlserializer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz#06cdc3eefb7e4d0b20a560a5a3aeb0d2d9a65923" + integrity sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg== + dependencies: + xml-name-validator "^4.0.0" + walker@^1.0.7, walker@^1.0.8, walker@~1.0.5: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" @@ -13026,6 +13277,11 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -13036,6 +13292,11 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + webpack-cli@^4.9.2: version "4.10.0" resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31" @@ -13164,11 +13425,47 @@ whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== + dependencies: + iconv-lite "0.6.3" + whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + +whatwg-url@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-10.0.0.tgz#37264f720b575b4a311bd4094ed8c760caaa05da" + integrity sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^8.0.0, whatwg-url@^8.5.0: version "8.7.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" @@ -13208,6 +13505,13 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" +wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + wildcard@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" @@ -13268,9 +13572,9 @@ write-file-atomic@^3.0.0, write-file-atomic@^3.0.3: typedarray-to-buffer "^3.1.5" write-file-atomic@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.1.tgz#9faa33a964c1c85ff6f849b80b42a88c2c537c8f" - integrity sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ== + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== dependencies: imurmurhash "^0.1.4" signal-exit "^3.0.7" @@ -13280,7 +13584,7 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== -ws@^8.4.2: +ws@^8.2.3, ws@^8.4.2: version "8.8.1" resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA== @@ -13290,6 +13594,11 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml-name-validator@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" + integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== + xml@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"