Merge branch 'develop' of https://github.com/opencv/cvat into upstream/develop

main
Maya 6 years ago
commit a46bba2df5

@ -9,7 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Siammask tracker as DL serverless function (<https://github.com/opencv/cvat/pull/1988>)
- [Datumaro] Added model info and source info commands (<https://github.com/opencv/cvat/pull/1973>)
- [Datumaro] Dataset statistics (<https://github.com/opencv/cvat/pull/1668>)
- Ability to change label color in tasks and predefined labels (<https://github.com/opencv/cvat/pull/2014>)
- [Datumaro] Multi-dataset merge (https://github.com/opencv/cvat/pull/1695)
- Link to django admin page from UI (<https://github.com/opencv/cvat/pull/2068>)
- Notification message when users use wrong browser (<https://github.com/opencv/cvat/pull/2070>)
### Changed
- Shape coordinates are rounded to 2 digits in dumped annotations (<https://github.com/opencv/cvat/pull/1970>)
@ -24,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Issue loading openvino models for semi-automatic and automatic annotation (<https://github.com/opencv/cvat/pull/1996>)
- Basic functions of CVAT works without activated nuclio dashboard
- Fixed error with creating task with labels with the same name (<https://github.com/opencv/cvat/pull/2031>)
- Django RQ dashboard view (<https://github.com/opencv/cvat/pull/2069>)
### Security
-

@ -55,7 +55,7 @@ RUN apt-get update && \
curl && \
curl https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \
apt-get --no-install-recommends install -y git-lfs && git lfs install && \
python3 -m pip install --no-cache-dir -U pip==20.0.1 setuptools>=49.1.0 && \
python3 -m pip install --no-cache-dir -U pip==20.0.1 setuptools>=49.1.0 wheel==0.35.1 && \
ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \
add-apt-repository --remove ppa:mc3man/gstffmpeg-keep -y && \

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.3.1",
"version": "3.5.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.4.0",
"version": "3.5.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {

@ -10,7 +10,6 @@
(() => {
const {
AttributeType,
colors,
} = require('./enums');
const { ArgumentError } = require('./exceptions');
@ -150,9 +149,6 @@
}
}
if (typeof (data.id) !== 'undefined') {
data.color = colors[data.id % colors.length];
}
data.attributes = [];
if (Object.prototype.hasOwnProperty.call(initialData, 'attributes')
@ -193,10 +189,10 @@
color: {
get: () => data.color,
set: (color) => {
if (colors.includes(color)) {
if (typeof color === 'string' && color.match(/^#[0-9a-f]{6}$|^$/)) {
data.color = color;
} else {
throw new ArgumentError('Trying to set unknown color');
throw new ArgumentError('Trying to set wrong color format');
}
},
},
@ -217,6 +213,7 @@
const object = {
name: this.name,
attributes: [...this.attributes.map((el) => el.toJSON())],
color: this.color,
};
if (typeof (this.id) !== 'undefined') {

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.7.1",
"version": "1.8.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -1046,6 +1046,11 @@
"integrity": "sha512-opgSsy+cEF9N8MgaVPnWVtdJ3o4mV2aMHvDq7thkQUFt0EuOHJon4rQpJfhjmNHB+ikl0Cd6WhWIErOyQ+f7tw==",
"dev": true
},
"@types/platform": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/platform/-/platform-1.3.2.tgz",
"integrity": "sha512-Tn6OuJDAG7bJbyi4R7HqcxXp1w2lmIxVXqyNhPt1Bm0FO2EWIi3CI87JVzF7ncqK0ZMPuUycS3wTMIk85EeF1Q=="
},
"@types/prop-types": {
"version": "15.7.3",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
@ -12247,6 +12252,7 @@
"convert-source-map": "^1.1.0",
"fs-readdir-recursive": "^1.1.0",
"glob": "^7.0.0",
"lodash": "^4.17.13",
"make-dir": "^2.1.0",
"slash": "^2.0.0",
"source-map": "^0.5.0"
@ -12286,6 +12292,7 @@
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.1",
"json5": "^2.1.0",
"lodash": "^4.17.13",
"resolve": "^1.3.2",
"semver": "^5.4.1",
"source-map": "^0.5.0"
@ -12313,6 +12320,7 @@
"requires": {
"@babel/types": "^7.8.7",
"jsesc": "^2.5.1",
"lodash": "^4.17.13",
"source-map": "^0.5.0"
}
},
@ -12371,7 +12379,8 @@
"integrity": "sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g==",
"requires": {
"@babel/helper-function-name": "^7.8.3",
"@babel/types": "^7.8.3"
"@babel/types": "^7.8.3",
"lodash": "^4.17.13"
}
},
"@babel/helper-explode-assignable-expression": {
@ -12435,7 +12444,8 @@
"@babel/helper-simple-access": "^7.8.3",
"@babel/helper-split-export-declaration": "^7.8.3",
"@babel/template": "^7.8.6",
"@babel/types": "^7.8.6"
"@babel/types": "^7.8.6",
"lodash": "^4.17.13"
}
},
"@babel/helper-optimise-call-expression": {
@ -12454,7 +12464,10 @@
"@babel/helper-regex": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.8.3.tgz",
"integrity": "sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ=="
"integrity": "sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ==",
"requires": {
"lodash": "^4.17.13"
}
},
"@babel/helper-remap-async-to-generator": {
"version": "7.8.3",
@ -12700,7 +12713,8 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz",
"integrity": "sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w==",
"requires": {
"@babel/helper-plugin-utils": "^7.8.3"
"@babel/helper-plugin-utils": "^7.8.3",
"lodash": "^4.17.13"
}
},
"@babel/plugin-transform-classes": {
@ -13038,7 +13052,8 @@
"@babel/parser": "^7.8.6",
"@babel/types": "^7.8.6",
"debug": "^4.1.0",
"globals": "^11.1.0"
"globals": "^11.1.0",
"lodash": "^4.17.13"
},
"dependencies": {
"debug": {
@ -13062,6 +13077,7 @@
"integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==",
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
@ -14212,7 +14228,10 @@
"catharsis": {
"version": "0.8.11",
"resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.11.tgz",
"integrity": "sha512-a+xUyMV7hD1BrDQA/3iPV7oc+6W26BgVJO05PGEoatMyIuPScQKsde6i3YorWX1qs+AZjnJ18NqdKoCtKiNh1g=="
"integrity": "sha512-a+xUyMV7hD1BrDQA/3iPV7oc+6W26BgVJO05PGEoatMyIuPScQKsde6i3YorWX1qs+AZjnJ18NqdKoCtKiNh1g==",
"requires": {
"lodash": "^4.17.14"
}
},
"chalk": {
"version": "2.4.2",
@ -15188,6 +15207,7 @@
"js-yaml": "^3.13.1",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.3.0",
"lodash": "^4.17.14",
"minimatch": "^3.0.4",
"mkdirp": "^0.5.1",
"natural-compare": "^1.4.0",
@ -15559,6 +15579,7 @@
"js-yaml": "^3.5.1",
"json-stable-stringify": "^1.0.0",
"levn": "^0.3.0",
"lodash": "^4.0.0",
"mkdirp": "^0.5.0",
"natural-compare": "^1.4.0",
"optionator": "^0.8.2",
@ -15633,6 +15654,7 @@
"cli-cursor": "^1.0.1",
"cli-width": "^2.0.0",
"figures": "^1.3.5",
"lodash": "^4.3.0",
"readline2": "^1.0.1",
"run-async": "^0.1.0",
"rx-lite": "^3.1.2",
@ -15717,6 +15739,7 @@
"ajv": "^4.7.0",
"ajv-keywords": "^1.0.0",
"chalk": "^1.1.1",
"lodash": "^4.0.0",
"slice-ansi": "0.0.4",
"string-width": "^2.0.0"
},
@ -17168,6 +17191,7 @@
"cli-width": "^2.0.0",
"external-editor": "^3.0.3",
"figures": "^2.0.0",
"lodash": "^4.17.12",
"mute-stream": "0.0.7",
"run-async": "^2.2.0",
"rxjs": "^6.4.0",
@ -18279,6 +18303,11 @@
"path-exists": "^3.0.0"
}
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
@ -19451,7 +19480,10 @@
"request-promise-core": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz",
"integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ=="
"integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==",
"requires": {
"lodash": "^4.17.15"
}
},
"request-promise-native": {
"version": "1.0.8",
@ -19492,7 +19524,10 @@
"requizzle": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz",
"integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ=="
"integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==",
"requires": {
"lodash": "^4.17.14"
}
},
"resolve": {
"version": "1.15.1",
@ -20174,6 +20209,7 @@
"integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==",
"requires": {
"ajv": "^6.10.2",
"lodash": "^4.17.14",
"slice-ansi": "^2.1.0",
"string-width": "^3.0.0"
}
@ -26159,6 +26195,11 @@
"find-up": "^3.0.0"
}
},
"platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
},
"portfinder": {
"version": "1.0.25",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz",

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.7.1",
"version": "1.8.1",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
@ -47,6 +47,7 @@
"worker-loader": "^2.0.0"
},
"dependencies": {
"@types/platform": "^1.3.2",
"@types/react": "^16.9.2",
"@types/react-color": "^3.0.2",
"@types/react-dom": "^16.9.0",
@ -62,6 +63,7 @@
"dotenv-webpack": "^1.7.0",
"error-stack-parser": "^2.0.6",
"moment": "^2.24.0",
"platform": "^1.3.6",
"prop-types": "^15.7.2",
"react": "^16.9.0",
"react-color": "^2.18.1",

@ -148,8 +148,6 @@ export enum AnnotationActionTypes {
GROUP_ANNOTATIONS_FAILED = 'GROUP_ANNOTATIONS_FAILED',
SPLIT_ANNOTATIONS_SUCCESS = 'SPLIT_ANNOTATIONS_SUCCESS',
SPLIT_ANNOTATIONS_FAILED = 'SPLIT_ANNOTATIONS_FAILED',
CHANGE_LABEL_COLOR_SUCCESS = 'CHANGE_LABEL_COLOR_SUCCESS',
CHANGE_LABEL_COLOR_FAILED = 'CHANGE_LABEL_COLOR_FAILED',
UPDATE_TAB_CONTENT_HEIGHT = 'UPDATE_TAB_CONTENT_HEIGHT',
COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR',
COLLAPSE_APPEARANCE = 'COLLAPSE_APPEARANCE',
@ -1288,44 +1286,6 @@ ThunkAction {
};
}
export function changeLabelColorAsync(
label: any,
color: string,
): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const {
filters,
showAllInterpolationTracks,
jobInstance,
frame,
} = receiveAnnotationsParameters();
const updatedLabel = label;
updatedLabel.color = color;
const states = await jobInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const history = await jobInstance.actions.get();
dispatch({
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS,
payload: {
label: updatedLabel,
history,
states,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_FAILED,
payload: {
error,
},
});
}
};
}
export function changeGroupColorAsync(
group: number,
color: string,

@ -344,10 +344,12 @@ function createTask(): AnyAction {
return action;
}
function createTaskSuccess(): AnyAction {
function createTaskSuccess(taskId: number): AnyAction {
const action = {
type: TasksActionTypes.CREATE_TASK_SUCCESS,
payload: {},
payload: {
taskId,
},
};
return action;
@ -434,10 +436,10 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
dispatch(createTask());
try {
await taskInstance.save((status: string): void => {
const savedTask = await taskInstance.save((status: string): void => {
dispatch(createTaskUpdateStatus(status));
});
dispatch(createTaskSuccess());
dispatch(createTaskSuccess(savedTask.id));
} catch (error) {
dispatch(createTaskFailed(error));
}

@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 14 14" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M13.7735 2.04722L11.9536 0.227468C11.6503 -0.0758228 11.1603 -0.0758228 10.857 0.227468L8.43052 2.6538L6.92951 1.16845L5.83292 2.26496L6.93729 3.36925L0 10.3061V14H3.69419L10.6315 7.06318L11.7358 8.16748L12.8324 7.07096L11.3392 5.57784L13.7657 3.15151C14.0768 2.84044 14.0768 2.35051 13.7735 2.04722V2.04722ZM3.04867 12.4447L1.55545 10.9515L7.8239 4.68352L9.31712 6.17664L3.04867 12.4447Z"/>
</svg>

After

Width:  |  Height:  |  Size: 514 B

@ -166,9 +166,9 @@ function AppearanceBlock(props: Props): JSX.Element {
<div className='cvat-objects-appearance-content'>
<Text type='secondary'>Color by</Text>
<Radio.Group value={colorBy} onChange={changeShapesColorBy}>
<Radio.Button value={ColorBy.LABEL}>{ColorBy.LABEL}</Radio.Button>
<Radio.Button value={ColorBy.INSTANCE}>{ColorBy.INSTANCE}</Radio.Button>
<Radio.Button value={ColorBy.GROUP}>{ColorBy.GROUP}</Radio.Button>
<Radio.Button value={ColorBy.LABEL}>{ColorBy.LABEL}</Radio.Button>
</Radio.Group>
<Text type='secondary'>Opacity</Text>
<Slider

@ -1,58 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { Row, Col } from 'antd/lib/grid';
import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text';
interface Props {
shortcut: string;
colors: string[];
onChange(color: string): void;
}
function ColorChanger(props: Props): JSX.Element {
const { shortcut, colors, onChange } = props;
const cols = 6;
const rows = Math.ceil(colors.length / cols);
const antdRows = [];
for (let row = 0; row < rows; row++) {
const antdCols = [];
for (let col = 0; col < cols; col++) {
const idx = row * cols + col;
if (idx >= colors.length) {
break;
}
const color = colors[idx];
antdCols.push(
<Col key={col} span={4}>
<Button
onClick={(): void => onChange(color)}
style={{ background: color }}
className='cvat-label-item-color-button'
/>
</Col>,
);
}
antdRows.push(
// eslint-disable-next-line react/no-children-prop
<Row key={row} children={antdCols} />,
);
}
return (
<div>
<Text>
{`Press ${shortcut} to set a random color`}
</Text>
{antdRows}
</div>
);
}
export default React.memo(ColorChanger);

@ -0,0 +1,131 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState } from 'react';
import { Row, Col } from 'antd/lib/grid';
import Icon from 'antd/lib/icon';
import Button from 'antd/lib/button';
import Popover from 'antd/lib/popover';
import Text from 'antd/lib/typography/Text';
import { SketchPicker } from 'react-color';
import Tooltip from 'antd/lib/tooltip';
import getCore from 'cvat-core-wrapper';
const core = getCore();
interface Props {
children: React.ReactNode;
value?: string;
visible?: boolean;
resetVisible?: boolean;
onChange?: (value: string) => void;
onVisibleChange?: (visible: boolean) => void;
placement?: 'left' | 'top' | 'right' | 'bottom' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' | 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom' | undefined;
}
function ColorPicker(props: Props, ref: React.Ref<any>): JSX.Element {
const {
children,
value,
visible,
resetVisible,
onChange,
onVisibleChange,
placement,
} = props;
const [colorState, setColorState] = useState(value);
const [pickerVisible, setPickerVisible] = useState(false);
const colors = [...core.enums.colors];
const changeVisible = (_visible: boolean): void => {
if (typeof onVisibleChange === 'function') {
onVisibleChange(_visible);
} else {
setPickerVisible(_visible);
}
};
return (
<Popover
content={(
<>
<SketchPicker
color={colorState}
onChange={(color) => setColorState(color.hex)}
presetColors={colors}
ref={ref}
disableAlpha
/>
<Row>
<Col span={9}>
{resetVisible !== false && (
<Button
onClick={() => {
if (typeof onChange === 'function') onChange('');
changeVisible(false);
}}
>
Reset
</Button>
)}
</Col>
<Col span={9}>
<Button
onClick={() => {
changeVisible(false);
}}
>
Cancel
</Button>
</Col>
<Col span={6}>
<Button
type='primary'
onClick={() => {
if (typeof onChange === 'function') onChange(colorState || '');
changeVisible(false);
}}
>
Ok
</Button>
</Col>
</Row>
</>
)}
title={(
<Row type='flex' justify='space-between' align='middle'>
<Col span={12}>
<Text strong>
Select color
</Text>
</Col>
<Col span={4}>
<Tooltip title='Cancel'>
<Button
type='link'
onClick={() => {
changeVisible(false);
}}
>
<Icon type='close' />
</Button>
</Tooltip>
</Col>
</Row>
)}
placement={placement || 'left'}
overlayClassName='cvat-label-color-picker'
trigger='click'
visible={typeof visible === 'boolean' ? visible : pickerVisible}
onVisibleChange={changeVisible}
>
{children}
</Popover>
);
}
export default React.forwardRef(ColorPicker);

@ -5,32 +5,26 @@
import React from 'react';
import { Row, Col } from 'antd/lib/grid';
import Icon from 'antd/lib/icon';
import Popover from 'antd/lib/popover';
import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text';
import ColorChanger from 'components/annotation-page/standard-workspace/objects-side-bar/color-changer';
interface Props {
labelName: string;
labelColor: string;
labelColors: string[];
visible: boolean;
statesHidden: boolean;
statesLocked: boolean;
changeColorShortcut: string;
hideStates(): void;
showStates(): void;
lockStates(): void;
unlockStates(): void;
changeColor(color: string): void;
}
function LabelItemComponent(props: Props): JSX.Element {
const {
labelName,
labelColor,
labelColors,
visible,
statesHidden,
statesLocked,
@ -38,8 +32,6 @@ function LabelItemComponent(props: Props): JSX.Element {
showStates,
lockStates,
unlockStates,
changeColor,
changeColorShortcut,
} = props;
return (
@ -51,19 +43,7 @@ function LabelItemComponent(props: Props): JSX.Element {
style={{ display: visible ? 'flex' : 'none' }}
>
<Col span={4}>
<Popover
placement='left'
trigger='click'
content={(
<ColorChanger
shortcut={changeColorShortcut}
onChange={changeColor}
colors={labelColors}
/>
)}
>
<Button style={{ background: labelColor }} className='cvat-label-item-color-button' />
</Popover>
<Button style={{ background: labelColor }} className='cvat-label-item-color-button' />
</Col>
<Col span={14}>
<Text strong className='cvat-text'>{labelName}</Text>

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import React, { useState } from 'react';
import { Row, Col } from 'antd/lib/grid';
import Icon from 'antd/lib/icon';
import Select, { OptionProps } from 'antd/lib/select';
@ -10,7 +10,7 @@ import Dropdown from 'antd/lib/dropdown';
import Text from 'antd/lib/typography/Text';
import Tooltip from 'antd/lib/tooltip';
import { ObjectType, ShapeType } from 'reducers/interfaces';
import { ObjectType, ShapeType, ColorBy } from 'reducers/interfaces';
import ItemMenu from './object-item-menu';
interface Props {
@ -20,14 +20,18 @@ interface Props {
labels: any[];
shapeType: ShapeType;
objectType: ObjectType;
color: string;
colorBy: ColorBy;
type: string;
locked: boolean;
changeColorShortcut: string;
copyShortcut: string;
pasteShortcut: string;
propagateShortcut: string;
toBackgroundShortcut: string;
toForegroundShortcut: string;
removeShortcut: string;
changeColor(color: string): void;
changeLabel(labelID: string): void;
copy(): void;
remove(): void;
@ -47,14 +51,18 @@ function ItemTopComponent(props: Props): JSX.Element {
labels,
shapeType,
objectType,
color,
colorBy,
type,
locked,
changeColorShortcut,
copyShortcut,
pasteShortcut,
propagateShortcut,
toBackgroundShortcut,
toForegroundShortcut,
removeShortcut,
changeColor,
changeLabel,
copy,
remove,
@ -66,6 +74,21 @@ function ItemTopComponent(props: Props): JSX.Element {
resetCuboidPerspective,
} = props;
const [menuVisible, setMenuVisible] = useState(false);
const [colorPickerVisible, setColorPickerVisible] = useState(false);
const changeMenuVisible = (visible: boolean): void => {
if (!visible && colorPickerVisible) return;
setMenuVisible(visible);
};
const changeColorPickerVisible = (visible: boolean): void => {
if (!visible) {
setMenuVisible(false);
}
setColorPickerVisible(visible);
};
return (
<Row type='flex' align='middle'>
<Col span={10}>
@ -99,18 +122,25 @@ function ItemTopComponent(props: Props): JSX.Element {
</Col>
<Col span={2}>
<Dropdown
visible={menuVisible}
onVisibleChange={changeMenuVisible}
placement='bottomLeft'
overlay={ItemMenu({
serverID,
locked,
shapeType,
objectType,
color,
colorBy,
colorPickerVisible,
changeColorShortcut,
copyShortcut,
pasteShortcut,
propagateShortcut,
toBackgroundShortcut,
toForegroundShortcut,
removeShortcut,
changeColor,
copy,
remove,
propagate,
@ -119,6 +149,7 @@ function ItemTopComponent(props: Props): JSX.Element {
toBackground,
toForeground,
resetCuboidPerspective,
changeColorPickerVisible,
})}
>
<Icon type='more' />

@ -9,20 +9,31 @@ import Button from 'antd/lib/button';
import Modal from 'antd/lib/modal';
import Tooltip from 'antd/lib/tooltip';
import { BackgroundIcon, ForegroundIcon, ResetPerspectiveIcon } from 'icons';
import { ObjectType, ShapeType } from 'reducers/interfaces';
import {
BackgroundIcon,
ForegroundIcon,
ResetPerspectiveIcon,
ColorizeIcon,
} from 'icons';
import { ObjectType, ShapeType, ColorBy } from 'reducers/interfaces';
import ColorPicker from './color-picker';
interface Props {
serverID: number | undefined;
locked: boolean;
shapeType: ShapeType;
objectType: ObjectType;
color: string;
colorBy: ColorBy;
colorPickerVisible: boolean;
changeColorShortcut: string;
copyShortcut: string;
pasteShortcut: string;
propagateShortcut: string;
toBackgroundShortcut: string;
toForegroundShortcut: string;
removeShortcut: string;
changeColor: (value: string) => void;
copy: (() => void);
remove: (() => void);
propagate: (() => void);
@ -31,6 +42,7 @@ interface Props {
toBackground: (() => void);
toForeground: (() => void);
resetCuboidPerspective: (() => void);
changeColorPickerVisible: (visible: boolean) => void;
}
export default function ItemMenu(props: Props): JSX.Element {
@ -39,12 +51,17 @@ export default function ItemMenu(props: Props): JSX.Element {
locked,
shapeType,
objectType,
color,
colorBy,
colorPickerVisible,
changeColorShortcut,
copyShortcut,
pasteShortcut,
propagateShortcut,
toBackgroundShortcut,
toForegroundShortcut,
removeShortcut,
changeColor,
copy,
remove,
propagate,
@ -53,6 +70,7 @@ export default function ItemMenu(props: Props): JSX.Element {
toBackground,
toForeground,
resetCuboidPerspective,
changeColorPickerVisible,
} = props;
return (
@ -111,6 +129,24 @@ export default function ItemMenu(props: Props): JSX.Element {
</Tooltip>
</Menu.Item>
)}
{[ColorBy.INSTANCE, ColorBy.GROUP].includes(colorBy) && (
<Menu.Item>
<ColorPicker
value={color}
onChange={changeColor}
visible={colorPickerVisible}
onVisibleChange={changeColorPickerVisible}
resetVisible={false}
>
<Tooltip title={`${changeColorShortcut}`} mouseLeaveDelay={0}>
<Button type='link'>
<Icon component={ColorizeIcon} />
{`Change ${colorBy.toLowerCase()} color`}
</Button>
</Tooltip>
</ColorPicker>
</Menu.Item>
)}
<Menu.Item>
<Tooltip title={`${removeShortcut}`} mouseLeaveDelay={0}>
<Button

@ -3,11 +3,13 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import Popover from 'antd/lib/popover';
import ObjectButtonsContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-buttons';
import ColorChanger from 'components/annotation-page/standard-workspace/objects-side-bar/color-changer';
import { ObjectType, ShapeType } from 'reducers/interfaces';
import {
ObjectType,
ShapeType,
ColorBy,
} from 'reducers/interfaces';
import ItemDetails, { attrValuesAreEqual } from './object-item-details';
import ItemBasics from './object-item-basics';
@ -23,7 +25,7 @@ interface Props {
locked: boolean;
attrValues: Record<number, string>;
color: string;
colors: string[];
colorBy: ColorBy;
labels: any[];
attributes: any[];
@ -57,6 +59,7 @@ function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean {
&& nextProps.labels === prevProps.labels
&& nextProps.attributes === prevProps.attributes
&& nextProps.normalizedKeyMap === prevProps.normalizedKeyMap
&& nextProps.colorBy === prevProps.colorBy
&& attrValuesAreEqual(nextProps.attrValues, prevProps.attrValues);
}
@ -71,7 +74,7 @@ function ObjectItemComponent(props: Props): JSX.Element {
attrValues,
labelID,
color,
colors,
colorBy,
attributes,
labels,
@ -101,22 +104,10 @@ function ObjectItemComponent(props: Props): JSX.Element {
return (
<div style={{ display: 'flex', marginBottom: '1px' }}>
<Popover
placement='left'
trigger='click'
content={(
<ColorChanger
shortcut={normalizedKeyMap.CHANGE_OBJECT_COLOR}
onChange={changeColor}
colors={colors}
/>
)}
>
<div
className='cvat-objects-sidebar-state-item-color'
style={{ background: `${color}` }}
/>
</Popover>
<div
className='cvat-objects-sidebar-state-item-color'
style={{ background: `${color}` }}
/>
<div
onMouseEnter={activate}
id={`cvat-objects-sidebar-state-item-${clientID}`}
@ -130,6 +121,8 @@ function ObjectItemComponent(props: Props): JSX.Element {
labels={labels}
shapeType={shapeType}
objectType={objectType}
color={color}
colorBy={colorBy}
type={type}
locked={locked}
copyShortcut={normalizedKeyMap.COPY_SHAPE}
@ -138,7 +131,9 @@ function ObjectItemComponent(props: Props): JSX.Element {
toBackgroundShortcut={normalizedKeyMap.TO_BACKGROUND}
toForegroundShortcut={normalizedKeyMap.TO_FOREGROUND}
removeShortcut={normalizedKeyMap.DELETE_OBJECT}
changeColorShortcut={normalizedKeyMap.CHANGE_OBJECT_COLOR}
changeLabel={changeLabel}
changeColor={changeColor}
copy={copy}
remove={remove}
propagate={propagate}

@ -285,3 +285,7 @@
}
}
}
.cvat-label-color-picker .sketch-picker {
box-shadow: unset !important;
}

@ -3,6 +3,8 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import { Row, Col } from 'antd/lib/grid';
import Alert from 'antd/lib/alert';
import Button from 'antd/lib/button';
@ -26,6 +28,7 @@ export interface CreateTaskData {
interface Props {
onCreate: (data: CreateTaskData) => void;
status: string;
taskId: number | null;
installedGit: boolean;
}
@ -49,22 +52,31 @@ const defaultState = {
},
};
export default class CreateTaskContent extends React.PureComponent<Props, State> {
class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps, State> {
private basicConfigurationComponent: any;
private advancedConfigurationComponent: any;
private fileManagerContainer: any;
public constructor(props: Props) {
public constructor(props: Props & RouteComponentProps) {
super(props);
this.state = { ...defaultState };
}
public componentDidUpdate(prevProps: Props): void {
const { status } = this.props;
const { status, history, taskId } = this.props;
if (status === 'CREATED' && prevProps.status !== 'CREATED') {
const btn = (
<Button
onClick={() => history.push(`/tasks/${taskId}`)}
>
Open task
</Button>
);
notification.info({
message: 'The task has been created',
btn,
});
this.basicConfigurationComponent.resetFields();
@ -253,3 +265,5 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
);
}
}
export default withRouter(CreateTaskContent);

@ -16,6 +16,7 @@ interface Props {
onCreate: (data: CreateTaskData) => void;
status: string;
error: string;
taskId: number | null;
installedGit: boolean;
}
@ -23,6 +24,7 @@ export default function CreateTaskPage(props: Props): JSX.Element {
const {
error,
status,
taskId,
onCreate,
installedGit,
} = props;
@ -66,6 +68,7 @@ export default function CreateTaskPage(props: Props): JSX.Element {
<Col md={20} lg={16} xl={14} xxl={9}>
<Text className='cvat-title'>Create a new task</Text>
<CreateTaskContent
taskId={taskId}
status={status}
onCreate={onCreate}
installedGit={installedGit}

@ -10,6 +10,8 @@ import { withRouter, RouteComponentProps } from 'react-router-dom';
import { GlobalHotKeys, ExtendedKeyMapOptions, configure } from 'react-hotkeys';
import Spin from 'antd/lib/spin';
import Layout from 'antd/lib/layout';
import { Row, Col } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text';
import notification from 'antd/lib/notification';
import GlobalErrorBoundary from 'components/global-error-boundary/global-error-boundary';
@ -21,11 +23,13 @@ import ModelsPageContainer from 'containers/models-page/models-page';
import AnnotationPageContainer from 'containers/annotation-page/annotation-page';
import LoginPageContainer from 'containers/login-page/login-page';
import RegisterPageContainer from 'containers/register-page/register-page';
import HeaderContainer from 'containers/header/header';
import Header from 'components/header/header';
import { customWaViewHit } from 'utils/enviroment';
import showPlatformNotification, { stopNotifications, platformInfo } from 'utils/platform-checker';
import getCore from 'cvat-core-wrapper';
import { NotificationsState } from 'reducers/interfaces';
import Modal from 'antd/lib/modal';
interface CVATAppProps {
loadFormats: () => void;
@ -267,12 +271,42 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
},
};
if (showPlatformNotification()) {
stopNotifications(false);
const info = platformInfo();
Modal.warning({
title: 'Unsupported platform detected',
content: (
<>
<Row>
<Col>
<Text>
{`The browser you are using is ${info.name} ${info.version} based on ${info.engine} .`
+ ' CVAT was tested in the latest versions of Chrome and Firefox.'
+ ' We recommend to use Chrome (or another Chromium based browser)'}
</Text>
</Col>
</Row>
<Row>
<Col>
<Text type='secondary'>
{`The operating system is ${info.os}`}
</Text>
</Col>
</Row>
</>
),
onOk: () => stopNotifications(true),
});
}
if (readyForRender) {
if (user) {
return (
<GlobalErrorBoundary>
<Layout>
<HeaderContainer> </HeaderContainer>
<Header />
<Layout.Content style={{ height: '100%' }}>
<ShorcutsDialog />
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers}>

@ -4,8 +4,8 @@
import './styles.scss';
import React from 'react';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { useHistory } from 'react-router';
import { Row, Col } from 'antd/lib/grid';
import Layout from 'antd/lib/layout';
import Icon from 'antd/lib/icon';
@ -15,50 +15,129 @@ import Dropdown from 'antd/lib/dropdown';
import Modal from 'antd/lib/modal';
import Text from 'antd/lib/typography/Text';
import { CVATLogo, AccountIcon } from 'icons';
import getCore from 'cvat-core-wrapper';
import consts from 'consts';
import { CVATLogo, AccountIcon } from 'icons';
import ChangePasswordDialog from 'components/change-password-modal/change-password-modal';
import { switchSettingsDialog as switchSettingsDialogAction } from 'actions/settings-actions';
import { logoutAsync, authActions } from 'actions/auth-actions';
import { SupportedPlugins, CombinedState } from 'reducers/interfaces';
import SettingsModal from './settings-modal/settings-modal';
interface HeaderContainerProps {
onLogout: () => void;
switchSettingsDialog: (show: boolean) => void;
switchChangePasswordDialog: (show: boolean) => void;
logoutFetching: boolean;
changePasswordFetching: boolean;
installedAnalytics: boolean;
serverHost: string;
username: string;
toolName: string;
serverVersion: string;
serverDescription: string;
coreVersion: string;
canvasVersion: string;
uiVersion: string;
const core = getCore();
interface Tool {
name: string;
description: string;
server: {
host: string;
version: string;
};
core: {
version: string;
};
canvas: {
version: string;
};
ui: {
version: string;
};
}
interface StateToProps {
user: any;
tool: Tool;
switchSettingsShortcut: string;
settingsDialogShown: boolean;
changePasswordDialogShown: boolean;
changePasswordFetching: boolean;
logoutFetching: boolean;
installedAnalytics: boolean;
renderChangePasswordItem: boolean;
}
type Props = HeaderContainerProps & RouteComponentProps;
interface DispatchToProps {
onLogout: () => void;
switchSettingsDialog: (show: boolean) => void;
switchChangePasswordDialog: (show: boolean) => void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
auth: {
user,
fetching: logoutFetching,
fetching: changePasswordFetching,
showChangePasswordDialog: changePasswordDialogShown,
allowChangePassword: renderChangePasswordItem,
},
plugins: {
list,
},
about: {
server,
packageVersion,
},
shortcuts: {
normalizedKeyMap,
},
settings: {
showDialog: settingsDialogShown,
},
} = state;
return {
user,
tool: {
name: server.name as string,
description: server.description as string,
server: {
host: core.config.backendAPI.slice(0, -7),
version: server.version as string,
},
canvas: {
version: packageVersion.canvas,
},
core: {
version: packageVersion.core,
},
ui: {
version: packageVersion.ui,
},
},
switchSettingsShortcut: normalizedKeyMap.SWITCH_SETTINGS,
settingsDialogShown,
changePasswordDialogShown,
changePasswordFetching,
logoutFetching,
installedAnalytics: list[SupportedPlugins.ANALYTICS],
renderChangePasswordItem,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onLogout: (): void => dispatch(logoutAsync()),
switchSettingsDialog: (show: boolean): void => dispatch(switchSettingsDialogAction(show)),
switchChangePasswordDialog: (show: boolean): void => (
dispatch(authActions.switchChangePasswordDialog(show))
),
};
}
type Props = StateToProps & DispatchToProps;
function HeaderContainer(props: Props): JSX.Element {
const {
user,
tool,
installedAnalytics,
username,
toolName,
serverHost,
serverVersion,
serverDescription,
coreVersion,
canvasVersion,
uiVersion,
onLogout,
logoutFetching,
changePasswordFetching,
settingsDialogShown,
switchSettingsShortcut,
onLogout,
switchSettingsDialog,
switchChangePasswordDialog,
renderChangePasswordItem,
@ -72,20 +151,22 @@ function HeaderContainer(props: Props): JSX.Element {
GITHUB_URL,
} = consts;
function aboutModal(): void {
const history = useHistory();
function showAboutModal(): void {
Modal.info({
title: `${toolName}`,
title: `${tool.name}`,
content: (
<div>
<p>
{`${serverDescription}`}
{`${tool.description}`}
</p>
<p>
<Text strong>
Server version:
</Text>
<Text type='secondary'>
{` ${serverVersion}`}
{` ${tool.server.version}`}
</Text>
</p>
<p>
@ -93,7 +174,7 @@ function HeaderContainer(props: Props): JSX.Element {
Core version:
</Text>
<Text type='secondary'>
{` ${coreVersion}`}
{` ${tool.core.version}`}
</Text>
</p>
<p>
@ -101,7 +182,7 @@ function HeaderContainer(props: Props): JSX.Element {
Canvas version:
</Text>
<Text type='secondary'>
{` ${canvasVersion}`}
{` ${tool.canvas.version}`}
</Text>
</p>
<p>
@ -109,7 +190,7 @@ function HeaderContainer(props: Props): JSX.Element {
UI version:
</Text>
<Text type='secondary'>
{` ${uiVersion}`}
{` ${tool.ui.version}`}
</Text>
</p>
<Row type='flex' justify='space-around'>
@ -131,16 +212,27 @@ function HeaderContainer(props: Props): JSX.Element {
const menu = (
<Menu className='cvat-header-menu' mode='vertical'>
{user.isStaff && (
<Menu.Item
onClick={(): void => {
// false positive
// eslint-disable-next-line
window.open(`${tool.server.host}/admin`, '_blank');
}}
>
<Icon type='control' />
Admin page
</Menu.Item>
)}
<Menu.Item
title={`Press ${switchSettingsShortcut} to switch`}
onClick={
(): void => switchSettingsDialog(true)
}
onClick={() => switchSettingsDialog(true)}
>
<Icon type='setting' />
Settings
</Menu.Item>
<Menu.Item onClick={() => aboutModal()}>
<Menu.Item onClick={showAboutModal}>
<Icon type='info-circle' />
About
</Menu.Item>
@ -174,8 +266,12 @@ function HeaderContainer(props: Props): JSX.Element {
className='cvat-header-button'
type='link'
value='tasks'
href='/tasks?page=1'
onClick={
(): void => props.history.push('/tasks?page=1')
(event: React.MouseEvent): void => {
event.preventDefault();
history.push('/tasks?page=1');
}
}
>
Tasks
@ -184,8 +280,12 @@ function HeaderContainer(props: Props): JSX.Element {
className='cvat-header-button'
type='link'
value='models'
href='/models'
onClick={
(): void => props.history.push('/models')
(event: React.MouseEvent): void => {
event.preventDefault();
history.push('/models');
}
}
>
Models
@ -195,11 +295,13 @@ function HeaderContainer(props: Props): JSX.Element {
<Button
className='cvat-header-button'
type='link'
href={`${tool.server.host}/analytics/app/kibana`}
onClick={
(): void => {
(event: React.MouseEvent): void => {
event.preventDefault();
// false positive
// eslint-disable-next-line
window.open(`${serverHost}/analytics/app/kibana`, '_blank');
window.open(`${tool.server.host}/analytics/app/kibana`, '_blank');
}
}
>
@ -211,8 +313,10 @@ function HeaderContainer(props: Props): JSX.Element {
<Button
className='cvat-header-button'
type='link'
href={GITHUB_URL}
onClick={
(): void => {
(event: React.MouseEvent): void => {
event.preventDefault();
// false positive
// eslint-disable-next-line security/detect-non-literal-fs-filename
window.open(GITHUB_URL, '_blank');
@ -225,11 +329,13 @@ function HeaderContainer(props: Props): JSX.Element {
<Button
className='cvat-header-button'
type='link'
href={`${tool.server.host}/documentation/user_guide.html`}
onClick={
(): void => {
(event: React.MouseEvent): void => {
event.preventDefault();
// false positive
// eslint-disable-next-line
window.open(`${serverHost}/documentation/user_guide.html`, '_blank')
window.open(`${tool.server.host}/documentation/user_guide.html`, '_blank')
}
}
>
@ -240,7 +346,7 @@ function HeaderContainer(props: Props): JSX.Element {
<span>
<Icon className='cvat-header-account-icon' component={AccountIcon} />
<Text strong>
{username.length > 14 ? `${username.slice(0, 10)} ...` : username}
{user.username.length > 14 ? `${user.username.slice(0, 10)} ...` : user.username}
</Text>
<Icon className='cvat-header-menu-icon' type='caret-down' />
</span>
@ -261,4 +367,20 @@ function HeaderContainer(props: Props): JSX.Element {
);
}
export default withRouter(HeaderContainer);
function propsAreTheSame(prevProps: Props, nextProps: Props): boolean {
let equal = true;
for (const prop in nextProps) {
if (prop in prevProps && (prevProps as any)[prop] !== (nextProps as any)[prop]) {
if (prop !== 'tool') {
equal = false;
}
}
}
return equal;
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(React.memo(HeaderContainer, propsAreTheSame));

@ -12,6 +12,7 @@ export interface Attribute {
export interface Label {
name: string;
color: string;
id: number;
attributes: Attribute[];
}
@ -61,6 +62,11 @@ export function validateParsedLabel(label: Label): void {
+ `Type of label id must be only a number or undefined. Got value ${label.id}`);
}
if (!label.color.match(/^#[0-9a-f]{6}$|^$/)) {
throw new Error(`Label "${label.name}". `
+ `Type of label color must be only a valid color string. Got value ${label.color}`);
}
if (!Array.isArray(label.attributes)) {
throw new Error(`Label "${label.name}". `
+ `attributes must be an array. Got type ${typeof (label.attributes)}`);

@ -8,14 +8,30 @@ import LabelForm from './label-form';
import { Label } from './common';
interface Props {
labelNames: string[];
onCreate: (label: Label | null) => void;
}
export default function ConstructorCreator(props: Props): JSX.Element {
const { onCreate } = props;
function compareProps(prevProps: Props, nextProps: Props): boolean {
if (prevProps.onCreate !== nextProps.onCreate) {
return false;
}
if (!(prevProps.labelNames.length === nextProps.labelNames.length
&& prevProps.labelNames.map((value, index) => value === nextProps.labelNames[index])
.reduce((prevValue, curValue) => prevValue && curValue, true)
)) {
return false;
}
return true;
}
function ConstructorCreator(props: Props): JSX.Element {
const { onCreate, labelNames } = props;
return (
<div className='cvat-label-constructor-creator'>
<LabelForm label={null} onSubmit={onCreate} />
<LabelForm label={null} onSubmit={onCreate} labelNames={labelNames} />
</div>
);
}
export default React.memo(ConstructorCreator, compareProps);

@ -7,6 +7,7 @@ import Icon from 'antd/lib/icon';
import Tooltip from 'antd/lib/tooltip';
import Text from 'antd/lib/typography/Text';
import consts from 'consts';
import { Label } from './common';
interface ConstructorViewerItemProps {
@ -25,7 +26,7 @@ export default function ConstructorViewerItem(props: ConstructorViewerItemProps)
} = props;
return (
<div style={{ background: color }} className='cvat-constructor-viewer-item'>
<div style={{ background: color || consts.NEW_LABEL_COLOR }} className='cvat-constructor-viewer-item'>
<Text>{label.name}</Text>
<Tooltip title='Update attributes' mouseLeaveDelay={0}>
<span

@ -16,27 +16,8 @@ interface ConstructorViewerProps {
onCreate: () => void;
}
const colors = [
'#ff811e', '#9013fe', '#0074d9',
'#549ca4', '#e8c720', '#3d9970',
'#6b2034', '#2c344c', '#2ecc40',
];
let currentColor = 0;
function nextColor(): string {
const color = colors[currentColor];
currentColor += 1;
if (currentColor >= colors.length) {
currentColor = 0;
}
return color;
}
export default function ConstructorViewer(props: ConstructorViewerProps): JSX.Element {
const { onCreate } = props;
currentColor = 0;
const list = [
<Button key='create' type='ghost' onClick={onCreate} className='cvat-constructor-viewer-new-item'>
Add label
@ -49,7 +30,7 @@ export default function ConstructorViewer(props: ConstructorViewerProps): JSX.El
onDelete={props.onDelete}
label={label}
key={label.id}
color={nextColor()}
color={label.color}
/>,
);
}

@ -12,8 +12,12 @@ import Tooltip from 'antd/lib/tooltip';
import Select from 'antd/lib/select';
import Form, { FormComponentProps } from 'antd/lib/form/Form';
import Text from 'antd/lib/typography/Text';
import Badge from 'antd/lib/badge';
import ColorPicker from 'components/annotation-page/standard-workspace/objects-side-bar/color-picker';
import { ColorizeIcon } from 'icons';
import patterns from 'utils/validation-patterns';
import consts from 'consts';
import {
equalArrayHead,
idGenerator,
@ -32,6 +36,7 @@ export enum AttributeType {
type Props = FormComponentProps & {
label: Label | null;
labelNames?: string[];
onSubmit: (label: Label | null) => void;
};
@ -57,6 +62,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
onSubmit({
name: formValues.labelName,
id: label ? label.id : idGenerator(),
color: formValues.labelColor,
attributes: formValues.keys.map((key: number, index: number): Attribute => {
let attrValues = formValues.values[key];
if (!Array.isArray(attrValues)) {
@ -384,6 +390,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
const {
label,
form,
labelNames,
} = this.props;
const value = label ? label.name : '';
const locked = label ? label.id >= 0 : false;
@ -399,6 +406,13 @@ class LabelForm extends React.PureComponent<Props, {}> {
}, {
pattern: patterns.validateAttributeName.pattern,
message: patterns.validateAttributeName.message,
}, {
validator:
async (_rule: any, labelName: string, callback: Function) => {
if (labelNames && labelNames.includes(labelName)) {
callback('Label name must be unique for the task');
}
},
}],
})(<Input disabled={locked} placeholder='Label name' />)}
</Form.Item>
@ -408,9 +422,13 @@ class LabelForm extends React.PureComponent<Props, {}> {
private renderNewAttributeButton(): JSX.Element {
return (
<Col span={3}>
<Col span={6}>
<Form.Item>
<Button type='ghost' onClick={this.addAttribute}>
<Button
type='ghost'
onClick={this.addAttribute}
className='cvat-new-attribute-button'
>
Add an attribute
<Icon type='plus' />
</Button>
@ -482,6 +500,37 @@ class LabelForm extends React.PureComponent<Props, {}> {
);
}
private renderChangeColorButton(): JSX.Element {
const { label, form } = this.props;
return (
<Col span={3}>
<Form.Item>
{
form.getFieldDecorator('labelColor', {
initialValue: (label && label.color) ? label.color : undefined,
})(
<ColorPicker placement='bottom'>
<Tooltip title='Change color of the label'>
<Button
type='default'
className='cvat-change-task-label-color-button'
>
<Badge
className='cvat-change-task-label-color-badge'
color={form.getFieldValue('labelColor') || consts.NEW_LABEL_COLOR}
text={(<Icon component={ColorizeIcon} />)}
/>
</Button>
</Tooltip>
</ColorPicker>,
)
}
</Form.Item>
</Col>
);
}
public render(): JSX.Element {
const {
label,
@ -502,6 +551,8 @@ class LabelForm extends React.PureComponent<Props, {}> {
<Row type='flex' justify='start' align='middle'>
{ this.renderLabelNameInput() }
<Col span={1} />
{ this.renderChangeColorButton() }
<Col span={1} />
{ this.renderNewAttributeButton() }
</Row>
{ attributeItems.length > 0

@ -64,6 +64,7 @@ export default class LabelsEditor
return {
name: label.name,
id: label.id || idGenerator(),
color: label.color,
attributes: label.attributes.map((attr: any): Attribute => (
{
id: attr.id || idGenerator(),
@ -198,6 +199,7 @@ export default class LabelsEditor
return {
name: label.name,
id: label.id < 0 ? undefined : label.id,
color: label.color,
attributes: label.attributes.map((attr: Attribute): any => (
{
name: attr.name,
@ -221,6 +223,7 @@ export default class LabelsEditor
}
public render(): JSX.Element {
const { labels } = this.props;
const {
savedLabels,
unsavedLabels,
@ -319,6 +322,7 @@ export default class LabelsEditor
constructorMode === ConstructorMode.CREATE
&& (
<ConstructorCreator
labelNames={labels.map((l) => l.name)}
onCreate={this.handleCreate}
/>
)

@ -28,6 +28,10 @@ class RawViewer extends React.PureComponent<Props> {
if (!Array.isArray(parsed)) {
callback('Field is expected to be a JSON array');
}
const labelNames = parsed.map((label: Label) => label.name);
if (new Set(labelNames).size !== labelNames.length) {
callback('Label names must be unique for the task');
}
for (const label of parsed) {
try {

@ -87,3 +87,21 @@ textarea.ant-input.cvat-raw-labels-viewer {
.cvat-delete-attribute-button:hover > i {
color: $danger-icon-color;
}
.cvat-new-attribute-button {
width: 100%;
}
.cvat-change-task-label-color-button {
width: 100%;
.ant-badge-status-text {
margin-left: 15px;
}
}
.cvat-change-task-label-color-badge .ant-badge-status-dot {
width: 15px;
height: 15px;
border-radius: unset;
}

@ -39,7 +39,7 @@ function CookieDrawer(): JSX.Element {
This site uses cookies for functionality, analytics, and advertising purposes
as described in our Cookie and Similar Technologies Notice.
To see what cookies we serve and set your preferences, please visit our
<a href='https://www.intel.com/cookies'>Cookie Consent Tool</a>
<a href='https://www.intel.com/cookies'> Cookie Consent Tool</a>
. By continuing to use our website, you agree to our use of cookies.
</Paragraph>
<Button onClick={onClose} size='large' type='primary'>

@ -138,7 +138,7 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(
<Input
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)' }} />}
prefix={<Icon type='user-add' style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
placeholder='First name'
/>,
)}
@ -159,7 +159,7 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(
<Input
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)' }} />}
prefix={<Icon type='user-add' style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
placeholder='Last name'
/>,
)}
@ -181,7 +181,7 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(
<Input
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)' }} />}
prefix={<Icon type='user-add' style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
placeholder='Username'
/>,
)}
@ -205,7 +205,7 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
})(
<Input
autoComplete='email'
prefix={<Icon type='mail' style={{ color: 'rgba(0,0,0,.25)' }} />}
prefix={<Icon type='mail' style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
placeholder='Email address'
/>,
)}
@ -227,7 +227,7 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(<Input.Password
autoComplete='new-password'
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)' }} />}
prefix={<Icon type='lock' style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
placeholder='Password'
/>)}
</Form.Item>
@ -248,7 +248,7 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(<Input.Password
autoComplete='new-password'
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)' }} />}
prefix={<Icon type='lock' style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
placeholder='Confirm password'
/>)}
</Form.Item>
@ -279,7 +279,7 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
target='_blank'
href={userAgreement.url}
>
{userAgreement.displayText}
{` ${userAgreement.displayText}`}
</a>
</Checkbox>,
)}

@ -14,6 +14,7 @@ const GITHUB_IMAGE_URL = 'https://raw.githubusercontent.com/opencv/cvat/develop/
const SHARE_MOUNT_GUIDE_URL = 'https://github.com/opencv/cvat/blob/master/cvat/apps/documentation/installation.md#share-path';
const NUCLIO_GUIDE = 'https://github.com/opencv/cvat/blob/develop/cvat/apps/documentation/installation.md#semi-automatic-and-automatic-annotation';
const CANVAS_BACKGROUND_COLORS = ['#ffffff', '#f1f1f1', '#e5e5e5', '#d8d8d8', '#CCCCCC', '#B3B3B3', '#999999'];
const NEW_LABEL_COLOR = '#b3b3b3';
export default {
UNDEFINED_ATTRIBUTE_VALUE,
@ -27,5 +28,6 @@ export default {
GITHUB_IMAGE_URL,
SHARE_MOUNT_GUIDE_URL,
CANVAS_BACKGROUND_COLORS,
NEW_LABEL_COLOR,
NUCLIO_GUIDE,
};

@ -5,10 +5,7 @@
import React from 'react';
import { connect } from 'react-redux';
import {
changeLabelColorAsync,
updateAnnotationsAsync,
} from 'actions/annotation-actions';
import { updateAnnotationsAsync } from 'actions/annotation-actions';
import LabelItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/label-item';
import { CombinedState } from 'reducers/interfaces';
@ -22,8 +19,6 @@ interface StateToProps {
label: any;
labelName: string;
labelColor: string;
labelColors: string[];
changeColorShortcut: string;
objectStates: any[];
jobInstance: any;
frameNumber: any;
@ -31,7 +26,6 @@ interface StateToProps {
interface DispatchToProps {
updateAnnotations(states: any[]): void;
changeLabelColor(label: any, color: string): void;
}
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
@ -49,10 +43,6 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
number: frameNumber,
},
},
colors: labelColors,
},
shortcuts: {
normalizedKeyMap,
},
} = state;
@ -63,11 +53,9 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
label,
labelColor: label.color,
labelName: label.name,
labelColors,
objectStates,
jobInstance,
frameNumber,
changeColorShortcut: normalizedKeyMap.CHANGE_OBJECT_COLOR,
};
}
@ -76,12 +64,6 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
updateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(states));
},
changeLabelColor(
label: any,
color: string,
): void {
dispatch(changeLabelColorAsync(label, color));
},
};
}
@ -151,15 +133,6 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
this.switchLock(false);
};
private changeColor = (color: string): void => {
const {
changeLabelColor,
label,
} = this.props;
changeLabelColor(label, color);
};
private switchHidden(value: boolean): void {
const {
updateAnnotations,
@ -196,24 +169,19 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
const {
labelName,
labelColor,
labelColors,
changeColorShortcut,
} = this.props;
return (
<LabelItemComponent
labelName={labelName}
labelColor={labelColor}
labelColors={labelColors}
visible={visible}
statesHidden={statesHidden}
statesLocked={statesLocked}
changeColorShortcut={changeColorShortcut}
hideStates={this.hideStates}
showStates={this.showStates}
lockStates={this.lockStates}
unlockStates={this.unlockStates}
changeColor={this.changeColor}
/>
);
}

@ -15,7 +15,6 @@ import {
} from 'reducers/interfaces';
import {
collapseObjectItems,
changeLabelColorAsync,
updateAnnotationsAsync,
changeFrameAsync,
removeObjectAsync,
@ -43,7 +42,6 @@ interface StateToProps {
activated: boolean;
colorBy: ColorBy;
ready: boolean;
colors: string[];
activeControl: ActiveControl;
minZLayer: number;
maxZLayer: number;
@ -58,7 +56,6 @@ interface DispatchToProps {
removeObject: (sessionInstance: any, objectState: any) => void;
copyShape: (objectState: any) => void;
propagateObject: (objectState: any) => void;
changeLabelColor(label: any, color: string): void;
changeGroupColor(group: number, color: string): void;
}
@ -88,7 +85,6 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
ready,
activeControl,
},
colors,
},
settings: {
shapes: {
@ -115,7 +111,6 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
ready,
activeControl,
colorBy,
colors,
jobInstance,
frameNumber,
activated: activatedStateID === own.clientID,
@ -149,12 +144,6 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
propagateObject(objectState: any): void {
dispatch(propagateObjectAction(objectState));
},
changeLabelColor(
label: any,
color: string,
): void {
dispatch(changeLabelColorAsync(label, color));
},
changeGroupColor(group: number, color: string): void {
dispatch(changeGroupColorAsync(group, color));
},
@ -273,7 +262,6 @@ class ObjectItemContainer extends React.PureComponent<Props> {
const {
objectState,
colorBy,
changeLabelColor,
changeGroupColor,
} = this.props;
@ -282,8 +270,6 @@ class ObjectItemContainer extends React.PureComponent<Props> {
this.commit();
} else if (colorBy === ColorBy.GROUP) {
changeGroupColor(objectState.group.id, color);
} else if (colorBy === ColorBy.LABEL) {
changeLabelColor(objectState.label, color);
}
};
@ -375,7 +361,6 @@ class ObjectItemContainer extends React.PureComponent<Props> {
attributes,
activated,
colorBy,
colors,
normalizedKeyMap,
} = this.props;
@ -399,10 +384,10 @@ class ObjectItemContainer extends React.PureComponent<Props> {
attrValues={{ ...objectState.attributes }}
labelID={objectState.label.id}
color={stateColor}
colors={colors}
attributes={attributes}
normalizedKeyMap={normalizedKeyMap}
labels={labels}
colorBy={colorBy}
collapsed={collapsed}
activate={this.activate}
remove={this.remove}

@ -15,7 +15,6 @@ import {
copyShape as copyShapeAction,
propagateObject as propagateObjectAction,
changeGroupColorAsync,
changeLabelColorAsync,
} from 'actions/annotation-actions';
import { Canvas } from 'cvat-canvas-wrapper';
import {
@ -53,7 +52,6 @@ interface DispatchToProps {
propagateObject: (objectState: any) => void;
changeFrame(frame: number): void;
changeGroupColor(group: number, color: string): void;
changeLabelColor(label: any, color: string): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -155,9 +153,6 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
changeGroupColor(group: number, color: string): void {
dispatch(changeGroupColorAsync(group, color));
},
changeLabelColor(label: any, color: string): void {
dispatch(changeLabelColorAsync(label, color));
},
};
}
@ -278,7 +273,6 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
jobInstance,
updateAnnotations,
changeGroupColor,
changeLabelColor,
removeObject,
copyShape,
propagateObject,
@ -399,15 +393,11 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
return;
}
if (colorBy === ColorBy.LABEL) {
const colorID = (colors.indexOf(state.label.color) + 1) % colors.length;
changeLabelColor(state.label, colors[colorID]);
return;
if (colorBy === ColorBy.INSTANCE) {
const colorID = (colors.indexOf(state.color) + 1) % colors.length;
state.color = colors[colorID];
updateAnnotations([state]);
}
const colorID = (colors.indexOf(state.color) + 1) % colors.length;
state.color = colors[colorID];
updateAnnotations([state]);
}
},
TO_BACKGROUND: (event: KeyboardEvent | undefined) => {

@ -10,6 +10,7 @@ import { CreateTaskData } from 'components/create-task-page/create-task-content'
import { createTaskAsync } from 'actions/tasks-actions';
interface StateToProps {
taskId: number | null;
status: string;
error: string;
installedGit: boolean;

@ -1,97 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { connect } from 'react-redux';
import getCore from 'cvat-core-wrapper';
import HeaderComponent from 'components/header/header';
import { SupportedPlugins, CombinedState } from 'reducers/interfaces';
import { logoutAsync, authActions } from 'actions/auth-actions';
import { switchSettingsDialog } from 'actions/settings-actions';
const core = getCore();
interface StateToProps {
logoutFetching: boolean;
installedAnalytics: boolean;
username: string;
toolName: string;
serverHost: string;
serverVersion: string;
serverDescription: string;
coreVersion: string;
canvasVersion: string;
uiVersion: string;
switchSettingsShortcut: string;
settingsDialogShown: boolean;
changePasswordDialogShown: boolean;
changePasswordFetching: boolean;
renderChangePasswordItem: boolean;
}
interface DispatchToProps {
onLogout: () => void;
switchSettingsDialog: (show: boolean) => void;
switchChangePasswordDialog: (show: boolean) => void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
auth: {
fetching: logoutFetching,
fetching: changePasswordFetching,
user: {
username,
},
showChangePasswordDialog: changePasswordDialogShown,
allowChangePassword: renderChangePasswordItem,
},
plugins: {
list,
},
about: {
server,
packageVersion,
},
shortcuts: {
normalizedKeyMap,
},
settings: {
showDialog: settingsDialogShown,
},
} = state;
return {
logoutFetching,
installedAnalytics: list[SupportedPlugins.ANALYTICS],
username,
toolName: server.name as string,
serverHost: core.config.backendAPI.slice(0, -7),
serverDescription: server.description as string,
serverVersion: server.version as string,
coreVersion: packageVersion.core,
canvasVersion: packageVersion.canvas,
uiVersion: packageVersion.ui,
switchSettingsShortcut: normalizedKeyMap.SWITCH_SETTINGS,
settingsDialogShown,
changePasswordFetching,
changePasswordDialogShown,
renderChangePasswordItem,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onLogout: (): void => dispatch(logoutAsync()),
switchSettingsDialog: (show: boolean): void => dispatch(switchSettingsDialog(show)),
switchChangePasswordDialog: (show: boolean): void => (
dispatch(authActions.switchChangePasswordDialog(show))
),
};
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(HeaderComponent);

@ -41,6 +41,7 @@ import SVGBackgroundIcon from './assets/background-icon.svg';
import SVGForegroundIcon from './assets/foreground-icon.svg';
import SVGCubeIcon from './assets/cube-icon.svg';
import SVGResetPerspectiveIcon from './assets/reset-perspective.svg';
import SVGColorizeIcon from './assets/colorize-icon.svg';
export const CVATLogo = React.memo(
(): JSX.Element => <SVGCVATLogo />,
@ -153,3 +154,6 @@ export const CubeIcon = React.memo(
export const ResetPerspectiveIcon = React.memo(
(): JSX.Element => <SVGResetPerspectiveIcon />,
);
export const ColorizeIcon = React.memo(
(): JSX.Element => <SVGColorizeIcon />,
);

@ -634,31 +634,6 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS: {
const {
label,
states,
history,
} = action.payload;
const { instance: job } = state.job;
const labels = [...job.task.labels];
const index = labels.indexOf(label);
labels[index] = label;
return {
...state,
job: {
...state.job,
labels,
},
annotations: {
...state.annotations,
states,
history,
},
};
}
case AnnotationActionTypes.ACTIVATE_OBJECT: {
const {
activatedStateID,

@ -60,6 +60,7 @@ export interface TasksState {
[tid: number]: boolean; // deleted (deleting if in dictionary)
};
creates: {
taskId: number | null;
status: string;
error: string;
};

@ -555,21 +555,6 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case AnnotationActionTypes.CHANGE_LABEL_COLOR_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
changingLabelColor: {
message: 'Could not change label color',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED: {
return {
...state,

@ -18,7 +18,7 @@ import {
const defaultState: SettingsState = {
shapes: {
colorBy: ColorBy.INSTANCE,
colorBy: ColorBy.LABEL,
opacity: 3,
selectedOpacity: 30,
blackBorders: false,

@ -31,6 +31,7 @@ const defaultState: TasksState = {
loads: {},
deletes: {},
creates: {
taskId: null,
status: '',
error: '',
},
@ -238,6 +239,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
activities: {
...state.activities,
creates: {
taskId: null,
status: '',
error: '',
},
@ -259,12 +261,14 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
};
}
case TasksActionTypes.CREATE_TASK_SUCCESS: {
const { taskId } = action.payload;
return {
...state,
activities: {
...state.activities,
creates: {
...state.activities.creates,
taskId,
status: 'CREATED',
},
},

@ -0,0 +1,40 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import platform from 'platform';
const engine = platform.layout || 'unknown';
const name = platform.name || 'unknown';
const version = platform.version || 'unknown';
const os = platform.os ? platform.os.toString() : 'unknown';
let platformNotificationShown = window.localStorage.getItem('platformNotiticationShown') !== null;
export function platformInfo(): {
engine: string;
name: string;
version: string;
os: string;
} {
return {
engine,
name,
version,
os,
};
}
export function stopNotifications(saveInStorage: boolean): void {
platformNotificationShown = true;
if (saveInStorage) {
window.localStorage.setItem('platformNotiticationShown', 'shown');
}
}
export default function showPlatformNotification(): boolean {
// Blick is engine of Chrome, Microsoft Edge >= v79
// Gecko is engine of Firefox, supported but works worse than in Chrome (let's show the message)
// WebKit is engine of Apple Safary, not supported
const unsupportedPlatform = !['Blink'].includes(engine);
return unsupportedPlatform && !platformNotificationShown;
}

@ -152,6 +152,7 @@ class TaskData:
("labels", [
("label", OrderedDict([
("name", db_label.name),
("color", db_label.color),
("attributes", [
("attribute", OrderedDict([
("name", db_attr.name),

@ -2,7 +2,6 @@
#
# SPDX-License-Identifier: MIT
import os.path as osp
from tempfile import TemporaryDirectory
from pyunpack import Archive
@ -10,11 +9,10 @@ from pyunpack import Archive
from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor,
import_dm_annotations)
from cvat.apps.dataset_manager.util import make_zip_archive
from datumaro.cli.util import make_file_name
from datumaro.components.project import Dataset
from datumaro.util.mask_tools import generate_colormap
from .registry import dm_env, exporter, importer
from .utils import make_colormap
@exporter(name='Segmentation mask', ext='ZIP', version='1.1')
@ -41,44 +39,3 @@ def _import(src_file, task_data):
masks_to_polygons = dm_env.transforms.get('masks_to_polygons')
dataset = dataset.transform(masks_to_polygons)
import_dm_annotations(dataset, task_data)
DEFAULT_COLORMAP_CAPACITY = 2000
DEFAULT_COLORMAP_PATH = osp.join(osp.dirname(__file__), 'predefined_colors.txt')
def parse_default_colors(file_path=None):
if file_path is None:
file_path = DEFAULT_COLORMAP_PATH
colors = {}
with open(file_path) as f:
for line in f:
line = line.strip()
if not line or line[0] == '#':
continue
_, label, color = line.split(':')
colors[label] = tuple(map(int, color.split(',')))
return colors
def normalize_label(label):
label = make_file_name(label) # basically, convert to ASCII lowercase
label = label.replace('-', '_')
return label
def make_colormap(task_data):
labels = sorted([label['name']
for _, label in task_data.meta['task']['labels']])
if 'background' in labels:
labels.remove('background')
labels.insert(0, 'background')
predefined = parse_default_colors()
# NOTE: using pop() to avoid collisions
colormap = {k: predefined.pop(normalize_label(k), None) for k in labels}
random_labels = [k for k in labels if not colormap[k]]
if random_labels:
colors = generate_colormap(DEFAULT_COLORMAP_CAPACITY + len(random_labels))
for i, label in enumerate(random_labels):
colormap[label] = colors[DEFAULT_COLORMAP_CAPACITY + i]
return {l: [c, [], []] for l, c in colormap.items()}

@ -0,0 +1,79 @@
# Copyright (C) 2019 Intel Corporation
#
# SPDX-License-Identifier: MIT
import os.path as osp
from pyhash import murmur3_32
from datumaro.cli.util import make_file_name
hasher = murmur3_32()
def get_color_from_index(index):
def get_bit(number, index):
return (number >> index) & 1
color = [0, 0, 0]
for j in range(7, -1, -1):
for c in range(3):
color[c] |= get_bit(index, c) << j
index >>= 3
return tuple(color)
DEFAULT_COLORMAP_CAPACITY = 2000
DEFAULT_COLORMAP_PATH = osp.join(osp.dirname(__file__), 'predefined_colors.txt')
def parse_default_colors(file_path=None):
if file_path is None:
file_path = DEFAULT_COLORMAP_PATH
colors = {}
with open(file_path) as f:
for line in f:
line = line.strip()
if not line or line[0] == '#':
continue
_, label, color = line.split(':')
colors[label] = tuple(map(int, color.split(',')))
return colors
def normalize_label(label):
label = make_file_name(label) # basically, convert to ASCII lowercase
label = label.replace('-', '_')
return label
def rgb2hex(color):
return '#{0:02x}{1:02x}{2:02x}'.format(*color)
def hex2rgb(color):
return tuple(int(color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
def make_colormap(task_data):
labels = [label for _, label in task_data.meta['task']['labels']]
label_names = [label['name'] for label in labels]
if 'background' not in label_names:
labels.insert(0, {
'name': 'background',
'color': '#000000',
}
)
return {label['name']: [hex2rgb(label['color']), [], []] for label in labels}
def get_label_color(label_name, label_names):
predefined = parse_default_colors()
normalized_names = [normalize_label(l_name) for l_name in label_names]
normalized_name = normalize_label(label_name)
color = predefined.get(normalized_name, None)
offset = hasher(normalized_name) + normalized_names.count(normalized_name)
if color is None:
color = get_color_from_index(DEFAULT_COLORMAP_CAPACITY + offset)
elif normalized_names.count(normalized_name):
color = get_color_from_index(DEFAULT_COLORMAP_CAPACITY + offset - 1)
return rgb2hex(color)

@ -569,26 +569,13 @@ server {
proxy_set_header Host $http_host;
proxy_pass_header Set-Cookie;
location ~* /api/.*|git/.*|analytics/.*|static/.*|admin|admin/.*|documentation/.* {
location ~* /api/.*|git/.*|analytics/.*|static/.*|admin(?:/(.*))?.*|documentation/.*|django-rq(?:/(.*))? {
proxy_pass http://cvat:8080;
}
# workaround for match location by arguments
location = / {
error_page 418 = @annotation_ui;
if ( $query_string ~ "^id=\d+.*" ) { return 418; }
proxy_pass http://cvat_ui;
}
location / {
proxy_pass http://cvat_ui;
}
# old annotation ui, will be removed in the future.
location @annotation_ui {
proxy_pass http://cvat:8080;
}
}
```

@ -0,0 +1,33 @@
# Generated by Django 2.2.13 on 2020-08-11 11:26
from django.db import migrations, models
from cvat.apps.dataset_manager.formats.utils import get_label_color
def alter_label_colors(apps, schema_editor):
Label = apps.get_model('engine', 'Label')
Task = apps.get_model('engine', 'Task')
for task in Task.objects.all():
labels = Label.objects.filter(task_id=task.id).order_by('id')
label_names = list()
for label in labels:
label.color = get_label_color(label.name, label_names)
label_names.append(label.name)
label.save()
class Migration(migrations.Migration):
dependencies = [
('engine', '0027_auto_20200719_1552'),
]
operations = [
migrations.AddField(
model_name='label',
name='color',
field=models.CharField(default='', max_length=8),
),
migrations.RunPython(
code=alter_label_colors,
reverse_code=migrations.RunPython.noop,
),
]

@ -258,6 +258,7 @@ class Job(models.Model):
class Label(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE)
name = SafeCharField(max_length=64)
color = models.CharField(default='', max_length=8)
def __str__(self):
return self.name

@ -11,6 +11,7 @@ from django.contrib.auth.models import User, Group
from cvat.apps.engine import models
from cvat.apps.engine.log import slogger
from cvat.apps.dataset_manager.formats.utils import get_label_color
class AttributeSerializer(serializers.ModelSerializer):
@ -37,9 +38,11 @@ class AttributeSerializer(serializers.ModelSerializer):
class LabelSerializer(serializers.ModelSerializer):
attributes = AttributeSerializer(many=True, source='attributespec_set',
default=[])
color = serializers.CharField(allow_blank=True, required=False)
class Meta:
model = models.Label
fields = ('id', 'name', 'attributes')
fields = ('id', 'name', 'color', 'attributes')
class JobCommitSerializer(serializers.ModelSerializer):
class Meta:
@ -252,8 +255,12 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
def create(self, validated_data):
labels = validated_data.pop('label_set')
db_task = models.Task.objects.create(**validated_data)
label_names = list()
for label in labels:
attributes = label.pop('attributespec_set')
if not label.get('color', None):
label['color'] = get_label_color(label['name'], label_names)
label_names.append(label['name'])
db_label = models.Label.objects.create(task=db_task, **label)
for attr in attributes:
models.AttributeSpec.objects.create(label=db_label, **attr)
@ -288,6 +295,14 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
else:
slogger.task[instance.id].info("{} label was updated"
.format(db_label.name))
if not label.get('color', None):
label_names = [l.name for l in
models.Label.objects.filter(task_id=instance.id).exclude(id=db_label.id).order_by('id')
]
db_label.color = get_label_color(db_label.name, label_names)
else:
db_label.color = label.get('color', db_label.color)
db_label.save()
for attr in attributes:
(db_attr, created) = models.AttributeSpec.objects.get_or_create(
label=db_label, name=attr['name'], defaults=attr)
@ -308,6 +323,15 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
instance.save()
return instance
def validate_labels(self, value):
if not value:
raise serializers.ValidationError('Label set must not be empty')
label_names = [label['name'] for label in value]
if len(label_names) != len(set(label_names)):
raise serializers.ValidationError('All label names must be unique for the task')
return value
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = models.Project

@ -10,12 +10,13 @@ Pillow==7.2.0
numpy==1.18.5
python-ldap==3.3.1
pytz==2020.1
pyhash==0.9.3
pyunpack==0.2.1
rcssmin==1.0.6
redis==3.5.3
rjsmin==1.1.0
requests==2.24.0
rq==1.4.2
rq==1.5.1
rq-scheduler==0.10.0
scipy==1.4.1
sqlparse==0.3.1
@ -31,7 +32,7 @@ Markdown==3.2.2
djangorestframework==3.11.1
Pygments==2.6.1
drf-yasg==1.17.1
Shapely==1.7.0
Shapely==1.7.1
pdf2image==1.13.1
pascal_voc_writer==0.1.4
django-rest-auth[with_social]==0.9.5

@ -3,7 +3,7 @@ astroid==2.4.2
isort==4.3.21
lazy-object-proxy==1.5.1
mccabe==0.6.1
pylint==2.5.3
pylint==2.6.0
pylint-django==2.3.0
pylint-plugin-utils==0.6
rope==0.17.0

@ -1,5 +1,5 @@
-r development.txt
fakeredis==1.4.2
fakeredis==1.4.3
# Fix dependencies for fakeredis 1.1.0
# Pip will not reinstall six package if it is installed already
six==1.15.0

@ -12,24 +12,11 @@ server {
proxy_set_header Host $http_host;
proxy_pass_header Set-Cookie;
location ~* /api/.*|git/.*|analytics/.*|static/.*|admin|admin/.*|documentation/.* {
location ~* /api/.*|git/.*|analytics/.*|static/.*|admin(?:/(.*))?.*|documentation/.*|django-rq(?:/(.*))? {
proxy_pass http://cvat:8080;
}
# workaround for match location by arguments
location = / {
error_page 418 = @annotation_ui;
if ( $query_string ~ "^id=\d+.*" ) { return 418; }
proxy_pass http://cvat_ui;
}
location / {
proxy_pass http://cvat_ui;
}
# old annotation ui, will be removed in the future.
location @annotation_ui {
proxy_pass http://cvat:8080;
}
}

@ -129,7 +129,7 @@ Plugins reside in plugin directories:
- `<project_dir>/.datumaro/plugins` for project-specific components
A plugin is a python file or package with any name, which exports some symbols.
To export a symbol put it to `exports` list of the module like this:
To export a symbol, put it to `exports` list of the module like this:
``` python
class MyComponent1: ...

@ -1,13 +1,13 @@
# Dataset Framework (Datumaro)
# Dataset Management Framework (Datumaro)
A framework to build, transform, and analyze datasets.
<!--lint disable fenced-code-flag-->
```
CVAT annotations -- ---> Annotation tool
... \ /
\ /
COCO-like dataset -----> Datumaro ---> dataset ------> Model training
... / \
/ \
VOC-like dataset -- ---> Publication etc.
```
<!--lint enable fenced-code-flag-->
@ -55,12 +55,12 @@ VOC-like dataset -- ---> Publication etc.
- Dataset building operations:
- Merging multiple datasets into one
- Dataset filtering with custom conditions, for instance:
- remove all annotations except polygons of a certain class
- remove polygons of a certain class
- remove images without a specific class
- remove occluded annotations from images
- remove `occluded` annotations from images
- keep only vertically-oriented images
- remove small area bounding boxes from annotations
- Annotation conversions, for instance
- Annotation conversions, for instance:
- polygons to instance masks and vise-versa
- apply a custom colormap for mask annotations
- rename or remove dataset labels

@ -579,7 +579,7 @@ def build_transform_parser(parser_ctor=argparse.ArgumentParser):
|n
Examples:|n
- Convert instance polygons to masks:|n
|s|stransform -n polygons_to_masks
|s|stransform -t polygons_to_masks
""" % ', '.join(builtins),
formatter_class=MultilineFormatter)

@ -409,6 +409,13 @@ class Rename(Transform, CliPlugin):
.format(item=item))
class RemapLabels(Transform, CliPlugin):
"""
Changes labels in the dataset.|n
Examples:|n
- Rename 'person' to 'car' and 'cat' to 'dog', keep 'bus', remove others:|n
|s|sremap_labels -l person:car -l bus:bus -l cat:dog --default delete
"""
DefaultAction = Enum('DefaultAction', ['keep', 'delete'])
@staticmethod
@ -428,7 +435,7 @@ class RemapLabels(Transform, CliPlugin):
parser.add_argument('--default',
choices=[a.name for a in cls.DefaultAction],
default=cls.DefaultAction.keep.name,
help="Action for unspecified labels")
help="Action for unspecified labels (default: %(default)s)")
return parser
def __init__(self, extractor, mapping, default=None):

@ -49,6 +49,8 @@ Datumaro is:
- Versioning (for images, annotations, subsets, sources etc., comparison)
- Documentation generation
- Provision of iterators for user code
- Dataset downloading
- Dataset generation
- Dataset building (export in a specific format, indexation, statistics, documentation)
- Dataset exporting to other formats
- Dataset debugging (run inference, generate dataset slices, compute statistics)
@ -111,17 +113,17 @@ can be downloaded by user to be operated on with Datumaro CLI.
- [ ] with TensorBoard
- Calculation of statistics for datasets
- [ ] Pixel mean, std
- [ ] Object counts (detection scenario)
- [ ] Image-Class distribution (classification scenario)
- [ ] Pixel-Class distribution (segmentation scenario)
- [ ] Image clusters
- [x] Pixel mean, std
- [x] Object counts (detection scenario)
- [x] Image-Class distribution (classification scenario)
- [x] Pixel-Class distribution (segmentation scenario)
- [ ] Image similarity clusters
- [ ] Custom statistics
- Dataset building
- [x] Composite dataset building
- [ ] Annotation remapping
- [ ] Subset splitting
- [x] Class remapping
- [x] Subset splitting
- [x] Dataset filtering (`extract`)
- [x] Dataset merging (`merge`)
- [ ] Dataset item editing (`edit`)
@ -129,7 +131,7 @@ can be downloaded by user to be operated on with Datumaro CLI.
- Dataset comparison (`diff`)
- [x] Annotation-annotation comparison
- [x] Annotation-inference comparison
- [ ] Annotation quality estimation (for CVAT)
- [x] Annotation quality estimation (for CVAT)
- Provide a simple method to check
annotation quality with a model and generate summary
@ -142,9 +144,9 @@ can be downloaded by user to be operated on with Datumaro CLI.
- [x] Task export
- [x] Datumaro project export
- [x] Dataset export
- [ ] Original raw data (images, a video file) can be downloaded (exported)
- [x] Original raw data (images, a video file) can be downloaded (exported)
together with annotations or just have links
on CVAT server (in the future support S3, etc)
on CVAT server (in future, support S3, etc)
- [x] Be able to use local files instead of remote links
- [ ] Specify cache directory
- [x] Use case "annotate for model training"
@ -154,7 +156,7 @@ can be downloaded by user to be operated on with Datumaro CLI.
- convert to a training format
- train a DL model
- [x] Use case "annotate - reannotate problematic images - merge"
- [ ] Use case "annotate and estimate quality"
- [x] Use case "annotate and estimate quality"
- create a task
- annotate
- estimate quality of annotations

@ -1,22 +1,28 @@
# Quick start guide
# User manual
## Contents
- [Installation](#installation)
- [Interfaces](#interfaces)
- [Supported dataset formats and annotations](#formats-support)
- [Supported dataset formats and annotations](#supported-formats)
- [Command line workflow](#command-line-workflow)
- [Command reference](#command-reference)
- [Convert datasets](#convert-datasets)
- [Create a project](#create-project)
- [Add and remove data](#add-and-remove-data)
- [Import a project](#import-project)
- [Extract a subproject](#extract-subproject)
- [Merge projects](#merge-project)
- [Update project (merge)](#update-project)
- [Merge projects](#merge-projects)
- [Export a project](#export-project)
- [Compare projects](#compare-projects)
- [Get project info](#get-project-info)
- [Obtaining project info](#get-project-info)
- [Obtaining project statistics](#get-project-statistics)
- [Register a model](#register-model)
- [Run inference](#run-inference)
- [Run inference explanation](#explain-inference)
- [Transform a project](#transform-project)
- [Extending](#extending)
- [Links](#links)
## Installation
@ -36,7 +42,7 @@ python -m virtualenv venv
. venv/bin/activate
```
Install Datumaro:
Install:
``` bash
pip install 'git+https://github.com/opencv/cvat#egg=datumaro&subdirectory=datumaro'
```
@ -68,57 +74,93 @@ As a python library:
import datumaro
```
## Formats support
## Supported Formats
List of supported formats:
- COCO (`image_info`, `instances`, `person_keypoints`, `captions`, `labels`*)
- MS COCO (`image_info`, `instances`, `person_keypoints`, `captions`, `labels`*)
- [Format specification](http://cocodataset.org/#format-data)
- [Dataset example](../tests/assets/coco_dataset)
- `labels` are our extension - like `instances` with only `category_id`
- PASCAL VOC (`classification`, `detection`, `segmentation` (class, instances), `action_classification`, `person_layout`)
- [Format specification](http://host.robots.ox.ac.uk/pascal/VOC/voc2012/htmldoc/index.html)
- [Dataset example](../tests/assets/voc_dataset)
- YOLO (`bboxes`)
- [Format specification](https://github.com/AlexeyAB/darknet#how-to-train-pascal-voc-data)
- [Dataset example](../tests/assets/yolo_dataset)
- TF Detection API (`bboxes`, `masks`)
- Format specifications: [bboxes](https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/using_your_own_dataset.md), [masks](https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/instance_segmentation.md)
- [Dataset example](../tests/assets/tf_detection_api_dataset)
- MOT sequences
- [Format specification](https://arxiv.org/pdf/1906.04567.pdf)
- [Dataset example](../tests/assets/mot_dataset)
- CVAT
- [Format specification](https://github.com/opencv/cvat/blob/develop/cvat/apps/documentation/xml_format.md)
- [Dataset example](../tests/assets/cvat_dataset)
- LabelMe
- [Format specification](http://labelme.csail.mit.edu/Release3.0)
- [Dataset example](../tests/assets/labelme_dataset)
List of supported annotation types:
- Labels
- Bounding boxes
- Polygons
- Polylines
- (Segmentation) Masks
- (Key-)Points
- Captions
- Masks
## Command line workflow
> **Note**: command invocation syntax is subject to change,
> **always refer to command --help output**
The key object is the Project. The Project is a combination of
a Project's own dataset, a number of external data sources and an environment.
The key object is a project, so most CLI commands operate on projects. However, there
are few commands operating on datasets directly. A project is a combination of
a project's own dataset, a number of external data sources and an environment.
An empty Project can be created by `project create` command,
an existing dataset can be imported with `project import` command.
A typical way to obtain projects is to export tasks in CVAT UI.
If you want to interact with models, you need to add them to project first.
## Command reference
> **Note**: command invocation syntax is subject to change,
> **always refer to command --help output**
Available CLI commands:
![CLI design doc](images/cli_design.png)
If you want to interact with models, you need to add them to project first.
### Convert datasets
This command allows to convert a dataset from one format into another. In fact, this
command is a combination of `project import` and `project export` and just provides a simpler
way to obtain the same result when no extra options is needed. A list of supported
formats can be found in the `--help` output of this command.
Usage:
``` bash
datum convert --help
datum convert \
-i <input path> \
-if <input format> \
-o <output path> \
-f <output format> \
-- [extra parameters for output format]
```
Example: convert a VOC-like dataset to a COCO-like one:
``` bash
datum convert --input-format voc --input-path <path/to/voc/> \
--output-format coco
```
### Import project
This command creates a Project from an existing dataset.
Supported formats are listed in the command help.
In Datumaro dataset formats are supported by Extractors and Importers.
An Extractor produces a list of dataset items corresponding
to the dataset. An Importer creates a Project from the
data source location. It is possible to add a custom Extractor and Importer.
To do this, you need to put an Extractor and Importer implementation scripts to
`<project_dir>/.datumaro/extractors` and `<project_dir>/.datumaro/importers`.
Supported formats are listed in the command help. Check [extending tips](#extending)
for information on extra format support.
Usage:
@ -178,7 +220,7 @@ datum project create -o my_dataset/
### Add and remove data
A Project can be attached to a number of external Data Sources. Each Source
A Project can contain a number of external Data Sources. Each Data Source
describes a way to produce dataset items. A Project combines dataset items from
all the sources and its own dataset into one composite dataset. You can manage
project sources by commands in the `source` command line context.
@ -195,11 +237,8 @@ is used in COCO format:
```
<!--lint enable fenced-code-flag-->
In Datumaro dataset formats are supported by Extractors.
An Extractor produces a list of dataset items corresponding
to the dataset. It is possible to add a custom Extractor.
To do this, you need to put an Extractor
definition script to `<project_dir>/.datumaro/extractors`.
Supported formats are listed in the command help. Check [extending tips](#extending)
for information on extra format support.
Usage:
@ -237,16 +276,16 @@ This command allows to create a sub-Project from a Project. The new project
includes only items satisfying some condition. [XPath](https://devhints.io/xpath)
is used as query format.
There are several filtering modes available ('-m/--mode' parameter).
There are several filtering modes available (`-m/--mode` parameter).
Supported modes:
- 'i', 'items'
- 'a', 'annotations'
- 'i+a', 'a+i', 'items+annotations', 'annotations+items'
- `i`, `items`
- `a`, `annotations`
- `i+a`, `a+i`, `items+annotations`, `annotations+items`
When filtering annotations, use the 'items+annotations'
When filtering annotations, use the `items+annotations`
mode to point that annotation-less dataset items should be
removed. To select an annotation, write an XPath that
returns 'annotation' elements (see examples).
returns `annotation` elements (see examples).
Usage:
@ -259,7 +298,7 @@ datum project extract \
-e '<xpath filter expression>'
```
Example: extract a dataset with only images which width < height
Example: extract a dataset with only images which `width` < `height`
``` bash
datum project extract \
@ -274,7 +313,7 @@ Example: extract a dataset with only large annotations of class `cat` and any no
datum project extract \
-p test_project \
-o test_project-extract \
--mode annotations -e '/item/annotation[(label="cat" and area > 999.5) or label!="person"]'
--mode annotations -e '/item/annotation[(label="cat" and area > 99.5) or label!="person"]'
```
Example: extract a dataset with only occluded annotations, remove empty images
@ -321,9 +360,9 @@ Item representations are available with `--dry-run` parameter:
</item>
```
### Merge projects
### Update project
This command combines multiple Projects into one.
This command updates items in a project from another one (check [Merge Projects](#merge-projects) for complex merging).
Usage:
@ -346,16 +385,40 @@ datum project merge \
second_project
```
### Merge projects
This command merges items from 2 or more projects and checks annotations for errors.
Spatial annotations are compared by distance and intersected, labels and attributes
are selected by voting.
Merge conflicts, missing items and annotations, other errors are saved into a `.json` file.
Usage:
``` bash
datum merge --help
datum merge <project dirs>
```
Example: merge 4 (partially-)intersecting projects,
- consider voting succeeded when there are 3+ same votes
- consider shapes intersecting when IoU >= 0.6
- check annotation groups to have `person`, `hand`, `head` and `foot` (`?` for optional)
``` bash
datum merge project1/ project2/ project3/ project4/ \
--quorum 3 \
-iou 0.6 \
--groups 'person,hand?,head,foot?'
```
### Export project
This command exports a Project in some format.
This command exports a Project as a dataset in some format.
Supported formats are listed in the command help.
In Datumaro dataset formats are supported by Converters.
A Converter produces a dataset of a specific format
from dataset items. It is possible to add a custom Converter.
To do this, you need to put a Converter
definition script to <project_dir>/.datumaro/converters.
Supported formats are listed in the command help. Check [extending tips](#extending)
for information on extra format support.
Usage:
@ -366,17 +429,17 @@ datum project export \
-p <project dir> \
-o <output dir> \
-f <format> \
[-- <additional format parameters>]
-- [additional format parameters]
```
Example: save project as VOC-like dataset, include images
Example: save project as VOC-like dataset, include images, convert images to `PNG`
``` bash
datum project export \
-p test_project \
-o test_project-export \
-f voc \
-- --save-images
-- --save-images --image-ext='.png'
```
### Get project info
@ -398,7 +461,7 @@ Example:
datum project info -p /test_project
Project:
name: test_project2
name: test_project
location: /test_project
Sources:
source 'instances_minival2014':
@ -419,6 +482,282 @@ Dataset:
labels: person, bicycle, car, motorcycle (and 76 more)
```
### Get project statistics
This command computes various project statistics, such as:
- image mean and std. dev.
- class and attribute balance
- mask pixel balance
- segment area distribution
Usage:
``` bash
datum project stats --help
datum project stats \
-p <project dir>
```
Example:
<details>
``` bash
datum project stats -p /test_project
{
"annotations": {
"labels": {
"attributes": {
"gender": {
"count": 358,
"distribution": {
"female": [
149,
0.41620111731843573
],
"male": [
209,
0.5837988826815642
]
},
"values count": 2,
"values present": [
"female",
"male"
]
},
"view": {
"count": 340,
"distribution": {
"__undefined__": [
4,
0.011764705882352941
],
"front": [
54,
0.1588235294117647
],
"left": [
14,
0.041176470588235294
],
"rear": [
235,
0.6911764705882353
],
"right": [
33,
0.09705882352941177
]
},
"values count": 5,
"values present": [
"__undefined__",
"front",
"left",
"rear",
"right"
]
}
},
"count": 2038,
"distribution": {
"car": [
340,
0.16683022571148184
],
"cyclist": [
194,
0.09519136408243375
],
"head": [
354,
0.17369970559371933
],
"ignore": [
100,
0.04906771344455348
],
"left_hand": [
238,
0.11678115799803729
],
"person": [
358,
0.17566241413150147
],
"right_hand": [
77,
0.037782139352306184
],
"road_arrows": [
326,
0.15996074582924436
],
"traffic_sign": [
51,
0.025024533856722278
]
}
},
"segments": {
"area distribution": [
{
"count": 1318,
"max": 11425.1,
"min": 0.0,
"percent": 0.9627465303140978
},
{
"count": 1,
"max": 22850.2,
"min": 11425.1,
"percent": 0.0007304601899196494
},
{
"count": 0,
"max": 34275.3,
"min": 22850.2,
"percent": 0.0
},
{
"count": 0,
"max": 45700.4,
"min": 34275.3,
"percent": 0.0
},
{
"count": 0,
"max": 57125.5,
"min": 45700.4,
"percent": 0.0
},
{
"count": 0,
"max": 68550.6,
"min": 57125.5,
"percent": 0.0
},
{
"count": 0,
"max": 79975.7,
"min": 68550.6,
"percent": 0.0
},
{
"count": 0,
"max": 91400.8,
"min": 79975.7,
"percent": 0.0
},
{
"count": 0,
"max": 102825.90000000001,
"min": 91400.8,
"percent": 0.0
},
{
"count": 50,
"max": 114251.0,
"min": 102825.90000000001,
"percent": 0.036523009495982466
}
],
"avg. area": 5411.624543462382,
"pixel distribution": {
"car": [
13655,
0.0018431496518735067
],
"cyclist": [
939005,
0.12674674030446592
],
"head": [
0,
0.0
],
"ignore": [
5501200,
0.7425510702956085
],
"left_hand": [
0,
0.0
],
"person": [
954654,
0.12885903974805205
],
"right_hand": [
0,
0.0
],
"road_arrows": [
0,
0.0
],
"traffic_sign": [
0,
0.0
]
}
}
},
"annotations by type": {
"bbox": {
"count": 548
},
"caption": {
"count": 0
},
"label": {
"count": 0
},
"mask": {
"count": 0
},
"points": {
"count": 669
},
"polygon": {
"count": 821
},
"polyline": {
"count": 0
}
},
"annotations count": 2038,
"dataset": {
"image mean": [
107.06903686941979,
79.12831698580979,
52.95829558185416
],
"image std": [
49.40237673503467,
43.29600731496902,
35.47373007603151
],
"images count": 100
},
"images count": 100,
"subsets": {},
"unannotated images": [
"img00051",
"img00052",
"img00053",
"img00054",
"img00055",
],
"unannotated images count": 5
}
```
</details>
### Register model
Supported models:
@ -557,6 +896,85 @@ datum explain \
-s 1000 --progressive
```
### Transform Project
This command allows to modify images or annotations in a project all at once.
``` bash
datum project transform --help
datum project transform \
-p <project_dir> \
-o <output_dir> \
-t <transform_name> \
-- [extra transform options]
```
Example: split a dataset randomly to `train` and `test` subsets, ratio is 2:1
``` bash
datum project transform -t random_split -- --subset train:.67 --subset test:.33
```
Example: convert polygons to masks, masks to boxes etc.:
``` bash
datum project transform -t boxes_to_masks
datum project transform -t masks_to_polygons
datum project transform -t polygons_to_masks
datum project transform -t shapes_to_boxes
```
Example: remap dataset labels, `person` to `car` and `cat` to `dog`, keep `bus`, remove others
``` bash
datum project transform -t remap_labels -- \
-l person:car -l bus:bus -l cat:dog \
--default delete
```
Example: rename dataset items by a regular expression
- Replace `pattern` with `replacement`
- Remove `frame_` from item ids
``` bash
datum project transform -t rename -- -e '|pattern|replacement|'
datum project transform -t rename -- -e '|frame_(\d+)|\\1|'
```
## Extending
There are few ways to extend and customize Datumaro behaviour, which is supported by plugins.
Check [our contribution guide](../CONTRIBUTING.md) for details on plugin implementation.
In general, a plugin is a Python code file. It must be put into a plugin directory:
- `<project_dir>/.datumaro/plugins` for project-specific plugins
- `<datumaro_dir>/plugins` for global plugins
### Dataset Formats
Dataset reading is supported by Extractors and Importers.
An Extractor produces a list of dataset items corresponding
to the dataset. An Importer creates a project from the data source location.
It is possible to add custom Extractors and Importers. To do this, you need
to put an Extractor and Importer implementation scripts to a plugin directory.
Dataset writing is supported by Converters.
A Converter produces a dataset of a specific format from dataset items.
It is possible to add custom Converters. To do this, you need to put a Converter
implementation script to a plugin directory.
### Dataset Conversions ("Transforms")
A Transform is a function for altering a dataset and producing a new one. It can update
dataset items, annotations, classes, and other properties.
A list of available transforms for dataset conversions can be extended by adding a Transform
implementation script into a plugin directory.
### Model launchers
A list of available launchers for model execution can be extended by adding a Launcher
implementation script into a plugin directory.
## Links
- [TensorFlow detection model zoo](https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md)
- [How to convert model to OpenVINO format](https://docs.openvinotoolkit.org/latest/_docs_MO_DG_prepare_model_convert_model_tf_specific_Convert_Object_Detection_API_Models.html)

@ -4,6 +4,10 @@
"viewportWidth": 1300,
"viewportHeight": 960,
"defaultCommandTimeout": 10000,
"env": {
"user": "admin",
"password": "12qwaszx"
},
"testFiles": [
"auth_page.js",
"issue_*.js"

@ -0,0 +1,27 @@
/*
* Copyright (C) 2020 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/// <reference types="cypress" />
context('When clicking on the Logout button, get the user session closed.', () => {
const issueId = '1810'
before(() => {
cy.visit('auth/login')
})
describe(`Testing issue "${issueId}"`, () => {
it('Login', () => {
cy.login()
cy.url().should('include', '/tasks')
})
it('Logout', () => {
cy.logout()
cy.url().should('include', '/auth/login')
})
})
})

@ -0,0 +1,72 @@
/*
* Copyright (C) 2020 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/// <reference types="cypress" />
context('First part of a splitted track is visible', () => {
const issueId = '1819'
const labelName = `Issue ${issueId}`
const taskName = `New annotation task for ${labelName}`
const attrName = `Attr for ${labelName}`
const textDefaultValue = 'Some default value for type Text'
const images = [`image_${issueId}_1.png`,
`image_${issueId}_2.png`,
`image_${issueId}_3.png`]
const width = 800
const height = 800
const posX = 10
const posY = 10
const color = 'gray'
const archiveName = `images_issue_${issueId}.zip`
const archivePath = `cypress/fixtures/${archiveName}`
const imagesFolder = `cypress/fixtures/image_issue_${issueId}`
const directoryToArchive = imagesFolder
before(() => {
cy.visit('auth/login')
cy.login()
for (let img of images) {
cy.imageGenerator(imagesFolder, img, width, height, color, posX, posY, labelName)
}
cy.createZipArchive(directoryToArchive, archivePath)
cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, archiveName)
cy.openTaskJob(taskName)
})
describe(`Testing issue "${issueId}"`, () => {
it('Create a track', () => {
cy.createTrack(309, 431, 616, 671)
})
it('Go next with a step', () => {
cy.get('.cvat-player-forward-button')
.click()
cy.get('.cvat-player-frame-selector').within(() => {
cy.get('input[role="spinbutton"]')
.should('have.value', '2')
})
})
it('Split track', () => {
cy.get('body')
.type('{alt}m')
cy.get('#cvat_canvas_shape_1')
.trigger('mousemove', {which: 1})
.trigger('click', {which: 1})
})
it('Go to previous frame', () => {
cy.get('.cvat-player-previous-button')
.click()
cy.get('.cvat-player-frame-selector').within(() => {
cy.get('input[role="spinbutton"]')
.should('have.value', '1')
})
})
it('First part of a splitted track is visible', () => {
cy.get('#cvat_canvas_shape_2')
.should('be.visible')
})
})
})

@ -0,0 +1,62 @@
/*
* Copyright (C) 2020 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/// <reference types="cypress" />
context('Being able to return to the job list for a task and start a new job without an infinite loading screen.', () => {
const issueId = '1944'
const labelName = `Issue ${issueId}`
const taskName = `New annotation task for ${labelName}`
const attrName = `Attr for ${labelName}`
const textDefaultValue = 'Some default value for type Text'
const imagesCount = 4
let images = []
for ( let i = 1; i <= imagesCount; i++) {
images.push(`image_${issueId}_${i}.png`)
}
const width = 800
const height = 800
const posX = 10
const posY = 10
const color = 'gray'
const archiveName = `images_issue_${issueId}.zip`
const archivePath = `cypress/fixtures/${archiveName}`
const imagesFolder = `cypress/fixtures/image_issue_${issueId}`
const directoryToArchive = imagesFolder
const multiJobs = true
before(() => {
cy.visit('auth/login')
cy.login()
for (let img of images) {
cy.imageGenerator(imagesFolder, img, width, height, color, posX, posY, labelName)
}
cy.createZipArchive(directoryToArchive, archivePath)
})
describe(`Testing issue "${issueId}"`, () => {
it('Create a multijob task', () => {
cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, archiveName, multiJobs)
})
it('Open the task. Open first job', () => {
cy.openTaskJob(taskName)
cy.get('input[role="spinbutton"]')
.should('have.value', '0')
})
it('Return to tasks page', () => {
cy.get('[value="tasks"]').click()
cy.url().should('include', '/tasks').and('not.contain', '/jobs')
})
it('Open the task. Open second job', () => {
cy.openTaskJob(taskName, 1)
cy.get('.cvat-annotation-header')
.should('exist')
cy.get('input[role="spinbutton"]')
.should('have.value', '1')
})
})
})

@ -10,17 +10,28 @@ require('cypress-file-upload')
require('../plugins/imageGenerator/imageGeneratorCommand')
require('../plugins/createZipArchive/createZipArchiveCommand')
Cypress.Commands.add('login', (username='admin', password='12qwaszx') => {
Cypress.Commands.add('login', (username=Cypress.env('user'), password=Cypress.env('password')) => {
cy.get('[placeholder="Username"]').type(username)
cy.get('[placeholder="Password"]').type(password)
cy.get('[type="submit"]').click()
})
Cypress.Commands.add('logout', (username=Cypress.env('user')) => {
cy.get('.cvat-right-header')
.find('.cvat-header-menu-dropdown')
.should('have.text', username)
.trigger('mouseover', {which: 1})
cy.get('.anticon-logout')
.click()
})
Cypress.Commands.add('createAnnotationTask', (taksName='New annotation task',
labelName='Some label',
attrName='Some attr name',
textDefaultValue='Some default value for type Text',
image='image.png') => {
image='image.png',
multiJobs=false,
segmentSize=1) => {
cy.contains('button', 'Create new task').click()
cy.url().should('include', '/tasks/create')
cy.get('[id="name"]').type(taksName)
@ -33,9 +44,14 @@ Cypress.Commands.add('createAnnotationTask', (taksName='New annotation task',
cy.get('[placeholder="Default value"]').type(textDefaultValue)
cy.contains('button', 'Done').click()
cy.get('input[type="file"]').attachFile(image, { subjectType: 'drag-n-drop' });
if (multiJobs) {
cy.contains('Advanced configuration').click()
cy.get('#segmentSize')
.type(segmentSize)
}
cy.contains('button', 'Submit').click()
cy.contains('The task has been created', {timeout: '8000'})
cy.get('button[value="tasks"]').click()
cy.get('[value="tasks"]').click()
cy.url().should('include', '/tasks?page=')
})
@ -46,24 +62,40 @@ Cypress.Commands.add('openTask', (taskName) => {
.click()
})
Cypress.Commands.add('openJob', () => {
cy.contains('a', 'Job #').click()
Cypress.Commands.add('openJob', (jobNumber=0) => {
cy.get('.ant-table-tbody')
.find('tr')
.eq(jobNumber)
.contains('a', 'Job #')
.click()
cy.url().should('include', '/jobs')
})
Cypress.Commands.add('openTaskJob', (taskName) => {
Cypress.Commands.add('openTaskJob', (taskName, jobNumber=0) => {
cy.openTask(taskName)
cy.openJob()
cy.openJob(jobNumber)
})
Cypress.Commands.add('createShape', (ferstX, ferstY, lastX, lastY) => {
Cypress.Commands.add('createShape', (firstX, firstY, lastX, lastY) => {
cy.get('.cvat-draw-rectangle-control').click()
cy.get('.cvat-draw-shape-popover-content')
.find('button')
.contains('Shape')
.click({force: true})
cy.get('.cvat-canvas-container')
.click(ferstX, ferstY)
.click(firstX, firstY)
cy.get('.cvat-canvas-container')
.click(lastX, lastY)
})
Cypress.Commands.add('createTrack', (firstX, firstY, lastX, lastY) => {
cy.get('.cvat-draw-rectangle-control').click()
cy.get('.cvat-draw-shape-popover-content')
.find('button')
.contains('Track')
.click({force: true})
cy.get('.cvat-canvas-container')
.click(firstX, firstY)
cy.get('.cvat-canvas-container')
.click(lastX, lastY)
})

Loading…
Cancel
Save