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