Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
color_vocabulary.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2022 Chris Elston.
4 Copyright (C) 2022 Martin Bařinka.
5 Copyright (C) 2022 Pascal Obry.
6 Copyright (C) 2022 Victor Forsiuk.
7 Copyright (C) 2023 Luca Zulberti.
8 Copyright (C) 2024 Aurélien PIERRE.
9
10 darktable is free software: you can redistribute it and/or modify
11 it under the terms of the GNU General Public License as published by
12 the Free Software Foundation, either version 3 of the License, or
13 (at your option) any later version.
14
15 darktable is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with darktable. If not, see <http://www.gnu.org/licenses/>.
22*/
23
25#include "common/utility.h"
26#include "system/macros.h"
27
28#include <glib/gi18n.h>
29
30// get a range of 2 x factor x std centered in avg
31static range_t
32_compute_range(const gaussian_stats_t stats, const float factor)
33{
35 out.bottom = stats.avg - factor * stats.std;
36 out.top = stats.avg + factor * stats.std;
37 return out;
38}
39
40// Human skin tones database
41// This is a racially-charged matter, tread with it carefully.
42
43// Usable data are : tabulated avg ± std (P < 0.05) models on skin color measurements
44// on more than 80 individuals under D65 illuminant.
45
46// Notice all these data are valid only under D65 illuminant and errors up to delta E = 6 have
47// been measured for A illuminant. Proper camera profiling and chromatic adaptation needs to be performed
48// or all the following is meaningless.
49
50// We use ranges of avg ± 2 std, giving 95 % of confidence in the prediction.
51
52// We use CIE Lab instead of Lch coordinates, because a and b parameters are physiologically meaningful :
53// - a (redness) is linked to blood flow and health,
54// - b (yellowness) is linked to melanine and sun tan.
55
56/* Reference :
57 XIAO, Kaida, YATES, Julian M., ZARDAWI, Faraedon, et al.
58 Characterising the variations in ethnic skin colours: a new calibrated data base for human skin.
59 Skin Research and Technology, 2017, vol. 23, no 1, p. 21-29.
60 https://onlinelibrary.wiley.com/doi/pdf/10.1111/srt.12295
61
62 Sample : 187 caucasian, 202 chinese, 145 kurdish and 426 thai.
63
64 DE RIGAL, Jean, DES MAZIS, Isabelle, DIRIDOLLOU, Stephane, et al.
65 The effect of age on skin color and color heterogeneity in four ethnic groups.
66 Skin Research and Technology, 2010, vol. 16, no 2, p. 168-178.
67 https://pubmed.ncbi.nlm.nih.gov/20456097/
68
69 Sample : 121 african-american, 64 mexican.
70 Note : the data have been read on the graph and are inaccurate and std is majorated.
71 The original authors have been contacted to get the tabulated, accurate data,
72 but the main author is retired, co-authors have changed jobs, and the L'Oréal head of R&D
73 did not respond. So the values here are given for what it's worth.
74*/
75
76// "Forearm" is the ventral forearm skin. It is the least sun-tanned part of skin.
77// Sun tan will depend the most on lifestyle, therefore the ventral forearm
78// is the least socially-biased skin color metric.
79
80// "Forehead" is the most sun-tanned part of skin. This translates to high b coordinate.
81
82// "Cheek" is the most redish part of skin. This translates to high a coordinate.
83
84// L decreases with age in all ethnicities and with b/yellowness/melanine/tan.
85
86
88{
89 const ethnicity_t ethnies[ETHNIE_END] =
90 { { .name = _("Chinese"), .ethnicity = ETHNIE_CHINESE },
91 { .name = _("Thai"), .ethnicity = ETHNIE_THAI },
92 { .name = _("Kurdish"), .ethnicity = ETHNIE_KURDISH },
93 { .name = _("Caucasian"), .ethnicity = ETHNIE_CAUCASIAN },
94 { .name = _("African-american"), .ethnicity = ETHNIE_AFRICAN_AM },
95 { .name = _("Mexican"), .ethnicity = ETHNIE_MEXICAN } };
96
97 return ethnies[index].name;
98}
99
100
101skin_color_t get_skin_color(const size_t index)
102{
103 const skin_color_t skin[SKINS] = {
104 { .name = _("forearm"),
105 .ethnicity = ETHNIE_CHINESE,
106 .L = { .avg = 60.9f, .std = 3.4f },
107 .a = { .avg = 7.0f, .std = 1.7f },
108 .b = { .avg = 15.0f, .std = 1.8f } },
109 { .name = _("forearm"),
110 .ethnicity = ETHNIE_THAI,
111 .L = { .avg = 61.9f, .std = 3.7f },
112 .a = { .avg = 7.1f, .std = 1.7f },
113 .b = { .avg = 17.4f, .std = 2.0f } },
114 { .name = _("forearm"),
115 .ethnicity = ETHNIE_KURDISH,
116 .L = { .avg = 60.6f, .std = 4.8f },
117 .a = { .avg = 6.5f, .std = 1.6f },
118 .b = { .avg = 16.4f, .std = 2.3f } },
119 { .name = _("forearm"),
120 .ethnicity = ETHNIE_CAUCASIAN,
121 .L = { .avg = 63.0f, .std = 5.5f },
122 .a = { .avg = 5.6f, .std = 1.9f },
123 .b = { .avg = 14.0f, .std = 2.9f } },
124 { .name = _("forehead"),
125 .ethnicity = ETHNIE_CHINESE,
126 .L = { .avg = 56.4f, .std = 3.2f },
127 .a = { .avg = 11.7f, .std = 2.1f },
128 .b = { .avg = 16.3f, .std = 1.4f } },
129 { .name = _("forehead"),
130 .ethnicity = ETHNIE_THAI,
131 .L = { .avg = 56.8f, .std = 4.1f },
132 .a = { .avg = 11.6f, .std = 2.2f },
133 .b = { .avg = 17.7f, .std = 1.8f } },
134 { .name = _("forehead"),
135 .ethnicity = ETHNIE_KURDISH,
136 .L = { .avg = 56.1f, .std = 4.5f },
137 .a = { .avg = 11.3f, .std = 2.1f },
138 .b = { .avg = 16.4f, .std = 2.2f } },
139 { .name = _("forehead"),
140 .ethnicity = ETHNIE_CAUCASIAN,
141 .L = { .avg = 59.2f, .std = 5.1f },
142 .a = { .avg = 11.6f, .std = 2.8f },
143 .b = { .avg = 15.1f, .std = 2.3f } },
144 { .name = _("forehead"),
145 .ethnicity = ETHNIE_AFRICAN_AM,
146 .L = { .avg = 44.0f, .std = 2.0f },
147 .a = { .avg = 14.0f, .std = 1.0f },
148 .b = { .avg = 19.0f, .std = 1.0f } },
149 { .name = _("forehead"),
150 .ethnicity = ETHNIE_MEXICAN,
151 .L = { .avg = 58.0f, .std = 1.0f },
152 .a = { .avg = 15.0f, .std = 1.0f },
153 .b = { .avg = 21.0f, .std = 1.0f } },
154 { .name = _("cheek"),
155 .ethnicity = ETHNIE_CHINESE,
156 .L = { .avg = 58.9f, .std = 3.1f },
157 .a = { .avg = 11.4f, .std = 2.1f },
158 .b = { .avg = 14.2f, .std = 1.5f } },
159 { .name = _("cheek"),
160 .ethnicity = ETHNIE_THAI,
161 .L = { .avg = 60.7f, .std = 4.0f },
162 .a = { .avg = 10.5f, .std = 2.3f },
163 .b = { .avg = 17.2f, .std = 2.1f } },
164 { .name = _("cheek"),
165 .ethnicity = ETHNIE_KURDISH,
166 .L = { .avg = 58.f, .std = 4.4f },
167 .a = { .avg = 11.7f, .std = 2.3f },
168 .b = { .avg = 15.8f, .std = 2.1f } },
169 { .name = _("cheek"),
170 .ethnicity = ETHNIE_CAUCASIAN,
171 .L = { .avg = 59.6f, .std = 5.5f },
172 .a = { .avg = 11.8f, .std = 3.1f },
173 .b = { .avg = 14.6f, .std = 2.6f } },
174 { .name = _("cheek"),
175 .ethnicity = ETHNIE_AFRICAN_AM,
176 .L = { .avg = 48.0f, .std = 1.0f },
177 .a = { .avg = 15.0f, .std = 1.0f },
178 .b = { .avg = 20.0f, .std = 1.0f } },
179 { .name = _("cheek"),
180 .ethnicity = ETHNIE_MEXICAN,
181 .L = { .avg = 63.0f, .std = 1.0f },
182 .a = { .avg = 16.0f, .std = 1.0f },
183 .b = { .avg = 21.0f, .std = 1.0f } } };
184
185 return skin[index];
186}
187
189#define LCH_HUE_SECTORS 15
190
192#define LCH_LIGHTNESS_SECTORS 5
193
194/* Colour names by [hue sector][lightness sector].
195 *
196 * Reference: https://chromatone.center/theory/color/models/perceptual/ -- ignored in places
197 * where it gets too lyrical, in favour of more down-to-earth names.
198 *
199 * Sectors 10 (240 deg) and 11 (264 deg) hold the same names on purpose: CIE Lab 1976 is
200 * poor in the blues, so the two were collapsed when this was a branch ladder. The
201 * duplication is kept explicit rather than special-cased, so the table reads as a plain
202 * grid and a future correction to either sector does not have to untangle a shared branch.
203 *
204 * N_() registers each name for translation; the lookup translates with _() at use.
205 */
207 { N_("deep purple"), N_("fuchsia"), N_("medium magenta"), N_("violet pink"), N_("plum violet") }, // 0°
208 { N_("dark red"), N_("red"), N_("crimson"), N_("salmon"), N_("pink") }, // 24°
209 { N_("maroon"), N_("dark orange red"), N_("orange red"), N_("coral"), N_("khaki") }, // 48°
210 { N_("brown"), N_("chocolate"), N_("dark gold"), N_("gold"), N_("sandy brown") }, // 72°
211 { N_("dark green"), N_("dark olive green"), N_("olive"), N_("khaki"), N_("beige") }, // 96°
212 { N_("dark green"), N_("forest green"), N_("olive drab"), N_("yellow green"), N_("pale green") }, // 120°
213 { N_("dark green"), N_("green"), N_("forest green"), N_("lime green"), N_("pale green") }, // 144°
214 { N_("dark sea green"), N_("sea green"), N_("teal"), N_("light sea green"), N_("turquoise") }, // 168°
215 { N_("dark slate gray"), N_("light slate gray"), N_("dark cyan"), N_("aqua"), N_("cyan") }, // 192°
216 { N_("navy blue"), N_("teal"), N_("dark cyan"), N_("deep sky blue"), N_("aquamarine blue") }, // 216°
217 { N_("dark blue"), N_("medium blue"), N_("azure blue"), N_("deep sky blue"), N_("aqua") }, // 240°
218 { N_("dark blue"), N_("medium blue"), N_("azure blue"), N_("deep sky blue"), N_("aqua") }, // 264°
219 { N_("dark blue"), N_("medium blue"), N_("blue"), N_("light sky blue"), N_("light blue") }, // 288°
220 { N_("indigo"), N_("dark violet"), N_("blue violet"), N_("violet"), N_("plum") }, // 312°
221 { N_("purple"), N_("dark magenta"), N_("magenta"), N_("violet"), N_("lavender") }, // 336°
222};
223
225{
226 // color must be Lch derivated from CIE Lab 1976 turned into polar coordinates
227
228 // First check if we have a gray (chromacity < epsilon)
229 if(color[1] < 2.0f) return g_strdup(_("gray"));
230
231 // Start with special cases : skin tones
233 dt_LCH_2_Lab(color, Lab);
234
235 gchar *out = NULL;
236 gboolean matches[ETHNIE_END] = { FALSE };
237
238 // Find a match against any body part and write the associated ethnicity
239 for(int elem = 0; elem < SKINS; ++elem)
240 {
241 skin_color_t skin = get_skin_color(elem);
242 range_t L = _compute_range(skin.L, 1.5f);
243 range_t a = _compute_range(skin.a, 1.5f);
244 range_t b = _compute_range(skin.b, 1.5f);
245
246 const gboolean match = (Lab[0] > L.bottom && Lab[0] < L.top)
247 && (Lab[1] > a.bottom && Lab[1] < a.top)
248 && (Lab[2] > b.bottom && Lab[2] < b.top);
249
250 if(match) matches[skin.ethnicity] = TRUE;
251 }
252
253 // Write all matching ethnicities
254 for(ethnicities_t elem = 0; elem < ETHNIE_END; ++elem)
255 if(matches[elem])
256 out = dt_util_dstrcat(out, _("average %s skin tone\n"), _get_ethnicity_name(elem));
257
258 if(!IS_NULL_PTR(out)) return out;
259
260 const float h = color[2] * 360.f; // degrees
261 const float L = color[0]; // percent
262
263 const int step_h = (int)h / 24;
264 const int step_L = (int)(fminf(L, 100.f)) / 20;
265
266 // h == 360 exactly (and any out-of-domain hue) lands outside the table.
267 if(step_h < 0 || step_h >= LCH_HUE_SECTORS) return g_strdup(_("color not found"));
268 if(step_L < 0 || step_L >= LCH_LIGHTNESS_SECTORS) return g_strdup(_("color not found"));
269
270 return g_strdup(_(_lch_color_names[step_h][step_L]));
271}
272
274{
275 float max_chroma = 0.f;
276 float min_chroma = 99999.f;
277
278 // Mind the fact that Lch uses atan2 which returns angles in ]-pi;pi]
279 float max_hue = -M_PI_F;
280 float min_hue = M_PI_F;
281
282 for(int elem = 0; elem < SKINS; ++elem)
283 {
284 skin_color_t skin = get_skin_color(elem);
285 range_t Lr = _compute_range(skin.L, 3.f);
286 range_t ar = _compute_range(skin.a, 1.5f);
287 range_t br = _compute_range(skin.b, 1.5f);
288
289 const float L[2] = { Lr.top, Lr.bottom };
290 const float a[2] = { ar.top, ar.bottom };
291 const float b[2] = { br.top, br.bottom };
292
293 for(int i = 0; i < 2; i++)
294 for(int j = 0; j < 2; j++)
295 for(int k = 0; k < 2; k++)
296 {
297 dt_aligned_pixel_t Lab = { L[i], a[j], b[k], 1.f };
298 dt_aligned_pixel_t XYZ = { 0.f };
299 dt_aligned_pixel_t xyY = { 0.f };
300 dt_aligned_pixel_t Lch = { 0.f };
303 dt_xyY_to_Lch(xyY, Lch);
304
305 if(Lch[2] > max_hue) max_hue = Lch[2];
306 if(Lch[2] < min_hue) min_hue = Lch[2];
307
308 if(Lch[1] < min_chroma) min_chroma = Lch[1];
309 if(Lch[1] > max_chroma) max_chroma = Lch[1];
310 }
311 }
312
313 fprintf(stdout, "Chroma : [%f;%f], Hue : [%f;%f]\n", min_chroma, max_chroma, min_hue, max_hue);
314}
315// clang-format off
316// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
317// vim: shiftwidth=2 expandtab tabstop=2 cindent
318// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
319// clang-format on
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
char * Lch_to_color_name(dt_aligned_pixel_t color)
Name the colour at color in plain language, e.g. "salmon" or "olive drab".
char * _get_ethnicity_name(const ethnicities_t index)
skin_color_t get_skin_color(const size_t index)
static range_t _compute_range(const gaussian_stats_t stats, const float factor)
static const char *const _lch_color_names[15][5]
void get_skin_tones_range()
#define LCH_LIGHTNESS_SECTORS
#define LCH_HUE_SECTORS
ethnicities_t
@ ETHNIE_CAUCASIAN
@ ETHNIE_KURDISH
@ ETHNIE_END
@ ETHNIE_CHINESE
@ ETHNIE_MEXICAN
@ ETHNIE_THAI
@ ETHNIE_AFRICAN_AM
#define SKINS
static float4 dt_XYZ_to_xyY(const float4 XYZ)
Definition colorspace.h:635
static dt_aligned_pixel_t xyY
dt_Lab_to_XYZ(Lab, XYZ)
static dt_aligned_pixel_t XYZ
static dt_aligned_pixel_t Lab
const dt_colormatrix_t dt_aligned_pixel_t out
static dt_aligned_pixel_t Lch
#define M_PI_F
GdkRGBA color[]
Definition geotagging.c:541
float *const restrict const size_t k
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
const float factor
Definition pdf.h:91
DT_ALIGNED_PIXEL float dt_aligned_pixel_t[4]
Definition simd.h:53
ethnicities_t ethnicity
gaussian_stats_t b
gaussian_stats_t L
gaussian_stats_t a
gchar * dt_util_dstrcat(gchar *str, const gchar *format,...)
Definition utility.c:99