Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
test_filmicrgb.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2010-2011 Henrik Andersson.
4 Copyright (C) 2010 johannes hanika.
5 Copyright (C) 2010 Pascal de Bruijn.
6 Copyright (C) 2012 Richard Wonka.
7 Copyright (C) 2013-2014 Jérémy Rosen.
8 Copyright (C) 2016 Tobias Ellinghaus.
9 Copyright (C) 2020 Aldric Renaudin.
10 Copyright (C) 2020-2021, 2025 Aurélien PIERRE.
11 Copyright (C) 2020 Martin Burri.
12 Copyright (C) 2020 Pascal Obry.
13 Copyright (C) 2021 luzpaz.
14 Copyright (C) 2021 Profoktor.
15 Copyright (C) 2022 Martin Bařinka.
16
17 darktable is free software: you can redistribute it and/or modify
18 it under the terms of the GNU General Public License as published by
19 the Free Software Foundation, either version 3 of the License, or
20 (at your option) any later version.
21
22 darktable is distributed in the hope that it will be useful,
23 but WITHOUT ANY WARRANTY; without even the implied warranty of
24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 GNU General Public License for more details.
26
27 You should have received a copy of the GNU General Public License
28 along with darktable. If not, see <http://www.gnu.org/licenses/>.
29*/
30/*
31 * cmocka unit tests for the module iop/filmicrgb.c
32 *
33 * Please see README.md for more detailed documentation.
34 */
35#include "system/mem_alloc.h" // dt_free
36#include <limits.h>
37#include <setjmp.h>
38#include <stdarg.h>
39#include <stddef.h>
40#include <stdio.h>
41#include <math.h>
42
43#include <cmocka.h>
44
45#include "../util/assert.h"
46#include "../util/tracing.h"
47#include "../util/testimg.h"
48
49#include "iop/filmicrgb.c"
50
51#ifdef _WIN32
52#include "win/main_wrapper.h"
53#endif
54
55/*
56 * DEFINITIONS
57 */
58
59// epsilon for floating point comparison (1e-6 is approximately 20 EV below pure
60// white):
61#define E 1e-6f
62
63/*
64 * MOCKED FUNCTIONS
65 */
66
68{
69 check_expected_ptr(module);
70 check_expected(update);
71}
72
73
74/*
75 * TEST FUNCTIONS
76 */
77
78static void test_name(void **state)
79{
80 // the underscore is the mnemonic marker (see "Modules: implement mnemonics on names")
81 assert_string_equal(name(), "fil_mic");
82}
83
84static void test_default_group(void **state)
85{
86 assert_int_equal(default_group(), IOP_GROUP_TONES);
87}
88
89static void test_clamp_simd(void **state)
90{
91 for (float x = -0.5f; x <= 1.5f; x += 0.1f)
92 {
93 if (x < 0.0f)
94 {
96 }
97 else if (x > 1.0f)
98 {
100 }
101 else
102 {
104 }
105 }
106}
107
109{
110 Testimg *ti;
111
112 TR_STEP("verify that norm is correct and in ]0.0; 1.0] for rgb values "
113 "in ]0.0; 1.0]");
116 {
117 p[3] = 2.0f; // to make sure pixel[3] has no influence
118 float norm = pixel_rgb_norm_power(p);
119 TR_DEBUG("pixel={%e, %e, %e) => norm=%e", p[0], p[1], p[2], norm);
120 float numerator = p[0] * p[0] * p[0] + p[1] * p[1] * p[1] + p[2] * p[2] * p[2];
121 float denominator = p[0] * p[0] + p[1] * p[1] + p[2] * p[2];
122 float exp_norm = numerator / denominator;
123 assert_float_equal(norm, exp_norm, E);
124 assert_true(norm > 0.0f);
125 assert_true(norm <= 1.0f + 1e-6f);
126 }
127 testimg_free(ti);
128
129 TR_STEP("verify that norm is equal to pixel (r=g=b) value on greyscale "
130 "values");
133 {
134 p[3] = 2.0f; // to make sure pixel[3] has no influence
135 float norm = pixel_rgb_norm_power(p);
136 TR_DEBUG("pixel={%e, %e, %e) => norm=%e", p[0], p[1], p[2], norm);
137 assert_float_equal(norm, p[0], E);
138 }
139 testimg_free(ti);
140
141 TR_STEP("verify that norm is in ]0; +inf[ for bad greyscale pixels in "
142 "]0; +inf[");
143 TR_BUG("norm is undefined for extreme values, thus values outside "
144 "[1e-6; 1e6] are excluded from assertion.");
147 {
148 float norm = pixel_rgb_norm_power(p);
149 TR_DEBUG("pixel={%e, %e, %e) => norm=%e", p[0], p[1], p[2], norm);
150 if (p[0] > 1e-6 && p[0] < 1e6)
151 {
152 assert_true(norm > 0.0f);
153 assert_true(norm <= FLT_MAX);
154 }
155 }
156 testimg_free(ti);
157
158 TR_STEP("verify that norm is in ]0; +inf[ for bad negative greyscale pixels "
159 "in ]-inf; 0]");
160 TR_BUG("norm is undefined for extreme values, thus values outside "
161 "[1e-6; 1e6] are excluded from assertion.");
162 TR_BUG("norm is 0 if input is 0.");
165 {
166 float norm = pixel_rgb_norm_power(p);
167 TR_DEBUG("pixel={%e, %e, %e) => norm=%e", p[0], p[1], p[2], norm);
168 if (fabsf(p[0]) > 1e-6 && fabsf(p[0]) < 1e6)
169 {
170 assert_true(norm > 0.0f);
171 assert_true(norm <= FLT_MAX);
172 }
173 if (p[0] > -FLT_MIN && p[0] < FLT_MIN) // translates to: if(p[0] == 0)
174 {
175 assert_float_equal(norm, 0.0f, FLT_MIN);
176 }
177 }
178 testimg_free(ti);
179}
180
181static void test_get_pixel_norm(void **state)
182{
183 Testimg *ti;
184 // dt_iop_order_iccprofile_info_t work_profile; // see TODOs below
185
186 TR_STEP("verify that max-rgb norm is correct and in ]0.0; 1.0] for rgb "
187 "values in ]0.0; 1.0]");
190 {
191 p[3] = 2.0f; // to make sure pixel[3] has no influence
192 float norm = get_pixel_norm(p, DT_FILMIC_METHOD_MAX_RGB, NULL);
193 TR_DEBUG("pixel={%e, %e, %e, %e} => norm=%e", p[0], p[1], p[2], p[3], norm);
194 assert_float_equal(norm, fmax(p[0], fmax(p[1], p[2])), E);
195 assert_true(norm > 0.0f);
196 assert_true(norm <= 1.0f + E);
197 }
198 testimg_free(ti);
199
200 TR_STEP("verify that max-rgb norm is equal to pixel (r=g=b) value on "
201 "greyscale values");
204 {
205 p[3] = 2.0f; // to make sure pixel[3] has no influence
206 float norm = get_pixel_norm(p, DT_FILMIC_METHOD_MAX_RGB, NULL);
207 TR_DEBUG("pixel={%e, %e, %e) => norm=%e", p[0], p[1], p[2], norm);
208 assert_float_equal(norm, p[0], E);
209 }
210 testimg_free(ti);
211
212 TR_STEP("verify that max-rgb norm is in ]0; +inf[ for bad greyscale pixels "
213 "in ]0; +inf[");
216 {
217 float norm = get_pixel_norm(p, DT_FILMIC_METHOD_MAX_RGB, NULL);
218 TR_DEBUG("pixel={%e, %e, %e, %e} => norm=%e", p[0], p[1], p[2], p[3], norm);
219 assert_true(norm > 0.0f);
220 assert_true(norm <= FLT_MAX);
221 }
222 testimg_free(ti);
223
224 TR_STEP("verify that max-rgb norm is in ]0; +inf[ for bad negative greyscale "
225 "pixels in ]-inf; 0]");
226 TR_BUG("max-rgb norm is unbounded and negative for pixels with all-negative "
227 "colors.");
230 {
231 float norm = get_pixel_norm(p, DT_FILMIC_METHOD_MAX_RGB, NULL);
232 TR_DEBUG("pixel={%e, %e, %e, %e} => norm=%e", p[0], p[1], p[2], p[3], norm);
233 // bug: assert_true(norm > 0.0f);
234 assert_true(norm <= FLT_MAX);
235 }
236 testimg_free(ti);
237
238 TR_STEP("verify luminance-y norm (verify subsequent function calls)");
239 // TODO: find out how to mock inline functions!
240
241 TR_STEP("verify power norm (verify subsequent function calls)");
242 // note: the norm itself is verified in test_pixel_rgb_norm_power(), so here
243 // we only verify that the function pixel_rgb_norm_power() is called.
244 // TODO: find out how to mock inline functions!
245}
246
247static void test_log_tonemapping(void **state)
248{
249 Testimg *ti;
250 float grey = 0.1845f;
251 float dyn_range = TESTIMG_STD_DYN_RANGE_EV;
252 float black = log2f(1.0f/grey) - dyn_range;
253 const float MIN = 0.0f;
254 const float MAX = 1.0f;
255
256 TR_STEP("verify that output is equal to log-mapped input for equal dynamic "
257 "range and grey/black points");
260 {
261 float ret = log_tonemapping(p[0], grey, black, dyn_range);
262 TR_DEBUG("%e => %e", p[0], ret);
263 float exp = testimg_val_to_log(p[0]);
264 if (exp < MIN)
265 {
266 assert_float_equal(ret, MIN, E); // bound to -16EV
267 }
268 else
269 {
270 assert_float_equal(ret, exp, E);
271 }
272 }
273 testimg_free(ti);
274
275 TR_STEP("verify that output is 1 EV brighter (and clipped to [0; 1]) when "
276 "grey is set to half");
279 {
280 float ret = log_tonemapping(p[0], (grey / 2.0f), black, dyn_range);
281 TR_DEBUG("%e => %e", p[0], ret);
282 float exp = testimg_val_to_log(p[0] * 2.0f); // *2.0 means +1EV
283 if (exp < MIN)
284 {
285 assert_float_equal(ret, MIN, E); // bound to 2^-16
286 }
287 else if (exp > MAX)
288 {
289 assert_float_equal(ret, MAX, E); // bound to 1.0
290 }
291 else
292 {
293 assert_float_equal(ret, exp, E);
294 }
295 }
296 testimg_free(ti);
297
298 TR_STEP("verify that output is bound to [0; 1] for all non-negative values");
301 {
302 float ret = log_tonemapping(p[0], grey, black, dyn_range);
303 TR_DEBUG("{%e, %e, %e, %e} => %e", p[0], p[1], p[2], p[3], ret);
304 assert_true(ret >= MIN);
305 assert_true(ret <= MAX);
306 }
307 testimg_free(ti);
308
309 TR_STEP("verify that output is bound to [0; 1] for all negative values "
310 "(incl. 0.0)");
313 {
314 float ret = log_tonemapping(p[0], grey, black, dyn_range);
315 TR_DEBUG("{%e, %e, %e, %e} => %e", p[0], p[1], p[2], p[3], ret);
316 assert_true(ret >= MIN);
317 assert_true(ret <= MAX);
318 }
319 testimg_free(ti);
320}
321
322static void test_filmic_spline(void **state)
323{
324 // TODO: write tests for the method test_filmic_spline
325 //
326 // The problem with this method is that it needs the spline parameters that
327 // are hard to figure out. We could call dt_iop_filmic_rgb_compute_spline() to
328 // get the parameters but then it is still hard to estimate what the asserts
329 // should look like.
330 //
331 // Done a code review of the method test_filmic_spline() and I think it is ok.
332
333 TR_NOTE("method verified by code review only since it is hard to test it and "
334 "the benefit is questionable");
335}
336
337// helper method to map gui saturation to internally used one:
338static float saturation_gui_to_internal(float saturation_percent)
339{
340 // TODO: there is a flaw in conversion of saturation from gui value to
341 // internal value. Discussed this with @aurelienpierre and decision was to
342 // leave it for the moment (Feb 2020). This code here needs to be adapted when
343 // the bug gets fixed.
344
345 TR_BUG("saturation conversion from gui to internal is wrong");
346 return (2.0f * saturation_percent / 100.0f + 1.0f); // copied from filmicrgb.c
347 //fix: return 100.0f / fmaxf(100.0f - saturation_percent, 1e-6);
348}
349
351{
352 Testimg *ti;
353
354 // input values
355 float lattitude_min = 0.2;
356 float lattitude_max = 0.2; // symmetrical
357 float saturation_percent = 5.0f;
358
359 // computed values
360 // copied 2 lines from filmicrgb.c:
361 float sigma_toe = powf(lattitude_min / 3.0f, 2.0f);
362 float sigma_shoulder = powf(lattitude_max / 3.0f, 2.0f);
363
364 float saturation = saturation_gui_to_internal(saturation_percent);
365
366 TR_STEP("verify values are correct for different latitudes");
367 TR_BUG("values inside latitude are not always 1.0 (but very close), "
368 "especially at the borders");
369 for (float latitude_min = 0.1f; latitude_min < 0.5f + E; latitude_min += 0.1f)
370 {
371 for (float latitude_max = 0.1f; latitude_max < 0.5f + E; latitude_max += 0.1f)
372 {
373 TR_DEBUG("saturation=%e", saturation);
374 TR_DEBUG("latitude_min=%e", latitude_min);
375 TR_DEBUG("latitude_max=%e", latitude_max);
376
377 // copied 2 lines from filmicrgb.c:
378 sigma_toe = powf(lattitude_min / 3.0f, 2.0f);
379 sigma_shoulder = powf(lattitude_max / 3.0f, 2.0f);
380
381 TR_DEBUG("sigma_toe=%e", sigma_toe);
382 TR_DEBUG("sigma_shoulder=%e", sigma_shoulder);
383
384 // filmic_desaturate works in log space:
385 // create image with values from 0.0 to 1.0 in 0.05 steps:
388 {
389 float ret =
390 filmic_desaturate_v1(p[0], sigma_toe, sigma_shoulder, saturation);
391 TR_DEBUG("%e => %e", p[0], ret);
392
393 if (lattitude_min == lattitude_max)
394 {
395 // values symmetric (due to sigma_shoulder = sigma_toe):
396 float *p1 = get_pixel(ti, ti->width - x - 1, y);
397 float exp = filmic_desaturate_v1(p1[0], sigma_toe, sigma_shoulder,
398 saturation);
399 assert_float_equal(ret, exp, E);
400 }
401
402 // values correct on extreme borders:
403 if (x == 0 || x == ti->width - 1)
404 {
405 assert_float_equal(ret, 1.0f - 1.0f / saturation, E);
406 }
407
408 //bug: values close to 1.0 during latitude, not exactly 1.0:
409 if (x > (lattitude_min * ti->width) &&
410 x < ((1.0f - lattitude_max) * ti->width - 1))
411 {
412 assert_float_equal(ret, 1.0f, 1e-2);
413 }
414 }
415 testimg_free(ti);
416 }
417 }
418
419 TR_STEP("verify return value is always 1.0 when saturation is set to maximum");
420 TR_BUG("values inside latitude are not always 1.0 (but very close), "
421 "especially at the borders");
422 // create image with values from 0.0 to 1.0 in 0.05 steps:
424 saturation = saturation_gui_to_internal(1e6); // TODO: take 100%
426 {
427 float ret = filmic_desaturate_v1(p[0], sigma_toe, sigma_shoulder, saturation);
428 TR_DEBUG("%e => %e", p[0], ret);
429 //bug: values close to 1.0 during latitude, not exactly 1.0:
430 assert_float_equal(ret, 1.0f, 1e-2);
431 }
432 // set saturation back:
433 saturation = saturation_gui_to_internal(saturation_percent);
434 testimg_free(ti);
435
436 TR_STEP("verify output is in ]0; 1] for bad values in ]0; +inf[");
439 {
440 float ret = filmic_desaturate_v1(p[0], sigma_toe, sigma_shoulder, saturation);
441 TR_DEBUG("{%e} => %e", p[0], ret);
442 assert_true(ret > 0.0f);
443 assert_true(ret <= 1.0f);
444 }
445 testimg_free(ti);
446
447 TR_STEP("verify output is in ]0; 1] for bad negative values in ]-inf; 0]");
450 {
451 float ret = filmic_desaturate_v1(p[0], sigma_toe, sigma_shoulder, saturation);
452 TR_DEBUG("{%e} => %e", p[0], ret);
453 assert_true(ret > 0.0f);
454 assert_true(ret <= 1.0f);
455 }
456 testimg_free(ti);
457}
458
459static void test_linear_saturation(void **state)
460{
461 Testimg *ti;
462
463 float luminance = 1.0f;
464 float saturation = 0.05f;
465 float ratios[] = { 0.2126, 0.7152, 0.0722 };
466
467 TR_STEP("verify that output is equal to value for greyscale values");
470 {
471 luminance = p[0]; // luminance := value, for greyscale values
472 float s0 = linear_saturation(p[0], luminance, saturation);
473 float s1 = linear_saturation(p[1], luminance, saturation);
474 float s2 = linear_saturation(p[2], luminance, saturation);
475 TR_DEBUG("pixel={%e, %e, %e) => linear_saturation={%e, %e, %e}",
476 p[0], p[1], p[2], s0, s1, s2);
477 assert_float_equal(s0, p[0], E);
478 assert_float_equal(s1, p[1], E);
479 assert_float_equal(s2, p[2], E);
480 }
481 testimg_free(ti);
482
483 TR_STEP("verify that output is equal to value for rgb values when saturation "
484 "is 1.0");
485 saturation = 1.0f;
488 {
489 luminance = p[0] * ratios[0] + p[1] * ratios[1] + p[2] * ratios[2];
490 float s0 = linear_saturation(p[0], luminance, saturation);
491 float s1 = linear_saturation(p[1], luminance, saturation);
492 float s2 = linear_saturation(p[2], luminance, saturation);
493 TR_DEBUG("pixel={%e, %e, %e) => linear_saturation={%e, %e, %e}",
494 p[0], p[1], p[2], s0, s1, s2);
495 assert_float_equal(s0, p[0], E);
496 assert_float_equal(s1, p[1], E);
497 assert_float_equal(s2, p[2], E);
498 }
499 testimg_free(ti);
500
501 TR_STEP("verify that output is pure grey, equal to luminance, for rgb values "
502 "when saturation is 0.0");
503 saturation = 0.0f;
506 {
507 luminance = p[0] * ratios[0] + p[1] * ratios[1] + p[2] * ratios[2];
508 float s0 = linear_saturation(p[0], luminance, saturation);
509 float s1 = linear_saturation(p[1], luminance, saturation);
510 float s2 = linear_saturation(p[2], luminance, saturation);
511 TR_DEBUG("pixel={%e, %e, %e) => linear_saturation={%e, %e, %e}",
512 p[0], p[1], p[2], s0, s1, s2);
513 assert_float_equal(s0, s1, E);
514 assert_float_equal(s0, s2, E);
516 }
517 testimg_free(ti);
518}
519
520/*
521 * MAIN FUNCTION
522 */
523int main(int argc, char* argv[])
524{
525 const struct CMUnitTest tests[] = {
526 cmocka_unit_test(test_name),
527 cmocka_unit_test(test_default_group),
528 cmocka_unit_test(test_clamp_simd),
529 cmocka_unit_test(test_pixel_rgb_norm_power),
530 cmocka_unit_test(test_get_pixel_norm),
531 cmocka_unit_test(test_log_tonemapping),
532 cmocka_unit_test(test_filmic_spline),
533 cmocka_unit_test(test_filmic_desaturate_v1),
534 cmocka_unit_test(test_linear_saturation)
535 };
536
537 TR_DEBUG("epsilon = %e", E);
538
539 return cmocka_run_group_tests(tests, NULL, NULL);
540}
541// clang-format off
542// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
543// vim: shiftwidth=2 expandtab tabstop=2 cindent
544// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
545// clang-format on
int default_group()
Definition ashift.c:183
#define assert_float_equal(a, b, epsilon)
Definition assert.h:35
static const float x
@ DT_FILMIC_METHOD_MAX_RGB
Definition filmicrgb.c:149
static float log_tonemapping(const float x, const float grey, const float black, const float dynamic_range)
Definition filmicrgb.c:1047
static float linear_saturation(const float x, const float luminance, const float saturation)
Definition filmicrgb.c:1193
@ IOP_GROUP_TONES
Definition imageop.h:156
float *const restrict luminance
static float clamp_simd(const float x)
const char * name
Definition pdf.h:90
int main()
Definition prova.c:47
const float uint32_t state[4]
int width
Definition testimg.h:29
#define E
static void test_filmic_desaturate_v1(void **state)
static float saturation_gui_to_internal(float saturation_percent)
static void test_log_tonemapping(void **state)
static void test_default_group(void **state)
static void test_get_pixel_norm(void **state)
static void test_linear_saturation(void **state)
static void test_pixel_rgb_norm_power(void **state)
static void test_filmic_spline(void **state)
static void test_name(void **state)
void __wrap_dt_iop_color_picker_reset(dt_iop_module_t *module, gboolean update)
static void test_clamp_simd(void **state)
void testimg_free(Testimg *const ti)
Definition testimg.c:45
Testimg * testimg_gen_grey_max_dr_neg()
Definition testimg.c:249
float testimg_val_to_log(const float val)
Definition testimg.c:109
Testimg * testimg_to_log(Testimg *ti)
Definition testimg.c:97
Testimg * testimg_gen_grey_space(const int width)
Definition testimg.c:158
Testimg * testimg_gen_grey_max_dr()
Definition testimg.c:223
Testimg * testimg_gen_rgb_space(const int width)
Definition testimg.c:200
static float * get_pixel(const Testimg *const ti, const int x, const int y)
Definition testimg.h:59
#define TESTIMG_STD_WIDTH
Definition testimg.h:44
#define for_testimg_pixels_p_yx(ti)
Definition testimg.h:71
#define for_testimg_pixels_p_xy(ti)
Definition testimg.h:66
#define TESTIMG_STD_DYN_RANGE_EV
Definition testimg.h:41
#define MIN(a, b)
Definition thinplate.c:32
#define MAX(a, b)
Definition thinplate.c:29
#define TR_NOTE(msg,...)
Definition tracing.h:48
#define TR_BUG(msg,...)
Definition tracing.h:43
#define TR_DEBUG(msg,...)
Definition tracing.h:53
#define TR_STEP(msg,...)
Definition tracing.h:38