diff --git a/CHANGELOG.md b/CHANGELOG.md index a85c3c33060..9d8ea49e03c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Siammask tracker as DL serverless function () - [Datumaro] Added model info and source info commands () - [Datumaro] Dataset statistics () +- Ability to change label color in tasks and predefined labels () - [Datumaro] Multi-dataset merge (https://github.com/opencv/cvat/pull/1695) ### Changed diff --git a/Dockerfile b/Dockerfile index 02535a02d4b..e27dde2892a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 && \ diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json index d79d54d0a64..0bc2c78700c 100644 --- a/cvat-core/package-lock.json +++ b/cvat-core/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "3.3.1", + "version": "3.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-core/package.json b/cvat-core/package.json index 7d75dbc525e..ca65f045f7d 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -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": { diff --git a/cvat-core/src/labels.js b/cvat-core/src/labels.js index fa92a61196e..ec2c0cbbd0d 100644 --- a/cvat-core/src/labels.js +++ b/cvat-core/src/labels.js @@ -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') { diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 1f47f08107d..f8f63da4640 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.7.2", + "version": "1.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index f1a563ee4e8..d47c1da670b 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -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": { diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 5aa6c457502..3bea5b6caa1 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -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): Promise => { - 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, diff --git a/cvat-ui/src/assets/colorize-icon.svg b/cvat-ui/src/assets/colorize-icon.svg new file mode 100644 index 00000000000..300f5408487 --- /dev/null +++ b/cvat-ui/src/assets/colorize-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/cvat-ui/src/components/annotation-page/appearance-block.tsx b/cvat-ui/src/components/annotation-page/appearance-block.tsx index d242c387d39..06a060fa72a 100644 --- a/cvat-ui/src/components/annotation-page/appearance-block.tsx +++ b/cvat-ui/src/components/annotation-page/appearance-block.tsx @@ -166,9 +166,9 @@ function AppearanceBlock(props: Props): JSX.Element {
Color by + {ColorBy.LABEL} {ColorBy.INSTANCE} {ColorBy.GROUP} - {ColorBy.LABEL} Opacity = colors.length) { - break; - } - const color = colors[idx]; - antdCols.push( - - + )} + + + + + + + + + + )} + title={( + + + + Select color + + + + + + + + + + )} + placement={placement || 'left'} + overlayClassName='cvat-label-color-picker' + trigger='click' + visible={typeof visible === 'boolean' ? visible : pickerVisible} + onVisibleChange={changeVisible} + > + {children} + + ); +} + +export default React.forwardRef(ColorPicker); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx index 96a99a7ef29..e5067658d02 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx @@ -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' }} > - - )} - > - + + + + )} @@ -491,6 +500,37 @@ class LabelForm extends React.PureComponent { ); } + private renderChangeColorButton(): JSX.Element { + const { label, form } = this.props; + + return ( + + + { + form.getFieldDecorator('labelColor', { + initialValue: (label && label.color) ? label.color : undefined, + })( + + + + + , + ) + } + + + ); + } + public render(): JSX.Element { const { label, @@ -511,6 +551,8 @@ class LabelForm extends React.PureComponent { { this.renderLabelNameInput() } + { this.renderChangeColorButton() } + { this.renderNewAttributeButton() } { attributeItems.length > 0 diff --git a/cvat-ui/src/components/labels-editor/labels-editor.tsx b/cvat-ui/src/components/labels-editor/labels-editor.tsx index 89245b96b98..8c0ad072981 100644 --- a/cvat-ui/src/components/labels-editor/labels-editor.tsx +++ b/cvat-ui/src/components/labels-editor/labels-editor.tsx @@ -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, diff --git a/cvat-ui/src/components/labels-editor/styles.scss b/cvat-ui/src/components/labels-editor/styles.scss index ba03ef1a934..1e34e992934 100644 --- a/cvat-ui/src/components/labels-editor/styles.scss +++ b/cvat-ui/src/components/labels-editor/styles.scss @@ -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; +} diff --git a/cvat-ui/src/consts.ts b/cvat-ui/src/consts.ts index e5df33c7ff1..0c1da29d4a4 100644 --- a/cvat-ui/src/consts.ts +++ b/cvat-ui/src/consts.ts @@ -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, }; diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx index 955df16dabe..65c6027e07d 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx @@ -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 { 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 { const { labelName, labelColor, - labelColors, - changeColorShortcut, } = this.props; return ( ); } diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index 34e8facf994..e0b29e369c0 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -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 { const { objectState, colorBy, - changeLabelColor, changeGroupColor, } = this.props; @@ -282,8 +270,6 @@ class ObjectItemContainer extends React.PureComponent { 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 { attributes, activated, colorBy, - colors, normalizedKeyMap, } = this.props; @@ -399,10 +384,10 @@ class ObjectItemContainer extends React.PureComponent { 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} diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx index 43e6e2659ef..14e445deee9 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx @@ -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 { jobInstance, updateAnnotations, changeGroupColor, - changeLabelColor, removeObject, copyShape, propagateObject, @@ -399,15 +393,11 @@ class ObjectsListContainer extends React.PureComponent { 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) => { diff --git a/cvat-ui/src/icons.tsx b/cvat-ui/src/icons.tsx index 1eed125bf32..fbf4860723d 100644 --- a/cvat-ui/src/icons.tsx +++ b/cvat-ui/src/icons.tsx @@ -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 => , @@ -153,3 +154,6 @@ export const CubeIcon = React.memo( export const ResetPerspectiveIcon = React.memo( (): JSX.Element => , ); +export const ColorizeIcon = React.memo( + (): JSX.Element => , +); diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 91756d9b640..4976977dcdc 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -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, diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index e4da70868d9..a869011f017 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -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, diff --git a/cvat-ui/src/reducers/settings-reducer.ts b/cvat-ui/src/reducers/settings-reducer.ts index 3bd4cf90c5f..eb4fd755555 100644 --- a/cvat-ui/src/reducers/settings-reducer.ts +++ b/cvat-ui/src/reducers/settings-reducer.ts @@ -18,7 +18,7 @@ import { const defaultState: SettingsState = { shapes: { - colorBy: ColorBy.INSTANCE, + colorBy: ColorBy.LABEL, opacity: 3, selectedOpacity: 30, blackBorders: false, diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 64921555021..30ec8bd9078 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -152,6 +152,7 @@ def _init_meta(self): ("labels", [ ("label", OrderedDict([ ("name", db_label.name), + ("color", db_label.color), ("attributes", [ ("attribute", OrderedDict([ ("name", db_attr.name), diff --git a/cvat/apps/dataset_manager/formats/mask.py b/cvat/apps/dataset_manager/formats/mask.py index 268eb347489..b1307a9cead 100644 --- a/cvat/apps/dataset_manager/formats/mask.py +++ b/cvat/apps/dataset_manager/formats/mask.py @@ -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 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()} \ No newline at end of file diff --git a/cvat/apps/dataset_manager/formats/utils.py b/cvat/apps/dataset_manager/formats/utils.py new file mode 100644 index 00000000000..2e30ab79200 --- /dev/null +++ b/cvat/apps/dataset_manager/formats/utils.py @@ -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) diff --git a/cvat/apps/engine/migrations/0028_labelcolor.py b/cvat/apps/engine/migrations/0028_labelcolor.py new file mode 100644 index 00000000000..a725eb9aa29 --- /dev/null +++ b/cvat/apps/engine/migrations/0028_labelcolor.py @@ -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, + ), + ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index d410ef7c823..f3fd13b420e 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -240,6 +240,7 @@ class Meta: 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 diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index a878e9e6766..f211d28b2b1 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -11,6 +11,7 @@ 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 @@ def to_representation(self, instance): 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 Meta: 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 @@ def update(self, instance, validated_data): 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) diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index 3889fed5cd4..46e90ff1ac0 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -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