UI Tracking with serverless functions (#2136)

* tmp

* Refactored

* Refactoring & added button to context menu

* Updated changelog, updated versions

* Improved styles

* Removed outdated code

* Updated icon
main
Boris Sekachev 6 years ago committed by GitHub
parent a5b63a4f53
commit 4e219299e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ability to work with data on the fly (https://github.com/opencv/cvat/pull/2007) - Ability to work with data on the fly (https://github.com/opencv/cvat/pull/2007)
- Annotation in process outline color wheel (<https://github.com/opencv/cvat/pull/2084>) - Annotation in process outline color wheel (<https://github.com/opencv/cvat/pull/2084>)
- On the fly annotation using DL detectors (<https://github.com/opencv/cvat/pull/2102>) - On the fly annotation using DL detectors (<https://github.com/opencv/cvat/pull/2102>)
- Automatic tracking of bounding boxes using serverless functions (<https://github.com/opencv/cvat/pull/2136>)
- [Datumaro] CLI command for dataset equality comparison (<https://github.com/opencv/cvat/pull/1989>) - [Datumaro] CLI command for dataset equality comparison (<https://github.com/opencv/cvat/pull/1989>)
- [Datumaro] Merging of datasets with different labels (<https://github.com/opencv/cvat/pull/2098>) - [Datumaro] Merging of datasets with different labels (<https://github.com/opencv/cvat/pull/2098>)

@ -155,10 +155,6 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.shapesWereUpdated = true; this.shapesWereUpdated = true;
this.canvas.off('mousedown.interaction', eventListener); this.canvas.off('mousedown.interaction', eventListener);
if (this.shouldRaiseEvent(false)) {
this.onInteraction(this.prepareResult(), true, false);
}
this.interact({ enabled: false }); this.interact({ enabled: false });
}).addClass('cvat_canvas_shape_drawing').attr({ }).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "3.6.0", "version": "3.6.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "3.6.0", "version": "3.6.1",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration", "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js", "main": "babel.config.js",
"scripts": { "scripts": {

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.9.2", "version": "1.9.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.9.2", "version": "1.9.3",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {

@ -27,6 +27,7 @@ import getCore from 'cvat-core-wrapper';
import logger, { LogType } from 'cvat-logger'; import logger, { LogType } from 'cvat-logger';
import { RectDrawingMethod } from 'cvat-canvas-wrapper'; import { RectDrawingMethod } from 'cvat-canvas-wrapper';
import { getCVATStore } from 'cvat-store'; import { getCVATStore } from 'cvat-store';
import { MutableRefObject } from 'react';
interface AnnotationsParameters { interface AnnotationsParameters {
filters: string[]; filters: string[];
@ -189,6 +190,7 @@ export enum AnnotationActionTypes {
SAVE_LOGS_SUCCESS = 'SAVE_LOGS_SUCCESS', SAVE_LOGS_SUCCESS = 'SAVE_LOGS_SUCCESS',
SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED', SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED',
INTERACT_WITH_CANVAS = 'INTERACT_WITH_CANVAS', INTERACT_WITH_CANVAS = 'INTERACT_WITH_CANVAS',
SET_AI_TOOLS_REF = 'SET_AI_TOOLS_REF',
} }
export function saveLogsAsync(): ThunkAction { export function saveLogsAsync(): ThunkAction {
@ -1397,6 +1399,16 @@ export function interactWithCanvas(activeInteractor: Model, activeLabelID: numbe
}; };
} }
export function setAIToolsRef(ref: MutableRefObject<any>): AnyAction {
return {
type: AnnotationActionTypes.SET_AI_TOOLS_REF,
payload: {
aiToolsRef: ref,
},
};
}
export function repeatDrawShapeAsync(): ThunkAction { export function repeatDrawShapeAsync(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const { const {
@ -1424,12 +1436,21 @@ export function repeatDrawShapeAsync(): ThunkAction {
let activeControl = ActiveControl.CURSOR; let activeControl = ActiveControl.CURSOR;
if (activeInteractor) { if (activeInteractor) {
canvasInstance.interact({ if (activeInteractor.type === 'tracker') {
enabled: true, canvasInstance.interact({
shapeType: 'points', enabled: true,
minPosVertices: 4, // TODO: Add parameter to interactor shapeType: 'rectangle',
}); });
dispatch(interactWithCanvas(activeInteractor, activeLabelID)); dispatch(interactWithCanvas(activeInteractor, activeLabelID));
} else {
canvasInstance.interact({
enabled: true,
shapeType: 'points',
minPosVertices: 4, // TODO: Add parameter to interactor
});
dispatch(interactWithCanvas(activeInteractor, activeLabelID));
}
return; return;
} }

@ -30,19 +30,16 @@ export function checkPluginsAsync(): ThunkAction {
const plugins: PluginObjects = { const plugins: PluginObjects = {
ANALYTICS: false, ANALYTICS: false,
GIT_INTEGRATION: false, GIT_INTEGRATION: false,
DEXTR_SEGMENTATION: false,
}; };
const promises: Promise<boolean>[] = [ const promises: Promise<boolean>[] = [
// check must return true/false with no exceptions // check must return true/false with no exceptions
PluginChecker.check(SupportedPlugins.ANALYTICS), PluginChecker.check(SupportedPlugins.ANALYTICS),
PluginChecker.check(SupportedPlugins.GIT_INTEGRATION), PluginChecker.check(SupportedPlugins.GIT_INTEGRATION),
PluginChecker.check(SupportedPlugins.DEXTR_SEGMENTATION),
]; ];
const values = await Promise.all(promises); const values = await Promise.all(promises);
[plugins.ANALYTICS, plugins.GIT_INTEGRATION, [plugins.ANALYTICS, plugins.GIT_INTEGRATION] = values;
plugins.DEXTR_SEGMENTATION] = values;
dispatch(pluginActions.checkedAllPlugins(plugins)); dispatch(pluginActions.checkedAllPlugins(plugins));
}; };
} }

@ -85,7 +85,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
preventDefault(event); preventDefault(event);
const drawing = [ActiveControl.DRAW_POINTS, ActiveControl.DRAW_POLYGON, const drawing = [ActiveControl.DRAW_POINTS, ActiveControl.DRAW_POLYGON,
ActiveControl.DRAW_POLYLINE, ActiveControl.DRAW_RECTANGLE, ActiveControl.DRAW_POLYLINE, ActiveControl.DRAW_RECTANGLE,
ActiveControl.DRAW_CUBOID, ActiveControl.INTERACTION].includes(activeControl); ActiveControl.DRAW_CUBOID, ActiveControl.AI_TOOLS].includes(activeControl);
if (!drawing) { if (!drawing) {
canvasInstance.cancel(); canvasInstance.cancel();
@ -98,7 +98,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
repeatDrawShape(); repeatDrawShape();
} }
} else { } else {
if (activeControl === ActiveControl.INTERACTION) { if (activeControl === ActiveControl.AI_TOOLS) {
// separated API method // separated API method
canvasInstance.interact({ enabled: false }); canvasInstance.interact({ enabled: false });
return; return;

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React, { MutableRefObject } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Icon from 'antd/lib/icon'; import Icon from 'antd/lib/icon';
import Popover from 'antd/lib/popover'; import Popover from 'antd/lib/popover';
@ -13,9 +13,11 @@ import Text from 'antd/lib/typography/Text';
import Tabs from 'antd/lib/tabs'; import Tabs from 'antd/lib/tabs';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import notification from 'antd/lib/notification'; import notification from 'antd/lib/notification';
import Progress from 'antd/lib/progress';
import { AIToolsIcon } from 'icons'; import { AIToolsIcon } from 'icons';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
import range from 'utils/range';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import { import {
CombinedState, CombinedState,
@ -32,6 +34,7 @@ import {
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import { InteractionResult } from 'cvat-canvas/src/typescript/canvas'; import { InteractionResult } from 'cvat-canvas/src/typescript/canvas';
import DetectorRunner from 'components/model-runner-modal/detector-runner'; import DetectorRunner from 'components/model-runner-modal/detector-runner';
import InputNumber from 'antd/lib/input-number';
interface StateToProps { interface StateToProps {
canvasInstance: Canvas; canvasInstance: Canvas;
@ -39,10 +42,13 @@ interface StateToProps {
states: any[]; states: any[];
activeLabelID: number; activeLabelID: number;
jobInstance: any; jobInstance: any;
isInteraction: boolean; isActivated: boolean;
frame: number; frame: number;
interactors: Model[]; interactors: Model[];
detectors: Model[]; detectors: Model[];
trackers: Model[];
curZOrder: number;
aiToolsRef: MutableRefObject<any>;
} }
interface DispatchToProps { interface DispatchToProps {
@ -60,18 +66,21 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { instance: jobInstance } = annotation.job; const { instance: jobInstance } = annotation.job;
const { instance: canvasInstance, activeControl } = annotation.canvas; const { instance: canvasInstance, activeControl } = annotation.canvas;
const { models } = state; const { models } = state;
const { interactors, detectors } = models; const { interactors, detectors, trackers } = models;
return { return {
interactors, interactors,
detectors, detectors,
isInteraction: activeControl === ActiveControl.INTERACTION, trackers,
isActivated: activeControl === ActiveControl.AI_TOOLS,
activeLabelID: annotation.drawing.activeLabelID, activeLabelID: annotation.drawing.activeLabelID,
labels: annotation.job.labels, labels: annotation.job.labels,
states: annotation.annotations.states, states: annotation.annotations.states,
canvasInstance, canvasInstance,
jobInstance, jobInstance,
frame, frame,
curZOrder: annotation.annotations.zLayer.cur,
aiToolsRef: annotation.aiToolsRef,
}; };
} }
@ -103,10 +112,14 @@ interface State {
activeInteractor: Model | null; activeInteractor: Model | null;
activeLabelID: number; activeLabelID: number;
interactiveStateID: number | null; interactiveStateID: number | null;
activeTracker: Model | null;
trackingProgress: number | null;
trackingFrames: number;
fetching: boolean; fetching: boolean;
mode: 'detection' | 'interaction' | 'tracking';
} }
class ToolsControlComponent extends React.PureComponent<Props, State> { export class ToolsControlComponent extends React.PureComponent<Props, State> {
private interactionIsAborted: boolean; private interactionIsAborted: boolean;
private interactionIsDone: boolean; private interactionIsDone: boolean;
@ -114,9 +127,13 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
super(props); super(props);
this.state = { this.state = {
activeInteractor: props.interactors.length ? props.interactors[0] : null, activeInteractor: props.interactors.length ? props.interactors[0] : null,
activeTracker: props.trackers.length ? props.trackers[0] : null,
activeLabelID: props.labels[0].id, activeLabelID: props.labels[0].id,
interactiveStateID: null, interactiveStateID: null,
trackingProgress: null,
trackingFrames: 10,
fetching: false, fetching: false,
mode: 'interaction',
}; };
this.interactionIsAborted = false; this.interactionIsAborted = false;
@ -124,16 +141,18 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
} }
public componentDidMount(): void { public componentDidMount(): void {
const { canvasInstance } = this.props; const { canvasInstance, aiToolsRef } = this.props;
aiToolsRef.current = this;
canvasInstance.html().addEventListener('canvas.interacted', this.interactionListener); canvasInstance.html().addEventListener('canvas.interacted', this.interactionListener);
canvasInstance.html().addEventListener('canvas.canceled', this.cancelListener); canvasInstance.html().addEventListener('canvas.canceled', this.cancelListener);
} }
public componentDidUpdate(prevProps: Props): void { public componentDidUpdate(prevProps: Props): void {
const { isInteraction } = this.props; const { isActivated } = this.props;
if (prevProps.isInteraction && !isInteraction) { if (prevProps.isActivated && !isActivated) {
window.removeEventListener('contextmenu', this.contextmenuDisabler); window.removeEventListener('contextmenu', this.contextmenuDisabler);
} else if (!prevProps.isInteraction && isInteraction) { } else if (!prevProps.isActivated && isActivated) {
// reset flags when start interaction/tracking
this.interactionIsDone = false; this.interactionIsDone = false;
this.interactionIsAborted = false; this.interactionIsAborted = false;
window.addEventListener('contextmenu', this.contextmenuDisabler); window.addEventListener('contextmenu', this.contextmenuDisabler);
@ -141,7 +160,8 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
const { canvasInstance } = this.props; const { canvasInstance, aiToolsRef } = this.props;
aiToolsRef.current = undefined;
canvasInstance.html().removeEventListener('canvas.interacted', this.interactionListener); canvasInstance.html().removeEventListener('canvas.interacted', this.interactionListener);
canvasInstance.html().removeEventListener('canvas.canceled', this.cancelListener); canvasInstance.html().removeEventListener('canvas.canceled', this.cancelListener);
} }
@ -162,14 +182,14 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
private cancelListener = async (): Promise<void> => { private cancelListener = async (): Promise<void> => {
const { const {
isInteraction, isActivated,
jobInstance, jobInstance,
frame, frame,
fetchAnnotations, fetchAnnotations,
} = this.props; } = this.props;
const { interactiveStateID, fetching } = this.state; const { interactiveStateID, fetching } = this.state;
if (isInteraction) { if (isActivated) {
if (fetching && !this.interactionIsDone) { if (fetching && !this.interactionIsDone) {
// user pressed ESC // user pressed ESC
this.setState({ fetching: false }); this.setState({ fetching: false });
@ -187,12 +207,13 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
} }
}; };
private interactionListener = async (e: Event): Promise<void> => { private onInteraction = async (e: Event): Promise<void> => {
const { const {
frame, frame,
labels, labels,
curZOrder,
jobInstance, jobInstance,
isInteraction, isActivated,
activeLabelID, activeLabelID,
fetchAnnotations, fetchAnnotations,
updateAnnotations, updateAnnotations,
@ -200,8 +221,8 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
const { activeInteractor, interactiveStateID, fetching } = this.state; const { activeInteractor, interactiveStateID, fetching } = this.state;
try { try {
if (!isInteraction) { if (!isActivated) {
throw Error('Canvas raises event "canvas.interacted" when interaction is off'); throw Error('Canvas raises event "canvas.interacted" when interaction with it is off');
} }
if (fetching) { if (fetching) {
@ -216,7 +237,6 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
this.setState({ fetching: true }); this.setState({ fetching: true });
try { try {
result = await core.lambda.call(jobInstance.task, interactor, { result = await core.lambda.call(jobInstance.task, interactor, {
task: jobInstance.task,
frame, frame,
points: convertShapesForInteractor((e as CustomEvent).detail.shapes), points: convertShapesForInteractor((e as CustomEvent).detail.shapes),
}); });
@ -241,7 +261,7 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
shapeType: ShapeType.POLYGON, shapeType: ShapeType.POLYGON,
points: result.flat(), points: result.flat(),
occluded: false, occluded: false,
zOrder: (e as CustomEvent).detail.zOrder, zOrder: curZOrder,
}); });
await jobInstance.annotations.put([object]); await jobInstance.annotations.put([object]);
@ -260,7 +280,7 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
shapeType: ShapeType.POLYGON, shapeType: ShapeType.POLYGON,
points: result.flat(), points: result.flat(),
occluded: false, occluded: false,
zOrder: (e as CustomEvent).detail.zOrder, zOrder: curZOrder,
}); });
// need a clientID of a created object to interact with it further // need a clientID of a created object to interact with it further
// so, we do not use createAnnotationAction // so, we do not use createAnnotationAction
@ -302,6 +322,71 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
} }
}; };
private onTracking = async (e: Event): Promise<void> => {
const {
isActivated,
jobInstance,
frame,
curZOrder,
fetchAnnotations,
} = this.props;
const { activeLabelID } = this.state;
const [label] = jobInstance.task.labels.filter(
(_label: any): boolean => _label.id === activeLabelID,
);
if (!(e as CustomEvent).detail.isDone) {
return;
}
this.interactionIsDone = true;
try {
if (!isActivated) {
throw Error('Canvas raises event "canvas.interacted" when interaction with it is off');
}
const { points } = (e as CustomEvent).detail.shapes[0];
const state = new core.classes.ObjectState({
shapeType: ShapeType.RECTANGLE,
objectType: ObjectType.TRACK,
zOrder: curZOrder,
label,
points,
frame,
occluded: false,
source: 'auto',
attributes: {},
});
const [clientID] = await jobInstance.annotations.put([state]);
// 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(),
message: 'Tracking error occured',
});
}
};
private interactionListener = async (e: Event): Promise<void> => {
const { mode } = this.state;
if (mode === 'interaction') {
await this.onInteraction(e);
}
if (mode === 'tracking') {
await this.onTracking(e);
}
};
private setActiveInteractor = (key: string): void => { private setActiveInteractor = (key: string): void => {
const { interactors } = this.props; const { interactors } = this.props;
this.setState({ this.setState({
@ -311,6 +396,72 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
}); });
}; };
private setActiveTracker = (key: string): void => {
const { trackers } = this.props;
this.setState({
activeTracker: trackers.filter(
(tracker: Model) => tracker.id === key,
)[0],
});
};
public async trackState(state: any): Promise<void> {
const { jobInstance, frame } = 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 });
}
}
public trackingAvailable(): boolean {
const { activeTracker, trackingFrames } = this.state;
const { trackers } = this.props;
return !!trackingFrames && !!trackers.length && activeTracker !== null;
}
private renderLabelBlock(): JSX.Element { private renderLabelBlock(): JSX.Element {
const { labels } = this.props; const { labels } = this.props;
const { activeLabelID } = this.state; const { activeLabelID } = this.state;
@ -355,10 +506,119 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
); );
} }
private renderTrackerBlock(): JSX.Element {
const {
trackers,
canvasInstance,
jobInstance,
frame,
onInteractionStart,
} = this.props;
const {
activeTracker,
activeLabelID,
fetching,
trackingFrames,
} = this.state;
if (!trackers.length) {
return (
<Row type='flex' justify='center' align='middle' style={{ marginTop: '5px' }}>
<Col>
<Text type='warning' className='cvat-text-color'>No available trackers found</Text>
</Col>
</Row>
);
}
return (
<>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text-color'>Tracker</Text>
</Col>
</Row>
<Row type='flex' align='middle' justify='center'>
<Col span={24}>
<Select
style={{ width: '100%' }}
defaultValue={trackers[0].name}
onChange={this.setActiveTracker}
>
{trackers.map((interactor: Model): JSX.Element => (
<Select.Option title={interactor.description} key={interactor.id}>
{interactor.name}
</Select.Option>
))}
</Select>
</Col>
</Row>
<Row type='flex' 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): void => {
if (typeof (value) !== 'undefined') {
this.setState({
trackingFrames: value,
});
}
}}
/>
</Col>
</Row>
<Row type='flex' align='middle' justify='end'>
<Col>
<Button
type='primary'
loading={fetching}
className='cvat-tools-track-button'
disabled={!activeTracker || fetching || frame === jobInstance.stopFrame}
onClick={() => {
this.setState({
mode: 'tracking',
});
if (activeTracker) {
canvasInstance.cancel();
canvasInstance.interact({
shapeType: 'rectangle',
enabled: true,
});
onInteractionStart(activeTracker, activeLabelID);
}
}}
>
Track
</Button>
</Col>
</Row>
</>
);
}
private renderInteractorBlock(): JSX.Element { private renderInteractorBlock(): JSX.Element {
const { interactors, canvasInstance, onInteractionStart } = this.props; const { interactors, canvasInstance, onInteractionStart } = this.props;
const { activeInteractor, activeLabelID, fetching } = this.state; const { activeInteractor, activeLabelID, fetching } = this.state;
if (!interactors.length) {
return (
<Row type='flex' justify='center' align='middle' style={{ marginTop: '5px' }}>
<Col>
<Text type='warning' className='cvat-text-color'>No available interactors found</Text>
</Col>
</Row>
);
}
return ( return (
<> <>
<Row type='flex' justify='start'> <Row type='flex' justify='start'>
@ -389,6 +649,10 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
className='cvat-tools-interact-button' className='cvat-tools-interact-button'
disabled={!activeInteractor || fetching} disabled={!activeInteractor || fetching}
onClick={() => { onClick={() => {
this.setState({
mode: 'interaction',
});
if (activeInteractor) { if (activeInteractor) {
canvasInstance.cancel(); canvasInstance.cancel();
canvasInstance.interact({ canvasInstance.interact({
@ -413,10 +677,21 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
const { const {
jobInstance, jobInstance,
detectors, detectors,
curZOrder,
frame, frame,
fetchAnnotations, fetchAnnotations,
} = this.props; } = this.props;
if (!detectors.length) {
return (
<Row type='flex' justify='center' align='middle' style={{ marginTop: '5px' }}>
<Col>
<Text type='warning' className='cvat-text-color'>No available detectors found</Text>
</Col>
</Row>
);
}
return ( return (
<DetectorRunner <DetectorRunner
withCleanup={false} withCleanup={false}
@ -424,6 +699,10 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
task={jobInstance.task} task={jobInstance.task}
runInference={async (task: any, model: Model, body: object) => { runInference={async (task: any, model: Model, body: object) => {
try { try {
this.setState({
mode: 'detection',
});
this.setState({ fetching: true }); this.setState({ fetching: true });
const result = await core.lambda.call(task, model, { const result = await core.lambda.call(task, model, {
...body, ...body,
@ -444,7 +723,7 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
occluded: false, occluded: false,
source: 'auto', source: 'auto',
attributes: {}, attributes: {},
zOrder: 0, // TODO: get current z order zOrder: curZOrder,
}) })
)); ));
@ -471,7 +750,7 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
<Text className='cvat-text-color' strong>AI Tools</Text> <Text className='cvat-text-color' strong>AI Tools</Text>
</Col> </Col>
</Row> </Row>
<Tabs> <Tabs type='card' tabBarGutter={8}>
<Tabs.TabPane key='interactors' tab='Interactors'> <Tabs.TabPane key='interactors' tab='Interactors'>
{ this.renderLabelBlock() } { this.renderLabelBlock() }
{ this.renderInteractorBlock() } { this.renderInteractorBlock() }
@ -479,24 +758,34 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
<Tabs.TabPane key='detectors' tab='Detectors'> <Tabs.TabPane key='detectors' tab='Detectors'>
{ this.renderDetectorBlock() } { this.renderDetectorBlock() }
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane key='trackers' tab='Trackers'>
{ this.renderLabelBlock() }
{ this.renderTrackerBlock() }
</Tabs.TabPane>
</Tabs> </Tabs>
</div> </div>
); );
} }
public render(): JSX.Element | null { public render(): JSX.Element | null {
const { interactors, isInteraction, canvasInstance } = this.props; const {
const { fetching } = this.state; interactors,
detectors,
trackers,
isActivated,
canvasInstance,
} = this.props;
const { fetching, trackingProgress } = this.state;
if (!interactors.length) return null; if (![...interactors, ...detectors, ...trackers].length) return null;
const dynamcPopoverPros = isInteraction ? { const dynamcPopoverPros = isActivated ? {
overlayStyle: { overlayStyle: {
display: 'none', display: 'none',
}, },
} : {}; } : {};
const dynamicIconProps = isInteraction ? { const dynamicIconProps = isActivated ? {
className: 'cvat-active-canvas-control cvat-tools-control', className: 'cvat-active-canvas-control cvat-tools-control',
onClick: (): void => { onClick: (): void => {
canvasInstance.interact({ enabled: false }); canvasInstance.interact({ enabled: false });
@ -517,12 +806,15 @@ class ToolsControlComponent extends React.PureComponent<Props, State> {
> >
<Text>Waiting for a server response..</Text> <Text>Waiting for a server response..</Text>
<Icon style={{ marginLeft: '10px' }} type='loading' /> <Icon style={{ marginLeft: '10px' }} type='loading' />
{ trackingProgress !== null && (
<Progress percent={+(trackingProgress * 100).toFixed(0)} status='active' />
)}
</Modal> </Modal>
<Popover <Popover
{...dynamcPopoverPros} {...dynamcPopoverPros}
placement='right' placement='right'
overlayClassName='cvat-tools-control-popover' overlayClassName='cvat-tools-control-popover'
content={interactors.length && this.renderPopoverContent()} content={this.renderPopoverContent()}
> >
<Icon {...dynamicIconProps} component={AIToolsIcon} /> <Icon {...dynamicIconProps} component={AIToolsIcon} />
</Popover> </Popover>

@ -41,6 +41,7 @@ interface Props {
toBackground(): void; toBackground(): void;
toForeground(): void; toForeground(): void;
resetCuboidPerspective(): void; resetCuboidPerspective(): void;
activateTracking(): void;
} }
function ItemTopComponent(props: Props): JSX.Element { function ItemTopComponent(props: Props): JSX.Element {
@ -72,6 +73,7 @@ function ItemTopComponent(props: Props): JSX.Element {
toBackground, toBackground,
toForeground, toForeground,
resetCuboidPerspective, resetCuboidPerspective,
activateTracking,
} = props; } = props;
const [menuVisible, setMenuVisible] = useState(false); const [menuVisible, setMenuVisible] = useState(false);
@ -150,6 +152,7 @@ function ItemTopComponent(props: Props): JSX.Element {
toForeground, toForeground,
resetCuboidPerspective, resetCuboidPerspective,
changeColorPickerVisible, changeColorPickerVisible,
activateTracking,
})} })}
> >
<Icon type='more' /> <Icon type='more' />

@ -33,16 +33,17 @@ interface Props {
toBackgroundShortcut: string; toBackgroundShortcut: string;
toForegroundShortcut: string; toForegroundShortcut: string;
removeShortcut: string; removeShortcut: string;
changeColor: (value: string) => void; changeColor(value: string): void;
copy: (() => void); copy(): void;
remove: (() => void); remove(): void;
propagate: (() => void); propagate(): void;
createURL: (() => void); createURL(): void;
switchOrientation: (() => void); switchOrientation(): void;
toBackground: (() => void); toBackground(): void;
toForeground: (() => void); toForeground(): void;
resetCuboidPerspective: (() => void); resetCuboidPerspective(): void;
changeColorPickerVisible: (visible: boolean) => void; changeColorPickerVisible(visible: boolean): void;
activateTracking(): void;
} }
export default function ItemMenu(props: Props): JSX.Element { export default function ItemMenu(props: Props): JSX.Element {
@ -71,6 +72,7 @@ export default function ItemMenu(props: Props): JSX.Element {
toForeground, toForeground,
resetCuboidPerspective, resetCuboidPerspective,
changeColorPickerVisible, changeColorPickerVisible,
activateTracking,
} = props; } = props;
return ( return (
@ -94,6 +96,16 @@ export default function ItemMenu(props: Props): JSX.Element {
</Button> </Button>
</Tooltip> </Tooltip>
</Menu.Item> </Menu.Item>
{objectType === ObjectType.TRACK && shapeType === ShapeType.RECTANGLE && (
<Menu.Item>
<Tooltip title='Run tracking with the active tracker' mouseLeaveDelay={0}>
<Button type='link' onClick={activateTracking}>
<Icon type='gateway' />
Track
</Button>
</Tooltip>
</Menu.Item>
)}
{ [ShapeType.POLYGON, ShapeType.POLYLINE, ShapeType.CUBOID].includes(shapeType) && ( { [ShapeType.POLYGON, ShapeType.POLYLINE, ShapeType.CUBOID].includes(shapeType) && (
<Menu.Item> <Menu.Item>
<Button type='link' icon='retweet' onClick={switchOrientation}> <Button type='link' icon='retweet' onClick={switchOrientation}>

@ -44,6 +44,7 @@ interface Props {
changeColor(color: string): void; changeColor(color: string): void;
collapse(): void; collapse(): void;
resetCuboidPerspective(): void; resetCuboidPerspective(): void;
activateTracking(): void;
} }
function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean { function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean {
@ -94,6 +95,7 @@ function ObjectItemComponent(props: Props): JSX.Element {
changeColor, changeColor,
collapse, collapse,
resetCuboidPerspective, resetCuboidPerspective,
activateTracking,
} = props; } = props;
const type = objectType === ObjectType.TAG ? ObjectType.TAG.toUpperCase() const type = objectType === ObjectType.TAG ? ObjectType.TAG.toUpperCase()
@ -142,6 +144,7 @@ function ObjectItemComponent(props: Props): JSX.Element {
toBackground={toBackground} toBackground={toBackground}
toForeground={toForeground} toForeground={toForeground}
resetCuboidPerspective={resetCuboidPerspective} resetCuboidPerspective={resetCuboidPerspective}
activateTracking={activateTracking}
/> />
<ObjectButtonsContainer <ObjectButtonsContainer
clientID={clientID} clientID={clientID}

@ -13,6 +13,7 @@ import CanvasContextMenuContainer from 'containers/annotation-page/standard-work
import ObjectSideBarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; import ObjectSideBarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar';
import CanvasPointContextMenuComponent from 'components/annotation-page/standard-workspace/canvas-point-context-menu'; import CanvasPointContextMenuComponent from 'components/annotation-page/standard-workspace/canvas-point-context-menu';
export default function StandardWorkspaceComponent(): JSX.Element { export default function StandardWorkspaceComponent(): JSX.Element {
return ( return (
<Layout hasSider className='cvat-standard-workspace'> <Layout hasSider className='cvat-standard-workspace'>

@ -92,6 +92,7 @@
} }
} }
.cvat-tools-track-button,
.cvat-tools-interact-button { .cvat-tools-interact-button {
width: 100%; width: 100%;
margin-top: 10px; margin-top: 10px;
@ -102,7 +103,7 @@
} }
.cvat-tools-control-popover-content { .cvat-tools-control-popover-content {
width: 350px; width: fit-content;
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
background: $background-color-2; background: $background-color-2;

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React, { MutableRefObject } from 'react';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -26,6 +26,7 @@ import {
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item'; import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item';
import { ToolsControlComponent } from 'components/annotation-page/standard-workspace/controls-side-bar/tools-control';
import { shift } from 'utils/math'; import { shift } from 'utils/math';
interface OwnProps { interface OwnProps {
@ -47,6 +48,7 @@ interface StateToProps {
minZLayer: number; minZLayer: number;
maxZLayer: number; maxZLayer: number;
normalizedKeyMap: Record<string, string>; normalizedKeyMap: Record<string, string>;
aiToolsRef: MutableRefObject<ToolsControlComponent>;
} }
interface DispatchToProps { interface DispatchToProps {
@ -86,6 +88,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
ready, ready,
activeControl, activeControl,
}, },
aiToolsRef,
}, },
settings: { settings: {
shapes: { shapes: {
@ -118,6 +121,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
minZLayer, minZLayer,
maxZLayer, maxZLayer,
normalizedKeyMap, normalizedKeyMap,
aiToolsRef,
}; };
} }
@ -259,6 +263,13 @@ class ObjectItemContainer extends React.PureComponent<Props> {
collapseOrExpand([objectState], !collapsed); collapseOrExpand([objectState], !collapsed);
}; };
private activateTracking = (): void => {
const { objectState, aiToolsRef } = this.props;
if (aiToolsRef.current && aiToolsRef.current.trackingAvailable()) {
aiToolsRef.current.trackState(objectState);
}
};
private changeColor = (color: string): void => { private changeColor = (color: string): void => {
const { const {
objectState, objectState,
@ -402,6 +413,7 @@ class ObjectItemContainer extends React.PureComponent<Props> {
changeLabel={this.changeLabel} changeLabel={this.changeLabel}
changeAttribute={this.changeAttribute} changeAttribute={this.changeAttribute}
collapse={this.collapse} collapse={this.collapse}
activateTracking={this.activateTracking}
resetCuboidPerspective={() => this.resetCuboidPerspective()} resetCuboidPerspective={() => this.resetCuboidPerspective()}
/> />
); );

@ -2,6 +2,7 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react';
import { AnyAction } from 'redux'; import { AnyAction } from 'redux';
import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; import { Canvas, CanvasMode } from 'cvat-canvas-wrapper';
@ -93,6 +94,7 @@ const defaultState: AnnotationState = {
collecting: false, collecting: false,
data: null, data: null,
}, },
aiToolsRef: React.createRef(),
colors: [], colors: [],
sidebarCollapsed: false, sidebarCollapsed: false,
appearanceCollapsed: false, appearanceCollapsed: false,
@ -1058,7 +1060,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
canvas: { canvas: {
...state.canvas, ...state.canvas,
activeControl: ActiveControl.INTERACTION, activeControl: ActiveControl.AI_TOOLS,
}, },
}; };
} }

@ -4,6 +4,7 @@
import { ExtendedKeyMapOptions } from 'react-hotkeys'; import { ExtendedKeyMapOptions } from 'react-hotkeys';
import { Canvas, RectDrawingMethod } from 'cvat-canvas-wrapper'; import { Canvas, RectDrawingMethod } from 'cvat-canvas-wrapper';
import { MutableRefObject } from 'react';
export type StringObject = { export type StringObject = {
[index: string]: string; [index: string]: string;
@ -275,7 +276,7 @@ export enum ActiveControl {
GROUP = 'group', GROUP = 'group',
SPLIT = 'split', SPLIT = 'split',
EDIT = 'edit', EDIT = 'edit',
INTERACTION = 'interaction', AI_TOOLS = 'ai_tools',
} }
export enum ShapeType { export enum ShapeType {
@ -394,6 +395,7 @@ export interface AnnotationState {
appearanceCollapsed: boolean; appearanceCollapsed: boolean;
tabContentHeight: number; tabContentHeight: number;
workspace: Workspace; workspace: Workspace;
aiToolsRef: MutableRefObject<any>;
} }
export enum Workspace { export enum Workspace {

@ -0,0 +1,27 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
export default function range(x: number, y?: number): number[] {
if (typeof (x) !== 'undefined' && typeof (y) !== 'undefined') {
if (typeof (x) !== 'number' && typeof (y) !== 'number') {
throw new Error(`Range() expects number arguments. Got ${typeof (x)}, ${typeof (y)}`);
}
if (x >= y) {
throw new Error(`Range() expects the first argument less or equal than the second. Got ${x}, ${y}`);
}
return Array.from(Array(y - x), (_: number, i: number) => i + x);
}
if (typeof (x) !== 'undefined') {
if (typeof (x) !== 'number') {
throw new Error(`Range() expects number arguments. Got ${typeof (x)}`);
}
return [...Array(x).keys()];
}
return [];
}
Loading…
Cancel
Save