React UI: ReID algorithm (#1406)
* Initial commit * Connected storage * Added core API method * Done implementation * Removed rule * Removed double cancel * Updated changelog * Fixed: Cannot read property toFixed of undefined * Update CHANGELOG.mdmain
parent
deac1b0bb6
commit
f1aee89589
@ -0,0 +1,227 @@
|
|||||||
|
// Copyright (C) 2020 Intel Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Row, Col } from 'antd/lib/grid';
|
||||||
|
import Modal from 'antd/lib/modal';
|
||||||
|
import Menu from 'antd/lib/menu';
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
import InputNumber from 'antd/lib/input-number';
|
||||||
|
import Tooltip from 'antd/lib/tooltip';
|
||||||
|
|
||||||
|
import { clamp } from 'utils/math';
|
||||||
|
import { run, cancel } from 'utils/reid-utils';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { CombinedState } from 'reducers/interfaces';
|
||||||
|
import { fetchAnnotationsAsync } from 'actions/annotation-actions';
|
||||||
|
|
||||||
|
interface InputModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onCancel(): void;
|
||||||
|
onSubmit(threshold: number, distance: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputModal(props: InputModalProps): JSX.Element {
|
||||||
|
const { visible, onCancel, onSubmit } = props;
|
||||||
|
const [threshold, setThreshold] = useState(0.5);
|
||||||
|
const [distance, setDistance] = useState(50);
|
||||||
|
|
||||||
|
const [thresholdMin, thresholdMax] = [0.05, 0.95];
|
||||||
|
const [distanceMin, distanceMax] = [1, 1000];
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
closable={false}
|
||||||
|
width={300}
|
||||||
|
visible={visible}
|
||||||
|
onCancel={onCancel}
|
||||||
|
onOk={() => onSubmit(threshold, distance)}
|
||||||
|
okText='Merge'
|
||||||
|
>
|
||||||
|
<Row type='flex'>
|
||||||
|
<Col span={10}>
|
||||||
|
<Tooltip title='Similarity of objects on neighbour frames is calculated using AI model'>
|
||||||
|
<Text>Similarity threshold: </Text>
|
||||||
|
</Tooltip>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
min={thresholdMin}
|
||||||
|
max={thresholdMax}
|
||||||
|
step={0.05}
|
||||||
|
value={threshold}
|
||||||
|
onChange={(value: number | undefined) => {
|
||||||
|
if (typeof (value) === 'number') {
|
||||||
|
setThreshold(clamp(value, thresholdMin, thresholdMax));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row type='flex'>
|
||||||
|
<Col span={10}>
|
||||||
|
<Tooltip title='The value defines max distance to merge (between centers of two objects on neighbour frames)'>
|
||||||
|
<Text>Max pixel distance: </Text>
|
||||||
|
</Tooltip>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
min={distanceMin}
|
||||||
|
max={distanceMax}
|
||||||
|
step={5}
|
||||||
|
value={distance}
|
||||||
|
onChange={(value: number | undefined) => {
|
||||||
|
if (typeof (value) === 'number') {
|
||||||
|
setDistance(clamp(value, distanceMin, distanceMax));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InProgressDialogProps {
|
||||||
|
visible: boolean;
|
||||||
|
progress: number;
|
||||||
|
onCancel(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InProgressDialog(props: InProgressDialogProps): JSX.Element {
|
||||||
|
const { visible, onCancel, progress } = props;
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
closable={false}
|
||||||
|
width={300}
|
||||||
|
visible={visible}
|
||||||
|
okText='Cancel'
|
||||||
|
okButtonProps={{
|
||||||
|
type: 'danger',
|
||||||
|
}}
|
||||||
|
onOk={onCancel}
|
||||||
|
cancelButtonProps={{
|
||||||
|
style: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>{`Merging is in progress ${progress}%`}</Text>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reidContainer = window.document.createElement('div');
|
||||||
|
reidContainer.setAttribute('id', 'cvat-reid-wrapper');
|
||||||
|
window.document.body.appendChild(reidContainer);
|
||||||
|
|
||||||
|
|
||||||
|
interface StateToProps {
|
||||||
|
jobInstance: any | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchToProps {
|
||||||
|
updateAnnotations(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: CombinedState): StateToProps {
|
||||||
|
const {
|
||||||
|
annotation: {
|
||||||
|
job: {
|
||||||
|
instance: jobInstance,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = state;
|
||||||
|
|
||||||
|
return {
|
||||||
|
jobInstance,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||||
|
return {
|
||||||
|
updateAnnotations(): void {
|
||||||
|
dispatch(fetchAnnotationsAsync());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ReIDPlugin(props: StateToProps & DispatchToProps): JSX.Element {
|
||||||
|
const { jobInstance, updateAnnotations, ...rest } = props;
|
||||||
|
const [showInputDialog, setShowInputDialog] = useState(false);
|
||||||
|
const [showInProgressDialog, setShowInProgressDialog] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ReactDOM.render((
|
||||||
|
<>
|
||||||
|
<InProgressDialog
|
||||||
|
visible={showInProgressDialog}
|
||||||
|
progress={progress}
|
||||||
|
onCancel={() => {
|
||||||
|
cancel(jobInstance.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<InputModal
|
||||||
|
visible={showInputDialog}
|
||||||
|
onCancel={() => setShowInputDialog(false)}
|
||||||
|
onSubmit={async (threshold: number, distance: number) => {
|
||||||
|
setProgress(0);
|
||||||
|
setShowInputDialog(false);
|
||||||
|
setShowInProgressDialog(true);
|
||||||
|
|
||||||
|
const onUpdatePercentage = (percent: number): void => {
|
||||||
|
setProgress(percent);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const annotations = await jobInstance.annotations.export();
|
||||||
|
const merged = await run({
|
||||||
|
threshold,
|
||||||
|
distance,
|
||||||
|
onUpdatePercentage,
|
||||||
|
jobID: jobInstance.id,
|
||||||
|
annotations,
|
||||||
|
});
|
||||||
|
await jobInstance.annotations.clear();
|
||||||
|
updateAnnotations(); // one more call to do not confuse canvas
|
||||||
|
await jobInstance.annotations.import(merged);
|
||||||
|
updateAnnotations();
|
||||||
|
} catch (error) {
|
||||||
|
Modal.error({
|
||||||
|
title: 'Could not merge annotations',
|
||||||
|
content: error.toString(),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setShowInProgressDialog(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
), reidContainer);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
{...rest}
|
||||||
|
key='run_reid'
|
||||||
|
title='Run algorithm that merges separated bounding boxes automatically'
|
||||||
|
onClick={() => {
|
||||||
|
if (jobInstance) {
|
||||||
|
setShowInputDialog(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Run ReID merge
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(ReIDPlugin);
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
// Copyright (C) 2020 Intel Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import getCore from 'cvat-core';
|
||||||
|
import { ShapeType, RQStatus } from 'reducers/interfaces';
|
||||||
|
|
||||||
|
|
||||||
|
const core = getCore();
|
||||||
|
const baseURL = core.config.backendAPI.slice(0, -7);
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
threshold: number;
|
||||||
|
distance: number;
|
||||||
|
onUpdatePercentage(percentage: number): void;
|
||||||
|
jobID: number;
|
||||||
|
annotations: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function run(params: Params): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const {
|
||||||
|
threshold,
|
||||||
|
distance,
|
||||||
|
onUpdatePercentage,
|
||||||
|
jobID,
|
||||||
|
annotations,
|
||||||
|
} = params;
|
||||||
|
const { shapes, ...rest } = annotations;
|
||||||
|
|
||||||
|
const boxes = shapes.filter((shape: any): boolean => shape.type === ShapeType.RECTANGLE);
|
||||||
|
const others = shapes.filter((shape: any): boolean => shape.type !== ShapeType.RECTANGLE);
|
||||||
|
|
||||||
|
core.server.request(
|
||||||
|
`${baseURL}/reid/start/job/${params.jobID}`, {
|
||||||
|
method: 'POST',
|
||||||
|
data: JSON.stringify({
|
||||||
|
threshold,
|
||||||
|
maxDistance: distance,
|
||||||
|
boxes,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).then(() => {
|
||||||
|
const timeoutCallback = (): void => {
|
||||||
|
core.server.request(
|
||||||
|
`${baseURL}/reid/check/${jobID}`, {
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
).then((response: any) => {
|
||||||
|
const { status } = response;
|
||||||
|
if (status === RQStatus.finished) {
|
||||||
|
if (!response.result) {
|
||||||
|
// cancelled
|
||||||
|
resolve(annotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = JSON.parse(response.result);
|
||||||
|
const collection = rest;
|
||||||
|
Array.prototype.push.apply(collection.tracks, result);
|
||||||
|
collection.shapes = others;
|
||||||
|
resolve(collection);
|
||||||
|
} else if (status === RQStatus.started) {
|
||||||
|
const { progress } = response;
|
||||||
|
if (typeof (progress) === 'number') {
|
||||||
|
onUpdatePercentage(+progress.toFixed(2));
|
||||||
|
}
|
||||||
|
setTimeout(timeoutCallback, 1000);
|
||||||
|
} else if (status === RQStatus.failed) {
|
||||||
|
reject(new Error(response.stderr));
|
||||||
|
} else if (status === RQStatus.unknown) {
|
||||||
|
reject(new Error('Unknown REID status has been received'));
|
||||||
|
} else {
|
||||||
|
setTimeout(timeoutCallback, 1000);
|
||||||
|
}
|
||||||
|
}).catch((error: Error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(timeoutCallback, 1000);
|
||||||
|
}).catch((error: Error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancel(jobID: number): void {
|
||||||
|
core.server.request(
|
||||||
|
`${baseURL}/reid/cancel/${jobID}`, {
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue