Skip to content

Commit c536e4e

Browse files
committed
pbio/color/util: Add heuristic distance for saturated colors.
The bicone mapping is highly distant dependent, which makes it suitable in limited cases, and it doesn't work great with the default colors. If only saturated or grayscale colors are in the mapping, we can generally get a much better result by looking at hue only for saturated colors and looking at value only for unsaturated colors, and use the saturation to decide which to pick. This also means we won't need the workaround of having negative V for None. The logic here is that if you do specify fine-grained, calibrated colors, then it will use the original bicone distance mapping.
1 parent cb1d150 commit c536e4e

File tree

4 files changed

+64
-2
lines changed

4 files changed

+64
-2
lines changed

lib/pbio/include/pbio/color.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ void pbio_color_hsv_expand(const pbio_color_compressed_hsv_t *compressed, pbio_c
122122
typedef int32_t (*pbio_color_distance_func_t)(const pbio_color_hsv_t *hsv_a, const pbio_color_hsv_t *hsv_b);
123123

124124
int32_t pbio_color_get_distance_bicone_squared(const pbio_color_hsv_t *hsv_a, const pbio_color_hsv_t *hsv_b);
125+
int32_t pbio_color_get_distance_saturation_heuristic(const pbio_color_hsv_t *hsv_a, const pbio_color_hsv_t *hsv_b);
125126

126127
#endif // _PBIO_COLOR_H_
127128

lib/pbio/src/color/util.c

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
// Copyright (c) 2018-2022 The Pybricks Authors
33

4+
#include <assert.h>
45
#include <pbio/color.h>
56
#include <pbio/int_math.h>
67

@@ -36,3 +37,47 @@ int32_t pbio_color_get_distance_bicone_squared(const pbio_color_hsv_t *hsv_a, co
3637
// Squared Euclidean distance (0, 400000000)
3738
return delta_x * delta_x + delta_y * delta_y + delta_z * delta_z;
3839
}
40+
41+
/**
42+
* Gets distance measure between a HSV color (a) and a fully or zero saturated
43+
* candidate color.
44+
*
45+
* @param [in] measurement The measured HSV color.
46+
* @param [in] candidate The candidate HSV color (an idealized color or grayscale).
47+
* @returns Heuristic distance.
48+
*/
49+
int32_t pbio_color_get_distance_saturation_heuristic(const pbio_color_hsv_t *measurement, const pbio_color_hsv_t *candidate) {
50+
51+
bool idealized_grayscale = candidate->s == 0 && candidate->h == 0;
52+
bool idealized_color = candidate->s == 100 && candidate->v == 100;
53+
54+
// Calling code needs to ensure this.
55+
assert(idealized_grayscale || idealized_color);
56+
57+
uint32_t hue_dist = pbio_int_math_abs(candidate->h - measurement->h);
58+
if (hue_dist > 180) {
59+
hue_dist = 360 - hue_dist;
60+
}
61+
62+
uint32_t value_dist = pbio_int_math_abs(candidate->v - measurement->v);
63+
64+
const uint32_t penalty = 1000;
65+
66+
if (measurement->s <= 40 || measurement->v <= 1) {
67+
// Measurement is unsaturated, so match to nearest grayscale; penalize color.
68+
if (idealized_grayscale) {
69+
// Match to nearest value.
70+
return value_dist;
71+
}
72+
// Looking for grayscale, so disqualify color candidate.
73+
return penalty + hue_dist;
74+
} else {
75+
// Measurement is saturated, so match to nearest full color; penalize grayscale.
76+
if (idealized_color) {
77+
// Match to nearest hue.
78+
return hue_dist;
79+
}
80+
// Looking for color, so disqualify grayscale candidate.
81+
return penalty + value_dist;
82+
}
83+
}

pybricks/parameters/pb_type_color.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const pb_type_Color_obj_t pb_Color_MAGENTA_obj = {
6262

6363
const pb_type_Color_obj_t pb_Color_NONE_obj = {
6464
{&pb_type_Color},
65-
.hsv = {0, 0, -40}
65+
.hsv = {0, 0, 0}
6666
};
6767

6868
const pb_type_Color_obj_t pb_Color_BLACK_obj = {

pybricks/util_pb/pb_color_map.c

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,23 @@ mp_obj_t pb_color_map_get_color(mp_obj_t *color_map, pbio_color_hsv_t *hsv) {
7373
int32_t cost_now = INT32_MAX;
7474
int32_t cost_min = INT32_MAX;
7575

76-
pbio_color_distance_func_t distance_func = pbio_color_get_distance_bicone_squared;
76+
pbio_color_distance_func_t distance_func = pbio_color_get_distance_saturation_heuristic;
77+
78+
// If user only provides fully saturated colors (hue, 100, 100) and/or fully
79+
// desaturated colors (0, 0, value), use a simplified heuristic matcher for
80+
// better default results that are distance independent. Otherwise use a
81+
// bicone color distance measure.
82+
for (size_t i = 0; i < n; i++) {
83+
const pbio_color_hsv_t *candidate = pb_type_Color_get_hsv(colors[i]);
84+
85+
// Use bicone mapping if custom (realistic) colors provided.
86+
bool idealized_grayscale = candidate->s == 0 && candidate->h == 0;
87+
bool idealized_color = candidate->s == 100 && candidate->v == 100;
88+
if (!idealized_grayscale && !idealized_color) {
89+
distance_func = pbio_color_get_distance_bicone_squared;
90+
break;
91+
}
92+
}
7793

7894
// Compute cost for each candidate
7995
for (size_t i = 0; i < n; i++) {

0 commit comments

Comments
 (0)