CVAT UI: tag annotation workspace (#1570)
parent
db29291d43
commit
27dc52a513
@ -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: </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
|
||||
<Text code>N</Text>
|
||||
or digits
|
||||
<Text code>0-9</Text>
|
||||
to add selected tag
|
||||
<br />
|
||||
or
|
||||
<Text code>→</Text>
|
||||
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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue