Merge branch 'develop' into develop
commit
681ea2b75a
@ -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