diff --git a/CHANGELOG.md b/CHANGELOG.md index 79bdeceb..127dabb6 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 () @@ -33,6 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implemented import and export of annotations with relative image paths () - Using only single click to start editing or remove a point () - Added support for attributes in VOC XML format (https://github.com/opencv/cvat/pull/1792) +- Added annotation attributes in COCO format (https://github.com/opencv/cvat/pull/1782) +- Colorized object items in the side panel () ### Deprecated - @@ -54,7 +57,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A couple of exceptions in AAM related with early object activation () - Propagation from the latest frame () - Number attribute value validation (didn't work well with floats) () -- Fixed Logout function () +- Logout doesn't work () +- Annotations aren't updated after reopening a task () +- Labels aren't updated after reopening a task () +- Canvas isn't fitted after collapsing side panel in attribute annotation mode () ### Security - SQL injection in Django `CVE-2020-9402` () diff --git a/cvat-canvas/webpack.config.js b/cvat-canvas/webpack.config.js index 2420c5e4..96723247 100644 --- a/cvat-canvas/webpack.config.js +++ b/cvat-canvas/webpack.config.js @@ -60,10 +60,12 @@ const webConfig = { target: 'web', mode: 'production', devtool: 'source-map', - entry: './src/typescript/canvas.ts', + entry: { + 'cvat-canvas': './src/typescript/canvas.ts', + }, output: { path: path.resolve(__dirname, 'dist'), - filename: 'cvat-canvas.js', + filename: '[name].[contenthash].js', library: 'canvas', libraryTarget: 'window', }, diff --git a/cvat-core/package.json b/cvat-core/package.json index 29bc84cd..dbff47de 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "3.0.1", + "version": "3.1.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "babel.config.js", "scripts": { diff --git a/cvat-core/src/annotations.js b/cvat-core/src/annotations.js index e2f452a7..9d7eadbf 100644 --- a/cvat-core/src/annotations.js +++ b/cvat-core/src/annotations.js @@ -77,6 +77,15 @@ } } + async function closeSession(session) { + const sessionType = session instanceof Task ? 'task' : 'job'; + const cache = getCache(sessionType); + + if (cache.has(session)) { + cache.delete(session); + } + } + async function getAnnotations(session, frame, allTracks, filters) { const sessionType = session instanceof Task ? 'task' : 'job'; const cache = getCache(sessionType); @@ -365,5 +374,6 @@ redoActions, clearActions, getActions, + closeSession, }; })(); diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 2af76d13..5d5bd7b2 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -1309,6 +1309,21 @@ }; } + /** + * Method removes all task related data from the client (annotations, history, etc.) + * @method close + * @returns {module:API.cvat.classes.Task} + * @memberof module:API.cvat.classes.Task + * @readonly + * @async + * @instance + * @throws {module:API.cvat.exceptions.PluginError} + */ + async close() { + const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.close); + return result; + } + /** * Method updates data of a created task or creates new task from scratch * @method save @@ -1372,6 +1387,7 @@ redoActions, clearActions, getActions, + closeSession, } = require('./annotations'); buildDublicatedAPI(Job.prototype); @@ -1576,6 +1592,15 @@ return result; }; + Task.prototype.close.implementation = function closeTask() { + for (const job of this.jobs) { + closeSession(job); + } + + closeSession(this); + return this; + }; + Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) { // TODO: Add ability to change an owner and an assignee if (typeof (this.id) !== 'undefined') { diff --git a/cvat-core/webpack.config.js b/cvat-core/webpack.config.js index 6e16f46a..99b9e64d 100644 --- a/cvat-core/webpack.config.js +++ b/cvat-core/webpack.config.js @@ -30,10 +30,12 @@ const webConfig = { target: 'web', mode: 'production', devtool: 'source-map', - entry: './src/api.js', + entry: { + 'cvat-core': './src/api.js', + }, output: { path: path.resolve(__dirname, 'dist'), - filename: 'cvat-core.min.js', + filename: '[name].[contenthash].min.js', library: 'cvat', libraryTarget: 'window', }, @@ -58,7 +60,7 @@ const webConfig = { loader: 'worker-loader', options: { publicPath: '/static/engine/js/3rdparty/', - name: '[name].js', + name: '[name].[contenthash].js', }, }, }, { @@ -68,7 +70,7 @@ const webConfig = { loader: 'worker-loader', options: { publicPath: '/static/engine/js/', - name: '[name].js', + name: '[name].[contenthash].js', }, }, }, diff --git a/cvat-data/webpack.config.js b/cvat-data/webpack.config.js index 1b3d09f7..298f6747 100644 --- a/cvat-data/webpack.config.js +++ b/cvat-data/webpack.config.js @@ -9,10 +9,12 @@ const CopyPlugin = require('copy-webpack-plugin'); const cvatData = { target: 'web', mode: 'production', - entry: './src/js/cvat-data.js', + entry: { + 'cvat-data': './src/js/cvat-data.js', + }, output: { path: path.resolve(__dirname, 'dist'), - filename: 'cvat-data.min.js', + filename: '[name].[contenthash].min.js', library: 'cvatData', libraryTarget: 'window', }, @@ -39,7 +41,7 @@ const cvatData = { loader: 'worker-loader', options: { publicPath: '/', - name: '[name].js', + name: '[name].[contenthash].js', }, }, }, { @@ -48,7 +50,7 @@ const cvatData = { loader: 'worker-loader', options: { publicPath: '/3rdparty/', - name: '3rdparty/[name].js', + name: '3rdparty/[name].[contenthash].js', }, }, }, diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index c5330cf4..516c9917 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.4.1", + "version": "1.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 466901f0..157bed2d 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.4.1", + "version": "1.5.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index b669cabf..c373486a 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -906,6 +906,19 @@ export function confirmCanvasReady(): AnyAction { }; } +export function closeJob(): ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + const { jobInstance } = receiveAnnotationsParameters(); + if (jobInstance) { + await jobInstance.task.close(); + } + + dispatch({ + type: AnnotationActionTypes.CLOSE_JOB, + }); + }; +} + export function getJobAsync( tid: number, jid: number, @@ -918,13 +931,6 @@ export function getJobAsync( const filters = initialFilters; const { showAllInterpolationTracks } = state.settings.workspace; - // Check if already loaded job is different from asking one - if (state.annotation.job.instance && state.annotation.job.instance.id !== jid) { - dispatch({ - type: AnnotationActionTypes.CLOSE_JOB, - }); - } - dispatch({ type: AnnotationActionTypes.GET_JOB, payload: { diff --git a/cvat-ui/src/base.scss b/cvat-ui/src/base.scss index eb7119ef..75b561ca 100644 --- a/cvat-ui/src/base.scss +++ b/cvat-ui/src/base.scss @@ -21,7 +21,8 @@ $danger-icon-color: #ff4136; $info-icon-color: #0074d9; $objects-bar-tabs-color: #bebebe; $objects-bar-icons-color: #242424; // #6e6e6e -$active-object-item-background-color: #d8ecff; +$active-label-background-color: #d8ecff; +$object-item-border-color: #000; $slider-color: #1890ff; $monospaced-fonts-stack: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace; 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/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx index 75ed16cb..51ff9b77 100644 --- a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx @@ -14,6 +14,7 @@ import { Row, Col } from 'antd/lib/grid'; import Text from 'antd/lib/typography/Text'; import Icon from 'antd/lib/icon'; +import { Canvas } from 'cvat-canvas-wrapper'; import { LogType } from 'cvat-logger'; import { activateObject as activateObjectAction, @@ -35,6 +36,7 @@ interface StateToProps { jobInstance: any; keyMap: Record; normalizedKeyMap: Record; + canvasInstance: Canvas; canvasIsReady: boolean; } @@ -60,6 +62,7 @@ function mapStateToProps(state: CombinedState): StateToProps { labels, }, canvas: { + instance: canvasInstance, ready: canvasIsReady, }, }, @@ -77,6 +80,7 @@ function mapStateToProps(state: CombinedState): StateToProps { states, keyMap, normalizedKeyMap, + canvasInstance, canvasIsReady, }; } @@ -103,6 +107,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. activateObject, keyMap, normalizedKeyMap, + canvasInstance, canvasIsReady, } = props; @@ -115,6 +120,19 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const collapse = (): void => { + const [collapser] = window.document + .getElementsByClassName('attribute-annotation-sidebar'); + + if (collapser) { + collapser.addEventListener('transitionend', () => { + canvasInstance.fitCanvas(); + }, { once: true }); + } + + setSidebarCollapsed(!sidebarCollapsed); + }; + const [activeObjectState] = activatedStateID === null ? [null] : states.filter((objectState: any): boolean => ( objectState.clientID === activatedStateID @@ -235,7 +253,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. className={`cvat-objects-sidebar-sider ant-layout-sider-zero-width-trigger ant-layout-sider-zero-width-trigger-left`} - onClick={() => setSidebarCollapsed(!sidebarCollapsed)} + onClick={collapse} > {sidebarCollapsed ? : } 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/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index ddcbf7a1..cd982fe0 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -858,7 +858,7 @@ function ObjectItemComponent(props: Props): JSX.Element { : 'cvat-objects-sidebar-state-item cvat-objects-sidebar-state-active-item'; return ( -
+
div:nth-child(1) { > div:nth-child(1) { @@ -218,7 +222,7 @@ } .cvat-objects-sidebar-label-active-item { - background: $active-object-item-background-color; + background: $active-label-background-color; } .cvat-objects-sidebar-label-item { 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/components/header/settings-modal/styles.scss b/cvat-ui/src/components/header/settings-modal/styles.scss index bcc778af..f0d23d20 100644 --- a/cvat-ui/src/components/header/settings-modal/styles.scss +++ b/cvat-ui/src/components/header/settings-modal/styles.scss @@ -111,6 +111,6 @@ } } -.cvat-settings-modal .ant-modal-body{ +.cvat-settings-modal .ant-modal-body { padding-top: 0; -} \ No newline at end of file +} diff --git a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx index 8c75eeff..83c70521 100644 --- a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx @@ -22,6 +22,7 @@ import { searchAnnotationsAsync, changeWorkspace as changeWorkspaceAction, activateObject, + closeJob as closeJobAction, } from 'actions/annotation-actions'; import { Canvas } from 'cvat-canvas-wrapper'; @@ -58,6 +59,7 @@ interface DispatchToProps { redo(sessionInstance: any, frameNumber: any): void; searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void; changeWorkspace(workspace: Workspace): void; + closeJob(): void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -153,6 +155,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { dispatch(activateObject(null, null)); dispatch(changeWorkspaceAction(workspace)); }, + closeJob(): void { + dispatch(closeJobAction()); + }, }; } @@ -177,13 +182,16 @@ class AnnotationTopBarContainer extends React.PureComponent { this.autoSaveInterval = window.setInterval(this.autoSave.bind(this), autoSaveInterval); this.unblock = history.block((location: any) => { - if (jobInstance.annotations.hasUnsavedChanges() && location.pathname !== '/settings' - && location.pathname !== `/tasks/${jobInstance.task.id}/jobs/${jobInstance.id}`) { + const { task, id: jobID } = jobInstance; + const { id: taskID } = task; + + if (jobInstance.annotations.hasUnsavedChanges() + && location.pathname !== `/tasks/${taskID}/jobs/${jobID}`) { return 'You have unsaved changes, please confirm leaving this page.'; } return undefined; }); - this.beforeUnloadCallback = this.beforeUnloadCallback.bind(this); + window.addEventListener('beforeunload', this.beforeUnloadCallback); } @@ -238,9 +246,11 @@ class AnnotationTopBarContainer extends React.PureComponent { } public componentWillUnmount(): void { + const { closeJob } = this.props; window.clearInterval(this.autoSaveInterval); window.removeEventListener('beforeunload', this.beforeUnloadCallback); this.unblock(); + closeJob(); } private undo = (): void => { @@ -443,6 +453,17 @@ class AnnotationTopBarContainer extends React.PureComponent { copy(url); }; + private beforeUnloadCallback = (event: BeforeUnloadEvent): string | undefined => { + const { jobInstance } = this.props; + if (jobInstance.annotations.hasUnsavedChanges()) { + const confirmationMessage = 'You have unsaved changes, please confirm leaving this page.'; + // eslint-disable-next-line no-param-reassign + event.returnValue = confirmationMessage; + return confirmationMessage; + } + return undefined; + }; + private autoSave(): void { const { autoSave, saving } = this.props; @@ -458,16 +479,6 @@ class AnnotationTopBarContainer extends React.PureComponent { } } - private beforeUnloadCallback(event: BeforeUnloadEvent): any { - const { jobInstance } = this.props; - if (jobInstance.annotations.hasUnsavedChanges()) { - const confirmationMessage = 'You have unsaved changes, please confirm leaving this page.'; - // eslint-disable-next-line no-param-reassign - event.returnValue = confirmationMessage; - return confirmationMessage; - } - return undefined; - } public render(): JSX.Element { const { diff --git a/cvat-ui/src/index.html b/cvat-ui/src/index.html index fa8b191f..b115e93b 100644 --- a/cvat-ui/src/index.html +++ b/cvat-ui/src/index.html @@ -15,6 +15,5 @@
- 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 { diff --git a/cvat-ui/webpack.config.js b/cvat-ui/webpack.config.js index 96b0e27d..c7423db8 100644 --- a/cvat-ui/webpack.config.js +++ b/cvat-ui/webpack.config.js @@ -14,10 +14,13 @@ module.exports = { target: 'web', mode: 'production', devtool: 'source-map', - entry: './src/index.tsx', + entry: { + 'cvat-ui': './src/index.tsx', + }, output: { path: path.resolve(__dirname, 'dist'), - filename: 'cvat-ui.min.js', + filename: '[name].[contenthash].min.js', + publicPath: '/', }, devServer: { contentBase: path.join(__dirname, 'dist'), @@ -79,7 +82,7 @@ module.exports = { loader: 'worker-loader', options: { publicPath: '/', - name: '3rdparty/[name].js', + name: '3rdparty/[name].[contenthash].js', }, }, }, { @@ -89,7 +92,7 @@ module.exports = { loader: 'worker-loader', options: { publicPath: '/', - name: '[name].js', + name: '[name].[contenthash].js', }, }, },], @@ -97,7 +100,7 @@ module.exports = { plugins: [ new HtmlWebpackPlugin({ template: "./src/index.html", - inject: false, + inject: 'body', }), new Dotenv({ systemvars: true, diff --git a/cvat/apps/documentation/faq.md b/cvat/apps/documentation/faq.md index 3bdea808..d81902ca 100644 --- a/cvat/apps/documentation/faq.md +++ b/cvat/apps/documentation/faq.md @@ -5,6 +5,10 @@ - [How to configure connected share folder on Windows](#how-to-configure-connected-share-folder-on-windows) - [How to make unassigned tasks not visible to all users](#how-to-make-unassigned-tasks-not-visible-to-all-users) - [Can Nvidia GPU be used to run inference with my own model](#can-nvidia-gpu-be-used-to-run-inference-with-my-own-model) +- [What versions of OpenVINO toolkit are supported](#what-versions-of-openvino-toolkit-are-supported) +- [Where are uploaded images/videos stored](#where-are-uploaded-imagesvideos-stored) +- [Where are annotations stored](#where-are-annotations-stored) + ## How to update CVAT Before upgrading, please follow the official docker @@ -79,3 +83,20 @@ Set [reduce_task_visibility](../../settings/base.py#L424) variable to `True`. Nvidia GPU can be used to accelerate inference of [tf_annotation](../../../components/tf_annotation/README.md) and [auto_segmentation](../../../components/auto_segmentation/README.md) models. OpenVino doesn't support Nvidia cards, so you can run your own models only on CPU. + +## What versions of OpenVINO toolkit are supported +These versions are supported: `2019 R3`, `2019 R3.1`, `2020 1`, `2020 2` + +## Where are uploaded images/videos stored +The uploaded data is stored in the `cvat_data` docker volume: +```yml +volumes: + - cvat_data:/home/django/data +``` + +## Where are annotations stored +Annotations are stored in the PostgreSQL database. The database files are stored in the `cvat_db` docker volume: +```yml +volumes: + - cvat_db:/var/lib/postgresql/data +``` diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index 9dfb5770..fae9a95f 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -8,7 +8,7 @@ django-rq==2.0.0 EasyProcess==0.3 Pillow==7.1.2 numpy==1.18.5 -python-ldap==3.3.0 +python-ldap==3.3.1 pytz==2020.1 pyunpack==0.2.1 rcssmin==1.0.6 diff --git a/cvat/requirements/development.txt b/cvat/requirements/development.txt index 6d8abb5b..b3f84626 100644 --- a/cvat/requirements/development.txt +++ b/cvat/requirements/development.txt @@ -8,6 +8,6 @@ pylint-django==2.0.15 pylint-plugin-utils==0.6 rope==0.17.0 wrapt==1.12.1 -django-extensions==2.2.9 +django-extensions==3.0.1 Werkzeug==1.0.1 snakeviz==2.1.0 diff --git a/datumaro/datumaro/plugins/coco_format/converter.py b/datumaro/datumaro/plugins/coco_format/converter.py index d5247bdb..0e7dd3a4 100644 --- a/datumaro/datumaro/plugins/coco_format/converter.py +++ b/datumaro/datumaro/plugins/coco_format/converter.py @@ -17,7 +17,7 @@ from datumaro.components.extractor import (DEFAULT_SUBSET_NAME, AnnotationType, Points ) from datumaro.components.cli_plugin import CliPlugin -from datumaro.util import find, cast +from datumaro.util import find, cast, str_to_bool from datumaro.util.image import save_image import datumaro.util.mask_tools as mask_tools import datumaro.util.annotation_tools as anno_tools @@ -110,6 +110,12 @@ class _TaskConverter: self._min_ann_id = max(ann_id, self._min_ann_id) return ann_id + @staticmethod + def _convert_attributes(ann): + return { k: v for k, v in ann.attributes.items() + if k not in {'is_crowd', 'score'} + } + class _ImageInfoConverter(_TaskConverter): def is_empty(self): return len(self._data['images']) == 0 @@ -141,6 +147,8 @@ class _CaptionsConverter(_TaskConverter): except Exception as e: log.warning("Item '%s', ann #%s: failed to convert " "attribute 'score': %e" % (item.id, ann_idx, e)) + if self._context._allow_attributes: + elem['attributes'] = self._convert_attributes(ann) self.annotations.append(elem) @@ -312,6 +320,8 @@ class _InstancesConverter(_TaskConverter): except Exception as e: log.warning("Item '%s': failed to convert attribute " "'score': %e" % (item.id, e)) + if self._context._allow_attributes: + elem['attributes'] = self._convert_attributes(ann) return elem @@ -428,6 +438,8 @@ class _LabelsConverter(_TaskConverter): except Exception as e: log.warning("Item '%s': failed to convert attribute " "'score': %e" % (item.id, e)) + if self._context._allow_attributes: + elem['attributes'] = self._convert_attributes(ann) self.annotations.append(elem) @@ -442,7 +454,7 @@ class _Converter: def __init__(self, extractor, save_dir, tasks=None, save_images=False, segmentation_mode=None, - crop_covered=False): + crop_covered=False, allow_attributes=True): assert tasks is None or isinstance(tasks, (CocoTask, list, str)) if tasks is None: tasks = list(self._TASK_CONVERTER) @@ -473,6 +485,7 @@ class _Converter: self._segmentation_mode = segmentation_mode self._crop_covered = crop_covered + self._allow_attributes = allow_attributes self._image_ids = {} @@ -549,25 +562,29 @@ class CocoConverter(Converter, CliPlugin): @classmethod def build_cmdline_parser(cls, **kwargs): + kwargs['description'] = """ + Segmentation save modes:|n + - '{sm.guess.name}': guess the mode for each instance,|n + |s|suse 'is_crowd' attribute as a hint|n + - '{sm.polygons.name}': save polygons,|n + |s|smerge and convert masks, prefer polygons|n + - '{sm.mask.name}': save masks,|n + |s|smerge and convert polygons, prefer masks + """.format(sm=SegmentationMode) parser = super().build_cmdline_parser(**kwargs) + parser.add_argument('--save-images', action='store_true', help="Save images (default: %(default)s)") parser.add_argument('--segmentation-mode', choices=[m.name for m in SegmentationMode], default=SegmentationMode.guess.name, - help=""" - Save mode for instance segmentation:|n - - '{sm.guess.name}': guess the mode for each instance,|n - |s|suse 'is_crowd' attribute as hint|n - - '{sm.polygons.name}': save polygons,|n - |s|smerge and convert masks, prefer polygons|n - - '{sm.mask.name}': save masks,|n - |s|smerge and convert polygons, prefer masks|n - Default: %(default)s. - """.format(sm=SegmentationMode)) + help="Save mode for instance segmentation (default: %(default)s)") parser.add_argument('--crop-covered', action='store_true', help="Crop covered segments so that background objects' " "segmentation was more accurate (default: %(default)s)") + parser.add_argument('--allow-attributes', + type=str_to_bool, default=True, + help="Allow export of attributes (default: %(default)s)") parser.add_argument('--tasks', type=cls._split_tasks_string, default=None, help="COCO task filter, comma-separated list of {%s} " @@ -576,7 +593,7 @@ class CocoConverter(Converter, CliPlugin): def __init__(self, tasks=None, save_images=False, segmentation_mode=None, - crop_covered=False): + crop_covered=False, allow_attributes=True): super().__init__() self._options = { @@ -584,6 +601,7 @@ class CocoConverter(Converter, CliPlugin): 'save_images': save_images, 'segmentation_mode': segmentation_mode, 'crop_covered': crop_covered, + 'allow_attributes': allow_attributes, } def __call__(self, extractor, save_dir): diff --git a/datumaro/datumaro/plugins/coco_format/extractor.py b/datumaro/datumaro/plugins/coco_format/extractor.py index 8ba0d87d..8bb6e464 100644 --- a/datumaro/datumaro/plugins/coco_format/extractor.py +++ b/datumaro/datumaro/plugins/coco_format/extractor.py @@ -145,6 +145,12 @@ class _CocoExtractor(SourceExtractor): ann_id = ann.get('id') attributes = {} + if 'attributes' in ann: + try: + attributes.update(ann['attributes']) + except Exception as e: + log.debug("item #%s: failed to read annotation attributes: %s", + image_info['id'], e) if 'score' in ann: attributes['score'] = ann['score'] diff --git a/datumaro/tests/test_coco_format.py b/datumaro/tests/test_coco_format.py index 7a31180b..08c75d0f 100644 --- a/datumaro/tests/test_coco_format.py +++ b/datumaro/tests/test_coco_format.py @@ -574,3 +574,23 @@ class CocoConverterTest(TestCase): with TestDir() as test_dir: self._test_save_and_load(TestExtractor(), CocoConverter(tasks='image_info', save_images=True), test_dir) + + def test_annotation_attributes(self): + class TestExtractor(Extractor): + def __iter__(self): + return iter([ + DatasetItem(id=1, image=np.ones((4, 2, 3)), annotations=[ + Polygon([0, 0, 4, 0, 4, 4], label=5, group=1, id=1, + attributes={'is_crowd': False, 'x': 5, 'y': 'abc'}), + ], attributes={'id': 1}) + ]) + + def categories(self): + label_categories = LabelCategories() + for i in range(10): + label_categories.add(str(i)) + return { AnnotationType.label: label_categories, } + + with TestDir() as test_dir: + self._test_save_and_load(TestExtractor(), + CocoConverter(), test_dir) \ No newline at end of file