Merge branch 'develop' into develop

main
Nikita Manovich 6 years ago committed by GitHub
commit 681ea2b75a
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>)
@ -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 (<https://github.com/opencv/cvat/pull/1463>)
- Using only single click to start editing or remove a point (<https://github.com/opencv/cvat/pull/1571>)
- 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 (<https://github.com/opencv/cvat/pull/1753>)
### 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 (<https://github.com/opencv/cvat/pull/1755>)
- Propagation from the latest frame (<https://github.com/opencv/cvat/pull/1800>)
- Number attribute value validation (didn't work well with floats) (<https://github.com/opencv/cvat/pull/1800>)
- Fixed Logout function (<https://github.com/opencv/cvat/issues/1810>)
- Logout doesn't work (<https://github.com/opencv/cvat/pull/1812>)
- Annotations aren't updated after reopening a task (<https://github.com/opencv/cvat/pull/1753>)
- Labels aren't updated after reopening a task (<https://github.com/opencv/cvat/pull/1753>)
- Canvas isn't fitted after collapsing side panel in attribute annotation mode (<https://github.com/opencv/cvat/pull/1753>)
### Security
- SQL injection in Django `CVE-2020-9402` (<https://github.com/opencv/cvat/pull/1657>)

@ -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',
},

@ -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": {

@ -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,
};
})();

@ -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') {

@ -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',
},
},
},

@ -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',
},
},
},

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

@ -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": {

@ -906,6 +906,19 @@ export function confirmCanvasReady(): AnyAction {
};
}
export function closeJob(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
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: {

@ -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;

@ -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>
);

@ -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<string, ExtendedKeyMapOptions>;
normalizedKeyMap: Record<string, string>;
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 ? <Icon type='menu-fold' title='Show' />
: <Icon type='menu-unfold' title='Hide' />}

@ -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;
}

@ -858,7 +858,7 @@ function ObjectItemComponent(props: Props): JSX.Element {
: 'cvat-objects-sidebar-state-item cvat-objects-sidebar-state-active-item';
return (
<div style={{ display: 'flex' }}>
<div style={{ display: 'flex', marginBottom: '1px' }}>
<Popover
placement='left'
trigger='click'
@ -872,14 +872,14 @@ function ObjectItemComponent(props: Props): JSX.Element {
>
<div
className='cvat-objects-sidebar-state-item-color'
style={{ background: ` ${color}` }}
style={{ background: `${color}` }}
/>
</Popover>
<div
onMouseEnter={activate}
id={`cvat-objects-sidebar-state-item-${clientID}`}
className={className}
style={{ borderColor: ` ${color}` }}
style={{ backgroundColor: `${color}88` }}
>
<ItemTop
serverID={serverID}

@ -106,11 +106,15 @@
overflow-y: auto;
}
.cvat-objects-sidebar-state-active-item {
background: $active-object-item-background-color;
.cvat-objects-sidebar-state-item.cvat-objects-sidebar-state-active-item {
border-top: 2px solid $object-item-border-color;
border-right: 2px solid $object-item-border-color;
border-bottom: 2px solid $object-item-border-color;
padding: 3px 1px 1px 3px;
}
.cvat-objects-sidebar-state-item-color {
border: 1px solid $object-item-border-color;
width: 7px;
opacity: 1;
@ -122,7 +126,7 @@
.cvat-objects-sidebar-state-item {
width: 100%;
padding: 5px 3px 3px 3px;
border-bottom: 1px dashed;
opacity: 1;
> 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 {

@ -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>

@ -111,6 +111,6 @@
}
}
.cvat-settings-modal .ant-modal-body{
.cvat-settings-modal .ant-modal-body {
padding-top: 0;
}
}

@ -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<Props> {
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<Props> {
}
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<Props> {
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<Props> {
}
}
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 {

@ -15,6 +15,5 @@
<body>
<div id="root"></div>
<script src="/cvat-ui.min.js" type="text/javascript"></script>
</body>
</html>

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

@ -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,

@ -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
```

@ -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

@ -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

@ -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):

@ -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']

@ -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)
Loading…
Cancel
Save