Label color (#2014)

* added color to django app and cvat-core

* temp

* temp

* Added label color to mask dump

* Fixed UI for label color picker

* npm packages and CHANGELOG

* fixed models and migrations

* Fixed default background color and using normalization

* Added setting label color with hash

* fixed error

* Added close icon to color picker

* Fixed CHANGELOG

* requested changes

* fixed menu visibility

* Fixed label hashing and algorithm

* Added wheel package to CI

* Fixed dockerfile

* moved wheel package from dockerfile to requirements

* fixed requirements

* Fixed requirements

Co-authored-by: Nikita Manovich <nikita.manovich@intel.com>
main
Dmitry Kalinin 6 years ago committed by GitHub
parent 822a3b5578
commit bee4c3799f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,6 +9,7 @@ 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)
### Changed

@ -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.2",
"version": "1.8.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.7.2",
"version": "1.8.0",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {

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

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

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

@ -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,
@ -58,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)) {
@ -417,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>
@ -491,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,
@ -511,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,

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

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

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

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

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

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

@ -240,6 +240,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,10 +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:
@ -250,8 +252,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)
@ -286,6 +292,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)

@ -10,6 +10,7 @@ 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

Loading…
Cancel
Save