diff --git a/CHANGELOG.md b/CHANGELOG.md index 84dbdf61..844cc02f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added password reset functionality () - Ability to work with data on the fly (https://github.com/opencv/cvat/pull/2007) - Annotation in process outline color wheel () +- On the fly annotation using DL detectors () - [Datumaro] CLI command for dataset equality comparison () - [Datumaro] Merging of datasets with different labels () diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 7370e0fa..5f7a0b86 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.9.1", + "version": "1.9.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 66ba9e86..f91eabd3 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.9.1", + "version": "1.9.2", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { 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 5bdae3d4..195466b2 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 @@ -10,6 +10,7 @@ import Select, { OptionProps } from 'antd/lib/select'; import Button from 'antd/lib/button'; import Modal from 'antd/lib/modal'; import Text from 'antd/lib/typography/Text'; +import Tabs from 'antd/lib/tabs'; import { Row, Col } from 'antd/lib/grid'; import notification from 'antd/lib/notification'; @@ -23,8 +24,14 @@ import { ObjectType, ShapeType, } from 'reducers/interfaces'; -import { interactWithCanvas, fetchAnnotationsAsync, updateAnnotationsAsync } from 'actions/annotation-actions'; +import { + interactWithCanvas, + fetchAnnotationsAsync, + updateAnnotationsAsync, + createAnnotationsAsync, +} from 'actions/annotation-actions'; import { InteractionResult } from 'cvat-canvas/src/typescript/canvas'; +import DetectorRunner from 'components/model-runner-modal/detector-runner'; interface StateToProps { canvasInstance: Canvas; @@ -35,11 +42,13 @@ interface StateToProps { isInteraction: boolean; frame: number; interactors: Model[]; + detectors: Model[]; } interface DispatchToProps { onInteractionStart(activeInteractor: Model, activeLabelID: number): void; updateAnnotations(statesToUpdate: any[]): void; + createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void; fetchAnnotations(): void; } @@ -51,10 +60,11 @@ function mapStateToProps(state: CombinedState): StateToProps { const { instance: jobInstance } = annotation.job; const { instance: canvasInstance, activeControl } = annotation.canvas; const { models } = state; - const { interactors } = models; + const { interactors, detectors } = models; return { interactors, + detectors, isInteraction: activeControl === ActiveControl.INTERACTION, activeLabelID: annotation.drawing.activeLabelID, labels: annotation.job.labels, @@ -65,19 +75,12 @@ function mapStateToProps(state: CombinedState): StateToProps { }; } -function mapDispatchToProps(dispatch: any): DispatchToProps { - return { - onInteractionStart(activeInteractor: Model, activeLabelID: number): void { - dispatch(interactWithCanvas(activeInteractor, activeLabelID)); - }, - updateAnnotations(statesToUpdate: any[]): void { - dispatch(updateAnnotationsAsync(statesToUpdate)); - }, - fetchAnnotations(): void { - dispatch(fetchAnnotationsAsync()); - }, - }; -} +const mapDispatchToProps = { + onInteractionStart: interactWithCanvas, + updateAnnotations: updateAnnotationsAsync, + fetchAnnotations: fetchAnnotationsAsync, + createAnnotations: createAnnotationsAsync, +}; function convertShapesForInteractor(shapes: InteractionResult[]): number[][] { const reducer = (acc: number[][], _: number, index: number, array: number[]): number[][] => { @@ -378,9 +381,10 @@ class ToolsControlComponent extends React.PureComponent { - - + + + + + + ); +} + + +export default React.memo(DetectorRunner, (prevProps: Props, nextProps: Props): boolean => ( + prevProps.task === nextProps.task + && prevProps.runInference === nextProps.runInference + && prevProps.models.length === nextProps.models.length + && nextProps.models.reduce((acc: boolean, model: Model, index: number): boolean => ( + acc && model.id === prevProps.models[index].id + ), true) +)); diff --git a/cvat-ui/src/components/model-runner-modal/model-runner-dialog.tsx b/cvat-ui/src/components/model-runner-modal/model-runner-dialog.tsx new file mode 100644 index 00000000..cff9914b --- /dev/null +++ b/cvat-ui/src/components/model-runner-modal/model-runner-dialog.tsx @@ -0,0 +1,88 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React from 'react'; +import { connect } from 'react-redux'; +import Modal from 'antd/lib/modal'; + +import { ThunkDispatch } from 'utils/redux'; +import { modelsActions, startInferenceAsync } from 'actions/models-actions'; +import { Model, CombinedState } from 'reducers/interfaces'; +import DetectorRunner from './detector-runner'; + + +interface StateToProps { + visible: boolean; + task: any; + detectors: Model[]; + reid: Model[]; +} + +interface DispatchToProps { + runInference(task: any, model: Model, body: object): void; + closeDialog(): void; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { models } = state; + const { detectors, reid } = models; + + return { + visible: models.visibleRunWindows, + task: models.activeRunTask, + reid, + detectors, + }; +} + +function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps { + return { + runInference(task: any, model: Model, body: object) { + dispatch(startInferenceAsync(task, model, body)); + }, + closeDialog() { + dispatch(modelsActions.closeRunModelDialog()); + }, + }; +} + +function ModelRunnerDialog(props: StateToProps & DispatchToProps): JSX.Element { + const { + reid, + detectors, + task, + visible, + runInference, + closeDialog, + } = props; + + const models = [...reid, ...detectors]; + + return ( + closeDialog()} + maskClosable + title='Automatic annotation' + > + { + closeDialog(); + runInference(...args); + }} + /> + + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(ModelRunnerDialog); diff --git a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx deleted file mode 100644 index d8fd959e..00000000 --- a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx +++ /dev/null @@ -1,450 +0,0 @@ -// Copyright (C) 2020 Intel Corporation -// -// SPDX-License-Identifier: MIT - -import './styles.scss'; -import React from 'react'; -import { Row, Col } from 'antd/lib/grid'; -import Icon from 'antd/lib/icon'; -import Select from 'antd/lib/select'; -import Checkbox from 'antd/lib/checkbox'; -import Tooltip from 'antd/lib/tooltip'; -import Modal from 'antd/lib/modal'; -import Tag from 'antd/lib/tag'; -import notification from 'antd/lib/notification'; -import Text from 'antd/lib/typography/Text'; -import InputNumber from 'antd/lib/input-number'; - -import { - Model, - StringObject, -} from 'reducers/interfaces'; - -interface Props { - reid: Model[]; - detectors: Model[]; - activeProcesses: StringObject; - visible: boolean; - taskInstance: any; - closeDialog(): void; - runInference( - taskInstance: any, - model: Model, - body: object, - ): void; -} - -interface State { - selectedModel: string | null; - cleanup: boolean; - mapping: StringObject; - colors: StringObject; - matching: { - model: string; - task: string; - }; - - threshold: number; - maxDistance: number; -} - -function colorGenerator(): () => string { - const values = [ - 'magenta', 'green', 'geekblue', - 'orange', 'red', 'cyan', - 'blue', 'volcano', 'purple', - ]; - - let index = 0; - - return (): string => { - const color = values[index++]; - if (index >= values.length) { - index = 0; - } - - return color; - }; -} - -const nextColor = colorGenerator(); - -export default class ModelRunnerModalComponent extends React.PureComponent { - public constructor(props: Props) { - super(props); - this.state = { - selectedModel: null, - mapping: {}, - colors: {}, - cleanup: false, - matching: { - model: '', - task: '', - }, - - threshold: 0.5, - maxDistance: 50, - }; - } - - public componentDidUpdate(prevProps: Props, prevState: State): void { - const { - reid, - detectors, - taskInstance, - visible, - } = this.props; - - const { selectedModel } = this.state; - const models = [...reid, ...detectors]; - - if (!prevProps.visible && visible) { - this.setState({ - selectedModel: null, - mapping: {}, - matching: { - model: '', - task: '', - }, - cleanup: false, - }); - } - - if (selectedModel && prevState.selectedModel !== selectedModel) { - const selectedModelInstance = models - .filter((model) => model.name === selectedModel)[0]; - - if (selectedModelInstance.type !== 'reid' && !selectedModelInstance.labels.length) { - notification.warning({ - message: 'The selected model does not include any lables', - }); - } - - let taskLabels: string[] = taskInstance.labels - .map((label: any): string => label.name); - const [defaultMapping, defaultColors]: StringObject[] = selectedModelInstance.labels - .reduce((acc: StringObject[], label): StringObject[] => { - if (taskLabels.includes(label)) { - acc[0][label] = label; - acc[1][label] = nextColor(); - taskLabels = taskLabels.filter((_label): boolean => _label !== label); - } - - return acc; - }, [{}, {}]); - - this.setState({ - mapping: defaultMapping, - colors: defaultColors, - }); - } - } - - private renderModelSelector(): JSX.Element { - const { reid, detectors } = this.props; - const models = [...reid, ...detectors]; - - return ( - - Model: - - - - - ); - } - - private renderMappingTag(modelLabel: string, taskLabel: string): JSX.Element { - const { colors, mapping } = this.state; - - return ( - - - {modelLabel} - - - {taskLabel} - - - - { - const newMapping = { ...mapping }; - delete newMapping[modelLabel]; - this.setState({ - mapping: newMapping, - }); - }} - /> - - - - ); - } - - private renderMappingInputSelector( - value: string, - current: string, - options: string[], - ): JSX.Element { - const { matching, mapping, colors } = this.state; - - return ( - - ); - } - - private renderMappingInput( - availableModelLabels: string[], - availableTaskLabels: string[], - ): JSX.Element { - const { matching } = this.state; - return ( - - - {this.renderMappingInputSelector( - matching.model, - 'Model', - availableModelLabels, - )} - - - {this.renderMappingInputSelector( - matching.task, - 'Task', - availableTaskLabels, - )} - - - - - - - - ); - } - - private renderReidContent(): JSX.Element { - const { threshold, maxDistance } = this.state; - - return ( -
- - - Threshold - - - - { - if (typeof (value) === 'number') { - this.setState({ - threshold: value, - }); - } - }} - /> - - - - - - Maximum distance - - - - { - if (typeof (value) === 'number') { - this.setState({ - maxDistance: value, - }); - } - }} - /> - - - -
- ); - } - - private renderContent(): JSX.Element { - const { selectedModel, cleanup, mapping } = this.state; - const { reid, detectors, taskInstance } = this.props; - - const models = [...reid, ...detectors]; - const model = selectedModel && models - .filter((_model): boolean => _model.name === selectedModel)[0]; - - const excludedModelLabels: string[] = Object.keys(mapping); - const isDetector = model && model.type === 'detector'; - const isReId = model && model.type === 'reid'; - const tags = isDetector ? excludedModelLabels - .map((modelLabel: string) => this.renderMappingTag( - modelLabel, - mapping[modelLabel], - )) : []; - - const availableModelLabels = model ? model.labels - .filter( - (label: string) => !excludedModelLabels.includes(label), - ) : []; - const taskLabels = taskInstance.labels.map( - (label: any) => label.name, - ); - - const mappingISAvailable = !!availableModelLabels.length - && !!taskLabels.length; - - return ( -
- { this.renderModelSelector() } - { isDetector && tags} - { isDetector - && mappingISAvailable - && this.renderMappingInput(availableModelLabels, taskLabels)} - { isDetector - && ( -
- this.setState({ - cleanup: e.target.checked, - })} - > - Clean old annotations - -
- )} - { isReId && this.renderReidContent() } -
- ); - } - - public render(): JSX.Element | false { - const { - selectedModel, - mapping, - cleanup, - threshold, - maxDistance, - } = this.state; - - const { - reid, - detectors, - visible, - taskInstance, - runInference, - closeDialog, - } = this.props; - - const models = [...reid, ...detectors]; - const activeModel = models.filter( - (model): boolean => model.name === selectedModel, - )[0]; - - const enabledSubmit = !!activeModel && (activeModel.type === 'reid' - || !!Object.keys(mapping).length); - - return ( - visible && ( - { - runInference( - taskInstance, - models - .filter((model): boolean => model.name === selectedModel)[0], - activeModel.type === 'detector' ? { - mapping, - cleanup, - } : { - threshold, - max_distance: maxDistance, - }, - ); - closeDialog(); - }} - onCancel={(): void => closeDialog()} - okButtonProps={{ disabled: !enabledSubmit }} - title='Automatic annotation' - visible - > - { this.renderContent() } - - ) - ); - } -} diff --git a/cvat-ui/src/components/model-runner-modal/styles.scss b/cvat-ui/src/components/model-runner-modal/styles.scss index 9c9bc0c0..80bef525 100644 --- a/cvat-ui/src/components/model-runner-modal/styles.scss +++ b/cvat-ui/src/components/model-runner-modal/styles.scss @@ -4,10 +4,10 @@ @import '../../base.scss'; -.cvat-run-model-dialog > div:not(first-child) { +.cvat-run-model-content > div:not(first-child) { margin-top: 10px; } -.cvat-run-model-dialog-remove-mapping-icon { +.cvat-run-model-content-remove-mapping-icon { color: $danger-icon-color; } diff --git a/cvat-ui/src/components/task-page/task-page.tsx b/cvat-ui/src/components/task-page/task-page.tsx index c66db671..ea6cdf80 100644 --- a/cvat-ui/src/components/task-page/task-page.tsx +++ b/cvat-ui/src/components/task-page/task-page.tsx @@ -12,7 +12,7 @@ import Result from 'antd/lib/result'; import DetailsContainer from 'containers/task-page/details'; import JobListContainer from 'containers/task-page/job-list'; -import ModelRunnerModalContainer from 'containers/model-runner-dialog/model-runner-dialog'; +import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog'; import { Task } from 'reducers/interfaces'; import TopBarComponent from './top-bar'; @@ -75,7 +75,7 @@ class TaskPageComponent extends React.PureComponent {
- + ); } diff --git a/cvat-ui/src/components/tasks-page/task-list.tsx b/cvat-ui/src/components/tasks-page/task-list.tsx index 68515a62..01bfcb6a 100644 --- a/cvat-ui/src/components/tasks-page/task-list.tsx +++ b/cvat-ui/src/components/tasks-page/task-list.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { Row, Col } from 'antd/lib/grid'; import Pagination from 'antd/lib/pagination'; -import ModelRunnerModalContainer from 'containers/model-runner-dialog/model-runner-dialog'; +import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog'; import TaskItem from 'containers/tasks-page/task-item'; export interface ContentListProps { @@ -46,7 +46,7 @@ export default function TaskListComponent(props: ContentListProps): JSX.Element /> - + ); } diff --git a/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx b/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx deleted file mode 100644 index 3f63bd7f..00000000 --- a/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (C) 2020 Intel Corporation -// -// SPDX-License-Identifier: MIT - -import { connect } from 'react-redux'; - -import ModelRunnerModalComponent from 'components/model-runner-modal/model-runner-modal'; -import { Model, CombinedState } from 'reducers/interfaces'; -import { startInferenceAsync, modelsActions } from 'actions/models-actions'; - -interface StateToProps { - reid: Model[]; - detectors: Model[]; - activeProcesses: { - [index: string]: string; - }; - taskInstance: any; - visible: boolean; -} - -interface DispatchToProps { - runInference( - taskInstance: any, - model: Model, - body: object, - ): void; - closeDialog(): void; -} - -function mapStateToProps(state: CombinedState): StateToProps { - const { models } = state; - - return { - reid: models.reid, - detectors: models.detectors, - activeProcesses: {}, - taskInstance: models.activeRunTask, - visible: models.visibleRunWindows, - }; -} - -function mapDispatchToProps(dispatch: any): DispatchToProps { - return ({ - runInference( - taskInstance: any, - model: Model, - body: object, - ): void { - dispatch(startInferenceAsync(taskInstance, model, body)); - }, - closeDialog(): void { - dispatch(modelsActions.closeRunModelDialog()); - }, - }); -} - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(ModelRunnerModalComponent); diff --git a/cvat-ui/src/styles.scss b/cvat-ui/src/styles.scss index cbd6a4c2..284b2772 100644 --- a/cvat-ui/src/styles.scss +++ b/cvat-ui/src/styles.scss @@ -43,6 +43,10 @@ hr { color: $info-icon-color; } +.cvat-danger-circle-icon { + color: $danger-icon-color; +} + #root { width: 100%; height: 100%;