diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f5f3dbddef..e618c46c9aee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Order of the label attributes in the object item details() - Order of labels in tasks and projects () - Added information to export CVAT_HOST when performing local installation for accessing over network () +- Fixed possible color collisions in the generated colormap () ### Security - TDB diff --git a/cvat/apps/dataset_manager/formats/utils.py b/cvat/apps/dataset_manager/formats/utils.py index 71044c14ae82..fdd940374a8b 100644 --- a/cvat/apps/dataset_manager/formats/utils.py +++ b/cvat/apps/dataset_manager/formats/utils.py @@ -1,9 +1,11 @@ -# Copyright (C) 2019 Intel Corporation +# Copyright (C) 2019-2021 Intel Corporation # # SPDX-License-Identifier: MIT import os.path as osp from hashlib import blake2s +import itertools +import operator from datumaro.util.os_util import make_file_name @@ -20,7 +22,6 @@ def get_bit(number, index): 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: @@ -61,19 +62,40 @@ def make_colormap(instance_data): return {label['name']: [hex2rgb(label['color']), [], []] for label in labels} +def generate_color(color, used_colors): + def tint_shade_color(): + for added_color in (255, 0): + for factor in range(1, 10): + yield tuple(map(lambda c: int(c + (added_color - c) * factor / 10), color)) -def get_label_color(label_name, label_names): + def get_unused_color(): + def get_avg_color(index): + sorted_colors = sorted(used_colors, key=operator.itemgetter(index)) + max_dist_pair = max(zip(sorted_colors, sorted_colors[1:]), + key=lambda c_pair: c_pair[1][index] - c_pair[0][index]) + return (max_dist_pair[0][index] + max_dist_pair[1][index]) // 2 + + return tuple(get_avg_color(i) for i in range(3)) + + #try to tint and shade color firstly + for new_color in tint_shade_color(): + if new_color not in used_colors: + return new_color + + return get_unused_color() + +def get_label_color(label_name, label_colors): predefined = parse_default_colors() - normalized_names = [normalize_label(l_name) for l_name in label_names] + label_colors = tuple(hex2rgb(c) for c in label_colors if c) + used_colors = set(itertools.chain(predefined.values(), label_colors)) normalized_name = normalize_label(label_name) color = predefined.get(normalized_name, None) - name_hash = int.from_bytes(blake2s(normalized_name.encode(), digest_size=4).digest(), byteorder="big") - offset = name_hash + 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) + name_hash = int.from_bytes(blake2s(normalized_name.encode(), digest_size=3).digest(), byteorder="big") + color = get_color_from_index(name_hash) + + if color in label_colors: + color = generate_color(color, used_colors) return rgb2hex(color) diff --git a/cvat/apps/engine/migrations/0028_labelcolor.py b/cvat/apps/engine/migrations/0028_labelcolor.py index a725eb9aa290..af30fbabd8d2 100644 --- a/cvat/apps/engine/migrations/0028_labelcolor.py +++ b/cvat/apps/engine/migrations/0028_labelcolor.py @@ -11,7 +11,7 @@ def alter_label_colors(apps, schema_editor): label_names = list() for label in labels: label.color = get_label_color(label.name, label_names) - label_names.append(label.name) + label_names.append(label.color) label.save() class Migration(migrations.Migration): diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 2cf801d3e5bf..72591b012d74 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -111,10 +111,10 @@ def update_instance(validated_data, parent_instance): db_label.delete() return if not validated_data.get('color', None): - label_names = [l.name for l in + label_colors = [l.color for l in instance[tuple(instance.keys())[0]].label_set.exclude(id=db_label.id).order_by('id') ] - db_label.color = get_label_color(db_label.name, label_names) + db_label.color = get_label_color(db_label.name, label_colors) else: db_label.color = validated_data.get('color', db_label.color) db_label.save() @@ -373,12 +373,12 @@ def create(self, validated_data): labels = validated_data.pop('label_set', []) db_task = models.Task.objects.create(**validated_data) - label_names = list() + label_colors = 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']) + label['color'] = get_label_color(label['name'], label_colors) + label_colors.append(label['color']) db_label = models.Label.objects.create(task=db_task, **label) for attr in attributes: models.AttributeSpec.objects.create(label=db_label, **attr) @@ -541,12 +541,12 @@ def create(self, validated_data): training_project=tr_p) else: db_project = models.Project.objects.create(**validated_data) - label_names = list() + label_colors = 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']) + label['color'] = get_label_color(label['name'], label_colors) + label_colors.append(label['color']) db_label = models.Label.objects.create(project=db_project, **label) for attr in attributes: models.AttributeSpec.objects.create(label=db_label, **attr) diff --git a/tests/cypress/integration/canvas3d_functionality_2/case_78_canvas3d_functionality_cuboid_label.js b/tests/cypress/integration/canvas3d_functionality_2/case_78_canvas3d_functionality_cuboid_label.js index 9738c0177d59..c79b7ad46d9a 100644 --- a/tests/cypress/integration/canvas3d_functionality_2/case_78_canvas3d_functionality_cuboid_label.js +++ b/tests/cypress/integration/canvas3d_functionality_2/case_78_canvas3d_functionality_cuboid_label.js @@ -8,16 +8,17 @@ import { taskName, labelName } from '../../support/const_canvas3d'; context('Canvas 3D functionality. Interaction with cuboid via sidebar.', () => { const caseId = '78'; - const secondLabel = 'car'; - const screenshotsPath = 'cypress/screenshots/canvas3d_functionality_2/case_78_canvas3d_functionality_cuboid_label.js'; const cuboidCreationParams = { - labelName: labelName, + labelName, }; + const secondLabel = 'car'; + const secondLabelAdditionalAttrs = false; + const secondLabelColorRed = 'ff0000'; before(() => { - cy.openTask(taskName) - cy.addNewLabel(secondLabel); + cy.openTask(taskName); + cy.addNewLabel(secondLabel, secondLabelAdditionalAttrs, secondLabelColorRed); cy.openJob(); cy.wait(1000); // Waiting for the point cloud to display cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_before_all'); @@ -32,7 +33,7 @@ context('Canvas 3D functionality. Interaction with cuboid via sidebar.', () => { cy.get('#cvat-objects-sidebar-state-item-1') .trigger('mouseover') .should('have.class', 'cvat-objects-sidebar-state-active-item') - .wait(1000); //Wating for cuboid activation + .wait(1000); // Wating for cuboid activation cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_after_activating_cuboid'); cy.compareImagesAndCheckResult( `${screenshotsPath}/canvas3d_perspective_before_all.png`, @@ -46,7 +47,10 @@ context('Canvas 3D functionality. Interaction with cuboid via sidebar.', () => { ['canvas3d_sideview_before_all.png', 'canvas3d_sideview_activating_cuboid.png'], ['canvas3d_frontview_before_all.png', 'canvas3d_frontview_activating_cuboid.png'], ].forEach(([viewBefore, viewAfterCubiodActivation]) => { - cy.compareImagesAndCheckResult(`${screenshotsPath}/${viewBefore}`, `${screenshotsPath}/${viewAfterCubiodActivation}`); + cy.compareImagesAndCheckResult( + `${screenshotsPath}/${viewBefore}`, + `${screenshotsPath}/${viewAfterCubiodActivation}`, + ); }); }); @@ -67,14 +71,17 @@ context('Canvas 3D functionality. Interaction with cuboid via sidebar.', () => { ['canvas3d_sideview_activating_cuboid.png', 'canvas3d_sideview_change_label_cuboid.png'], ['canvas3d_frontview_activating_cuboid.png', 'canvas3d_frontview_change_label_cuboid.png'], ].forEach(([viewAfterCubiodActivation, viewAfterCubiodChangeLabel]) => { - cy.compareImagesAndCheckResult(`${screenshotsPath}/${viewAfterCubiodActivation}`, `${screenshotsPath}/${viewAfterCubiodChangeLabel}`); + cy.compareImagesAndCheckResult( + `${screenshotsPath}/${viewAfterCubiodActivation}`, + `${screenshotsPath}/${viewAfterCubiodChangeLabel}`, + ); }); }); it('Lock/unlock a cuboid via sidear. The control points of the cuboid on the top/side/front view are locked/unlocked.', () => { cy.get('#cvat-objects-sidebar-state-item-1') .find('.cvat-object-item-button-lock') - .click({force: true}); // Lock the cubiod + .click({ force: true }); // Lock the cubiod cy.get('.cvat-object-item-button-lock-enabled').should('exist'); ['topview', 'sideview', 'frontview'].forEach((view) => { cy.customScreenshot(`.cvat-canvas3d-${view}`, `canvas3d_${view}_lock_cuboid`); @@ -84,9 +91,12 @@ context('Canvas 3D functionality. Interaction with cuboid via sidebar.', () => { ['canvas3d_sideview_change_label_cuboid.png', 'canvas3d_sideview_lock_cuboid.png'], ['canvas3d_frontview_change_label_cuboid.png', 'canvas3d_frontview_lock_cuboid.png'], ].forEach(([viewAfterCubiodChangeLabel, viewAfterCubiodLock]) => { - cy.compareImagesAndCheckResult(`${screenshotsPath}/${viewAfterCubiodChangeLabel}`, `${screenshotsPath}/${viewAfterCubiodLock}`); + cy.compareImagesAndCheckResult( + `${screenshotsPath}/${viewAfterCubiodChangeLabel}`, + `${screenshotsPath}/${viewAfterCubiodLock}`, + ); }); - cy.get('.cvat-object-item-button-lock-enabled').click({force: true}); // Unlock the cubiod + cy.get('.cvat-object-item-button-lock-enabled').click({ force: true }); // Unlock the cubiod cy.get('.cvat-object-item-button-lock').should('exist').trigger('mouseout'); ['topview', 'sideview', 'frontview'].forEach((view) => { cy.customScreenshot(`.cvat-canvas3d-${view}`, `canvas3d_${view}_unlock_cuboid`); @@ -96,20 +106,23 @@ context('Canvas 3D functionality. Interaction with cuboid via sidebar.', () => { ['canvas3d_sideview_lock_cuboid.png', 'canvas3d_sideview_unlock_cuboid.png'], ['canvas3d_frontview_lock_cuboid.png', 'canvas3d_frontview_unlock_cuboid.png'], ].forEach(([viewAfterCubiodLock, viewAfterCubiodUnlock]) => { - cy.compareImagesAndCheckResult(`${screenshotsPath}/${viewAfterCubiodLock}`, `${screenshotsPath}/${viewAfterCubiodUnlock}`); + cy.compareImagesAndCheckResult( + `${screenshotsPath}/${viewAfterCubiodLock}`, + `${screenshotsPath}/${viewAfterCubiodUnlock}`, + ); }); }); it('Switch occluded property for a cuboid via sidear. The cuboid on the perpective view are occluded.', () => { cy.get('#cvat-objects-sidebar-state-item-1') .find('.cvat-object-item-button-occluded') - .click({force: true}); // Switch occluded property + .click({ force: true }); // Switch occluded property cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_enable_occlud_cuboid'); cy.compareImagesAndCheckResult( `${screenshotsPath}/canvas3d_perspective_after_activating_cuboid.png`, `${screenshotsPath}/canvas3d_perspective_enable_occlud_cuboid.png`, ); - cy.get('.cvat-object-item-button-occluded-enabled').click({force: true}); // Switch occluded property again + cy.get('.cvat-object-item-button-occluded-enabled').click({ force: true }); // Switch occluded property again cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_disable_occlud_cuboid'); cy.compareImagesAndCheckResult( `${screenshotsPath}/canvas3d_perspective_enable_occlud_cuboid.png`, @@ -120,7 +133,7 @@ context('Canvas 3D functionality. Interaction with cuboid via sidebar.', () => { it('Hide/unhide a cuboid via sidear. The cuboid on the perpective/top/side/front view be hidden/unhidden.', () => { cy.get('#cvat-objects-sidebar-state-item-1') .find('.cvat-object-item-button-hidden') - .click({force: true}); // Hide the cuboid + .click({ force: true }); // Hide the cuboid cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_hide_cuboid'); cy.compareImagesAndCheckResult( `${screenshotsPath}/canvas3d_perspective_disable_occlud_cuboid.png`, @@ -134,9 +147,12 @@ context('Canvas 3D functionality. Interaction with cuboid via sidebar.', () => { ['canvas3d_sideview_unlock_cuboid.png', 'canvas3d_sideview_hide_cuboid.png'], ['canvas3d_frontview_unlock_cuboid.png', 'canvas3d_frontview_hide_cuboid.png'], ].forEach(([viewAfterCubiodUnlock, viewAfterCubiodHide]) => { - cy.compareImagesAndCheckResult(`${screenshotsPath}/${viewAfterCubiodUnlock}`, `${screenshotsPath}/${viewAfterCubiodHide}`); + cy.compareImagesAndCheckResult( + `${screenshotsPath}/${viewAfterCubiodUnlock}`, + `${screenshotsPath}/${viewAfterCubiodHide}`, + ); }); - cy.get('.cvat-object-item-button-hidden-enabled').click({force: true}); // Unhide the cuboid + cy.get('.cvat-object-item-button-hidden-enabled').click({ force: true }); // Unhide the cuboid cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_unhide_cuboid'); cy.compareImagesAndCheckResult( `${screenshotsPath}/canvas3d_perspective_hide_cuboid.png`, @@ -150,7 +166,10 @@ context('Canvas 3D functionality. Interaction with cuboid via sidebar.', () => { ['canvas3d_sideview_hide_cuboid.png', 'canvas3d_sideview_unhide_cuboid.png'], ['canvas3d_frontview_hide_cuboid.png', 'canvas3d_frontview_unhide_cuboid.png'], ].forEach(([viewAfterCubiodHide, viewAfterCubiodUnhide]) => { - cy.compareImagesAndCheckResult(`${screenshotsPath}/${viewAfterCubiodHide}`, `${screenshotsPath}/${viewAfterCubiodUnhide}`); + cy.compareImagesAndCheckResult( + `${screenshotsPath}/${viewAfterCubiodHide}`, + `${screenshotsPath}/${viewAfterCubiodUnhide}`, + ); }); }); });