Merge pull request #1241 from opencv/dk/cvat-ui-tags

React UI: tags support
main
Dmitry Kalinin 6 years ago committed by GitHub
commit 6a7bf6d267
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1084,6 +1084,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.activeElement = { ...activeElement };
const shape = this.svgShapes[clientID];
let text = this.svgTexts[clientID];
if (!text) {
text = this.addText(state);

@ -802,7 +802,9 @@
let minimumState = null;
for (const state of objectStates) {
checkObjectType('object state', state, null, ObjectState);
if (state.outside || state.hidden) continue;
if (state.outside || state.hidden || state.objectType === ObjectType.TAG) {
continue;
}
const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') {
@ -810,9 +812,9 @@
'The object has not been saved yet. Call annotations.put([state]) before',
);
}
const distance = object.constructor.distance(state.points, x, y);
if (distance !== null && (minimumDistance === null || distance < minimumDistance)) {
if (distance !== null && (minimumDistance === null
|| distance < minimumDistance)) {
minimumDistance = distance;
minimumState = state;
}

@ -8,7 +8,10 @@
*/
const jsonpath = require('jsonpath');
const { AttributeType } = require('./enums');
const {
AttributeType,
ObjectType,
} = require('./enums');
const { ArgumentError } = require('./exceptions');
@ -165,18 +168,21 @@ class AnnotationsFilter {
let xbr = Number.MIN_SAFE_INTEGER;
let ytl = Number.MAX_SAFE_INTEGER;
let ybr = Number.MIN_SAFE_INTEGER;
let [width, height] = [null, null];
if (state.objectType !== ObjectType.TAG) {
state.points.forEach((coord, idx) => {
if (idx % 2) { // y
ytl = Math.min(ytl, coord);
ybr = Math.max(ybr, coord);
} else { // x
xtl = Math.min(xtl, coord);
xbr = Math.max(xbr, coord);
}
});
[width, height] = [xbr - xtl, ybr - ytl];
}
state.points.forEach((coord, idx) => {
if (idx % 2) { // y
ytl = Math.min(ytl, coord);
ybr = Math.max(ybr, coord);
} else { // x
xtl = Math.min(xtl, coord);
xbr = Math.max(xbr, coord);
}
});
const [width, height] = [xbr - xtl, ybr - ytl];
const attributes = {};
Object.keys(state.attributes).reduce((acc, key) => {
const attr = labelAttributes[key];

@ -1139,6 +1139,7 @@
attributes: { ...this.attributes },
label: this.label,
group: this.groupObject,
color: this.color,
updated: this.updated,
frame,
};
@ -1171,6 +1172,10 @@
this._saveLock(data.lock);
}
if (updated.color) {
this._saveColor(data.color);
}
this.updateTimestamp(updated);
updated.reset();

@ -84,10 +84,10 @@ export enum AnnotationActionTypes {
COPY_SHAPE = 'COPY_SHAPE',
PASTE_SHAPE = 'PASTE_SHAPE',
EDIT_SHAPE = 'EDIT_SHAPE',
DRAW_SHAPE = 'DRAW_SHAPE',
REPEAT_DRAW_SHAPE = 'REPEAT_DRAW_SHAPE',
SHAPE_DRAWN = 'SHAPE_DRAWN',
RESET_CANVAS = 'RESET_CANVAS',
REMEMBER_CREATED_OBJECT = 'REMEMBER_CREATED_OBJECT',
UPDATE_ANNOTATIONS_SUCCESS = 'UPDATE_ANNOTATIONS_SUCCESS',
UPDATE_ANNOTATIONS_FAILED = 'UPDATE_ANNOTATIONS_FAILED',
CREATE_ANNOTATIONS_SUCCESS = 'CREATE_ANNOTATIONS_SUCCESS',
@ -843,15 +843,17 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
};
}
export function drawShape(
shapeType: ShapeType,
labelID: number,
export function rememberObject(
objectType: ObjectType,
labelID: number,
shapeType?: ShapeType,
points?: number,
rectDrawingMethod?: RectDrawingMethod,
): AnyAction {
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (shapeType === ShapeType.POLYGON) {
let activeControl = ActiveControl.CURSOR;
if (shapeType === ShapeType.RECTANGLE) {
activeControl = ActiveControl.DRAW_RECTANGLE;
} else if (shapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (shapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
@ -860,7 +862,7 @@ export function drawShape(
}
return {
type: AnnotationActionTypes.DRAW_SHAPE,
type: AnnotationActionTypes.REMEMBER_CREATED_OBJECT,
payload: {
shapeType,
labelID,
@ -1153,12 +1155,28 @@ export function searchAnnotationsAsync(
export function pasteShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const initialState = getStore().getState().annotation.drawing.activeInitialState;
const { instance: canvasInstance } = getStore().getState().annotation.canvas;
const {
canvas: {
instance: canvasInstance,
},
job: {
instance: jobInstance,
},
player: {
frame: {
number: frameNumber,
},
},
drawing: {
activeInitialState: initialState,
},
} = getStore().getState().annotation;
if (initialState) {
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (initialState.shapeType === ShapeType.POINTS) {
let activeControl = ActiveControl.CURSOR;
if (initialState.shapeType === ShapeType.RECTANGLE) {
activeControl = ActiveControl.DRAW_RECTANGLE;
} else if (initialState.shapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} else if (initialState.shapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
@ -1174,10 +1192,20 @@ export function pasteShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction>
});
canvasInstance.cancel();
canvasInstance.draw({
enabled: true,
initialState,
});
if (initialState.objectType === ObjectType.TAG) {
const objectState = new cvat.classes.ObjectState({
objectType: ObjectType.TAG,
label: initialState.label,
attributes: initialState.attributes,
frame: frameNumber,
});
dispatch(createAnnotationsAsync(jobInstance, frameNumber, [objectState]));
} else {
canvasInstance.draw({
enabled: true,
initialState,
});
}
}
};
}
@ -1185,20 +1213,36 @@ export function pasteShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction>
export function repeatDrawShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
activeShapeType,
activeNumOfPoints,
activeRectDrawingMethod,
} = getStore().getState().annotation.drawing;
const { instance: canvasInstance } = getStore().getState().annotation.canvas;
canvas: {
instance: canvasInstance,
},
job: {
labels,
instance: jobInstance,
},
player: {
frame: {
number: frameNumber,
},
},
drawing: {
activeObjectType,
activeLabelID,
activeShapeType,
activeNumOfPoints,
activeRectDrawingMethod,
},
} = getStore().getState().annotation;
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (activeShapeType === ShapeType.POLYGON) {
let activeControl = ActiveControl.CURSOR;
if (activeShapeType === ShapeType.RECTANGLE) {
activeControl = ActiveControl.DRAW_RECTANGLE;
} else if (activeShapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} else if (activeShapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (activeShapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
} else if (activeShapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
}
dispatch({
@ -1209,12 +1253,21 @@ export function repeatDrawShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAc
});
canvasInstance.cancel();
canvasInstance.draw({
enabled: true,
rectDrawingMethod: activeRectDrawingMethod,
numberOfPoints: activeNumOfPoints,
shapeType: activeShapeType,
crosshair: activeShapeType === ShapeType.RECTANGLE,
});
if (activeObjectType === ObjectType.TAG) {
const objectState = new cvat.classes.ObjectState({
objectType: ObjectType.TAG,
label: labels.filter((label: any) => label.id === activeLabelID)[0],
frame: frameNumber,
});
dispatch(createAnnotationsAsync(jobInstance, frameNumber, [objectState]));
} else {
canvasInstance.draw({
enabled: true,
rectDrawingMethod: activeRectDrawingMethod,
numberOfPoints: activeNumOfPoints,
shapeType: activeShapeType,
crosshair: activeShapeType === ShapeType.RECTANGLE,
});
}
};
}

@ -345,7 +345,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} = this.props;
if (frameData !== null) {
canvasInstance.setup(frameData, annotations);
canvasInstance.setup(frameData, annotations
.filter((e) => e.objectType !== ObjectType.TAG));
canvasInstance.rotate(frameAngle);
}
}

@ -6,9 +6,7 @@ import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import {
Icon,
Layout,
Tooltip,
} from 'antd';
import {
@ -16,10 +14,6 @@ import {
Rotation,
} from 'reducers/interfaces';
import {
TagIcon,
} from 'icons';
import {
Canvas,
} from 'cvat-canvas';
@ -33,6 +27,7 @@ import DrawRectangleControl from './draw-rectangle-control';
import DrawPolygonControl from './draw-polygon-control';
import DrawPolylineControl from './draw-polyline-control';
import DrawPointsControl from './draw-points-control';
import SetupTagControl from './setup-tag-control';
import MergeControl from './merge-control';
import GroupControl from './group-control';
import SplitControl from './split-control';
@ -221,9 +216,10 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
isDrawing={activeControl === ActiveControl.DRAW_POINTS}
/>
<Tooltip title='Setup a tag' placement='right'>
<Icon component={TagIcon} style={{ pointerEvents: 'none', opacity: 0.5 }} />
</Tooltip>
<SetupTagControl
canvasInstance={canvasInstance}
isDrawing={false}
/>
<hr />

@ -0,0 +1,48 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import {
Popover,
Icon,
} from 'antd';
import { Canvas } from 'cvat-canvas';
import { TagIcon } from 'icons';
import SetupTagPopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover';
interface Props {
canvasInstance: Canvas;
isDrawing: boolean;
}
function SetupTagControl(props: Props): JSX.Element {
const {
isDrawing,
} = props;
const dynamcPopoverPros = isDrawing ? {
overlayStyle: {
display: 'none',
},
} : {};
return (
<Popover
{...dynamcPopoverPros}
placement='right'
overlayClassName='cvat-draw-shape-popover'
content={(
<SetupTagPopoverContainer />
)}
>
<Icon
component={TagIcon}
/>
</Popover>
);
}
export default React.memo(SetupTagControl);

@ -0,0 +1,75 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import {
Row,
Col,
Select,
Button,
} from 'antd';
import Text from 'antd/lib/typography/Text';
interface Props {
labels: any[];
selectedLabeID: number;
onChangeLabel(value: string): void;
onSetup(
labelID: number,
): void;
}
function setupTagPopover(props: Props): JSX.Element {
const {
labels,
selectedLabeID,
onChangeLabel,
onSetup,
} = props;
return (
<div className='cvat-draw-shape-popover-content'>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text-color' strong>Setup tag</Text>
</Col>
</Row>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text-color'>Label</Text>
</Col>
</Row>
<Row type='flex' justify='center'>
<Col span={24}>
<Select
value={`${selectedLabeID}`}
onChange={onChangeLabel}
>
{
labels.map((label: any) => (
<Select.Option
key={label.id}
value={`${label.id}`}
>
{label.name}
</Select.Option>
))
}
</Select>
</Col>
</Row>
<Row type='flex' justify='space-around'>
<Col span={24}>
<Button onClick={() => onSetup(selectedLabeID)}>
Tag
</Button>
</Col>
</Row>
</div>
);
}
export default React.memo(setupTagPopover);

@ -43,6 +43,7 @@ import {
function ItemMenu(
serverID: number | undefined,
locked: boolean,
objectType: ObjectType,
copy: (() => void),
remove: (() => void),
propagate: (() => void),
@ -67,18 +68,22 @@ function ItemMenu(
Propagate
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' onClick={toBackground}>
<Icon component={BackgroundIcon} />
To background
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' onClick={toForeground}>
<Icon component={ForegroundIcon} />
To foreground
</Button>
</Menu.Item>
{ objectType !== ObjectType.TAG && (
<>
<Menu.Item>
<Button type='link' onClick={toBackground}>
<Icon component={BackgroundIcon} />
To background
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' onClick={toForeground}>
<Icon component={ForegroundIcon} />
To foreground
</Button>
</Menu.Item>
</>
)}
<Menu.Item>
<Button
type='link'
@ -109,6 +114,7 @@ interface ItemTopComponentProps {
serverID: number | undefined;
labelID: number;
labels: any[];
objectType: ObjectType;
type: string;
locked: boolean;
changeLabel(labelID: string): void;
@ -126,6 +132,7 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
serverID,
labelID,
labels,
objectType,
type,
locked,
changeLabel,
@ -159,6 +166,7 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
overlay={ItemMenu(
serverID,
locked,
objectType,
copy,
remove,
propagate,
@ -302,6 +310,22 @@ function ItemButtonsComponent(props: ItemButtonsComponentProps): JSX.Element {
);
}
if (objectType === ObjectType.TAG) {
return (
<Row type='flex' align='middle' justify='space-around'>
<Col span={20} style={{ textAlign: 'center' }}>
<Row type='flex' justify='space-around'>
<Col>
{ locked
? <Icon type='lock' onClick={unlock} />
: <Icon type='unlock' onClick={lock} />}
</Col>
</Row>
</Col>
</Row>
);
}
return (
<Row type='flex' align='middle' justify='space-around'>
<Col span={20} style={{ textAlign: 'center' }}>
@ -714,7 +738,6 @@ function ObjectItemComponent(props: Props): JSX.Element {
style={{ background: ` ${color}` }}
/>
</Popover>
<div
onMouseEnter={activate}
id={`cvat-objects-sidebar-state-item-${clientID}`}
@ -726,6 +749,7 @@ function ObjectItemComponent(props: Props): JSX.Element {
clientID={clientID}
labelID={labelID}
labels={labels}
objectType={objectType}
type={type}
locked={locked}
changeLabel={changeLabel}

@ -13,7 +13,7 @@ import {
} from 'reducers/interfaces';
import {
drawShape,
rememberObject,
} from 'actions/annotation-actions';
import { Canvas, RectDrawingMethod } from 'cvat-canvas';
import DrawShapePopoverComponent from 'components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover';
@ -47,7 +47,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
points?: number,
rectDrawingMethod?: RectDrawingMethod,
): void {
dispatch(drawShape(shapeType, labelID, objectType, points, rectDrawingMethod));
dispatch(rememberObject(objectType, labelID, shapeType, points, rectDrawingMethod));
},
};
}

@ -0,0 +1,144 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import {
CombinedState,
ObjectType,
} from 'reducers/interfaces';
import {
createAnnotationsAsync,
rememberObject,
} from 'actions/annotation-actions';
import SetupTagPopoverComponent from 'components/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover';
import { Canvas } from 'cvat-canvas';
import getCore from 'cvat-core';
const cvat = getCore();
interface DispatchToProps {
onAnnotationCreate(sessionInstance: any, frame: number, states: any[]): void;
onRememberObject(labelID: number): void;
}
interface StateToProps {
canvasInstance: Canvas;
jobInstance: any;
labels: any[];
frame: number;
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onAnnotationCreate(sessionInstance: any, frame: number, states: any[]): void {
dispatch(createAnnotationsAsync(sessionInstance, frame, states));
},
onRememberObject(labelID: number): void {
dispatch(rememberObject(ObjectType.TAG, labelID));
},
};
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
canvas: {
instance: canvasInstance,
},
job: {
instance: jobInstance,
labels,
},
player: {
frame: {
number: frame,
},
},
},
} = state;
return {
canvasInstance,
jobInstance,
labels,
frame,
};
}
type Props = StateToProps & DispatchToProps;
interface State {
selectedLabelID: number;
}
class DrawShapePopoverContainer extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
const defaultLabelID = props.labels[0].id;
this.state = {
selectedLabelID: defaultLabelID,
};
}
private onChangeLabel = (value: string): void => {
this.setState({
selectedLabelID: +value,
});
};
private onSetup(): void {
const {
frame,
labels,
jobInstance,
canvasInstance,
onAnnotationCreate,
onRememberObject,
} = this.props;
const { selectedLabelID } = this.state;
canvasInstance.cancel();
onRememberObject(selectedLabelID);
const objectState = new cvat.classes.ObjectState({
objectType: ObjectType.TAG,
label: labels.filter((label: any) => label.id === selectedLabelID)[0],
frame,
});
onAnnotationCreate(jobInstance, frame, [objectState]);
}
public render(): JSX.Element {
const {
selectedLabelID,
} = this.state;
const {
labels,
} = this.props;
this.onSetup = this.onSetup.bind(this);
return (
<SetupTagPopoverComponent
labels={labels}
selectedLabeID={selectedLabelID}
onChangeLabel={this.onChangeLabel}
onSetup={this.onSetup}
/>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(DrawShapePopoverContainer);

@ -13,6 +13,7 @@ import {
import {
collapseObjectItems,
changeLabelColorAsync,
createAnnotationsAsync,
updateAnnotationsAsync,
changeFrameAsync,
removeObjectAsync,
@ -25,6 +26,7 @@ import {
import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item';
interface OwnProps {
clientID: number;
}
@ -48,6 +50,7 @@ interface StateToProps {
interface DispatchToProps {
changeFrame(frame: number): void;
updateState(sessionInstance: any, frameNumber: number, objectState: any): void;
createAnnotations(sessionInstance: any, frameNumber: number, state: any): void;
collapseOrExpand(objectStates: any[], collapsed: boolean): void;
activateObject: (activatedStateID: number | null) => void;
removeObject: (sessionInstance: any, objectState: any) => void;
@ -124,6 +127,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
updateState(sessionInstance: any, frameNumber: number, state: any): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, [state]));
},
createAnnotations(sessionInstance: any, frameNumber: number, state: any): void {
dispatch(createAnnotationsAsync(sessionInstance, frameNumber, state));
},
collapseOrExpand(objectStates: any[], collapsed: boolean): void {
dispatch(collapseObjectItems(objectStates, collapsed));
},

@ -84,7 +84,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
objectStates.forEach((objectState: any) => {
const { clientID, lock } = objectState;
if (!lock) {
statesHidden = statesHidden && objectState.hidden;
if (objectState.objectType !== ObjectType.TAG) {
statesHidden = statesHidden && objectState.hidden;
}
statesLocked = statesLocked && objectState.lock;
}
const stateCollapsed = clientID in collapsed ? collapsed[clientID] : true;
@ -462,14 +464,14 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
COPY_SHAPE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
if (state) {
copyShape(state);
}
},
PROPAGATE_OBJECT: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
if (state) {
propagateObject(state);
}
},

@ -393,7 +393,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.DRAW_SHAPE: {
case AnnotationActionTypes.REMEMBER_CREATED_OBJECT: {
const {
shapeType,
labelID,

Loading…
Cancel
Save