You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
422 lines
16 KiB
TypeScript
422 lines
16 KiB
TypeScript
// Copyright (C) 2021 Intel Corporation
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
import React from 'react';
|
|
import { connect } from 'react-redux';
|
|
import { Row, Col } from 'antd/lib/grid';
|
|
import Popover from 'antd/lib/popover';
|
|
import Icon, { ScissorOutlined } from '@ant-design/icons';
|
|
import Text from 'antd/lib/typography/Text';
|
|
import Tabs from 'antd/lib/tabs';
|
|
import Button from 'antd/lib/button';
|
|
import Progress from 'antd/lib/progress';
|
|
import notification from 'antd/lib/notification';
|
|
|
|
import { OpenCVIcon } from 'icons';
|
|
import { Canvas, convertShapesForInteractor } from 'cvat-canvas-wrapper';
|
|
import getCore from 'cvat-core-wrapper';
|
|
import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper';
|
|
import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors';
|
|
import {
|
|
CombinedState, ActiveControl, OpenCVTool, ObjectType,
|
|
} from 'reducers/interfaces';
|
|
import {
|
|
interactWithCanvas,
|
|
fetchAnnotationsAsync,
|
|
updateAnnotationsAsync,
|
|
createAnnotationsAsync,
|
|
} from 'actions/annotation-actions';
|
|
import LabelSelector from 'components/label-selector/label-selector';
|
|
import CVATTooltip from 'components/common/cvat-tooltip';
|
|
import withVisibilityHandling from './handle-popover-visibility';
|
|
|
|
interface Props {
|
|
labels: any[];
|
|
canvasInstance: Canvas;
|
|
jobInstance: any;
|
|
isActivated: boolean;
|
|
states: any[];
|
|
frame: number;
|
|
curZOrder: number;
|
|
}
|
|
|
|
interface DispatchToProps {
|
|
onInteractionStart(activeInteractor: OpenCVTool, activeLabelID: number): void;
|
|
updateAnnotations(statesToUpdate: any[]): void;
|
|
createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void;
|
|
fetchAnnotations(): void;
|
|
}
|
|
|
|
interface State {
|
|
libraryInitialized: boolean;
|
|
initializationError: boolean;
|
|
initializationProgress: number;
|
|
activeLabelID: number;
|
|
}
|
|
|
|
const core = getCore();
|
|
const CustomPopover = withVisibilityHandling(Popover, 'opencv-control');
|
|
|
|
function mapStateToProps(state: CombinedState): Props {
|
|
const {
|
|
annotation: {
|
|
annotations: {
|
|
states,
|
|
zLayer: { cur: curZOrder },
|
|
},
|
|
job: { instance: jobInstance, labels },
|
|
canvas: { activeControl, instance: canvasInstance },
|
|
player: {
|
|
frame: { number: frame },
|
|
},
|
|
},
|
|
} = state;
|
|
|
|
return {
|
|
isActivated: activeControl === ActiveControl.OPENCV_TOOLS,
|
|
canvasInstance: canvasInstance as Canvas,
|
|
jobInstance,
|
|
curZOrder,
|
|
labels,
|
|
states,
|
|
frame,
|
|
};
|
|
}
|
|
|
|
const mapDispatchToProps = {
|
|
onInteractionStart: interactWithCanvas,
|
|
updateAnnotations: updateAnnotationsAsync,
|
|
fetchAnnotations: fetchAnnotationsAsync,
|
|
createAnnotations: createAnnotationsAsync,
|
|
};
|
|
|
|
class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps, State> {
|
|
private activeTool: IntelligentScissors | null;
|
|
private interactiveStateID: number | null;
|
|
private interactionIsDone: boolean;
|
|
|
|
public constructor(props: Props & DispatchToProps) {
|
|
super(props);
|
|
const { labels } = props;
|
|
|
|
this.activeTool = null;
|
|
this.interactiveStateID = null;
|
|
this.interactionIsDone = false;
|
|
|
|
this.state = {
|
|
libraryInitialized: openCVWrapper.isInitialized,
|
|
initializationError: false,
|
|
initializationProgress: -1,
|
|
activeLabelID: labels.length ? labels[0].id : null,
|
|
};
|
|
}
|
|
|
|
public componentDidMount(): void {
|
|
const { canvasInstance } = this.props;
|
|
canvasInstance.html().addEventListener('canvas.interacted', this.interactionListener);
|
|
canvasInstance.html().addEventListener('canvas.canceled', this.cancelListener);
|
|
}
|
|
|
|
public componentDidUpdate(prevProps: Props): void {
|
|
const { isActivated } = this.props;
|
|
if (!prevProps.isActivated && isActivated) {
|
|
// reset flags when before using a tool
|
|
if (this.activeTool) {
|
|
this.activeTool.reset();
|
|
}
|
|
this.interactiveStateID = null;
|
|
this.interactionIsDone = false;
|
|
}
|
|
}
|
|
|
|
public componentWillUnmount(): void {
|
|
const { canvasInstance } = this.props;
|
|
canvasInstance.html().removeEventListener('canvas.interacted', this.interactionListener);
|
|
canvasInstance.html().removeEventListener('canvas.canceled', this.cancelListener);
|
|
}
|
|
|
|
private getInteractiveState(): any | null {
|
|
const { states } = this.props;
|
|
return states.filter((_state: any): boolean => _state.clientID === this.interactiveStateID)[0] || null;
|
|
}
|
|
|
|
private cancelListener = async (): Promise<void> => {
|
|
const {
|
|
fetchAnnotations, isActivated, jobInstance, frame,
|
|
} = this.props;
|
|
|
|
if (isActivated) {
|
|
if (this.interactiveStateID !== null) {
|
|
const state = this.getInteractiveState();
|
|
this.interactiveStateID = null;
|
|
await state.delete(frame);
|
|
fetchAnnotations();
|
|
}
|
|
|
|
await jobInstance.actions.freeze(false);
|
|
}
|
|
};
|
|
|
|
private interactionListener = async (e: Event): Promise<void> => {
|
|
const {
|
|
fetchAnnotations, updateAnnotations, isActivated, jobInstance, frame, labels, curZOrder,
|
|
} = this.props;
|
|
const { activeLabelID } = this.state;
|
|
if (!isActivated || !this.activeTool) {
|
|
return;
|
|
}
|
|
|
|
const {
|
|
shapesUpdated, isDone, threshold, shapes,
|
|
} = (e as CustomEvent).detail;
|
|
const pressedPoints = convertShapesForInteractor(shapes, 0).flat();
|
|
this.interactionIsDone = isDone;
|
|
|
|
try {
|
|
let points: number[] = [];
|
|
if (shapesUpdated) {
|
|
points = await this.runCVAlgorithm(pressedPoints, threshold);
|
|
}
|
|
|
|
if (this.interactiveStateID === null) {
|
|
if (!this.interactionIsDone) {
|
|
await jobInstance.actions.freeze(true);
|
|
}
|
|
|
|
const object = new core.classes.ObjectState({
|
|
...this.activeTool.params.shape,
|
|
frame,
|
|
objectType: ObjectType.SHAPE,
|
|
label: labels.filter((label: any) => label.id === activeLabelID)[0],
|
|
points,
|
|
occluded: false,
|
|
zOrder: curZOrder,
|
|
});
|
|
// need a clientID of a created object to interact with it further
|
|
// so, we do not use createAnnotationAction
|
|
const [clientID] = await jobInstance.annotations.put([object]);
|
|
this.interactiveStateID = clientID;
|
|
|
|
// update annotations on a canvas
|
|
fetchAnnotations();
|
|
return;
|
|
}
|
|
|
|
const state = this.getInteractiveState();
|
|
if ((e as CustomEvent).detail.isDone) {
|
|
const finalObject = new core.classes.ObjectState({
|
|
frame: state.frame,
|
|
objectType: state.objectType,
|
|
label: state.label,
|
|
shapeType: state.shapeType,
|
|
// need to recalculate without the latest sliding point
|
|
points: points = await this.runCVAlgorithm(pressedPoints, threshold),
|
|
occluded: state.occluded,
|
|
zOrder: state.zOrder,
|
|
});
|
|
this.interactiveStateID = null;
|
|
await state.delete(frame);
|
|
await jobInstance.actions.freeze(false);
|
|
await jobInstance.annotations.put([finalObject]);
|
|
fetchAnnotations();
|
|
} else {
|
|
state.points = points;
|
|
updateAnnotations([state]);
|
|
fetchAnnotations();
|
|
}
|
|
} catch (error) {
|
|
notification.error({
|
|
description: error.toString(),
|
|
message: 'Processing error occured',
|
|
});
|
|
}
|
|
};
|
|
|
|
private async runCVAlgorithm(pressedPoints: number[], threshold: number): Promise<number[]> {
|
|
// Getting image data
|
|
const canvas: HTMLCanvasElement | undefined = window.document.getElementById('cvat_canvas_background') as
|
|
| HTMLCanvasElement
|
|
| undefined;
|
|
if (!canvas) {
|
|
throw new Error('Element #cvat_canvas_background was not found');
|
|
}
|
|
|
|
const { width, height } = canvas;
|
|
const context = canvas.getContext('2d');
|
|
if (!context) {
|
|
throw new Error('Canvas context is empty');
|
|
}
|
|
|
|
const [x, y] = pressedPoints.slice(-2);
|
|
const startX = Math.round(Math.max(0, x - threshold));
|
|
const startY = Math.round(Math.max(0, y - threshold));
|
|
const segmentWidth = Math.min(2 * threshold, width - startX);
|
|
const segmentHeight = Math.min(2 * threshold, height - startY);
|
|
const imageData = context.getImageData(startX, startY, segmentWidth, segmentHeight);
|
|
|
|
if (!this.activeTool) return [];
|
|
|
|
// Handling via OpenCV.js
|
|
const points = await this.activeTool.run(pressedPoints, imageData, startX, startY);
|
|
|
|
// Increasing number of points artificially
|
|
let minNumberOfPoints = 1;
|
|
// eslint-disable-next-line: eslintdot-notation
|
|
if (this.activeTool.params.shape.shapeType === 'polyline') {
|
|
minNumberOfPoints = 2;
|
|
} else if (this.activeTool.params.shape.shapeType === 'polygon') {
|
|
minNumberOfPoints = 3;
|
|
}
|
|
while (points.length < minNumberOfPoints * 2) {
|
|
points.push(...points.slice(points.length - 2));
|
|
}
|
|
|
|
return points;
|
|
}
|
|
|
|
private renderDrawingContent(): JSX.Element {
|
|
const { activeLabelID } = this.state;
|
|
const { labels, canvasInstance, onInteractionStart } = this.props;
|
|
|
|
return (
|
|
<>
|
|
<Row justify='center'>
|
|
<Col span={24}>
|
|
<LabelSelector
|
|
style={{ width: '100%' }}
|
|
labels={labels}
|
|
value={activeLabelID}
|
|
onChange={(label: any) => this.setState({ activeLabelID: label.id })}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
<Row justify='start' className='cvat-opencv-drawing-tools'>
|
|
<Col>
|
|
<CVATTooltip title='Intelligent scissors' className='cvat-opencv-drawing-tool'>
|
|
<Button
|
|
onClick={() => {
|
|
this.activeTool = openCVWrapper.segmentation.intelligentScissorsFactory();
|
|
canvasInstance.cancel();
|
|
onInteractionStart(this.activeTool, activeLabelID);
|
|
canvasInstance.interact({
|
|
enabled: true,
|
|
...this.activeTool.params.canvas,
|
|
});
|
|
}}
|
|
>
|
|
<ScissorOutlined />
|
|
</Button>
|
|
</CVATTooltip>
|
|
</Col>
|
|
</Row>
|
|
</>
|
|
);
|
|
}
|
|
|
|
private renderContent(): JSX.Element {
|
|
const { libraryInitialized, initializationProgress, initializationError } = this.state;
|
|
|
|
return (
|
|
<div className='cvat-opencv-control-popover-content'>
|
|
<Row justify='start'>
|
|
<Col>
|
|
<Text className='cvat-text-color' strong>
|
|
OpenCV
|
|
</Text>
|
|
</Col>
|
|
</Row>
|
|
{libraryInitialized ? (
|
|
<Tabs tabBarGutter={8}>
|
|
<Tabs.TabPane key='drawing' tab='Drawing' className='cvat-opencv-control-tabpane'>
|
|
{this.renderDrawingContent()}
|
|
</Tabs.TabPane>
|
|
<Tabs.TabPane disabled key='image' tab='Image' className='cvat-opencv-control-tabpane' />
|
|
</Tabs>
|
|
) : (
|
|
<>
|
|
<Row justify='start' align='middle'>
|
|
<Col span={initializationProgress >= 0 ? 17 : 24}>
|
|
<Button
|
|
disabled={initializationProgress !== -1}
|
|
className='cvat-opencv-initialization-button'
|
|
onClick={async () => {
|
|
try {
|
|
this.setState({
|
|
initializationError: false,
|
|
initializationProgress: 0,
|
|
});
|
|
await openCVWrapper.initialize((progress: number) => {
|
|
this.setState({ initializationProgress: progress });
|
|
});
|
|
this.setState({ libraryInitialized: true });
|
|
} catch (error) {
|
|
notification.error({
|
|
description: error.toString(),
|
|
message: 'Could not initialize OpenCV library',
|
|
});
|
|
this.setState({
|
|
initializationError: true,
|
|
initializationProgress: -1,
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
Load OpenCV
|
|
</Button>
|
|
</Col>
|
|
{initializationProgress >= 0 && (
|
|
<Col span={6} offset={1}>
|
|
<Progress
|
|
width={8 * 5}
|
|
percent={initializationProgress}
|
|
type='circle'
|
|
status={initializationError ? 'exception' : undefined}
|
|
/>
|
|
</Col>
|
|
)}
|
|
</Row>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
public render(): JSX.Element {
|
|
const { isActivated, canvasInstance, labels } = this.props;
|
|
const dynamcPopoverPros = isActivated ?
|
|
{
|
|
overlayStyle: {
|
|
display: 'none',
|
|
},
|
|
} :
|
|
{};
|
|
|
|
const dynamicIconProps = isActivated ?
|
|
{
|
|
className: 'cvat-opencv-control cvat-active-canvas-control',
|
|
onClick: (): void => {
|
|
canvasInstance.interact({ enabled: false });
|
|
},
|
|
} :
|
|
{
|
|
className: 'cvat-tools-control',
|
|
};
|
|
|
|
return !labels.length ? (
|
|
<Icon className='cvat-opencv-control cvat-disabled-canvas-control' component={OpenCVIcon} />
|
|
) : (
|
|
<CustomPopover
|
|
{...dynamcPopoverPros}
|
|
placement='right'
|
|
overlayClassName='cvat-opencv-control-popover'
|
|
content={this.renderContent()}
|
|
>
|
|
<Icon {...dynamicIconProps} component={OpenCVIcon} />
|
|
</CustomPopover>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(OpenCVControlComponent);
|