CVAT UI: tag annotation workspace (#1570)

main
Dmitry Kalinin 6 years ago committed by GitHub
parent db29291d43
commit 27dc52a513
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Highlights for the first point of a polygon/polyline and direction (<https://github.com/opencv/cvat/pull/1571>)
- Ability to change orientation for poylgons/polylines in context menu (<https://github.com/opencv/cvat/pull/1571>)
- Ability to set the first point for polygons in points context menu (<https://github.com/opencv/cvat/pull/1571>)
- Added new tag annotation workspace (<https://github.com/opencv/cvat/pull/1570>)
### Changed
- Removed information about e-mail from the basic user information (<https://github.com/opencv/cvat/pull/1627>)

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

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

@ -13,6 +13,7 @@ import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-ba
import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal';
import StandardWorkspaceComponent from './standard-workspace/standard-workspace';
import AttributeAnnotationWorkspace from './attribute-annotation-workspace/attribute-annotation-workspace';
import TagAnnotationWorkspace from './tag-annotation-workspace/tag-annotation-workspace';
interface Props {
job: any | null | undefined;
@ -70,15 +71,21 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
<Layout.Header className='cvat-annotation-header'>
<AnnotationTopBarContainer />
</Layout.Header>
{ workspace === Workspace.STANDARD ? (
{ workspace === Workspace.STANDARD && (
<Layout.Content>
<StandardWorkspaceComponent />
</Layout.Content>
) : (
)}
{ workspace === Workspace.ATTRIBUTE_ANNOTATION && (
<Layout.Content>
<AttributeAnnotationWorkspace />
</Layout.Content>
)}
{ workspace === Workspace.TAG_ANNOTATION && (
<Layout.Content>
<TagAnnotationWorkspace />
</Layout.Content>
)}
<StatisticsModalContainer />
</Layout>
);

@ -226,8 +226,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
if (prevProps.frame !== frameData.number
&& resetZoom
&& workspace !== Workspace.ATTRIBUTE_ANNOTATION
&& ((resetZoom && workspace !== Workspace.ATTRIBUTE_ANNOTATION) ||
workspace === Workspace.TAG_ANNOTATION)
) {
canvasInstance.html().addEventListener('canvas.setup', () => {
canvasInstance.fit();
@ -462,7 +462,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onActivateObject,
} = this.props;
if (workspace === Workspace.ATTRIBUTE_ANNOTATION) {
if (workspace !== Workspace.STANDARD) {
return;
}

@ -0,0 +1,47 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
@import 'base.scss';
.cvat-tag-annotation-workspace.ant-layout {
height: 100%;
}
.cvat-tag-annotation-sidebar {
background: $background-color-2;
padding: 5px;
> div > .ant-row-flex > .ant-col > .ant-tag {
margin: 4px;
}
}
.cvat-tag-annotation-sidebar-label-select {
padding-top: 40px;
padding-bottom: 15px;
> .ant-col > .ant-select {
padding-left: 5px;
width: 230px;
}
}
.cvat-tag-annotation-sidebar-shortcut-help {
padding-top: 15px;
text-align: center;
}
.cvat-tag-annotation-sidebar-buttons,
.cvat-tag-anntation-sidebar-checkbox-skip-frame {
padding-bottom: 15px;
}
.cvat-tag-annotation-label-selects {
padding-top: 10px;
.ant-select {
width: 230px;
padding: 5px 10px;
}
}

@ -0,0 +1,125 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState, useEffect, Fragment } from 'react';
import { useSelector } from 'react-redux';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import { Row, Col } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text';
import Select from 'antd/lib/select';
import { CombinedState } from 'reducers/interfaces';
import { shift } from 'utils/math';
interface ShortcutLabelMap {
[index: number]: any;
}
type Props = {
onAddTag(labelID: number): void;
};
const defaultShortcutLabelMap = {
1: '',
2: '',
3: '',
4: '',
5: '',
6: '',
7: '',
8: '',
9: '',
0: '',
} as ShortcutLabelMap;
const ShortcutsSelect = (props: Props): JSX.Element => {
const { onAddTag } = props;
const { labels } = useSelector((state: CombinedState) => state.annotation.job);
const [shortcutLabelMap, setShortcutLabelMap] = useState(defaultShortcutLabelMap);
const keyMap: KeyMap = {};
const handlers: {
[key: string]: (keyEvent?: KeyboardEvent) => void;
} = {};
useEffect(() => {
const newShortcutLabelMap = { ...shortcutLabelMap };
(labels as any[]).slice(0, 10).forEach((label, index) => {
newShortcutLabelMap[(index + 1) % 10] = label.id;
});
setShortcutLabelMap(newShortcutLabelMap);
}, []);
Object.keys(shortcutLabelMap).map((id) => Number.parseInt(id, 10))
.filter((id) => shortcutLabelMap[id]).forEach((id: number): void => {
const [label] = labels.filter((_label) => _label.id === shortcutLabelMap[id]);
const key = `SETUP_${id}_TAG`;
keyMap[key] = {
name: `Setup ${label.name} tag`,
description: `Setup tag with "${label.name}" label`,
sequence: `${id}`,
action: 'keydown',
};
handlers[key] = (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
onAddTag(label.id);
};
});
const onChangeShortcutLabel = (value: string, id: number): void => {
const newShortcutLabelMap = { ...shortcutLabelMap };
newShortcutLabelMap[id] = value ? Number.parseInt(value, 10) : '';
setShortcutLabelMap(newShortcutLabelMap);
};
return (
<div className='cvat-tag-annotation-label-selects'>
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers} allowChanges />
<Row>
<Col>
<Text strong>Shortcuts for labels:</Text>
</Col>
</Row>
{shift(Object.keys(shortcutLabelMap), 1).slice(0, Math.min(labels.length, 10)).map((id) => (
<Row key={id}>
<Col>
<Text strong>{`Key ${id}:`}</Text>
<Select
value={`${shortcutLabelMap[Number.parseInt(id, 10)]}`}
onChange={(value: string) => {
onChangeShortcutLabel(value, Number.parseInt(id, 10));
}}
size='default'
style={{ width: 200 }}
className='cvat-tag-annotation-label-select'
>
<Select.Option value=''>
<Text type='secondary'>
None
</Text>
</Select.Option>
{
(labels as any[]).map((label: any) => (
<Select.Option
key={label.id}
value={`${label.id}`}
>
{label.name}
</Select.Option>
))
}
</Select>
</Col>
</Row>
))}
</div>
);
};
export default ShortcutsSelect;

@ -0,0 +1,310 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { GlobalHotKeys, ExtendedKeyMapOptions } from 'react-hotkeys';
import { Action } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { Row, Col } from 'antd/lib/grid';
import Layout, { SiderProps } from 'antd/lib/layout';
import Button from 'antd/lib/button/button';
import Icon from 'antd/lib/icon';
import Text from 'antd/lib/typography/Text';
import Select from 'antd/lib/select';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox/Checkbox';
import {
createAnnotationsAsync,
removeObjectAsync,
changeFrameAsync,
rememberObject,
} from 'actions/annotation-actions';
import { Canvas } from 'cvat-canvas-wrapper';
import { CombinedState, ObjectType } from 'reducers/interfaces';
import Tag from 'antd/lib/tag';
import getCore from 'cvat-core-wrapper';
import ShortcutsSelect from './shortcuts-select';
const cvat = getCore();
interface StateToProps {
states: any[];
labels: any[];
jobInstance: any;
canvasInstance: Canvas;
frameNumber: number;
keyMap: Record<string, ExtendedKeyMapOptions>;
normalizedKeyMap: Record<string, string>;
}
interface DispatchToProps {
removeObject(jobInstance: any, objectState: any): void;
createAnnotations(jobInstance: any, frame: number, objectStates: any[]): void;
changeFrame(frame: number, fillBuffer?: boolean, frameStep?: number): void;
onRememberObject(labelID: number): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
player: {
frame: {
number: frameNumber,
},
},
annotations: {
states,
},
job: {
instance: jobInstance,
labels,
},
canvas: {
instance: canvasInstance,
},
},
shortcuts: {
keyMap,
normalizedKeyMap,
},
} = state;
return {
jobInstance,
labels,
states,
canvasInstance,
frameNumber,
keyMap,
normalizedKeyMap,
};
}
function mapDispatchToProps(dispatch: ThunkDispatch<CombinedState, {}, Action>): DispatchToProps {
return {
changeFrame(frame: number, fillBuffer?: boolean, frameStep?: number): void {
dispatch(changeFrameAsync(frame, fillBuffer, frameStep));
},
createAnnotations(jobInstance: any, frame: number, objectStates: any[]): void {
dispatch(createAnnotationsAsync(jobInstance, frame, objectStates));
},
removeObject(jobInstance: any, objectState: any): void {
dispatch(removeObjectAsync(jobInstance, objectState, true));
},
onRememberObject(labelID: number): void {
dispatch(rememberObject(ObjectType.TAG, labelID));
},
};
}
function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Element {
const {
states,
labels,
removeObject,
jobInstance,
changeFrame,
canvasInstance,
frameNumber,
onRememberObject,
createAnnotations,
keyMap,
} = props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
if (event) {
event.preventDefault();
}
};
const defaultLabelID = labels[0].id;
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [frameTags, setFrameTags] = useState([] as any[]);
const [selectedLabelID, setSelectedLabelID] = useState(defaultLabelID);
const [skipFrame, setSkipFrame] = useState(false);
useEffect(() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}, []);
useEffect(() => {
const listener = (event: Event): void => {
if ((event as TransitionEvent).propertyName === 'width'
&& ((event.target as any).classList as DOMTokenList).contains('cvat-tag-annotation-sidebar')) {
canvasInstance.fitCanvas();
canvasInstance.fit();
}
};
const [sidebar] = window.document.getElementsByClassName('cvat-tag-annotation-sidebar');
sidebar.addEventListener('transitionend', listener);
return () => {
sidebar.removeEventListener('transitionend', listener);
};
}, []);
useEffect(() => {
setFrameTags(states.filter(
(objectState: any): boolean => objectState.objectType === ObjectType.TAG,
));
}, [states]);
const siderProps: SiderProps = {
className: 'cvat-tag-annotation-sidebar',
theme: 'light',
width: 300,
collapsedWidth: 0,
reverseArrow: true,
collapsible: true,
trigger: null,
collapsed: sidebarCollapsed,
};
const onChangeLabel = (value: string): void => {
setSelectedLabelID(Number.parseInt(value, 10));
};
const onRemoveState = (objectState: any): void => {
removeObject(jobInstance, objectState);
};
const onChangeFrame = (): void => {
const frame = Math.min(jobInstance.stopFrame, frameNumber + 1);
if (canvasInstance.isAbleToChangeFrame()) {
changeFrame(frame);
}
};
const onAddTag = (labelID: number): void => {
onRememberObject(labelID);
const objectState = new cvat.classes.ObjectState({
objectType: ObjectType.TAG,
label: labels.filter((label: any) => label.id === labelID)[0],
frame: frameNumber,
});
createAnnotations(jobInstance, frameNumber, [objectState]);
if (skipFrame) onChangeFrame();
};
const subKeyMap = {
SWITCH_DRAW_MODE: keyMap.SWITCH_DRAW_MODE,
};
const handlers = {
SWITCH_DRAW_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
onAddTag(selectedLabelID);
},
};
return (
<>
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} allowChanges />
<Layout.Sider {...siderProps}>
{/* eslint-disable-next-line */}
<span
className={`cvat-objects-sidebar-sider
ant-layout-sider-zero-width-trigger
ant-layout-sider-zero-width-trigger-left`}
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
>
{sidebarCollapsed ? <Icon type='menu-fold' title='Show' />
: <Icon type='menu-unfold' title='Hide' />}
</span>
<Row type='flex' justify='start' className='cvat-tag-annotation-sidebar-label-select'>
<Col>
<Text strong>Tag label</Text>
<Select
value={`${selectedLabelID}`}
onChange={onChangeLabel}
size='default'
>
{
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' className='cvat-tag-annotation-sidebar-buttons'>
<Col span={8}>
<Button onClick={() => onAddTag(selectedLabelID)}>Add tag</Button>
</Col>
<Col span={8}>
<Button onClick={onChangeFrame}>Skip frame</Button>
</Col>
</Row>
<Row type='flex' className='cvat-tag-anntation-sidebar-checkbox-skip-frame'>
<Col>
<Checkbox
checked={skipFrame}
onChange={(event: CheckboxChangeEvent): void => {
setSkipFrame(event.target.checked);
}}
>
Automatically go to the next frame
</Checkbox>
</Col>
</Row>
<Row type='flex' justify='start'>
<Col>
<Text strong>Frame tags:&nbsp;</Text>
{frameTags.map((tag: any) => (
<Tag
color={tag.label.color}
onClose={() => { onRemoveState(tag); }}
key={tag.clientID}
closable
>
{tag.label.name}
</Tag>
))}
</Col>
</Row>
<Row>
<Col>
<ShortcutsSelect onAddTag={onAddTag} />
</Col>
</Row>
<Row type='flex' justify='center' className='cvat-tag-annotation-sidebar-shortcut-help'>
<Col>
<Text>
Use&nbsp;
<Text code>N</Text>
&nbsp;or digits&nbsp;
<Text code>0-9</Text>
&nbsp;to add selected tag
<br />
or&nbsp;
<Text code></Text>
&nbsp;to skip frame
</Text>
</Col>
</Row>
</Layout.Sider>
</>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(TagAnnotationSidebar);

@ -0,0 +1,19 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React from 'react';
import Layout from 'antd/lib/layout';
import CanvasWrapperContainer from 'containers/annotation-page/standard-workspace/canvas-wrapper';
import TagAnnotationSidebar from './tag-annotation-sidebar/tag-annotation-sidebar';
export default function TagAnnotationWorkspace(): JSX.Element {
return (
<Layout hasSider className='cvat-tag-annotation-workspace'>
<CanvasWrapperContainer />
<TagAnnotationSidebar />
</Layout>
);
}

@ -48,18 +48,11 @@ function RightGroup(props: Props): JSX.Element {
onChange={changeWorkspace}
value={workspace}
>
<Select.Option
key={Workspace.STANDARD}
value={Workspace.STANDARD}
>
{Workspace.STANDARD}
</Select.Option>
<Select.Option
key={Workspace.ATTRIBUTE_ANNOTATION}
value={Workspace.ATTRIBUTE_ANNOTATION}
>
{Workspace.ATTRIBUTE_ANNOTATION}
</Select.Option>
{Object.values(Workspace).map((ws) => (
<Select.Option key={ws} value={ws} >
{ws}
</Select.Option>
))}
</Select>
</div>
</Col>

@ -403,6 +403,7 @@ export interface AnnotationState {
export enum Workspace {
STANDARD = 'Standard',
ATTRIBUTE_ANNOTATION = 'Attribute annotation',
TAG_ANNOTATION = 'Tag annotation',
}
export enum GridColor {

Loading…
Cancel
Save