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
parent
822a3b5578
commit
bee4c3799f
@ -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 |
@ -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);
|
||||||
@ -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,
|
||||||
|
),
|
||||||
|
]
|
||||||
Loading…
Reference in New Issue