|
|
|
|
@ -2,9 +2,15 @@
|
|
|
|
|
//
|
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
|
|
import React, { MutableRefObject } from 'react';
|
|
|
|
|
import React, { ReactPortal } from 'react';
|
|
|
|
|
import ReactDOM from 'react-dom';
|
|
|
|
|
import { connect } from 'react-redux';
|
|
|
|
|
import Icon, { LoadingOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
|
|
|
|
import Icon, {
|
|
|
|
|
EnvironmentFilled,
|
|
|
|
|
EnvironmentOutlined,
|
|
|
|
|
LoadingOutlined,
|
|
|
|
|
QuestionCircleOutlined,
|
|
|
|
|
} from '@ant-design/icons';
|
|
|
|
|
import Popover from 'antd/lib/popover';
|
|
|
|
|
import Select from 'antd/lib/select';
|
|
|
|
|
import Button from 'antd/lib/button';
|
|
|
|
|
@ -14,14 +20,11 @@ import Tabs from 'antd/lib/tabs';
|
|
|
|
|
import { Row, Col } from 'antd/lib/grid';
|
|
|
|
|
import notification from 'antd/lib/notification';
|
|
|
|
|
import message from 'antd/lib/message';
|
|
|
|
|
import Progress from 'antd/lib/progress';
|
|
|
|
|
import InputNumber from 'antd/lib/input-number';
|
|
|
|
|
import Dropdown from 'antd/lib/dropdown';
|
|
|
|
|
import lodash from 'lodash';
|
|
|
|
|
|
|
|
|
|
import { AIToolsIcon } from 'icons';
|
|
|
|
|
import { Canvas, convertShapesForInteractor } from 'cvat-canvas-wrapper';
|
|
|
|
|
import range from 'utils/range';
|
|
|
|
|
import getCore from 'cvat-core-wrapper';
|
|
|
|
|
import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper';
|
|
|
|
|
import {
|
|
|
|
|
@ -29,12 +32,15 @@ import {
|
|
|
|
|
} from 'reducers/interfaces';
|
|
|
|
|
import {
|
|
|
|
|
interactWithCanvas,
|
|
|
|
|
switchNavigationBlocked as switchNavigationBlockedAction,
|
|
|
|
|
fetchAnnotationsAsync,
|
|
|
|
|
updateAnnotationsAsync,
|
|
|
|
|
createAnnotationsAsync,
|
|
|
|
|
} from 'actions/annotation-actions';
|
|
|
|
|
import DetectorRunner from 'components/model-runner-modal/detector-runner';
|
|
|
|
|
import LabelSelector from 'components/label-selector/label-selector';
|
|
|
|
|
import CVATTooltip from 'components/common/cvat-tooltip';
|
|
|
|
|
|
|
|
|
|
import ApproximationAccuracy, {
|
|
|
|
|
thresholdFromAccuracy,
|
|
|
|
|
} from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy';
|
|
|
|
|
@ -54,7 +60,6 @@ interface StateToProps {
|
|
|
|
|
detectors: Model[];
|
|
|
|
|
trackers: Model[];
|
|
|
|
|
curZOrder: number;
|
|
|
|
|
aiToolsRef: MutableRefObject<any>;
|
|
|
|
|
defaultApproxPolyAccuracy: number;
|
|
|
|
|
toolsBlockerState: ToolsBlockerState;
|
|
|
|
|
}
|
|
|
|
|
@ -64,7 +69,8 @@ interface DispatchToProps {
|
|
|
|
|
updateAnnotations(statesToUpdate: any[]): void;
|
|
|
|
|
createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void;
|
|
|
|
|
fetchAnnotations(): void;
|
|
|
|
|
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState):void;
|
|
|
|
|
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState): void;
|
|
|
|
|
switchNavigationBlocked(navigationBlocked: boolean): void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const core = getCore();
|
|
|
|
|
@ -92,7 +98,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
|
|
|
|
|
jobInstance,
|
|
|
|
|
frame,
|
|
|
|
|
curZOrder: annotation.annotations.zLayer.cur,
|
|
|
|
|
aiToolsRef: annotation.aiToolsRef,
|
|
|
|
|
defaultApproxPolyAccuracy: settings.workspace.defaultApproxPolyAccuracy,
|
|
|
|
|
toolsBlockerState,
|
|
|
|
|
};
|
|
|
|
|
@ -104,21 +109,81 @@ const mapDispatchToProps = {
|
|
|
|
|
createAnnotations: createAnnotationsAsync,
|
|
|
|
|
fetchAnnotations: fetchAnnotationsAsync,
|
|
|
|
|
onSwitchToolsBlockerState: switchToolsBlockerState,
|
|
|
|
|
switchNavigationBlocked: switchNavigationBlockedAction,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type Props = StateToProps & DispatchToProps;
|
|
|
|
|
interface TrackedShape {
|
|
|
|
|
clientID: number;
|
|
|
|
|
serverlessState: any;
|
|
|
|
|
shapePoints: number[];
|
|
|
|
|
trackerModel: Model;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface State {
|
|
|
|
|
activeInteractor: Model | null;
|
|
|
|
|
activeLabelID: number;
|
|
|
|
|
activeTracker: Model | null;
|
|
|
|
|
trackingProgress: number | null;
|
|
|
|
|
trackingFrames: number;
|
|
|
|
|
trackedShapes: TrackedShape[];
|
|
|
|
|
fetching: boolean;
|
|
|
|
|
pointsRecieved: boolean;
|
|
|
|
|
approxPolyAccuracy: number;
|
|
|
|
|
mode: 'detection' | 'interaction' | 'tracking';
|
|
|
|
|
portals: React.ReactPortal[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trackedRectangleMapper(shape: number[]): number[] {
|
|
|
|
|
return shape.reduce(
|
|
|
|
|
(acc: number[], value: number, index: number): number[] => {
|
|
|
|
|
if (index % 2) {
|
|
|
|
|
// y
|
|
|
|
|
acc[1] = Math.min(acc[1], value);
|
|
|
|
|
acc[3] = Math.max(acc[3], value);
|
|
|
|
|
} else {
|
|
|
|
|
// x
|
|
|
|
|
acc[0] = Math.min(acc[0], value);
|
|
|
|
|
acc[2] = Math.max(acc[2], value);
|
|
|
|
|
}
|
|
|
|
|
return acc;
|
|
|
|
|
},
|
|
|
|
|
[Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function registerPlugin(): (callback: null | (() => void)) => void {
|
|
|
|
|
let onTrigger: null | (() => void) = null;
|
|
|
|
|
const listener = {
|
|
|
|
|
name: 'Remove annotations listener',
|
|
|
|
|
description: 'Tracker needs to know when annotations is reset in the job',
|
|
|
|
|
cvat: {
|
|
|
|
|
classes: {
|
|
|
|
|
Job: {
|
|
|
|
|
prototype: {
|
|
|
|
|
annotations: {
|
|
|
|
|
clear: {
|
|
|
|
|
leave(self: any, result: any) {
|
|
|
|
|
if (typeof onTrigger === 'function') {
|
|
|
|
|
onTrigger();
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
core.plugins.register(listener);
|
|
|
|
|
|
|
|
|
|
return (callback: null | (() => void)) => {
|
|
|
|
|
onTrigger = callback;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onRemoveAnnotations = registerPlugin();
|
|
|
|
|
|
|
|
|
|
export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
private interaction: {
|
|
|
|
|
id: string | null;
|
|
|
|
|
@ -143,11 +208,11 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
activeTracker: props.trackers.length ? props.trackers[0] : null,
|
|
|
|
|
activeLabelID: props.labels.length ? props.labels[0].id : null,
|
|
|
|
|
approxPolyAccuracy: props.defaultApproxPolyAccuracy,
|
|
|
|
|
trackingProgress: null,
|
|
|
|
|
trackingFrames: 10,
|
|
|
|
|
trackedShapes: [],
|
|
|
|
|
fetching: false,
|
|
|
|
|
pointsRecieved: false,
|
|
|
|
|
mode: 'interaction',
|
|
|
|
|
portals: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.interaction = {
|
|
|
|
|
@ -161,15 +226,29 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public componentDidMount(): void {
|
|
|
|
|
const { canvasInstance, aiToolsRef } = this.props;
|
|
|
|
|
aiToolsRef.current = this;
|
|
|
|
|
const { canvasInstance } = this.props;
|
|
|
|
|
onRemoveAnnotations(() => {
|
|
|
|
|
this.setState({ trackedShapes: [] });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
|
portals: this.collectTrackerPortals(),
|
|
|
|
|
});
|
|
|
|
|
canvasInstance.html().addEventListener('canvas.interacted', this.interactionListener);
|
|
|
|
|
canvasInstance.html().addEventListener('canvas.canceled', this.cancelListener);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public componentDidUpdate(prevProps: Props, prevState: State): void {
|
|
|
|
|
const { isActivated, defaultApproxPolyAccuracy, canvasInstance } = this.props;
|
|
|
|
|
const { approxPolyAccuracy, mode } = this.state;
|
|
|
|
|
const {
|
|
|
|
|
isActivated, defaultApproxPolyAccuracy, canvasInstance, states,
|
|
|
|
|
} = this.props;
|
|
|
|
|
const { approxPolyAccuracy, mode, activeTracker } = this.state;
|
|
|
|
|
|
|
|
|
|
if (prevProps.states !== states || prevState.activeTracker !== activeTracker) {
|
|
|
|
|
this.setState({
|
|
|
|
|
portals: this.collectTrackerPortals(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (prevProps.isActivated && !isActivated) {
|
|
|
|
|
window.removeEventListener('contextmenu', this.contextmenuDisabler);
|
|
|
|
|
@ -211,11 +290,13 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.checkTrackedStates(prevProps);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public componentWillUnmount(): void {
|
|
|
|
|
const { canvasInstance, aiToolsRef } = this.props;
|
|
|
|
|
aiToolsRef.current = undefined;
|
|
|
|
|
const { canvasInstance } = this.props;
|
|
|
|
|
onRemoveAnnotations(null);
|
|
|
|
|
canvasInstance.html().removeEventListener('canvas.interacted', this.interactionListener);
|
|
|
|
|
canvasInstance.html().removeEventListener('canvas.canceled', this.cancelListener);
|
|
|
|
|
}
|
|
|
|
|
@ -339,6 +420,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private onTracking = async (e: Event): Promise<void> => {
|
|
|
|
|
const { trackedShapes, activeTracker } = this.state;
|
|
|
|
|
const {
|
|
|
|
|
isActivated, jobInstance, frame, curZOrder, fetchAnnotations,
|
|
|
|
|
} = this.props;
|
|
|
|
|
@ -365,18 +447,25 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
points,
|
|
|
|
|
frame,
|
|
|
|
|
occluded: false,
|
|
|
|
|
source: 'auto',
|
|
|
|
|
attributes: {},
|
|
|
|
|
descriptions: [`Trackable (${activeTracker?.name})`],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [clientID] = await jobInstance.annotations.put([state]);
|
|
|
|
|
this.setState({
|
|
|
|
|
trackedShapes: [
|
|
|
|
|
...trackedShapes,
|
|
|
|
|
{
|
|
|
|
|
clientID,
|
|
|
|
|
serverlessState: null,
|
|
|
|
|
shapePoints: points,
|
|
|
|
|
trackerModel: activeTracker as Model,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// update annotations on a canvas
|
|
|
|
|
fetchAnnotations();
|
|
|
|
|
|
|
|
|
|
const states = await jobInstance.annotations.get(frame);
|
|
|
|
|
const [objectState] = states.filter((_state: any): boolean => _state.clientID === clientID);
|
|
|
|
|
await this.trackState(objectState);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
notification.error({
|
|
|
|
|
description: err.toString(),
|
|
|
|
|
@ -411,7 +500,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private onChangeToolsBlockerState = (event:string):void => {
|
|
|
|
|
private onChangeToolsBlockerState = (event: string): void => {
|
|
|
|
|
const { isActivated, onSwitchToolsBlockerState } = this.props;
|
|
|
|
|
if (isActivated && event === 'keydown') {
|
|
|
|
|
onSwitchToolsBlockerState({ algorithmsLocked: true });
|
|
|
|
|
@ -420,6 +509,275 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private collectTrackerPortals(): React.ReactPortal[] {
|
|
|
|
|
const { states, fetchAnnotations } = this.props;
|
|
|
|
|
const { trackedShapes, activeTracker } = this.state;
|
|
|
|
|
|
|
|
|
|
const trackedClientIDs = trackedShapes.map((trackedShape: TrackedShape) => trackedShape.clientID);
|
|
|
|
|
const portals = !activeTracker ?
|
|
|
|
|
[] :
|
|
|
|
|
states
|
|
|
|
|
.filter((objectState) => objectState.objectType === 'track' && objectState.shapeType === 'rectangle')
|
|
|
|
|
.map((objectState: any): React.ReactPortal | null => {
|
|
|
|
|
const { clientID } = objectState;
|
|
|
|
|
const selectorID = `#cvat-objects-sidebar-state-item-${clientID}`;
|
|
|
|
|
let targetElement = window.document.querySelector(
|
|
|
|
|
`${selectorID} .cvat-object-item-button-prev-keyframe`,
|
|
|
|
|
) as HTMLElement;
|
|
|
|
|
|
|
|
|
|
const isTracked = trackedClientIDs.includes(clientID);
|
|
|
|
|
if (targetElement) {
|
|
|
|
|
targetElement = targetElement.parentElement?.parentElement as HTMLElement;
|
|
|
|
|
return ReactDOM.createPortal(
|
|
|
|
|
<Col>
|
|
|
|
|
{isTracked ? (
|
|
|
|
|
<CVATTooltip overlay='Disable tracking'>
|
|
|
|
|
<EnvironmentFilled
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const filteredStates = trackedShapes.filter(
|
|
|
|
|
(trackedShape: TrackedShape) =>
|
|
|
|
|
trackedShape.clientID !== clientID,
|
|
|
|
|
);
|
|
|
|
|
/* eslint no-param-reassign: ["error", { "props": false }] */
|
|
|
|
|
objectState.descriptions = [];
|
|
|
|
|
objectState.save().then(() => {
|
|
|
|
|
this.setState({
|
|
|
|
|
trackedShapes: filteredStates,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
fetchAnnotations();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</CVATTooltip>
|
|
|
|
|
) : (
|
|
|
|
|
<CVATTooltip overlay={`Enable tracking using ${activeTracker.name}`}>
|
|
|
|
|
<EnvironmentOutlined
|
|
|
|
|
onClick={() => {
|
|
|
|
|
objectState.descriptions = [`Trackable (${activeTracker.name})`];
|
|
|
|
|
objectState.save().then(() => {
|
|
|
|
|
this.setState({
|
|
|
|
|
trackedShapes: [
|
|
|
|
|
...trackedShapes,
|
|
|
|
|
{
|
|
|
|
|
clientID,
|
|
|
|
|
serverlessState: null,
|
|
|
|
|
shapePoints: objectState.points,
|
|
|
|
|
trackerModel: activeTracker,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
fetchAnnotations();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</CVATTooltip>
|
|
|
|
|
)}
|
|
|
|
|
</Col>,
|
|
|
|
|
targetElement,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
})
|
|
|
|
|
.filter((portal: ReactPortal | null) => portal !== null);
|
|
|
|
|
|
|
|
|
|
return portals as React.ReactPortal[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async checkTrackedStates(prevProps: Props): Promise<void> {
|
|
|
|
|
const {
|
|
|
|
|
frame,
|
|
|
|
|
jobInstance,
|
|
|
|
|
states: objectStates,
|
|
|
|
|
trackers,
|
|
|
|
|
fetchAnnotations,
|
|
|
|
|
switchNavigationBlocked,
|
|
|
|
|
} = this.props;
|
|
|
|
|
const { trackedShapes } = this.state;
|
|
|
|
|
let withServerRequest = false;
|
|
|
|
|
|
|
|
|
|
type AccumulatorType = {
|
|
|
|
|
statefull: {
|
|
|
|
|
[index: string]: {
|
|
|
|
|
// tracker id
|
|
|
|
|
clientIDs: number[];
|
|
|
|
|
states: any[];
|
|
|
|
|
shapes: number[][];
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
stateless: {
|
|
|
|
|
[index: string]: {
|
|
|
|
|
// tracker id
|
|
|
|
|
clientIDs: number[];
|
|
|
|
|
shapes: number[][];
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (prevProps.frame !== frame && trackedShapes.length) {
|
|
|
|
|
// 1. find all trackable objects on the current frame
|
|
|
|
|
// 2. devide them into two groups: with relevant state, without relevant state
|
|
|
|
|
const trackingData = trackedShapes.reduce<AccumulatorType>(
|
|
|
|
|
(acc: AccumulatorType, trackedShape: TrackedShape): AccumulatorType => {
|
|
|
|
|
const {
|
|
|
|
|
serverlessState, shapePoints, clientID, trackerModel,
|
|
|
|
|
} = trackedShape;
|
|
|
|
|
const [clientState] = objectStates.filter((_state: any): boolean => _state.clientID === clientID);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!clientState ||
|
|
|
|
|
clientState.keyframes.prev !== frame - 1 ||
|
|
|
|
|
clientState.keyframes.last >= frame
|
|
|
|
|
) {
|
|
|
|
|
return acc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (clientState && !clientState.outside) {
|
|
|
|
|
const { points } = clientState;
|
|
|
|
|
withServerRequest = true;
|
|
|
|
|
const stateIsRelevant =
|
|
|
|
|
serverlessState !== null &&
|
|
|
|
|
points.length === shapePoints.length &&
|
|
|
|
|
points.every((coord: number, i: number) => coord === shapePoints[i]);
|
|
|
|
|
if (stateIsRelevant) {
|
|
|
|
|
const container = acc.statefull[trackerModel.id] || {
|
|
|
|
|
clientIDs: [],
|
|
|
|
|
shapes: [],
|
|
|
|
|
states: [],
|
|
|
|
|
};
|
|
|
|
|
container.clientIDs.push(clientID);
|
|
|
|
|
container.shapes.push(points);
|
|
|
|
|
container.states.push(serverlessState);
|
|
|
|
|
acc.statefull[trackerModel.id] = container;
|
|
|
|
|
} else {
|
|
|
|
|
const container = acc.stateless[trackerModel.id] || {
|
|
|
|
|
clientIDs: [],
|
|
|
|
|
shapes: [],
|
|
|
|
|
};
|
|
|
|
|
container.clientIDs.push(clientID);
|
|
|
|
|
container.shapes.push(points);
|
|
|
|
|
acc.stateless[trackerModel.id] = container;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return acc;
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
statefull: {},
|
|
|
|
|
stateless: {},
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (withServerRequest) {
|
|
|
|
|
switchNavigationBlocked(true);
|
|
|
|
|
}
|
|
|
|
|
// 3. get relevant state for the second group
|
|
|
|
|
for (const trackerID of Object.keys(trackingData.stateless)) {
|
|
|
|
|
let hideMessage = null;
|
|
|
|
|
try {
|
|
|
|
|
const [tracker] = trackers.filter((_tracker: Model) => _tracker.id === trackerID);
|
|
|
|
|
if (!tracker) {
|
|
|
|
|
throw new Error(`Suitable tracker with ID ${trackerID} not found in tracker list`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const trackableObjects = trackingData.stateless[trackerID];
|
|
|
|
|
const numOfObjects = trackableObjects.clientIDs.length;
|
|
|
|
|
hideMessage = message.loading(
|
|
|
|
|
`${tracker.name}: states are being initialized for ${numOfObjects} ${
|
|
|
|
|
numOfObjects > 1 ? 'objects' : 'object'
|
|
|
|
|
} ..`,
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
|
const response = await core.lambda.call(jobInstance.task, tracker, {
|
|
|
|
|
task: jobInstance.task,
|
|
|
|
|
frame: frame - 1,
|
|
|
|
|
shapes: trackableObjects.shapes,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { states: serverlessStates } = response;
|
|
|
|
|
const statefullContainer = trackingData.statefull[trackerID] || {
|
|
|
|
|
clientIDs: [],
|
|
|
|
|
shapes: [],
|
|
|
|
|
states: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Array.prototype.push.apply(statefullContainer.clientIDs, trackableObjects.clientIDs);
|
|
|
|
|
Array.prototype.push.apply(statefullContainer.shapes, trackableObjects.shapes);
|
|
|
|
|
Array.prototype.push.apply(statefullContainer.states, serverlessStates);
|
|
|
|
|
trackingData.statefull[trackerID] = statefullContainer;
|
|
|
|
|
delete trackingData.stateless[trackerID];
|
|
|
|
|
} catch (error) {
|
|
|
|
|
notification.error({
|
|
|
|
|
message: 'Tracker initialization error',
|
|
|
|
|
description: error.toString(),
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
if (hideMessage) hideMessage();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const trackerID of Object.keys(trackingData.statefull)) {
|
|
|
|
|
// 4. run tracking for all the objects
|
|
|
|
|
let hideMessage = null;
|
|
|
|
|
try {
|
|
|
|
|
const [tracker] = trackers.filter((_tracker: Model) => _tracker.id === trackerID);
|
|
|
|
|
if (!tracker) {
|
|
|
|
|
throw new Error(`Suitable tracker with ID ${trackerID} not found in tracker list`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const trackableObjects = trackingData.statefull[trackerID];
|
|
|
|
|
const numOfObjects = trackableObjects.clientIDs.length;
|
|
|
|
|
hideMessage = message.loading(
|
|
|
|
|
`${tracker.name}: ${numOfObjects} ${
|
|
|
|
|
numOfObjects > 1 ? 'objects are' : 'object is'
|
|
|
|
|
} being tracked..`,
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
|
const response = await core.lambda.call(jobInstance.task, tracker, {
|
|
|
|
|
task: jobInstance.task,
|
|
|
|
|
frame: frame - 1,
|
|
|
|
|
shapes: trackableObjects.shapes,
|
|
|
|
|
states: trackableObjects.states,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
response.shapes = response.shapes.map(trackedRectangleMapper);
|
|
|
|
|
for (let i = 0; i < trackableObjects.clientIDs.length; i++) {
|
|
|
|
|
const clientID = trackableObjects.clientIDs[i];
|
|
|
|
|
const shape = response.shapes[i];
|
|
|
|
|
const state = response.states[i];
|
|
|
|
|
const [objectState] = objectStates.filter(
|
|
|
|
|
(_state: any): boolean => _state.clientID === clientID,
|
|
|
|
|
);
|
|
|
|
|
const [trackedShape] = trackedShapes.filter(
|
|
|
|
|
(_trackedShape: TrackedShape) => _trackedShape.clientID === clientID,
|
|
|
|
|
);
|
|
|
|
|
objectState.points = shape;
|
|
|
|
|
objectState.save().then(() => {
|
|
|
|
|
trackedShape.serverlessState = state;
|
|
|
|
|
trackedShape.shapePoints = shape;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
notification.error({
|
|
|
|
|
message: 'Tracking error',
|
|
|
|
|
description: error.toString(),
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
if (hideMessage) hideMessage();
|
|
|
|
|
fetchAnnotations();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
if (withServerRequest) {
|
|
|
|
|
switchNavigationBlocked(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private constructFromPoints(points: number[][]): void {
|
|
|
|
|
const {
|
|
|
|
|
frame, labels, curZOrder, jobInstance, activeLabelID, createAnnotations,
|
|
|
|
|
@ -457,70 +815,6 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
return points;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async trackState(state: any): Promise<void> {
|
|
|
|
|
const { jobInstance, frame, fetchAnnotations } = this.props;
|
|
|
|
|
const { activeTracker, trackingFrames } = this.state;
|
|
|
|
|
const { clientID, points } = state;
|
|
|
|
|
|
|
|
|
|
const tracker = activeTracker as Model;
|
|
|
|
|
try {
|
|
|
|
|
this.setState({ trackingProgress: 0, fetching: true });
|
|
|
|
|
let response = await core.lambda.call(jobInstance.task, tracker, {
|
|
|
|
|
task: jobInstance.task,
|
|
|
|
|
frame,
|
|
|
|
|
shape: points,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const offset of range(1, trackingFrames + 1)) {
|
|
|
|
|
/* eslint-disable no-await-in-loop */
|
|
|
|
|
const states = await jobInstance.annotations.get(frame + offset);
|
|
|
|
|
const [objectState] = states.filter((_state: any): boolean => _state.clientID === clientID);
|
|
|
|
|
response = await core.lambda.call(jobInstance.task, tracker, {
|
|
|
|
|
task: jobInstance.task,
|
|
|
|
|
frame: frame + offset,
|
|
|
|
|
shape: response.points,
|
|
|
|
|
state: response.state,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const reduced = response.shape.reduce(
|
|
|
|
|
(acc: number[], value: number, index: number): number[] => {
|
|
|
|
|
if (index % 2) {
|
|
|
|
|
// y
|
|
|
|
|
acc[1] = Math.min(acc[1], value);
|
|
|
|
|
acc[3] = Math.max(acc[3], value);
|
|
|
|
|
} else {
|
|
|
|
|
// x
|
|
|
|
|
acc[0] = Math.min(acc[0], value);
|
|
|
|
|
acc[2] = Math.max(acc[2], value);
|
|
|
|
|
}
|
|
|
|
|
return acc;
|
|
|
|
|
},
|
|
|
|
|
[
|
|
|
|
|
Number.MAX_SAFE_INTEGER,
|
|
|
|
|
Number.MAX_SAFE_INTEGER,
|
|
|
|
|
Number.MIN_SAFE_INTEGER,
|
|
|
|
|
Number.MIN_SAFE_INTEGER,
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
objectState.points = reduced;
|
|
|
|
|
await objectState.save();
|
|
|
|
|
|
|
|
|
|
this.setState({ trackingProgress: offset / trackingFrames });
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
this.setState({ trackingProgress: null, fetching: false });
|
|
|
|
|
fetchAnnotations();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public trackingAvailable(): boolean {
|
|
|
|
|
const { activeTracker, trackingFrames } = this.state;
|
|
|
|
|
const { trackers } = this.props;
|
|
|
|
|
|
|
|
|
|
return !!trackingFrames && !!trackers.length && activeTracker !== null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderLabelBlock(): JSX.Element {
|
|
|
|
|
const { labels } = this.props;
|
|
|
|
|
const { activeLabelID } = this.state;
|
|
|
|
|
@ -549,9 +843,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
const {
|
|
|
|
|
trackers, canvasInstance, jobInstance, frame, onInteractionStart,
|
|
|
|
|
} = this.props;
|
|
|
|
|
const {
|
|
|
|
|
activeTracker, activeLabelID, fetching, trackingFrames,
|
|
|
|
|
} = this.state;
|
|
|
|
|
const { activeTracker, activeLabelID, fetching } = this.state;
|
|
|
|
|
|
|
|
|
|
if (!trackers.length) {
|
|
|
|
|
return (
|
|
|
|
|
@ -589,27 +881,6 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
</Select>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
<Row align='middle' justify='start' style={{ marginTop: '5px' }}>
|
|
|
|
|
<Col>
|
|
|
|
|
<Text>Tracking frames</Text>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col offset={2}>
|
|
|
|
|
<InputNumber
|
|
|
|
|
value={trackingFrames}
|
|
|
|
|
step={1}
|
|
|
|
|
min={1}
|
|
|
|
|
precision={0}
|
|
|
|
|
max={jobInstance.stopFrame - frame}
|
|
|
|
|
onChange={(value: number | undefined | string | null): void => {
|
|
|
|
|
if (typeof value !== 'undefined' && value !== null) {
|
|
|
|
|
this.setState({
|
|
|
|
|
trackingFrames: +value,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
<Row align='middle' justify='end'>
|
|
|
|
|
<Col>
|
|
|
|
|
<Button
|
|
|
|
|
@ -797,10 +1068,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
</Text>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
<Tabs
|
|
|
|
|
type='card'
|
|
|
|
|
tabBarGutter={8}
|
|
|
|
|
>
|
|
|
|
|
<Tabs type='card' tabBarGutter={8}>
|
|
|
|
|
<Tabs.TabPane key='interactors' tab='Interactors'>
|
|
|
|
|
{this.renderLabelBlock()}
|
|
|
|
|
{this.renderInteractorBlock()}
|
|
|
|
|
@ -822,7 +1090,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
interactors, detectors, trackers, isActivated, canvasInstance, labels,
|
|
|
|
|
} = this.props;
|
|
|
|
|
const {
|
|
|
|
|
fetching, trackingProgress, approxPolyAccuracy, pointsRecieved, mode,
|
|
|
|
|
fetching, approxPolyAccuracy, pointsRecieved, mode, portals,
|
|
|
|
|
} = this.state;
|
|
|
|
|
|
|
|
|
|
if (![...interactors, ...detectors, ...trackers].length) return null;
|
|
|
|
|
@ -849,8 +1117,6 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
const showAnyContent = !!labels.length;
|
|
|
|
|
const showInteractionContent = isActivated && mode === 'interaction' && pointsRecieved;
|
|
|
|
|
const showDetectionContent = fetching && mode === 'detection';
|
|
|
|
|
const showTrackingContent = fetching && mode === 'tracking' && trackingProgress !== null;
|
|
|
|
|
const formattedTrackingProgress = showTrackingContent ? +((trackingProgress as number) * 100).toFixed(0) : null;
|
|
|
|
|
|
|
|
|
|
const interactionContent: JSX.Element | null = showInteractionContent ? (
|
|
|
|
|
<>
|
|
|
|
|
@ -863,23 +1129,19 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
</>
|
|
|
|
|
) : null;
|
|
|
|
|
|
|
|
|
|
const trackOrDetectModal: JSX.Element | null =
|
|
|
|
|
showDetectionContent || showTrackingContent ? (
|
|
|
|
|
<Modal
|
|
|
|
|
title='Making a server request'
|
|
|
|
|
zIndex={Number.MAX_SAFE_INTEGER}
|
|
|
|
|
visible
|
|
|
|
|
destroyOnClose
|
|
|
|
|
closable={false}
|
|
|
|
|
footer={[]}
|
|
|
|
|
>
|
|
|
|
|
<Text>Waiting for a server response..</Text>
|
|
|
|
|
<LoadingOutlined style={{ marginLeft: '10px' }} />
|
|
|
|
|
{showTrackingContent ? (
|
|
|
|
|
<Progress percent={formattedTrackingProgress as number} status='active' />
|
|
|
|
|
) : null}
|
|
|
|
|
</Modal>
|
|
|
|
|
) : null;
|
|
|
|
|
const detectionContent: JSX.Element | null = showDetectionContent ? (
|
|
|
|
|
<Modal
|
|
|
|
|
title='Making a server request'
|
|
|
|
|
zIndex={Number.MAX_SAFE_INTEGER}
|
|
|
|
|
visible
|
|
|
|
|
destroyOnClose
|
|
|
|
|
closable={false}
|
|
|
|
|
footer={[]}
|
|
|
|
|
>
|
|
|
|
|
<Text>Waiting for a server response..</Text>
|
|
|
|
|
<LoadingOutlined style={{ marginLeft: '10px' }} />
|
|
|
|
|
</Modal>
|
|
|
|
|
) : null;
|
|
|
|
|
|
|
|
|
|
return showAnyContent ? (
|
|
|
|
|
<>
|
|
|
|
|
@ -887,7 +1149,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
|
|
|
|
<Icon {...dynamicIconProps} component={AIToolsIcon} />
|
|
|
|
|
</CustomPopover>
|
|
|
|
|
{interactionContent}
|
|
|
|
|
{trackOrDetectModal}
|
|
|
|
|
{detectionContent}
|
|
|
|
|
{portals}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<Icon className=' cvat-tools-control cvat-disabled-canvas-control' component={AIToolsIcon} />
|
|
|
|
|
|