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 "common/darktable.h"
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 assert_string_equal(name(), "filmic rgb");
81}
82
83static void test_default_group(void **state)
84{
86}
87
88static void test_clamp_simd(void **state)
89{
90 for (float x = -0.5f; x <= 1.5f; x += 0.1f)
91 {
92 if (x < 0.0f)
93 {
95 }
96 else if (x > 1.0f)
97 {
99 }
100 else
101 {
103 }
104 }
105}
106
108{
109 Testimg *ti;
110
111 TR_STEP("verify that norm is correct and in ]0.0; 1.0] for rgb values "
112 "in ]0.0; 1.0]");
115 {
116 p[3] = 2.0f; // to make sure pixel[3] has no influence
117 float norm = pixel_rgb_norm_power(p);
118 TR_DEBUG("pixel={%e, %e, %e) => norm=%e", p[0], p[1], p[2], norm);
119 float numerator = p[0] * p[0] * p[0] + p[1] * p[1] * p[1] + p[2] * p[2] * p[2];
120 float denominator = p[0] * p[0] + p[1] * p[1] + p[2] * p[2];
121 float exp_norm = numerator / denominator;
122 assert_float_equal(norm, exp_norm, E);
123 assert_true(norm > 0.0f);
124 assert_true(norm <= 1.0f + 1e-6f);
125 }
126 testimdt_free(ti);
127
128 TR_STEP("verify that norm is equal to pixel (r=g=b) value on greyscale "
129 "values");
132 {
133 p[3] = 2.0f; // to make sure pixel[3] has no influence
134 float norm = pixel_rgb_norm_power(p);
135 TR_DEBUG("pixel={%e, %e, %e) => norm=%e", p[0], p[1], p[2], norm);
136 assert_float_equal(norm, p[0], E);
137 }
138 testimdt_free(ti);
139
140 TR_STEP("verify that norm is in ]0; +inf[ for bad greyscale pixels in "
141 "]0; +inf[");
142 TR_BUG("norm is undefined for extreme values, thus values outside "
143 "[1e-6; 1e6] are excluded from assertion.");
146 {
147 float norm = pixel_rgb_norm_power(p);
148 TR_DEBUG("pixel={%e, %e, %e) => norm=%e", p[0], p[1], p[2], norm);
149 if (p[0] > 1e-6 && p[0] < 1e6)
150 {
151 assert_true(norm > 0.0f);
152 assert_true(norm <= FLT_MAX);
153 }
154 }
155 testimdt_free(ti);
156
157 TR_STEP("verify that norm is in ]0; +inf[ for bad negative greyscale pixels "
158 "in ]-inf; 0]");
159 TR_BUG("norm is undefined for extreme values, thus values outside "
160 "[1e-6; 1e6] are excluded from assertion.");
161 TR_BUG("norm is 0 if input is 0.");
164 {
165 float norm = pixel_rgb_norm_power(p);
166 TR_DEBUG("pixel={%e, %e, %e) => norm=%e", p[0], p[1], p[2], norm);
167 if (fabsf(p[0]) > 1e-6 && fabsf(p[0]) < 1e6)
168 {
169 assert_true(norm > 0.0f);
170 assert_true(norm <= FLT_MAX);
171 }
172 if (p[0] > -FLT_MIN && p[0] < FLT_MIN) // translates to: if(p[0] == 0)
173 {
174 assert_float_equal(norm, 0.0f, FLT_MIN);
175 }
176 }
177 testimdt_free(ti);
178}
179
180static void test_get_pixel_norm(void **state)
181{
182 Testimg *ti;
183 // dt_iop_order_iccprofile_info_t work_profile; // see TODOs below
184
185 TR_STEP("verify that max-rgb norm is correct and in ]0.0; 1.0] for rgb "
186 "values in ]0.0; 1.0]");
189 {
190 p[3] = 2.0f; // to make sure pixel[3] has no influence
191 float norm = get_pixel_norm(p, DT_FILMIC_METHOD_MAX_RGB, NULL);
192 TR_DEBUG("pixel={%e, %e, %e, %e} => norm=%e", p[0], p[1], p[2], p[3], norm);
193 assert_float_equal(norm, fmax(p[0], fmax(p[1], p[2])), E);
194 assert_true(norm > 0.0f);
195 assert_true(norm <= 1.0f + E);
196 }
197 testimdt_free(ti);
198
199 TR_STEP("verify that max-rgb norm is equal to pixel (r=g=b) value on "
200 "greyscale values");
203 {
204 p[3] = 2.0f; // to make sure pixel[3] has no influence
205 float norm = get_pixel_norm(p, DT_FILMIC_METHOD_MAX_RGB, NULL);
206 TR_DEBUG("pixel={%e, %e, %e) => norm=%e", p[0], p[1], p[2], norm);
207 assert_float_equal(norm, p[0], E);
208 }
209 testimdt_free(ti);
210
211 TR_STEP("verify that max-rgb norm is in ]0; +inf[ for bad greyscale pixels "
212 "in ]0; +inf[");
215 {
216 float norm = get_pixel_norm(p, DT_FILMIC_METHOD_MAX_RGB, NULL);
217 TR_DEBUG("pixel={%e, %e, %e, %e} => norm=%e", p[0], p[1], p[2], p[3], norm);
218 assert_true(norm > 0.0f);
219 assert_true(norm <= FLT_MAX);
220 }
221 testimdt_free(ti);
222
223 TR_STEP("verify that max-rgb norm is in ]0; +inf[ for bad negative greyscale "
224 "pixels in ]-inf; 0]");
225 TR_BUG("max-rgb norm is unbounded and negative for pixels with all-negative "
226 "colors.");
229 {
230 float norm = get_pixel_norm(p, DT_FILMIC_METHOD_MAX_RGB, NULL);
231 TR_DEBUG("pixel={%e, %e, %e, %e} => norm=%e", p[0], p[1], p[2], p[3], norm);
232 // bug: assert_true(norm > 0.0f);
233 assert_true(norm <= FLT_MAX);
234 }
235 testimdt_free(ti);
236
237 TR_STEP("verify luminance-y norm (verify subsequent function calls)");
238 // TODO: find out how to mock inline functions!
239
240 TR_STEP("verify power norm (verify subsequent function calls)");
241 // note: the norm itself is verified in test_pixel_rgb_norm_power(), so here
242 // we only verify that the function pixel_rgb_norm_power() is called.
243 // TODO: find out how to mock inline functions!
244}
245
247{
248 Testimg *ti;
249 float grey = 0.1845f;
250 float dyn_range = TESTIMG_STD_DYN_RANGE_EV;
251 float black = log2f(1.0f/grey) - dyn_range;
252 const float MIN = 0.0f;
253 const float MAX = 1.0f;
254
255 TR_STEP("verify that output is equal to log-mapped input for equal dynamic "
256 "range and grey/black points");
259 {
260 float ret = log_tonemapping_v2(p[0], grey, black, dyn_range);
261 TR_DEBUG("%e => %e", p[0], ret);
262 float exp = testimg_val_to_log(p[0]);
263 if (exp < MIN)
264 {
265 assert_float_equal(ret, MIN, E); // bound to -16EV
266 }
267 else
268 {
269 assert_float_equal(ret, exp, E);
270 }
271 }
272 testimdt_free(ti);
273
274 TR_STEP("verify that output is 1 EV brighter (and clipped to [0; 1]) when "
275 "grey is set to half");
278 {
279 float ret = log_tonemapping_v2(p[0], (grey / 2.0f), black, dyn_range);
280 TR_DEBUG("%e => %e", p[0], ret);
281 float exp = testimg_val_to_log(p[0] * 2.0f); // *2.0 means +1EV
282 if (exp < MIN)
283 {
284 assert_float_equal(ret, MIN, E); // bound to 2^-16
285 }
286 else if (exp > MAX)
287 {
288 assert_float_equal(ret, MAX, E); // bound to 1.0
289 }
290 else
291 {
292 assert_float_equal(ret, exp, E);
293 }
294 }
295 testimdt_free(ti);
296
297 TR_STEP("verify that output is bound to [0; 1] for all non-negative values");
300 {
301 float ret = log_tonemapping_v2(p[0], grey, black, dyn_range);
302 TR_DEBUG("{%e, %e, %e, %e} => %e", p[0], p[1], p[2], p[3], ret);
303 assert_true(ret >= MIN);
304 assert_true(ret <= MAX);
305 }
306 testimdt_free(ti);
307
308 TR_STEP("verify that output is bound to [0; 1] for all negative values "
309 "(incl. 0.0)");
312 {
313 float ret = log_tonemapping_v2(p[0], grey, black, dyn_range);
314 TR_DEBUG("{%e, %e, %e, %e} => %e", p[0], p[1], p[2], p[3], ret);
315 assert_true(ret >= MIN);
316 assert_true(ret <= MAX);
317 }
318 testimdt_free(ti);
319}
320
321static void test_filmic_spline(void **state)
322{
323 // TODO: write tests for the method test_filmic_spline
324 //
325 // The problem with this method is that it needs the spline parameters that
326 // are hard to figure out. We could call dt_iop_filmic_rgb_compute_spline() to
327 // get the parameters but then it is still hard to estimate what the asserts
328 // should look like.
329 //
330 // Done a code review of the method test_filmic_spline() and I think it is ok.
331
332 TR_NOTE("method verified by code review only since it is hard to test it and "
333 "the benefit is questionable");
334}
335
336// helper method to map gui saturation to internally used one:
337static float saturation_gui_to_internal(float saturation_percent)
338{
339 // TODO: there is a flaw in conversion of saturation from gui value to
340 // internal value. Discussed this with @aurelienpierre and decision was to
341 // leave it for the moment (Feb 2020). This code here needs to be adapted when
342 // the bug gets fixed.
343
344 TR_BUG("saturation conversion from gui to internal is wrong");
345 return (2.0f * saturation_percent / 100.0f + 1.0f); // copied from filmicrgb.c
346 //fix: return 100.0f / fmaxf(100.0f - saturation_percent, 1e-6);
347}
348
350{
351 Testimg *ti;
352
353 // input values
354 float lattitude_min = 0.2;
355 float lattitude_max = 0.2; // symmetrical
356 float saturation_percent = 5.0f;
357
358 // computed values
359 // copied 2 lines from filmicrgb.c:
360 float sigma_toe = powf(lattitude_min / 3.0f, 2.0f);
361 float sigma_shoulder = powf(lattitude_max / 3.0f, 2.0f);
362
363 float saturation = saturation_gui_to_internal(saturation_percent);
364
365 TR_STEP("verify values are correct for different latitudes");
366 TR_BUG("values inside latitude are not always 1.0 (but very close), "
367 "especially at the borders");
368 for (float latitude_min = 0.1f; latitude_min < 0.5f + E; latitude_min += 0.1f)
369 {
370 for (float latitude_max = 0.1f; latitude_max < 0.5f + E; latitude_max += 0.1f)
371 {
372 TR_DEBUG("saturation=%e", saturation);
373 TR_DEBUG("latitude_min=%e", latitude_min);
374 TR_DEBUG("latitude_max=%e", latitude_max);
375
376 // copied 2 lines from filmicrgb.c:
377 sigma_toe = powf(lattitude_min / 3.0f, 2.0f);
378 sigma_shoulder = powf(lattitude_max / 3.0f, 2.0f);
379
380 TR_DEBUG("sigma_toe=%e", sigma_toe);
381 TR_DEBUG("sigma_shoulder=%e", sigma_shoulder);
382
383 // filmic_desaturate works in log space:
384 // create image with values from 0.0 to 1.0 in 0.05 steps:
387 {
388 float ret =
389 filmic_desaturate_v1(p[0], sigma_toe, sigma_shoulder, saturation);
390 TR_DEBUG("%e => %e", p[0], ret);
391
392 if (lattitude_min == lattitude_max)
393 {
394 // values symmetric (due to sigma_shoulder = sigma_toe):
395 float *p1 = get_pixel(ti, ti->width - x - 1, y);
396 float exp = filmic_desaturate_v1(p1[0], sigma_toe, sigma_shoulder,
397 saturation);
398 assert_float_equal(ret, exp, E);
399 }
400
401 // values correct on extreme borders:
402 if (x == 0 || x == ti->width - 1)
403 {
404 assert_float_equal(ret, 1.0f - 1.0f / saturation, E);
405 }
406
407 //bug: values close to 1.0 during latitude, not exactly 1.0:
408 if (x > (lattitude_min * ti->width) &&
409 x < ((1.0f - lattitude_max) * ti->width - 1))
410 {
411 assert_float_equal(ret, 1.0f, 1e-2);
412 }
413 }
414 testimdt_free(ti);
415 }
416 }
417
418 TR_STEP("verify return value is always 1.0 when saturation is set to maximum");
419 TR_BUG("values inside latitude are not always 1.0 (but very close), "
420 "especially at the borders");
421 // create image with values from 0.0 to 1.0 in 0.05 steps:
423 saturation = saturation_gui_to_internal(1e6); // TODO: take 100%
425 {
426 float ret = filmic_desaturate_v1(p[0], sigma_toe, sigma_shoulder, saturation);
427 TR_DEBUG("%e => %e", p[0], ret);
428 //bug: values close to 1.0 during latitude, not exactly 1.0:
429 assert_float_equal(ret, 1.0f, 1e-2);
430 }
431 // set saturation back:
432 saturation = saturation_gui_to_internal(saturation_percent);
433 testimdt_free(ti);
434
435 TR_STEP("verify output is in ]0; 1] for bad values in ]0; +inf[");
438 {
439 float ret = filmic_desaturate_v1(p[0], sigma_toe, sigma_shoulder, saturation);
440 TR_DEBUG("{%e} => %e", p[0], ret);
441 assert_true(ret > 0.0f);
442 assert_true(ret <= 1.0f);
443 }
444 testimdt_free(ti);
445
446 TR_STEP("verify output is in ]0; 1] for bad negative values in ]-inf; 0]");
449 {
450 float ret = filmic_desaturate_v1(p[0], sigma_toe, sigma_shoulder, saturation);
451 TR_DEBUG("{%e} => %e", p[0], ret);
452 assert_true(ret > 0.0f);
453 assert_true(ret <= 1.0f);
454 }
455 testimdt_free(ti);
456}
457
458static void test_linear_saturation(void **state)
459{
460 Testimg *ti;
461
462 float luminance = 1.0f;
463 float saturation = 0.05f;
464 float ratios[] = { 0.2126, 0.7152, 0.0722 };
465
466 TR_STEP("verify that output is equal to value for greyscale values");
469 {
470 luminance = p[0]; // luminance := value, for greyscale values
471 float s0 = linear_saturation(p[0], luminance, saturation);
472 float s1 = linear_saturation(p[1], luminance, saturation);
473 float s2 = linear_saturation(p[2], luminance, saturation);
474 TR_DEBUG("pixel={%e, %e, %e) => linear_saturation={%e, %e, %e}",
475 p[0], p[1], p[2], s0, s1, s2);
476 assert_float_equal(s0, p[0], E);
477 assert_float_equal(s1, p[1], E);
478 assert_float_equal(s2, p[2], E);
479 }
480 testimdt_free(ti);
481
482 TR_STEP("verify that output is equal to value for rgb values when saturation "
483 "is 1.0");
484 saturation = 1.0f;
487 {
488 luminance = p[0] * ratios[0] + p[1] * ratios[1] + p[2] * ratios[2];
489 float s0 = linear_saturation(p[0], luminance, saturation);
490 float s1 = linear_saturation(p[1], luminance, saturation);
491 float s2 = linear_saturation(p[2], luminance, saturation);
492 TR_DEBUG("pixel={%e, %e, %e) => linear_saturation={%e, %e, %e}",
493 p[0], p[1], p[2], s0, s1, s2);
494 assert_float_equal(s0, p[0], E);
495 assert_float_equal(s1, p[1], E);
496 assert_float_equal(s2, p[2], E);
497 }
498 testimdt_free(ti);
499
500 TR_STEP("verify that output is pure grey, equal to luminance, for rgb values "
501 "when saturation is 0.0");
502 saturation = 0.0f;
505 {
506 luminance = p[0] * ratios[0] + p[1] * ratios[1] + p[2] * ratios[2];
507 float s0 = linear_saturation(p[0], luminance, saturation);
508 float s1 = linear_saturation(p[1], luminance, saturation);
509 float s2 = linear_saturation(p[2], luminance, saturation);
510 TR_DEBUG("pixel={%e, %e, %e) => linear_saturation={%e, %e, %e}",
511 p[0], p[1], p[2], s0, s1, s2);
512 assert_float_equal(s0, s1, E);
513 assert_float_equal(s0, s2, E);
515 }
516 testimdt_free(ti);
517}
518
519/*
520 * MAIN FUNCTION
521 */
522int main(int argc, char* argv[])
523{
524 const struct CMUnitTest tests[] = {
525 cmocka_unit_test(test_name),
526 cmocka_unit_test(test_default_group),
527 cmocka_unit_test(test_clamp_simd),
528 cmocka_unit_test(test_pixel_rgb_norm_power),
529 cmocka_unit_test(test_get_pixel_norm),
530 cmocka_unit_test(test_log_tonemapping_v2),
531 cmocka_unit_test(test_filmic_spline),
532 cmocka_unit_test(test_filmic_desaturate_v1),
533 cmocka_unit_test(test_linear_saturation)
534 };
535
536 TR_DEBUG("epsilon = %e", E);
537
538 return cmocka_run_group_tests(tests, NULL, NULL);
539}
540// clang-format off
541// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
542// vim: shiftwidth=2 expandtab tabstop=2 cindent
543// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
544// clang-format on
int default_group()
Definition ashift.c:175
#define assert_float_equal(a, b, epsilon)
Definition assert.h:35
static const dt_aligned_pixel_simd_t const dt_adaptation_t const float p
char * name
@ DT_FILMIC_METHOD_MAX_RGB
Definition filmicrgb.c:127
static float linear_saturation(const float x, const float luminance, const float saturation)
Definition filmicrgb.c:1091
@ IOP_GROUP_TECHNICAL
Definition imageop.h:143
@ IOP_GROUP_TONES
Definition imageop.h:137
static const float x
float *const restrict luminance
static float clamp_simd(const float x)
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_v2(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)
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
#define TESTIMG_STD_WIDTH
Definition testimg.h:44
#define for_testimg_pixels_p_yx(ti)
Definition testimg.h:71
float * get_pixel(const Testimg *const ti, const int x, const int y)
Definition testimg.h:59
#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