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
26// get a range of 2 x factor x std centered in avg
27static range_t
28_compute_range(const gaussian_stats_t stats, const float factor)
29{
31 out.bottom = stats.avg - factor * stats.std;
32 out.top = stats.avg + factor * stats.std;
33 return out;
34}
35
36// Human skin tones database
37// This is a racially-charged matter, tread with it carefully.
38
39// Usable data are : tabulated avg ± std (P < 0.05) models on skin color measurements
40// on more than 80 individuals under D65 illuminant.
41
42// Notice all these data are valid only under D65 illuminant and errors up to delta E = 6 have
43// been measured for A illuminant. Proper camera profiling and chromatic adaptation needs to be performed
44// or all the following is meaningless.
45
46// We use ranges of avg ± 2 std, giving 95 % of confidence in the prediction.
47
48// We use CIE Lab instead of Lch coordinates, because a and b parameters are physiologically meaningful :
49// - a (redness) is linked to blood flow and health,
50// - b (yellowness) is linked to melanine and sun tan.
51
52/* Reference :
53 XIAO, Kaida, YATES, Julian M., ZARDAWI, Faraedon, et al.
54 Characterising the variations in ethnic skin colours: a new calibrated data base for human skin.
55 Skin Research and Technology, 2017, vol. 23, no 1, p. 21-29.
56 https://onlinelibrary.wiley.com/doi/pdf/10.1111/srt.12295
57
58 Sample : 187 caucasian, 202 chinese, 145 kurdish and 426 thai.
59
60 DE RIGAL, Jean, DES MAZIS, Isabelle, DIRIDOLLOU, Stephane, et al.
61 The effect of age on skin color and color heterogeneity in four ethnic groups.
62 Skin Research and Technology, 2010, vol. 16, no 2, p. 168-178.
63 https://pubmed.ncbi.nlm.nih.gov/20456097/
64
65 Sample : 121 african-american, 64 mexican.
66 Note : the data have been read on the graph and are inaccurate and std is majorated.
67 The original authors have been contacted to get the tabulated, accurate data,
68 but the main author is retired, co-authors have changed jobs, and the L'Oréal head of R&D
69 did not respond. So the values here are given for what it's worth.
70*/
71
72// "Forearm" is the ventral forearm skin. It is the least sun-tanned part of skin.
73// Sun tan will depend the most on lifestyle, therefore the ventral forearm
74// is the least socially-biased skin color metric.
75
76// "Forehead" is the most sun-tanned part of skin. This translates to high b coordinate.
77
78// "Cheek" is the most redish part of skin. This translates to high a coordinate.
79
80// L decreases with age in all ethnicities and with b/yellowness/melanine/tan.
81
82
84{
85 const ethnicity_t ethnies[ETHNIE_END] =
86 { { .name = _("Chinese"), .ethnicity = ETHNIE_CHINESE },
87 { .name = _("Thai"), .ethnicity = ETHNIE_THAI },
88 { .name = _("Kurdish"), .ethnicity = ETHNIE_KURDISH },
89 { .name = _("Caucasian"), .ethnicity = ETHNIE_CAUCASIAN },
90 { .name = _("African-american"), .ethnicity = ETHNIE_AFRICAN_AM },
91 { .name = _("Mexican"), .ethnicity = ETHNIE_MEXICAN } };
92
93 return ethnies[index].name;
94}
95
96
97skin_color_t get_skin_color(const size_t index)
98{
99 const skin_color_t skin[SKINS] = {
100 { .name = _("forearm"),
101 .ethnicity = ETHNIE_CHINESE,
102 .L = { .avg = 60.9f, .std = 3.4f },
103 .a = { .avg = 7.0f, .std = 1.7f },
104 .b = { .avg = 15.0f, .std = 1.8f } },
105 { .name = _("forearm"),
106 .ethnicity = ETHNIE_THAI,
107 .L = { .avg = 61.9f, .std = 3.7f },
108 .a = { .avg = 7.1f, .std = 1.7f },
109 .b = { .avg = 17.4f, .std = 2.0f } },
110 { .name = _("forearm"),
111 .ethnicity = ETHNIE_KURDISH,
112 .L = { .avg = 60.6f, .std = 4.8f },
113 .a = { .avg = 6.5f, .std = 1.6f },
114 .b = { .avg = 16.4f, .std = 2.3f } },
115 { .name = _("forearm"),
116 .ethnicity = ETHNIE_CAUCASIAN,
117 .L = { .avg = 63.0f, .std = 5.5f },
118 .a = { .avg = 5.6f, .std = 1.9f },
119 .b = { .avg = 14.0f, .std = 2.9f } },
120 { .name = _("forehead"),
121 .ethnicity = ETHNIE_CHINESE,
122 .L = { .avg = 56.4f, .std = 3.2f },
123 .a = { .avg = 11.7f, .std = 2.1f },
124 .b = { .avg = 16.3f, .std = 1.4f } },
125 { .name = _("forehead"),
126 .ethnicity = ETHNIE_THAI,
127 .L = { .avg = 56.8f, .std = 4.1f },
128 .a = { .avg = 11.6f, .std = 2.2f },
129 .b = { .avg = 17.7f, .std = 1.8f } },
130 { .name = _("forehead"),
131 .ethnicity = ETHNIE_KURDISH,
132 .L = { .avg = 56.1f, .std = 4.5f },
133 .a = { .avg = 11.3f, .std = 2.1f },
134 .b = { .avg = 16.4f, .std = 2.2f } },
135 { .name = _("forehead"),
136 .ethnicity = ETHNIE_CAUCASIAN,
137 .L = { .avg = 59.2f, .std = 5.1f },
138 .a = { .avg = 11.6f, .std = 2.8f },
139 .b = { .avg = 15.1f, .std = 2.3f } },
140 { .name = _("forehead"),
141 .ethnicity = ETHNIE_AFRICAN_AM,
142 .L = { .avg = 44.0f, .std = 2.0f },
143 .a = { .avg = 14.0f, .std = 1.0f },
144 .b = { .avg = 19.0f, .std = 1.0f } },
145 { .name = _("forehead"),
146 .ethnicity = ETHNIE_MEXICAN,
147 .L = { .avg = 58.0f, .std = 1.0f },
148 .a = { .avg = 15.0f, .std = 1.0f },
149 .b = { .avg = 21.0f, .std = 1.0f } },
150 { .name = _("cheek"),
151 .ethnicity = ETHNIE_CHINESE,
152 .L = { .avg = 58.9f, .std = 3.1f },
153 .a = { .avg = 11.4f, .std = 2.1f },
154 .b = { .avg = 14.2f, .std = 1.5f } },
155 { .name = _("cheek"),
156 .ethnicity = ETHNIE_THAI,
157 .L = { .avg = 60.7f, .std = 4.0f },
158 .a = { .avg = 10.5f, .std = 2.3f },
159 .b = { .avg = 17.2f, .std = 2.1f } },
160 { .name = _("cheek"),
161 .ethnicity = ETHNIE_KURDISH,
162 .L = { .avg = 58.f, .std = 4.4f },
163 .a = { .avg = 11.7f, .std = 2.3f },
164 .b = { .avg = 15.8f, .std = 2.1f } },
165 { .name = _("cheek"),
166 .ethnicity = ETHNIE_CAUCASIAN,
167 .L = { .avg = 59.6f, .std = 5.5f },
168 .a = { .avg = 11.8f, .std = 3.1f },
169 .b = { .avg = 14.6f, .std = 2.6f } },
170 { .name = _("cheek"),
171 .ethnicity = ETHNIE_AFRICAN_AM,
172 .L = { .avg = 48.0f, .std = 1.0f },
173 .a = { .avg = 15.0f, .std = 1.0f },
174 .b = { .avg = 20.0f, .std = 1.0f } },
175 { .name = _("cheek"),
176 .ethnicity = ETHNIE_MEXICAN,
177 .L = { .avg = 63.0f, .std = 1.0f },
178 .a = { .avg = 16.0f, .std = 1.0f },
179 .b = { .avg = 21.0f, .std = 1.0f } } };
180
181 return skin[index];
182}
183
185{
186 // color must be Lch derivated from CIE Lab 1976 turned into polar coordinates
187
188 // First check if we have a gray (chromacity < epsilon)
189
190 if(color[1] < 2.0f)
191 return _("gray");
192
193 // Start with special cases : skin tones
194
196 dt_LCH_2_Lab(color, Lab);
197
198 gchar *out = NULL;
199 int is_skin = FALSE;
200 gboolean matches[ETHNIE_END] = { FALSE };
201
202 // Find a match against any body part and write the associated ethnicity
203 for(int elem = 0; elem < SKINS; ++elem)
204 {
205 skin_color_t skin = get_skin_color(elem);
206 range_t L = _compute_range(skin.L, 1.5f);
207 range_t a = _compute_range(skin.a, 1.5f);
208 range_t b = _compute_range(skin.b, 1.5f);
209
210 const int match = (Lab[0] > L.bottom && Lab[0] < L.top) && (Lab[1] > a.bottom && Lab[1] < a.top)
211 && (Lab[2] > b.bottom && Lab[2] < b.top);
212
213 is_skin = is_skin || match;
214 if(match) matches[skin.ethnicity] = TRUE;
215 }
216
217 // Write all matching ethnicities
218 for(ethnicities_t elem = 0; elem < ETHNIE_END; ++elem)
219 if(matches[elem])
220 out = dt_util_dstrcat(out, _("average %s skin tone\n"), _get_ethnicity_name(elem));
221
222 if(is_skin) return out;
223
224 // Reference for color names : https://chromatone.center/theory/color/models/perceptual/
225 // Though we ignore them sometimes when they get too lyrical for some more down-to-earth names
226 // Color are read for chroma = [80 - 100].
227 const float h = color[2] * 360.f; // °
228 const float L = color[0];
229 //const float c = color[1];
230
231 // h in degrees - split into 15 hue sectors of 24°
232 const int step_h = (int)(h) / 24;
233
234 // L in % - split into 5 L sectors of 20 %
235 const int step_L = (int)(fminf(L, 100.f)) / 20;
236
237 if(step_h == 0)
238 {
239 // 0° - pinkish red
240 if(step_L == 0) return _("deep purple"); // L = 10 %
241 if(step_L == 1) return _("fuchsia"); // L = 30 %
242 if(step_L == 2) return _("medium magenta"); // L = 50 %
243 if(step_L == 3) return _("violet pink"); // L = 70 %
244 if(step_L == 4) return _("plum violet"); // L = 90 %
245 }
246 else if(step_h == 1)
247 {
248 // 24° - red
249 if(step_L == 0) return _("dark red");
250 if(step_L == 1) return _("red");
251 if(step_L == 2) return _("crimson");
252 if(step_L == 3) return _("salmon");
253 if(step_L == 4) return _("pink");
254 }
255 else if(step_h == 2)
256 {
257 // 48° - orangy red
258 if(step_L == 0) return _("maroon");
259 if(step_L == 1) return _("dark orange red");
260 if(step_L == 2) return _("orange red");
261 if(step_L == 3) return _("coral");
262 if(step_L == 4) return _("khaki");
263 }
264 else if(step_h == 3)
265 {
266 // 72° - orange
267 if(step_L == 0) return _("brown");
268 if(step_L == 1) return _("chocolate");
269 if(step_L == 2) return _("dark gold");
270 if(step_L == 3) return _("gold");
271 if(step_L == 4) return _("sandy brown");
272 }
273 else if(step_h == 4)
274 {
275 // 96° - yellow olive
276 if(step_L == 0) return _("dark green");
277 if(step_L == 1) return _("dark olive green");
278 if(step_L == 2) return _("olive");
279 if(step_L == 3) return _("khaki");
280 if(step_L == 4) return _("beige");
281 }
282 else if(step_h == 5)
283 {
284 // 120° - green
285 if(step_L == 0) return _("dark green");
286 if(step_L == 1) return _("forest green");
287 if(step_L == 2) return _("olive drab");
288 if(step_L == 3) return _("yellow green");
289 if(step_L == 4) return _("pale green");
290 }
291 else if(step_h == 6)
292 {
293 // 144° - blueish green
294 if(step_L == 0) return _("dark green");
295 if(step_L == 1) return _("green");
296 if(step_L == 2) return _("forest green");
297 if(step_L == 3) return _("lime green");
298 if(step_L == 4) return _("pale green");
299 }
300 else if(step_h == 7)
301 {
302 // 168° - greenish cyian
303 if(step_L == 0) return _("dark sea green");
304 if(step_L == 1) return _("sea green");
305 if(step_L == 2) return _("teal");
306 if(step_L == 3) return _("light sea green");
307 if(step_L == 4) return _("turquoise");
308 }
309 else if(step_h == 8)
310 {
311 // 192° - cyan
312 if(step_L == 0) return _("dark slate gray");
313 if(step_L == 1) return _("light slate gray");
314 if(step_L == 2) return _("dark cyan");
315 if(step_L == 3) return _("aqua");
316 if(step_L == 4) return _("cyan");
317 }
318 else if(step_h == 9)
319 {
320 // 216° - medium blue
321 if(step_L == 0) return _("navy blue");
322 if(step_L == 1) return _("teal");
323 if(step_L == 2) return _("dark cyan");
324 if(step_L == 3) return _("deep sky blue");
325 if(step_L == 4) return _("aquamarine blue");
326 }
327 else if(step_h == 10 || step_h == 11)
328 {
329 // 240° - blue and 264° - bluer than blue
330 // these are collapsed because CIE Lab 1976 sucks for blues
331 if(step_L == 0) return _("dark blue");
332 if(step_L == 1) return _("medium blue");
333 if(step_L == 2) return _("azure blue");
334 if(step_L == 3) return _("deep sky blue");
335 if(step_L == 4) return _("aqua");
336 }
337 else if(step_h == 12)
338 {
339 // 288° - more blue
340 if(step_L == 0) return _("dark blue");
341 if(step_L == 1) return _("medium blue");
342 if(step_L == 2) return _("blue");
343 if(step_L == 3) return _("light sky blue");
344 if(step_L == 4) return _("light blue");
345 }
346 else if(step_h == 13)
347 {
348 // 312° - violet
349 if(step_L == 0) return _("indigo");
350 if(step_L == 1) return _("dark violet");
351 if(step_L == 2) return _("blue violet");
352 if(step_L == 3) return _("violet");
353 if(step_L == 4) return _("plum");
354 }
355 else if(step_h == 14)
356 {
357 if(step_L == 0) return _("purple");
358 if(step_L == 1) return _("dark magenta");
359 if(step_L == 2) return _("magenta");
360 if(step_L == 3) return _("violet");
361 if(step_L == 4) return _("lavender");
362 }
363
364 return _("color not found");
365}
366
368{
369 float max_chroma = 0.f;
370 float min_chroma = 99999.f;
371
372 // Mind the fact that Lch uses atan2 which returns angles in ]-pi;pi]
373 float max_hue = -M_PI_F;
374 float min_hue = M_PI_F;
375
376 for(int elem = 0; elem < SKINS; ++elem)
377 {
378 skin_color_t skin = get_skin_color(elem);
379 range_t Lr = _compute_range(skin.L, 3.f);
380 range_t ar = _compute_range(skin.a, 1.5f);
381 range_t br = _compute_range(skin.b, 1.5f);
382
383 const float L[2] = { Lr.top, Lr.bottom };
384 const float a[2] = { ar.top, ar.bottom };
385 const float b[2] = { br.top, br.bottom };
386
387 for(int i = 0; i < 2; i++)
388 for(int j = 0; j < 2; j++)
389 for(int k = 0; k < 2; k++)
390 {
391 dt_aligned_pixel_t Lab = { L[i], a[j], b[k], 1.f };
392 dt_aligned_pixel_t XYZ = { 0.f };
393 dt_aligned_pixel_t xyY = { 0.f };
394 dt_aligned_pixel_t Lch = { 0.f };
397 dt_xyY_to_Lch(xyY, Lch);
398
399 if(Lch[2] > max_hue) max_hue = Lch[2];
400 if(Lch[2] < min_hue) min_hue = Lch[2];
401
402 if(Lch[1] < min_chroma) min_chroma = Lch[1];
403 if(Lch[1] > max_chroma) max_chroma = Lch[1];
404 }
405 }
406
407 fprintf(stdout, "Chroma : [%f;%f], Hue : [%f;%f]\n", min_chroma, max_chroma, min_hue, max_hue);
408}
409// clang-format off
410// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
411// vim: shiftwidth=2 expandtab tabstop=2 cindent
412// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
413// clang-format on
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
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)
void get_skin_tones_range()
const char * Lch_to_color_name(dt_aligned_pixel_t color)
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
float *const restrict const size_t k
float dt_aligned_pixel_t[4]
const float factor
Definition pdf.h:90
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:95