diff --git a/CHANGELOG.md b/CHANGELOG.md index b599a0d9..36f0bc91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 () - Ability to change orientation for poylgons/polylines in context menu () - Ability to set the first point for polygons in points context menu () +- Added new tag annotation workspace () ### Changed - Removed information about e-mail from the basic user information () diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 8f0c1722..516c9917 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.4.2", + "version": "1.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index e965092c..157bed2d 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -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": { diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index 5572ebdd..5f845112 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -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 { - { workspace === Workspace.STANDARD ? ( + { workspace === Workspace.STANDARD && ( - ) : ( + )} + { workspace === Workspace.ATTRIBUTE_ANNOTATION && ( )} + { workspace === Workspace.TAG_ANNOTATION && ( + + + + )} ); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index ff4be1d3..652338cf 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -226,8 +226,8 @@ export default class CanvasWrapperComponent extends React.PureComponent { } 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 { onActivateObject, } = this.props; - if (workspace === Workspace.ATTRIBUTE_ANNOTATION) { + if (workspace !== Workspace.STANDARD) { return; } diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss new file mode 100644 index 00000000..a647412a --- /dev/null +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss @@ -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; + } +} diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/shortcuts-select.tsx b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/shortcuts-select.tsx new file mode 100644 index 00000000..8351a578 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/shortcuts-select.tsx @@ -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 ( +
+ + + + Shortcuts for labels: + + + {shift(Object.keys(shortcutLabelMap), 1).slice(0, Math.min(labels.length, 10)).map((id) => ( + + + {`Key ${id}:`} + + + + ))} +
+ ); +}; + +export default ShortcutsSelect; diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx new file mode 100644 index 00000000..4e6c5ae3 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx @@ -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; + normalizedKeyMap: Record; +} + +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): 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 ( + <> + + + {/* eslint-disable-next-line */} + setSidebarCollapsed(!sidebarCollapsed)} + > + {sidebarCollapsed ? + : } + + + + Tag label + + + + + + + + + + + + + + { + setSkipFrame(event.target.checked); + }} + > + Automatically go to the next frame + + + + + + Frame tags:  + {frameTags.map((tag: any) => ( + { onRemoveState(tag); }} + key={tag.clientID} + closable + > + {tag.label.name} + + ))} + + + + + + + + + + + Use  + N +  or digits  + 0-9 +  to add selected tag +
+ or  + +  to skip frame +
+ +
+
+ + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(TagAnnotationSidebar); diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-workspace.tsx b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-workspace.tsx new file mode 100644 index 00000000..b08d1238 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-workspace.tsx @@ -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 ( + + + + + ); +} diff --git a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx index 36f813eb..ae7f8832 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx @@ -48,18 +48,11 @@ function RightGroup(props: Props): JSX.Element { onChange={changeWorkspace} value={workspace} > - - {Workspace.STANDARD} - - - {Workspace.ATTRIBUTE_ANNOTATION} - + {Object.values(Workspace).map((ws) => ( + + {ws} + + ))} diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 6563d84a..5af1ef99 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -403,6 +403,7 @@ export interface AnnotationState { export enum Workspace { STANDARD = 'Standard', ATTRIBUTE_ANNOTATION = 'Attribute annotation', + TAG_ANNOTATION = 'Tag annotation', } export enum GridColor {