diff --git a/CHANGELOG.md b/CHANGELOG.md index 775c8bb06bc..14c7b1b7050 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dedicated message with clarifications when share is unmounted (https://github.com/opencv/cvat/pull/1373) - Ability to create one tracked point (https://github.com/opencv/cvat/pull/1383) - Tutorial: instructions for CVAT over HTTPS +- Added deep extreme cut (semi-automatic segmentation) to the new UI (https://github.com/opencv/cvat/pull/1398) ### Changed - Increase preview size of a task till 256, 256 on the server diff --git a/cvat-ui/src/actions/plugins-actions.ts b/cvat-ui/src/actions/plugins-actions.ts index 9e88b055969..36175683771 100644 --- a/cvat-ui/src/actions/plugins-actions.ts +++ b/cvat-ui/src/actions/plugins-actions.ts @@ -33,6 +33,7 @@ export function checkPluginsAsync(): ThunkAction { GIT_INTEGRATION: false, TF_ANNOTATION: false, TF_SEGMENTATION: false, + DEXTR_SEGMENTATION: false, }; const promises: Promise[] = [ @@ -41,15 +42,12 @@ export function checkPluginsAsync(): ThunkAction { PluginChecker.check(SupportedPlugins.GIT_INTEGRATION), PluginChecker.check(SupportedPlugins.TF_ANNOTATION), PluginChecker.check(SupportedPlugins.TF_SEGMENTATION), + PluginChecker.check(SupportedPlugins.DEXTR_SEGMENTATION), ]; const values = await Promise.all(promises); - [plugins.ANALYTICS] = values; - [, plugins.AUTO_ANNOTATION] = values; - [,, plugins.GIT_INTEGRATION] = values; - [,,, plugins.TF_ANNOTATION] = values; - [,,,, plugins.TF_SEGMENTATION] = values; - + [plugins.ANALYTICS, plugins.AUTO_ANNOTATION, plugins.GIT_INTEGRATION, + plugins.TF_ANNOTATION, plugins.TF_SEGMENTATION, plugins.DEXTR_SEGMENTATION] = values; dispatch(pluginActions.checkedAllPlugins(plugins)); }; } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/dextr-plugin.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/dextr-plugin.tsx new file mode 100644 index 00000000000..1215b33ea9f --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/dextr-plugin.tsx @@ -0,0 +1,90 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useState } from 'react'; +import { connect } from 'react-redux'; +import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox'; +import Tooltip from 'antd/lib/tooltip'; + +import { Canvas } from 'cvat-canvas'; +import { CombinedState } from 'reducers/interfaces'; +import { activate as activatePlugin, deactivate as deactivatePlugin } from 'utils/dextr-utils'; + + +interface StateToProps { + pluginEnabled: boolean; + canvasInstance: Canvas; +} + +interface DispatchToProps { + activate(canvasInstance: Canvas): void; + deactivate(canvasInstance: Canvas): void; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { + plugins: { + list, + }, + annotation: { + canvas: { + instance: canvasInstance, + }, + }, + } = state; + + return { + canvasInstance, + pluginEnabled: list.DEXTR_SEGMENTATION, + }; +} + +function mapDispatchToProps(): DispatchToProps { + return { + activate(canvasInstance: Canvas): void { + activatePlugin(canvasInstance); + }, + deactivate(canvasInstance: Canvas): void { + deactivatePlugin(canvasInstance); + }, + }; +} + +function DEXTRPlugin(props: StateToProps & DispatchToProps): JSX.Element | null { + const { + pluginEnabled, + canvasInstance, + activate, + deactivate, + } = props; + const [pluginActivated, setActivated] = useState(false); + + return ( + pluginEnabled ? ( + + { + setActivated(event.target.checked); + if (event.target.checked) { + activate(canvasInstance); + } else { + deactivate(canvasInstance); + } + }} + > + Make AI polygon + + + ) : null + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(DEXTRPlugin); + +// TODO: Add dialog window with cancel button diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx index e5145fd9c80..c940c39b7e8 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx @@ -3,7 +3,6 @@ // SPDX-License-Identifier: MIT import React from 'react'; - import { Row, Col } from 'antd/lib/grid'; import Select from 'antd/lib/select'; import Button from 'antd/lib/button'; @@ -15,6 +14,7 @@ import Text from 'antd/lib/typography/Text'; import { RectDrawingMethod } from 'cvat-canvas'; import { ShapeType } from 'reducers/interfaces'; import { clamp } from 'utils/math'; +import DEXTRPlugin from './dextr-plugin'; interface Props { shapeType: ShapeType; @@ -81,6 +81,9 @@ function DrawShapePopoverComponent(props: Props): JSX.Element { + { + shapeType === ShapeType.POLYGON && + } { shapeType === ShapeType.RECTANGLE ? ( <> diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index d794ffd7238..017d98726b3 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -74,6 +74,7 @@ export enum SupportedPlugins { AUTO_ANNOTATION = 'AUTO_ANNOTATION', TF_ANNOTATION = 'TF_ANNOTATION', TF_SEGMENTATION = 'TF_SEGMENTATION', + DEXTR_SEGMENTATION = 'DEXTR_SEGMENTATION', ANALYTICS = 'ANALYTICS', } diff --git a/cvat-ui/src/reducers/plugins-reducer.ts b/cvat-ui/src/reducers/plugins-reducer.ts index ceae9dbca03..334c51527dc 100644 --- a/cvat-ui/src/reducers/plugins-reducer.ts +++ b/cvat-ui/src/reducers/plugins-reducer.ts @@ -4,6 +4,7 @@ import { PluginsActionTypes, PluginActions } from 'actions/plugins-actions'; import { registerGitPlugin } from 'utils/git-utils'; +import { registerDEXTRPlugin } from 'utils/dextr-utils'; import { PluginsState, } from './interfaces'; @@ -16,6 +17,7 @@ const defaultState: PluginsState = { AUTO_ANNOTATION: false, TF_ANNOTATION: false, TF_SEGMENTATION: false, + DEXTR_SEGMENTATION: false, ANALYTICS: false, }, }; @@ -39,6 +41,10 @@ export default function ( registerGitPlugin(); } + if (!state.list.DEXTR_SEGMENTATION && list.DEXTR_SEGMENTATION) { + registerDEXTRPlugin(); + } + return { ...state, initialized: true, diff --git a/cvat-ui/src/utils/dextr-utils.ts b/cvat-ui/src/utils/dextr-utils.ts new file mode 100644 index 00000000000..9e815374201 --- /dev/null +++ b/cvat-ui/src/utils/dextr-utils.ts @@ -0,0 +1,254 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import getCore from 'cvat-core'; +import { Canvas } from 'cvat-canvas'; +import { ShapeType, RQStatus } from 'reducers/interfaces'; + +const core = getCore(); +const baseURL = core.config.backendAPI.slice(0, -7); + +interface DEXTRPlugin { + name: string; + description: string; + cvat: { + classes: { + Job: { + prototype: { + annotations: { + put: { + enter(self: any, objects: any[]): Promise; + }; + }; + }; + }; + }; + }; + data: { + canceled: boolean; + enabled: boolean; + }; +} + +interface Point { + x: number; + y: number; +} + +const antModalRoot = document.createElement('div'); +const antModalMask = document.createElement('div'); +antModalMask.classList.add('ant-modal-mask'); +const antModalWrap = document.createElement('div'); +antModalWrap.classList.add('ant-modal-wrap'); +antModalWrap.setAttribute('role', 'dialog'); +const antModal = document.createElement('div'); +antModal.classList.add('ant-modal'); +antModal.style.width = '300px'; +antModal.style.top = '40%'; +antModal.setAttribute('role', 'document'); +const antModalContent = document.createElement('div'); +antModalContent.classList.add('ant-modal-content'); +const antModalBody = document.createElement('div'); +antModalBody.classList.add('ant-modal-body'); +antModalBody.style.textAlign = 'center'; +const antModalSpan = document.createElement('span'); +antModalSpan.innerText = 'Segmentation request is being processed'; +antModalSpan.style.display = 'block'; +const antModalButton = document.createElement('button'); +antModalButton.disabled = true; +antModalButton.classList.add('ant-btn', 'ant-btn-primary'); +antModalButton.style.width = '100px'; +antModalButton.style.margin = '10px auto'; +const antModalButtonSpan = document.createElement('span'); +antModalButtonSpan.innerText = 'Cancel'; + +antModalBody.append(antModalSpan, antModalButton); +antModalButton.append(antModalButtonSpan); +antModalContent.append(antModalBody); +antModal.append(antModalContent); +antModalWrap.append(antModal); +antModalRoot.append(antModalMask, antModalWrap); + + +function serverRequest( + plugin: DEXTRPlugin, + jid: number, + frame: number, + points: number[], +): Promise { + return new Promise((resolve, reject) => { + const reducer = (acc: Point[], _: number, index: number, array: number[]): Point[] => { + if (!(index % 2)) { // 0, 2, 4 + acc.push({ + x: array[index], + y: array[index + 1], + }); + } + + return acc; + }; + + const reducedPoints = points.reduce(reducer, []); + core.server.request( + `${baseURL}/dextr/create/${jid}`, { + method: 'POST', + data: JSON.stringify({ + frame, + points: reducedPoints, + }), + headers: { + 'Content-Type': 'application/json', + }, + }, + ).then(() => { + const timeoutCallback = (): void => { + core.server.request( + `${baseURL}/dextr/check/${jid}`, { + method: 'GET', + }, + ).then((response: any) => { + const { status } = response; + if (status === RQStatus.finished) { + resolve(response.result.split(/\s|,/).map((coord: string) => +coord)); + } else if (status === RQStatus.failed) { + reject(new Error(response.stderr)); + } else if (status === RQStatus.unknown) { + reject(new Error('Unknown DEXTR status has been received')); + } else { + if (status === RQStatus.queued) { + antModalButton.disabled = false; + } + if (!plugin.data.canceled) { + setTimeout(timeoutCallback, 1000); + } else { + core.server.request( + `${baseURL}/dextr/cancel/${jid}`, { + method: 'GET', + }, + ).then(() => { + resolve(points); + }).catch((error: Error) => { + reject(error); + }); + } + } + }).catch((error: Error) => { + reject(error); + }); + }; + + setTimeout(timeoutCallback, 1000); + }).catch((error: Error) => { + reject(error); + }); + }); + + // start checking +} + +const plugin: DEXTRPlugin = { + name: 'Deep extreme cut', + description: 'Plugin allows to get a polygon from extreme points using AI', + cvat: { + classes: { + Job: { + prototype: { + annotations: { + put: { + async enter(self: DEXTRPlugin, objects: any[]): Promise { + try { + if (self.data.enabled) { + document.body.append(antModalRoot); + const promises: Record> = {}; + for (let i = 0; i < objects.length; i++) { + if (objects[i].points.length >= 8) { + promises[i] = serverRequest( + self, + (this as any).id, + objects[i].frame, + objects[i].points, + ); + } else { + promises[i] = new Promise((resolve) => { + resolve(objects[i].points); + }); + } + } + + const transformed = await Promise + .all(Object.values(promises)); + for (let i = 0; i < objects.length; i++) { + // eslint-disable-next-line no-param-reassign + objects[i] = new core.classes.ObjectState({ + frame: objects[i].frame, + objectType: objects[i].objectType, + label: objects[i].label, + shapeType: ShapeType.POLYGON, + points: transformed[i], + occluded: objects[i].occluded, + zOrder: objects[i].zOrder, + }); + } + } + + return; + } catch (error) { + throw new core.exceptions.PluginError(error.toString()); + } finally { + // eslint-disable-next-line no-param-reassign + self.data.canceled = false; + antModalButton.disabled = true; + document.body.removeChild(antModalRoot); + } + }, + }, + }, + }, + }, + }, + }, + data: { + canceled: false, + enabled: false, + }, +}; + + +antModalButton.onclick = () => { + plugin.data.canceled = true; +}; + +export function activate(canvasInstance: Canvas): void { + if (!plugin.data.enabled) { + // eslint-disable-next-line no-param-reassign + canvasInstance.draw = (drawData: any): void => { + if (drawData.enabled && drawData.shapeType === ShapeType.POLYGON + && (typeof (drawData.numberOfPoints) === 'undefined' || drawData.numberOfPoints >= 4) + && (typeof (drawData.initialState) === 'undefined') + ) { + const patchedData = { ...drawData }; + patchedData.shapeType = ShapeType.POINTS; + patchedData.crosshair = true; + Object.getPrototypeOf(canvasInstance) + .draw.call(canvasInstance, patchedData); + } else { + Object.getPrototypeOf(canvasInstance) + .draw.call(canvasInstance, drawData); + } + }; + plugin.data.enabled = true; + } +} + +export function deactivate(canvasInstance: Canvas): void { + if (plugin.data.enabled) { + // eslint-disable-next-line no-param-reassign + canvasInstance.draw = Object.getPrototypeOf(canvasInstance).draw; + plugin.data.enabled = false; + } +} + +export function registerDEXTRPlugin(): void { + core.plugins.register(plugin); +} diff --git a/cvat-ui/src/utils/plugin-checker.ts b/cvat-ui/src/utils/plugin-checker.ts index 38762b06bf1..93b4768f55a 100644 --- a/cvat-ui/src/utils/plugin-checker.ts +++ b/cvat-ui/src/utils/plugin-checker.ts @@ -35,6 +35,9 @@ class PluginChecker { case SupportedPlugins.TF_SEGMENTATION: { return isReachable(`${serverHost}/tensorflow/segmentation/meta/get`, 'OPTIONS'); } + case SupportedPlugins.DEXTR_SEGMENTATION: { + return isReachable(`${serverHost}/dextr/enabled`, 'GET'); + } case SupportedPlugins.ANALYTICS: { return isReachable(`${serverHost}/analytics/app/kibana`, 'GET'); } diff --git a/cvat/apps/dextr_segmentation/urls.py b/cvat/apps/dextr_segmentation/urls.py index 82e415bd7f6..11a92983ef3 100644 --- a/cvat/apps/dextr_segmentation/urls.py +++ b/cvat/apps/dextr_segmentation/urls.py @@ -8,5 +8,6 @@ urlpatterns = [ path('create/', views.create), path('cancel/', views.cancel), - path('check/', views.check) -] \ No newline at end of file + path('check/', views.check), + path('enabled', views.enabled) +] diff --git a/cvat/apps/dextr_segmentation/views.py b/cvat/apps/dextr_segmentation/views.py index a4827e99afc..dd78a2b01e4 100644 --- a/cvat/apps/dextr_segmentation/views.py +++ b/cvat/apps/dextr_segmentation/views.py @@ -123,3 +123,6 @@ def check(request, jid): except Exception as ex: slogger.job[jid].error("can't check a dextr request for the job {}".format(jid), exc_info=True) return HttpResponseBadRequest(str(ex)) + +def enabled(request): + return HttpResponse()