Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
knee.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with darktable. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19// R9 sensor-rolloff (knee) estimation and inversion (CPU + OpenCL). (implementation; see knee.h for the public
20// API.)
21
22#include "common/darktable.h"
24#include "develop/imageop.h"
26#include "iop/highlights/knee.h"
27#include <math.h>
28#include <stdlib.h>
29
30static inline float _knee_lift_of(const _hl_knee_curve_t *const k, const float x)
31{
32 const float step
33 = (DT_HL_KNEE_DET - DT_HL_KNEE_LO) / (float)DT_HL_KNEE_BINS; // bin width over the band [LO, DET)
34 const float bin_pos
35 = (x - (DT_HL_KNEE_LO + 0.5f * step)) / step; // x in bin-center units (knot 0 sits at LO + step/2)
36
37 if(bin_pos <= -0.5f) return 0.f; // at/below LO: no lift (identity anchor)
38 if(bin_pos <= 0.f)
39 return k->lift[0] * 2.f * (bin_pos + 0.5f); // first half-bin: ramp 0 -> lift[0] for a smooth start
40 if(bin_pos >= (float)(DT_HL_KNEE_BINS - 1))
41 return k->lift[DT_HL_KNEE_BINS - 1]; // past last center: flat-extend the lift
42
43 const int i = (int)bin_pos; // lower knot (bin) index
44 const float bin_frac = bin_pos - (float)i; // interpolation weight toward the next knot
45 return k->lift[i] * (1.f - bin_frac) + k->lift[i + 1] * bin_frac; // linear blend of adjacent per-bin lifts
46}
47
48// Blur up to four PLANAR planes in one four-channel pass: the recursive gaussian's per-pixel
49// recursion is the bottleneck and its 4-channel variant runs the four lanes in SIMD, so this
50// is ~3x cheaper than four single-plane calls. Pack/unpack are cheap linear passes. Per plane
51// the result is identical to _knee_blur (the channels never mix in the recursion).
53static void _knee_blur4(const float *const planes[4], float *const outs[4], const int n_planes, const int region_w,
54 const int region_h, const float sigma, float *const restrict pack_in,
55 float *const restrict pack_out)
56{
57 const size_t region_pixels = (size_t)region_w * region_h;
58 dt_gaussian_t *const gaussian = _hl_gauss_get(region_w, region_h, 4, sigma);
59
60 if(!gaussian)
61 {
62 for(int k = 0; k < n_planes; k++) memcpy(outs[k], planes[k], region_pixels * sizeof(float));
63 return;
64 }
65
66 HL_PFOR()
67 for(size_t i = 0; i < region_pixels; i++)
68 for(int k = 0; k < 4; k++) pack_in[i * 4 + k] = (k < n_planes) ? planes[k][i] : 0.f;
69
70 dt_gaussian_blur_4c(gaussian, pack_in, pack_out);
71
72 HL_PFOR()
73 for(size_t i = 0; i < region_pixels; i++)
74 for(int k = 0; k < 4; k++)
75 if(k < n_planes) outs[k][i] = pack_out[i * 4 + k];
76}
77
78// qsort comparator for floats (ascending)
79static int _knee_cmp_float(const void *ptr_a, const void *ptr_b)
80{
81 const float float_a = *(const float *)ptr_a;
82 const float float_b = *(const float *)ptr_b;
83 return (float_a > float_b) - (float_a < float_b);
84}
85
86// Median of values[0..count-1]; sorts in place. Serves both the robust per-bin lift
87// median{ v_hat_i - v_i } and the MAD spread of those same votes (Step 2).
88static float _knee_median(float *const values, const size_t count)
89{
90 qsort(values, count, sizeof(float), _knee_cmp_float);
91 return (count & 1) ? values[count / 2] : 0.5f * (values[count / 2 - 1] + values[count / 2]);
92}
93
94// Symmetric second-moment plane index for channels (chan_a, chan_b) in the joint moment buffer
95// layout: planes 0 = n (trusted mass), 1..3 = means R G B, 4..9 = second moments RR RG RB GG GB BB.
96// Once divided by n and de-meaned these give Var(u_a) / Cov(u_a,u_b), the entries of the 2x2 normal
97// matrix (indexing is symmetric: _knee_p2(a,b) == _knee_p2(b,a)).
98static inline int _knee_p2(const int chan_a, const int chan_b)
99{
100 static const int plane_lut[3][3] = { { 4, 5, 6 }, { 5, 7, 8 }, { 6, 8, 9 } };
101 return plane_lut[chan_a][chan_b];
102}
103
105void _hl_knee_estimate(const float *const restrict input, const size_t width, const size_t height,
106 const uint32_t filters, const dt_iop_roi_t *const roi_in, const uint8_t (*const xtrans)[6],
107 const dt_aligned_pixel_t clipval_raw, _hl_knee_curve_t curves[3],
108 const dt_dev_pixelpipe_t *pipe)
109{
110 for(int c = 0; c < 3; c++)
111 {
112 curves[c].engaged = 0;
113 memset(curves[c].lift, 0, sizeof(curves[c].lift));
114 }
115
116 // The curve is a global (per-channel) property, binned to <= ~1.5 Mpx. The base cell must
117 // hold every CFA colour with a consistent phase: 2x2 for Bayer, 6x6 for X-Trans (the full
118 // pattern period -- any smaller cell can miss a colour at some alignments).
119 const int base = xtrans ? 6 : 2;
120 int downsample = 1;
121 while((width / ((size_t)base * downsample)) * (height / ((size_t)base * downsample)) > 1500000) downsample++;
122
123 const int quad_size = base * downsample;
124 const size_t bin_w = width / quad_size;
125 const size_t bin_h = height / quad_size;
126 const size_t bin_pixels = bin_w * bin_h;
127 if(bin_w < 16 || bin_h < 16) return;
128
129 float *const restrict binned
130 = dt_pixelpipe_cache_alloc_align_float(bin_pixels * 3, pipe); // planar, clip-normalized
131 float *const restrict pred = dt_pixelpipe_cache_alloc_align_float(bin_pixels * 3, pipe);
132 float *const restrict r2_scores = dt_pixelpipe_cache_alloc_align_float(bin_pixels * 3, pipe);
133 float *const restrict joint_moments
134 = dt_pixelpipe_cache_alloc_align_float(bin_pixels * 10, pipe); // joint moment planes
135 float *const restrict pair_moments
136 = dt_pixelpipe_cache_alloc_align_float(bin_pixels * 6, pipe); // pair moment planes
137 float *const restrict votes = dt_pixelpipe_cache_alloc_align_float(bin_pixels, pipe); // lift-fit bin scratch
138 float *const restrict pk_in
139 = dt_pixelpipe_cache_alloc_align_float(bin_pixels * 4, pipe); // _knee_blur4 pack scratch
140 float *const restrict pk_out = dt_pixelpipe_cache_alloc_align_float(bin_pixels * 4, pipe);
141 uint8_t *const restrict done = calloc(bin_pixels * 3, sizeof(uint8_t));
142
143 if(IS_NULL_PTR(binned) || IS_NULL_PTR(pred) || IS_NULL_PTR(r2_scores) || IS_NULL_PTR(joint_moments)
144 || IS_NULL_PTR(pair_moments) || IS_NULL_PTR(votes) || IS_NULL_PTR(pk_in) || IS_NULL_PTR(pk_out)
145 || done == NULL)
146 goto cleanup;
147
148 // Bin the CFA per channel into clip-normalized planar planes: every qs x qs cell averages the
149 // sites of each CFA colour it contains (phase-consistent, no inter-site interpolation).
150 HL_PFOR(collapse(2))
151 for(size_t i = 0; i < bin_h; i++)
152 for(size_t j = 0; j < bin_w; j++)
153 {
154 dt_aligned_pixel_t accum = { 0.f, 0.f, 0.f, 0.f };
155 dt_aligned_pixel_t counts = { 0.f, 0.f, 0.f, 0.f };
156
157 for(int y = 0; y < quad_size; y++)
158 for(int cell_x = 0; cell_x < quad_size; cell_x++)
159 {
160 const size_t row = i * quad_size + y;
161 const size_t col = j * quad_size + cell_x;
162 const size_t c = xtrans ? (size_t)FCxtrans((int)row, (int)col, roi_in, xtrans) : FC(row, col, filters);
163
164 if(c <= 2)
165 {
166 accum[c] += input[row * width + col];
167 counts[c] += 1.f;
168 }
169 }
170
171 // per cell: co-located R / mean-G / B, each normalized to clip units v/(clip level) so the band
172 // sits at [LO, DET); empty colours (no site of that colour in the cell) write 0
173 for(int c = 0; c < 3; c++)
174 binned[c * bin_pixels + i * bin_w + j] = (counts[c] > 0.f) ? accum[c] / (counts[c] * clipval_raw[c]) : 0.f;
175 }
176
177 // Band mass per channel: count binned cells in [LO, DET) -- the near-clip band [0.8c, 0.995c) the
178 // knee corrects. A channel without a real band (< 200 cells) cannot trace a curve -> stays identity.
179 size_t nband[3] = { 0, 0, 0 };
180 for(size_t pixel = 0; pixel < bin_pixels; pixel++)
181 for(int c = 0; c < 3; c++)
182 if(binned[c * bin_pixels + pixel] >= DT_HL_KNEE_LO && binned[c * bin_pixels + pixel] < DT_HL_KNEE_DET)
183 nband[c]++;
184
185 if(nband[0] < 200 && nband[1] < 200 && nband[2] < 200) goto cleanup;
186
187 // Multi-scale windowed colour-line predictions: per pixel keep the FINEST window that held
188 // enough trusted mass. Joint 2-guide regression first (resolves two latent factors), then a
189 // single-guide fallback where only one guide is itself trusted at the pixel. Sigmas are in
190 // quad-cell units (x2 in CFA pixels), matching the prototype's 8..128 at scene resolution.
191 const float sigmas[DT_HL_KNEE_NSIGMAS] = { 4.f, 8.f, 16.f, 32.f, 64.f };
192
193 for(int sigma_index = 0; sigma_index < DT_HL_KNEE_NSIGMAS; sigma_index++)
194 {
195 const float sigma = sigmas[sigma_index];
196
197 // ---- joint moments: weight w = 1 only where all three channels are trusted (< LO), so clipped
198 // cells never vote; shared by every target channel. These ten raw planes, once blurred by
199 // G_sigma below, become the windowed sums sum_y w G_sigma (...) feeding the normal equations. ----
200 // All ten raw planes in one pass, then blurred 4-wide in place (via the pack scratch).
201 HL_PFOR()
202 for(size_t pixel = 0; pixel < bin_pixels; pixel++)
203 {
204 const float x_red = binned[0 * bin_pixels + pixel];
205 const float x_green = binned[1 * bin_pixels + pixel];
206 const float x_blue = binned[2 * bin_pixels + pixel];
207 const float weight = (x_red < DT_HL_KNEE_LO && x_green < DT_HL_KNEE_LO && x_blue < DT_HL_KNEE_LO)
208 ? 1.f
209 : 0.f; // trust mask w
210 joint_moments[0 * bin_pixels + pixel] = weight; // plane 0: n = sum w (trusted mass)
211 joint_moments[1 * bin_pixels + pixel] = weight * x_red; // plane 1: sum w*R -> E[R]
212 joint_moments[2 * bin_pixels + pixel] = weight * x_green; // plane 2: sum w*G -> E[G]
213 joint_moments[3 * bin_pixels + pixel] = weight * x_blue; // plane 3: sum w*B -> E[B]
214 joint_moments[4 * bin_pixels + pixel] = weight * x_red * x_red; // plane 4: sum w*R*R -> E[R^2]
215 joint_moments[5 * bin_pixels + pixel] = weight * x_red * x_green; // plane 5: sum w*R*G -> E[R*G]
216 joint_moments[6 * bin_pixels + pixel] = weight * x_red * x_blue; // plane 6: sum w*R*B -> E[R*B]
217 joint_moments[7 * bin_pixels + pixel] = weight * x_green * x_green; // plane 7: sum w*G*G -> E[G^2]
218 joint_moments[8 * bin_pixels + pixel] = weight * x_green * x_blue; // plane 8: sum w*G*B -> E[G*B]
219 joint_moments[9 * bin_pixels + pixel] = weight * x_blue * x_blue; // plane 9: sum w*B*B -> E[B^2]
220 }
221
222 for(int plane_base = 0; plane_base < 10; plane_base += 4)
223 {
224 const int n_planes = MIN(4, 10 - plane_base);
225 const float *plane_in[4] = { 0 };
226 float *plane_out[4] = { 0 };
227 for(int k = 0; k < n_planes; k++)
228 plane_in[k] = plane_out[k] = joint_moments + (size_t)(plane_base + k) * bin_pixels;
229 _knee_blur4(plane_in, plane_out, n_planes, bin_w, bin_h, sigma, pk_in, pk_out);
230 }
231
232 // Joint 2-guide colour-line fit v_hat = a*u1 + b*u2 + d for each target channel c, solved from
233 // the blurred moments via the 2x2 normal equations (Cramer's rule). Guides are the other two
234 // channels; the two-factor solve resolves scenes a single guide would under-predict.
235 for(int c = 0; c < 3; c++)
236 {
237 if(nband[c] < 200) continue;
238
239 const int guide1 = (c == 0) ? 1 : 0; // u1 = first guide channel
240 const int guide2 = (c == 2) ? 1 : 2; // u2 = second guide channel
241
242 HL_PFOR()
243 for(size_t pixel = 0; pixel < bin_pixels; pixel++)
244 {
245 if(done[c * bin_pixels + pixel]) continue; // finer sigma already served this cell (multi-scale)
246
247 const float x_val = binned[c * bin_pixels + pixel]; // measured band value v of target c
248 const float x_guide1 = binned[guide1 * bin_pixels + pixel]; // guide u1 at this cell
249 const float x_guide2 = binned[guide2 * bin_pixels + pixel]; // guide u2 at this cell
250 const float weight_sum = joint_moments[pixel]; // n = windowed trusted mass at this cell
251
252 if(!(x_val >= DT_HL_KNEE_LO && x_val < DT_HL_KNEE_DET)) continue; // only band cells [LO, DET) vote
253 if(!(x_guide1 < DT_HL_KNEE_LO && x_guide2 < DT_HL_KNEE_LO)) continue; // both guides must be trusted here
254 if(weight_sum <= DT_HL_KNEE_FMIN) continue; // too little trusted mass in the window -> skip
255
256 const float inv_weight = 1.f / weight_sum; // 1/n, converts summed moments to expectations
257 // windowed means E[.] = (sum w*.)/n
258 const float mean_target = joint_moments[(size_t)(1 + c) * bin_pixels + pixel] * inv_weight; // E[v]
259 const float mean_guide1 = joint_moments[(size_t)(1 + guide1) * bin_pixels + pixel] * inv_weight; // E[u1]
260 const float mean_guide2 = joint_moments[(size_t)(1 + guide2) * bin_pixels + pixel] * inv_weight; // E[u2]
261 // second moments de-meaned = Var/Cov, centered about the per-window mean to avoid the float
262 // E[u^2]-E[u]^2 cancellation on smooth content (squared mean dwarfs the variance)
263 const float var_11 // Var(u1) = E[u1^2] - E[u1]^2 (normal-matrix diagonal, guide 1)
264 = fmaxf(joint_moments[(size_t)_knee_p2(guide1, guide1) * bin_pixels + pixel] * inv_weight
265 - mean_guide1 * mean_guide1,
266 0.f);
267 const float var_22 // Var(u2) = E[u2^2] - E[u2]^2 (normal-matrix diagonal, guide 2)
268 = fmaxf(joint_moments[(size_t)_knee_p2(guide2, guide2) * bin_pixels + pixel] * inv_weight
269 - mean_guide2 * mean_guide2,
270 0.f);
271 const float var_12 = joint_moments[(size_t)_knee_p2(guide1, guide2) * bin_pixels + pixel] * inv_weight
272 - mean_guide1 * mean_guide2; // Cov(u1,u2) (off-diagonal of the normal matrix)
273 const float cov_1 = joint_moments[(size_t)_knee_p2(c, guide1) * bin_pixels + pixel] * inv_weight
274 - mean_target * mean_guide1; // Cov(v,u1) (RHS of the normal equations)
275 const float cov_2 = joint_moments[(size_t)_knee_p2(c, guide2) * bin_pixels + pixel] * inv_weight
276 - mean_target * mean_guide2; // Cov(v,u2) (RHS of the normal equations)
277 const float var_target = fmaxf(joint_moments[(size_t)_knee_p2(c, c) * bin_pixels + pixel] * inv_weight
278 - mean_target * mean_target,
279 0.f); // Var(v), for the R^2 quality score
280
281 // relative Tikhonov (ridge) damping lambda = 1e-3 * (Var u1 + Var u2)/2: scales with the
282 // signal, never eats a weak-but-real slope
283 const float lambda = 1e-3f * 0.5f * (var_11 + var_22) + 1e-12f;
284 const float diag_11 = var_11 + lambda; // ridged normal-matrix diagonal [0][0]
285 const float diag_22 = var_22 + lambda; // ridged normal-matrix diagonal [1][1]
286 const float determinant = fmaxf(diag_11 * diag_22 - var_12 * var_12, 1e-18f); // det of the 2x2 system
287 const float slope_1 = (diag_22 * cov_1 - var_12 * cov_2) / determinant; // a = slope on u1 (Cramer's rule)
288 const float slope_2 = (diag_11 * cov_2 - var_12 * cov_1) / determinant; // b = slope on u2 (Cramer's rule)
289
290 // v_hat(x) = E[v] + a*(u1 - E[u1]) + b*(u2 - E[u2]) (intercept d folded into the centering)
291 pred[c * bin_pixels + pixel]
292 = mean_target + slope_1 * (x_guide1 - mean_guide1) + slope_2 * (x_guide2 - mean_guide2);
293 // R^2 = (a*Cov(v,u1) + b*Cov(v,u2)) / Var(v): explained-variance fraction, the vote's fit quality
294 r2_scores[c * bin_pixels + pixel]
295 = CLAMP((slope_1 * cov_1 + slope_2 * cov_2) / (var_target + 1e-12f), 0.f, 1.f);
296 done[c * bin_pixels + pixel] = 1; // cell served at this (finest-so-far) sigma; coarser passes skip it
297 }
298 }
299
300 // ---- single-guide fallback: simple regression v_hat = a*u + d where only one guide is itself
301 // trusted at the cell (the joint fit needs both). Weight w = 1 where the target-guide PAIR is
302 // trusted; slope from Cov(v,u)/Var(u). Fills cells the joint pass left `done == 0`. ----
303 for(int chan_a = 0; chan_a < 3; chan_a++)
304 for(int chan_b = chan_a + 1; chan_b < 3; chan_b++)
305 {
306 if(nband[chan_a] < 200 && nband[chan_b] < 200) continue;
307
308 // pair moment planes: 0 = n (=sum w), 1 = sum w*a, 2 = sum w*b, 3 = sum w*a*a, 4 = sum w*b*b,
309 // 5 = sum w*a*b -- all raw in one pass, then blurred 4-wide in place (via the pack scratch).
310 HL_PFOR()
311 for(size_t pixel = 0; pixel < bin_pixels; pixel++)
312 {
313 const float val_a = binned[chan_a * bin_pixels + pixel];
314 const float val_b = binned[chan_b * bin_pixels + pixel];
315 const float weight = (val_a < DT_HL_KNEE_LO && val_b < DT_HL_KNEE_LO) ? 1.f : 0.f; // pair trust mask w
316 pair_moments[0 * bin_pixels + pixel] = weight;
317 pair_moments[1 * bin_pixels + pixel] = weight * val_a;
318 pair_moments[2 * bin_pixels + pixel] = weight * val_b;
319 pair_moments[3 * bin_pixels + pixel] = weight * val_a * val_a;
320 pair_moments[4 * bin_pixels + pixel] = weight * val_b * val_b;
321 pair_moments[5 * bin_pixels + pixel] = weight * val_a * val_b;
322 }
323
324 for(int plane_base = 0; plane_base < 6; plane_base += 4)
325 {
326 const int n_planes = MIN(4, 6 - plane_base);
327 const float *plane_in[4] = { 0 };
328 float *plane_out[4] = { 0 };
329 for(int k = 0; k < n_planes; k++)
330 plane_in[k] = plane_out[k] = pair_moments + (size_t)(plane_base + k) * bin_pixels;
331 _knee_blur4(plane_in, plane_out, n_planes, bin_w, bin_h, sigma, pk_in, pk_out);
332 }
333
334 // both orientations of the pair: predict a from b, then b from a (select which plane holds
335 // the target's vs the guide's mean/second-moment accordingly)
336 for(int orient = 0; orient < 2; orient++)
337 {
338 const int target_ch = orient ? chan_b : chan_a; // target channel v
339 const int guide_ch = orient ? chan_a : chan_b; // guide channel u
340 const int target_mean_plane = orient ? 2 : 1; // plane holding sum w*v
341 const int guide_mean_plane = orient ? 1 : 2; // plane holding sum w*u
342 const int target_sq_plane = orient ? 4 : 3; // plane holding sum w*v*v
343 const int guide_sq_plane = orient ? 3 : 4; // plane holding sum w*u*u
344
345 if(nband[target_ch] < 200) continue;
346
347 HL_PFOR()
348 for(size_t pixel = 0; pixel < bin_pixels; pixel++)
349 {
350 if(done[target_ch * bin_pixels + pixel]) continue; // already served (joint or finer sigma)
351
352 const float x_val = binned[target_ch * bin_pixels + pixel]; // measured band value v
353 const float x_guide = binned[guide_ch * bin_pixels + pixel]; // guide u
354 const float weight_sum = pair_moments[pixel]; // n = windowed trusted mass
355
356 if(!(x_val >= DT_HL_KNEE_LO && x_val < DT_HL_KNEE_DET)) continue; // only band cells vote
357 if(!(x_guide < DT_HL_KNEE_LO)) continue; // the single guide must be trusted
358 if(weight_sum <= DT_HL_KNEE_FMIN) continue; // too little trusted mass -> skip
359
360 const float inv_weight = 1.f / weight_sum; // 1/n
361 const float mean_target
362 = pair_moments[(size_t)target_mean_plane * bin_pixels + pixel] * inv_weight; // E[v]
363 const float mean_guide
364 = pair_moments[(size_t)guide_mean_plane * bin_pixels + pixel] * inv_weight; // E[u]
365 const float covariance // Cov(v,u) = E[v*u] - E[v]E[u] (plane 5 holds sum w*a*b)
366 = pair_moments[(size_t)5 * bin_pixels + pixel] * inv_weight - mean_target * mean_guide;
367 const float var_guide = fmaxf(pair_moments[(size_t)guide_sq_plane * bin_pixels + pixel] * inv_weight
368 - mean_guide * mean_guide,
369 0.f); // Var(u) = E[u^2] - E[u]^2
370 const float var_target = fmaxf(pair_moments[(size_t)target_sq_plane * bin_pixels + pixel] * inv_weight
371 - mean_target * mean_target,
372 0.f); // Var(v), for the R^2 score
373 const float slope = covariance / (var_guide * (1.f + 1e-3f) + 1e-12f); // a = Cov(v,u)/Var(u), ridged
374
375 pred[target_ch * bin_pixels + pixel]
376 = mean_target + slope * (x_guide - mean_guide); // v_hat = E[v] + a*(u-E[u])
377 r2_scores[target_ch * bin_pixels + pixel] // R^2 = Cov^2 / (Var(u) Var(v)) for a single guide
378 = CLAMP(covariance * covariance / (var_guide * var_target + 1e-18f), 0.f, 1.f);
379 done[target_ch * bin_pixels + pixel] = 1; // cell now served
380 }
381 }
382 }
383 }
384
385 // ---- Step 2, curve fit: per channel, pool the votes v_hat_i - v_i into 24 bins over the band,
386 // take each bin's robust median lift (the median{ v_hat_i - v_i } of the equation), keep it only
387 // when statistically significant, then make the curve monotone + raise-only. ----
388 for(int c = 0; c < 3; c++)
389 {
390 if(nband[c] < 200) continue;
391
392 // counting sort of the votes into DT_HL_KNEE_BINS = 24 bins by measured value v (offset[] is the
393 // exclusive prefix-sum giving each bin's slot range in the flat `votes` scratch)
394 size_t count[DT_HL_KNEE_BINS] = { 0 };
395 size_t offset[DT_HL_KNEE_BINS + 1] = { 0 };
396 const float bin_width = (DT_HL_KNEE_DET - DT_HL_KNEE_LO) / (float)DT_HL_KNEE_BINS; // band width / 24
397
398 // pass 1: count votes per bin -- only cells that got a prediction (done) and cleared the fit-
399 // quality gate R^2 > R2MIN participate (a poorly-fit pair does not get to vote)
400 for(size_t pixel = 0; pixel < bin_pixels; pixel++)
401 {
402 if(!done[c * bin_pixels + pixel] || r2_scores[c * bin_pixels + pixel] <= DT_HL_KNEE_R2MIN) continue;
403 const int bin_index // which of the 24 bins the measured value v falls in
404 = CLAMP((int)((binned[c * bin_pixels + pixel] - DT_HL_KNEE_LO) / bin_width), 0, DT_HL_KNEE_BINS - 1);
405 count[bin_index]++;
406 }
407
408 for(int i = 0; i < DT_HL_KNEE_BINS; i++)
409 offset[i + 1] = offset[i] + count[i]; // prefix-sum -> per-bin slot base
410
411 size_t fill[DT_HL_KNEE_BINS];
412 memcpy(fill, offset, sizeof(fill)); // running write cursor per bin, seeded at each bin's base
413
414 // pass 2: scatter each vote's lift v_hat_i - v_i (pred - measured) into its bin's slots
415 for(size_t pixel = 0; pixel < bin_pixels; pixel++)
416 {
417 if(!done[c * bin_pixels + pixel] || r2_scores[c * bin_pixels + pixel] <= DT_HL_KNEE_R2MIN) continue;
418 const float x_val = binned[c * bin_pixels + pixel]; // measured v
419 const int bin_index = CLAMP((int)((x_val - DT_HL_KNEE_LO) / bin_width), 0, DT_HL_KNEE_BINS - 1);
420 votes[fill[bin_index]++] = pred[c * bin_pixels + pixel] - x_val; // one pixel's vote v_hat_i - v_i
421 }
422
423 // per-bin robust lift, accepted only when significant vs the bin median's standard error --
424 // the raise-only clamp would otherwise rectify zero-mean noise into fake lift
425 float lift[DT_HL_KNEE_BINS];
426 int seen[DT_HL_KNEE_BINS];
427 int nseen = 0;
428
429 for(int i = 0; i < DT_HL_KNEE_BINS; i++)
430 {
431 lift[i] = 0.f;
432 seen[i] = 0;
433 if(count[i] < DT_HL_KNEE_MINVOTES) continue; // need >= 100 votes so the error estimate itself is stable
434
435 float *const bin_votes = votes + offset[i];
436 const float median_lift = _knee_median(bin_votes, count[i]); // median{ v_hat_i - v_i } = the bin's raw lift
437
438 // median absolute deviation (MAD) around the median, a robust spread estimate
439 // (d is sorted, values get overwritten -- fine, last use)
440 for(size_t k = 0; k < count[i]; k++) bin_votes[k] = fabsf(bin_votes[k] - median_lift); // |lift_i - median|
441 const float median_abs_dev = _knee_median(bin_votes, count[i]); // MAD = median|lift_i - median(lift)|
442 // SE of the bin median = 1.858*MAD/sqrt(n); 1.858 = 1.4826 (MAD->sigma) * 1.2533 (sigma->SE of median)
443 const float std_err = 1.858f * median_abs_dev / sqrtf((float)count[i]);
444
445 seen[i] = 1; // this bin is populated (has a usable estimate), whether or not the lift is significant
446 nseen++;
447 // significance gate: accept the lift only if median > NSIGMA*SE (2*SE, ~95% one-sided) -- otherwise
448 // it stays 0, so the raise-only clamp below cannot rectify zero-mean noise into a fake lift
449 if(median_lift > DT_HL_KNEE_NSIGMA * std_err) lift[i] = median_lift;
450 }
451
452 if(nseen < 3) continue; // too few populated bins to trust a curve -> leave channel at identity
453
454 // interpolate lift over unseen (under-populated) bins (flat-extend past the first/last seen bin),
455 // linearly between two seen bins -- the C twin of the prototype's np.interp over centers[seen]
456 int prev = -1; // index of the nearest seen bin to the left (-1 = none yet)
457
458 for(int i = 0; i < DT_HL_KNEE_BINS; i++)
459 {
460 if(seen[i])
461 {
462 prev = i;
463 continue;
464 }
465
466 int next = -1;
467 for(int k = i + 1; k < DT_HL_KNEE_BINS; k++)
468 if(seen[k])
469 {
470 next = k;
471 break;
472 }
473
474 if(prev < 0 && next < 0)
475 lift[i] = 0.f;
476 else if(prev < 0)
477 lift[i] = lift[next];
478 else if(next < 0)
479 lift[i] = lift[prev];
480 else
481 lift[i] = lift[prev] + (lift[next] - lift[prev]) * (float)(i - prev) / (float)(next - prev);
482 }
483
484 // monotone raise-only clamp: cumulative max makes the curve non-decreasing (rolloff bias grows
485 // toward clip) and drops any residual negatives -- the C twin of np.maximum.accumulate(max(lift,0))
486 float running_max = 0.f;
487 float lift_max = 0.f;
488
489 for(int i = 0; i < DT_HL_KNEE_BINS; i++)
490 {
491 running_max = fmaxf(running_max, fmaxf(lift[i], 0.f)); // running max enforces monotone non-decreasing
492 curves[c].lift[i] = running_max; // final per-bin lift knot for this channel
493 lift_max = fmaxf(lift_max, running_max); // peak lift, for the engage test below
494 }
495
496 // engage threshold: a peak lift below ENGAGE = 0.005 is noise -> stay identity (the no-op guarantee:
497 // hard-clipped data yields near-zero medians, so the correction costs nothing)
498 curves[c].engaged = (lift_max >= DT_HL_KNEE_ENGAGE);
499 if(!curves[c].engaged) memset(curves[c].lift, 0, sizeof(curves[c].lift));
500 }
501
502cleanup:;
506 dt_pixelpipe_cache_free_align(joint_moments);
507 dt_pixelpipe_cache_free_align(pair_moments);
511 free(done);
512}
513
515void _hl_knee_apply_interpolated(float *const restrict interpolated, const size_t npix,
516 const dt_aligned_pixel_t clipvaln, const dt_aligned_pixel_t wb4,
517 const _hl_knee_curve_t curves[3])
518{
519 HL_PFOR()
520 for(size_t pixel = 0; pixel < npix; pixel++)
521 {
522 int touched = 0;
523
524 for(int c = 0; c < 3; c++)
525 {
526 if(!curves[c].engaged) continue; // channel with no measured rolloff -> pass through untouched
527
528 const float norm_val = interpolated[pixel * 4 + c] / clipvaln[c]; // v in clip units
529
530 if(norm_val >= DT_HL_KNEE_LO && norm_val < DT_HL_KNEE_DET) // only band values are corrected
531 {
532 const float lift = _knee_lift_of(&curves[c], norm_val); // L(v) from the fitted curve
533
534 if(lift > 0.f)
535 {
536 interpolated[pixel * 4 + c] = (norm_val + lift) * clipvaln[c]; // v + L(v), back to raw-scaled units
537 touched = 1;
538 }
539 }
540 }
541
542 if(touched) // rebuild norm = || white-balanced RGB || so the guide norm stays consistent
543 {
544 const float val_r = interpolated[pixel * 4 + 0] * wb4[0];
545 const float val_g = interpolated[pixel * 4 + 1] * wb4[1];
546 const float val_b = interpolated[pixel * 4 + 2] * wb4[2];
547 interpolated[pixel * 4 + 3] = sqrtf(sqf(val_r) + sqf(val_g) + sqf(val_b));
548 }
549 }
550}
551
553void _hl_knee_apply_cfa(const float *const restrict input, float *const restrict input_corr, const size_t width,
554 const size_t height, const uint32_t filters, const dt_iop_roi_t *const roi_in,
555 const uint8_t (*const xtrans)[6], const dt_aligned_pixel_t clipval_raw,
556 const _hl_knee_curve_t curves[3])
557{
558 HL_PFOR(collapse(2))
559 for(size_t i = 0; i < height; i++)
560 for(size_t j = 0; j < width; j++)
561 {
562 const size_t idx = i * width + j;
563 const size_t c
564 = xtrans ? (size_t)FCxtrans((int)i, (int)j, roi_in, xtrans) : FC(i, j, filters); // CFA colour here
565 float value = input[idx];
566
567 if(c <= 2 && curves[c].engaged)
568 {
569 const float norm_val = value / clipval_raw[c]; // v in clip units
570
571 if(norm_val >= DT_HL_KNEE_LO && norm_val < DT_HL_KNEE_DET) // only band pixels get k^-1(v) = v + L(v)
572 value = (norm_val + _knee_lift_of(&curves[c], norm_val)) * clipval_raw[c];
573 }
574
575 input_corr[idx] = value; // unclipped/clipped/out-of-band values pass through unchanged
576 }
577}
578
579// env-gated CPU/GPU parity self-tests (same translation unit, see the file header)
580
581// ============================ OpenCL ============================
582
583#ifdef HAVE_OPENCL
584cl_int _hl_knee_estimate_cl(const int devid, void *gd_void, cl_mem dev_in, const size_t width, const size_t height,
585 const uint32_t filters, const dt_iop_roi_t *const roi_in, cl_mem dev_xtrans,
586 const int is_xtrans, const dt_aligned_pixel_t clipval_raw, _hl_knee_curve_t curves[3],
587 const dt_dev_pixelpipe_t *pipe)
588{
590 cl_int cl_err = DT_OPENCL_DEFAULT_ERROR;
591 dt_gaussian_cl_t *gsig = NULL; // one blur handle per sigma (9 blurs each)
592
593 for(int c = 0; c < 3; c++)
594 {
595 curves[c].engaged = 0;
596 memset(curves[c].lift, 0, sizeof(curves[c].lift));
597 }
598
599 const int base = is_xtrans ? 6 : 2;
600 int downsample = 1;
601 while((width / ((size_t)base * downsample)) * (height / ((size_t)base * downsample)) > 1500000) downsample++;
602
603 const int quad_size = base * downsample;
604 const size_t bin_w = width / quad_size;
605 const size_t bin_h = height / quad_size;
606 const size_t bin_pixels = bin_w * bin_h;
607 if(bin_w < 16 || bin_h < 16) return CL_SUCCESS; // like the CPU: no estimate, identity curves
608
609 const int bin_w_int = (int)bin_w, bin_h_int = (int)bin_h;
610 size_t work_sizes[3] = { ROUNDUPDWD(bin_w_int, devid), ROUNDUPDHT(bin_h_int, devid), 1 };
611
612 cl_mem dev_binned = dt_opencl_alloc_device_buffer(devid, sizeof(float) * bin_pixels * 3);
613 cl_mem dev_pred = dt_opencl_alloc_device_buffer(devid, sizeof(float) * bin_pixels * 3);
614 cl_mem dev_r2 = dt_opencl_alloc_device_buffer(devid, sizeof(float) * bin_pixels * 3);
615 cl_mem dev_done = dt_opencl_alloc_device_buffer(devid, bin_pixels * 3);
616 cl_mem moment_a = dt_opencl_alloc_device(devid, bin_w_int, bin_h_int, 4 * sizeof(float));
617 cl_mem moment_b = dt_opencl_alloc_device(devid, bin_w_int, bin_h_int, 4 * sizeof(float));
618 cl_mem moment_c = dt_opencl_alloc_device(devid, bin_w_int, bin_h_int, 4 * sizeof(float));
619 cl_mem blur_a = dt_opencl_alloc_device(devid, bin_w_int, bin_h_int, 4 * sizeof(float));
620 cl_mem blur_b = dt_opencl_alloc_device(devid, bin_w_int, bin_h_int, 4 * sizeof(float));
621 cl_mem blur_c = dt_opencl_alloc_device(devid, bin_w_int, bin_h_int, 4 * sizeof(float));
622 float *binned = dt_pixelpipe_cache_alloc_align_float(bin_pixels * 3, pipe);
623 float *pred = dt_pixelpipe_cache_alloc_align_float(bin_pixels * 3, pipe);
624 float *r2_scores = dt_pixelpipe_cache_alloc_align_float(bin_pixels * 3, pipe);
625 float *votes = dt_pixelpipe_cache_alloc_align_float(bin_pixels, pipe);
626 uint8_t *done = calloc(bin_pixels * 3, sizeof(uint8_t));
627 if(!dev_binned || !dev_pred || !dev_r2 || !dev_done || !moment_a || !moment_b || !moment_c || !blur_a || !blur_b
628 || !blur_c || !binned || !pred || !r2_scores || !votes || !done)
629 goto cleanup;
630
631 cl_err = dt_opencl_write_buffer_to_device(devid, done, dev_done, 0, bin_pixels * 3, CL_TRUE);
632 if(cl_err != CL_SUCCESS) goto cleanup;
633
634 // ---- binning ----
635 {
636 const int kernel = global_data->kernel_hl_knee_bin;
637 const int width_int = (int)width;
638 const int height_int = (int)height;
639 const int quad_size_int = quad_size;
640 const int roi_x = roi_in ? roi_in->x : 0;
641 const int roi_y = roi_in ? roi_in->y : 0;
642 const cl_float4 clip4 = { { clipval_raw[0], clipval_raw[1], clipval_raw[2], 1.f } };
643 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &dev_in);
644 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &dev_binned);
645 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(int), &width_int);
646 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(int), &height_int);
647 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &bin_w_int);
648 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &bin_h_int);
649 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &quad_size_int);
650 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(uint32_t), &filters);
651 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &roi_x);
652 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(int), &roi_y);
653 dt_opencl_set_kernel_arg(devid, kernel, 10, sizeof(int), &is_xtrans);
654 dt_opencl_set_kernel_arg(devid, kernel, 11, sizeof(cl_mem), &dev_xtrans);
655 dt_opencl_set_kernel_arg(devid, kernel, 12, sizeof(cl_float4), &clip4);
656 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, work_sizes);
657 if(cl_err != CL_SUCCESS) goto cleanup;
658 }
659
660 // the binned planes come home once: Phase B needs them, and they carry the band mass
661 cl_err
662 = dt_opencl_read_buffer_from_device(devid, binned, dev_binned, 0, sizeof(float) * bin_pixels * 3, CL_TRUE);
663 if(cl_err != CL_SUCCESS) goto cleanup;
664
665 // band mass per channel: count binned cells in [LO, DET) (mirrors the CPU); a channel with < 200
666 // stays identity
667 size_t nband[3] = { 0, 0, 0 };
668 for(size_t pixel = 0; pixel < bin_pixels; pixel++)
669 for(int c = 0; c < 3; c++)
670 if(binned[c * bin_pixels + pixel] >= DT_HL_KNEE_LO && binned[c * bin_pixels + pixel] < DT_HL_KNEE_DET)
671 nband[c]++;
672
673 if(nband[0] < 200 && nband[1] < 200 && nband[2] < 200)
674 {
675 cl_err = CL_SUCCESS;
676 goto cleanup;
677 }
678
679 // ---- Phase A: multi-scale windowed regressions on the device ----
680 {
681 const float sigmas[DT_HL_KNEE_NSIGMAS] = { 4.f, 8.f, 16.f, 32.f, 64.f };
682 const float knee_lo = DT_HL_KNEE_LO;
683 const float knee_det = DT_HL_KNEE_DET;
684 const float knee_fmin = DT_HL_KNEE_FMIN;
685
686 for(int sigma_index = 0; sigma_index < DT_HL_KNEE_NSIGMAS; sigma_index++)
687 {
688 const float sigma = sigmas[sigma_index];
689 dt_gaussian_free_cl(gsig); // previous sigma's handle
690 gsig = NULL;
691
692 // joint moments (n, means, second moments; 10 planes packed in 3 float4 images), then blurred
693 // by G_sigma to realise the windowed sums sum_y w G_sigma(...) of the 2x2 normal equations
694 {
695 const int kernel = global_data->kernel_hl_knee_jmom;
696 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &dev_binned);
697 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &moment_a);
698 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &moment_b);
699 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &moment_c);
700 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &bin_w_int);
701 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &bin_h_int);
702 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(float), &knee_lo);
703 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, work_sizes);
704 if(cl_err != CL_SUCCESS) goto cleanup;
705 }
706 gsig = _region_blur_handle(devid, bin_w_int, bin_h_int, sigma);
707 if(!gsig)
708 {
710 goto cleanup;
711 }
712 cl_err = dt_gaussian_blur_cl(gsig, moment_a, blur_a);
713 if(cl_err == CL_SUCCESS) cl_err = dt_gaussian_blur_cl(gsig, moment_b, blur_b);
714 if(cl_err == CL_SUCCESS) cl_err = dt_gaussian_blur_cl(gsig, moment_c, blur_c);
715 if(cl_err != CL_SUCCESS) goto cleanup;
716
717 // joint 2-guide regression v_hat = a*u1 + b*u2 + d per target channel c, solving the 2x2 normal
718 // system from the blurred moments (the kernel does the Cramer's-rule solve; writes pred/r2/done)
719 for(int c = 0; c < 3; c++)
720 {
721 if(nband[c] < 200) continue;
722 const int guide1 = (c == 0) ? 1 : 0; // u1 guide channel
723 const int guide2 = (c == 2) ? 1 : 2; // u2 guide channel
724 const int kernel = global_data->kernel_hl_knee_joint_reg;
725 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &dev_binned);
726 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &blur_a);
727 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &blur_b);
728 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &blur_c);
729 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(cl_mem), &dev_pred);
730 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(cl_mem), &dev_r2);
731 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(cl_mem), &dev_done);
732 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &bin_w_int);
733 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &bin_h_int);
734 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(int), &c);
735 dt_opencl_set_kernel_arg(devid, kernel, 10, sizeof(int), &guide1);
736 dt_opencl_set_kernel_arg(devid, kernel, 11, sizeof(int), &guide2);
737 dt_opencl_set_kernel_arg(devid, kernel, 12, sizeof(float), &knee_lo);
738 dt_opencl_set_kernel_arg(devid, kernel, 13, sizeof(float), &knee_det);
739 dt_opencl_set_kernel_arg(devid, kernel, 14, sizeof(float), &knee_fmin);
740 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, work_sizes);
741 if(cl_err != CL_SUCCESS) goto cleanup;
742 }
743
744 // single-guide fallback v_hat = a*u + d (a = Cov(v,u)/Var(u)) for cells the joint pass left
745 // done==0, both orientations of each pair (predict a from b, then b from a)
746 for(int chan_a = 0; chan_a < 3; chan_a++)
747 for(int chan_b = chan_a + 1; chan_b < 3; chan_b++)
748 {
749 if(nband[chan_a] < 200 && nband[chan_b] < 200) continue;
750 {
751 const int kernel = global_data->kernel_hl_knee_pmom;
752 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &dev_binned);
753 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &moment_a);
754 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &moment_b);
755 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(int), &bin_w_int);
756 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &bin_h_int);
757 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &chan_a);
758 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &chan_b);
759 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(float), &knee_lo);
760 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, work_sizes);
761 if(cl_err != CL_SUCCESS) goto cleanup;
762 }
763 cl_err = dt_gaussian_blur_cl(gsig, moment_a, blur_a);
764 if(cl_err == CL_SUCCESS) cl_err = dt_gaussian_blur_cl(gsig, moment_b, blur_b);
765 if(cl_err != CL_SUCCESS) goto cleanup;
766
767 for(int orient = 0; orient < 2; orient++)
768 {
769 const int target_ch = orient ? chan_b : chan_a;
770 const int guide_ch = orient ? chan_a : chan_b;
771 const int is_first_orient = (orient == 0);
772 if(nband[target_ch] < 200) continue;
773 const int kernel = global_data->kernel_hl_knee_pair_reg;
774 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &dev_binned);
775 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &blur_a);
776 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &blur_b);
777 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &dev_pred);
778 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(cl_mem), &dev_r2);
779 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(cl_mem), &dev_done);
780 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &bin_w_int);
781 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &bin_h_int);
782 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &target_ch);
783 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(int), &guide_ch);
784 dt_opencl_set_kernel_arg(devid, kernel, 10, sizeof(int), &is_first_orient);
785 dt_opencl_set_kernel_arg(devid, kernel, 11, sizeof(float), &knee_lo);
786 dt_opencl_set_kernel_arg(devid, kernel, 12, sizeof(float), &knee_det);
787 dt_opencl_set_kernel_arg(devid, kernel, 13, sizeof(float), &knee_fmin);
788 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, work_sizes);
789 if(cl_err != CL_SUCCESS) goto cleanup;
790 }
791 }
792 }
793 }
794
795 cl_err = dt_opencl_read_buffer_from_device(devid, pred, dev_pred, 0, sizeof(float) * bin_pixels * 3, CL_TRUE);
796 if(cl_err == CL_SUCCESS)
797 cl_err
798 = dt_opencl_read_buffer_from_device(devid, r2_scores, dev_r2, 0, sizeof(float) * bin_pixels * 3, CL_TRUE);
799 if(cl_err == CL_SUCCESS)
800 cl_err = dt_opencl_read_buffer_from_device(devid, done, dev_done, 0, bin_pixels * 3, CL_TRUE);
801 if(cl_err != CL_SUCCESS) goto cleanup;
802
803 // ---- Phase B (host, identical to the CPU _hl_knee_estimate; see there for the full maths):
804 // pool the votes v_hat_i - v_i into 24 band bins, take each bin's significant median lift, then
805 // make the curve monotone + raise-only. ----
806 for(int c = 0; c < 3; c++)
807 {
808 if(nband[c] < 200) continue;
809
810 size_t count[DT_HL_KNEE_BINS] = { 0 };
811 size_t offset[DT_HL_KNEE_BINS + 1] = { 0 };
812 const float bin_width = (DT_HL_KNEE_DET - DT_HL_KNEE_LO) / (float)DT_HL_KNEE_BINS; // band width / 24
813
814 // pass 1: count votes per bin (only predicted cells clearing the R^2 > R2MIN fit-quality gate)
815 for(size_t pixel = 0; pixel < bin_pixels; pixel++)
816 {
817 if(!done[c * bin_pixels + pixel] || r2_scores[c * bin_pixels + pixel] <= DT_HL_KNEE_R2MIN) continue;
818 const int bin_index // bin of the measured value v
819 = CLAMP((int)((binned[c * bin_pixels + pixel] - DT_HL_KNEE_LO) / bin_width), 0, DT_HL_KNEE_BINS - 1);
820 count[bin_index]++;
821 }
822 for(int i = 0; i < DT_HL_KNEE_BINS; i++) offset[i + 1] = offset[i] + count[i]; // prefix-sum -> per-bin base
823
824 size_t fill[DT_HL_KNEE_BINS];
825 memcpy(fill, offset, sizeof(fill));
826 // pass 2: scatter each vote v_hat_i - v_i (pred - measured) into its bin's slots
827 for(size_t pixel = 0; pixel < bin_pixels; pixel++)
828 {
829 if(!done[c * bin_pixels + pixel] || r2_scores[c * bin_pixels + pixel] <= DT_HL_KNEE_R2MIN) continue;
830 const float x_val = binned[c * bin_pixels + pixel]; // measured v
831 const int bin_index = CLAMP((int)((x_val - DT_HL_KNEE_LO) / bin_width), 0, DT_HL_KNEE_BINS - 1);
832 votes[fill[bin_index]++] = pred[c * bin_pixels + pixel] - x_val; // one pixel's vote
833 }
834
835 float lift[DT_HL_KNEE_BINS];
836 int seen[DT_HL_KNEE_BINS];
837 int nseen = 0;
838 for(int i = 0; i < DT_HL_KNEE_BINS; i++)
839 {
840 lift[i] = 0.f;
841 seen[i] = 0;
842 if(count[i] < DT_HL_KNEE_MINVOTES) continue; // need >= 100 votes for a stable error estimate
843 float *const bin_votes = votes + offset[i];
844 const float median_lift = _knee_median(bin_votes, count[i]); // median{ v_hat_i - v_i } = raw bin lift
845 for(size_t k = 0; k < count[i]; k++) bin_votes[k] = fabsf(bin_votes[k] - median_lift); // |lift_i - median|
846 const float median_abs_dev = _knee_median(bin_votes, count[i]); // MAD (robust spread)
847 // SE of the bin median = 1.858*MAD/sqrt(n); 1.858 = 1.4826 (MAD->sigma) * 1.2533 (sigma->SE of median)
848 const float std_err = 1.858f * median_abs_dev / sqrtf((float)count[i]);
849 seen[i] = 1;
850 nseen++;
851 if(median_lift > DT_HL_KNEE_NSIGMA * std_err)
852 lift[i] = median_lift; // accept only if lift > 2*SE (~95% gate)
853 }
854 if(nseen < 3) continue; // too few populated bins -> identity
855
856 // interpolate lift over unseen bins (flat-extend the ends, linear between two seen bins)
857 int prev = -1;
858 for(int i = 0; i < DT_HL_KNEE_BINS; i++)
859 {
860 if(seen[i])
861 {
862 prev = i;
863 continue;
864 }
865 int next = -1;
866 for(int k = i + 1; k < DT_HL_KNEE_BINS; k++)
867 if(seen[k])
868 {
869 next = k;
870 break;
871 }
872 if(prev < 0 && next < 0)
873 lift[i] = 0.f;
874 else if(prev < 0)
875 lift[i] = lift[next]; // flat-extend before the first seen bin
876 else if(next < 0)
877 lift[i] = lift[prev]; // flat-extend past the last seen bin
878 else
879 lift[i] = lift[prev] + (lift[next] - lift[prev]) * (float)(i - prev) / (float)(next - prev); // linear
880 }
881
882 // monotone raise-only clamp: cumulative max (rolloff bias grows toward clip), negatives dropped
883 float running_max = 0.f;
884 float lift_max = 0.f;
885 for(int i = 0; i < DT_HL_KNEE_BINS; i++)
886 {
887 running_max = fmaxf(running_max, fmaxf(lift[i], 0.f));
888 curves[c].lift[i] = running_max;
889 lift_max = fmaxf(lift_max, running_max);
890 }
891 // engage threshold: peak lift below ENGAGE = 0.005 is noise -> identity (no-op guarantee)
892 curves[c].engaged = (lift_max >= DT_HL_KNEE_ENGAGE);
893 if(!curves[c].engaged) memset(curves[c].lift, 0, sizeof(curves[c].lift));
894 }
895 cl_err = CL_SUCCESS;
896
897cleanup:
913 free(done);
914 return cl_err;
915}
916
917cl_int _hl_knee_apply_cfa_cl(const int devid, void *gd_void, cl_mem dev_in, cl_mem dev_out, const size_t width,
918 const size_t height, const uint32_t filters, const dt_iop_roi_t *const roi_in,
919 cl_mem dev_xtrans, const int is_xtrans, const dt_aligned_pixel_t clipval_raw,
920 const _hl_knee_curve_t curves[3])
921{
923 const int width_int = (int)width;
924 const int height_int = (int)height;
925 size_t work_sizes[3] = { ROUNDUPDWD(width_int, devid), ROUNDUPDHT(height_int, devid), 1 };
926
927 float lift[3 * DT_HL_KNEE_BINS];
928 for(int c = 0; c < 3; c++) memcpy(lift + c * DT_HL_KNEE_BINS, curves[c].lift, sizeof(curves[c].lift));
929 cl_mem dev_lift = _sp_cl_upload(devid, lift, sizeof(lift));
930 if(!dev_lift) return DT_OPENCL_DEFAULT_ERROR;
931
932 const int kernel = global_data->kernel_hl_knee_apply;
933 const int roi_x = roi_in ? roi_in->x : 0;
934 const int roi_y = roi_in ? roi_in->y : 0;
935 const cl_float4 clip4 = { { clipval_raw[0], clipval_raw[1], clipval_raw[2], 1.f } };
936 const cl_int4 engaged_flags = { { curves[0].engaged, curves[1].engaged, curves[2].engaged, 0 } };
937 const float knee_lo = DT_HL_KNEE_LO;
938 const float knee_det = DT_HL_KNEE_DET;
939 const int bins = DT_HL_KNEE_BINS;
940 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &dev_in);
941 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &dev_out);
942 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(int), &width_int);
943 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(int), &height_int);
944 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(uint32_t), &filters);
945 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &roi_x);
946 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &roi_y);
947 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &is_xtrans);
948 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(cl_mem), &dev_xtrans);
949 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(cl_float4), &clip4);
950 dt_opencl_set_kernel_arg(devid, kernel, 10, sizeof(cl_mem), &dev_lift);
951 dt_opencl_set_kernel_arg(devid, kernel, 11, sizeof(cl_int4), &engaged_flags);
952 dt_opencl_set_kernel_arg(devid, kernel, 12, sizeof(float), &knee_lo);
953 dt_opencl_set_kernel_arg(devid, kernel, 13, sizeof(float), &knee_det);
954 dt_opencl_set_kernel_arg(devid, kernel, 14, sizeof(int), &bins);
955 const cl_int cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, work_sizes);
957 return cl_err;
958}
959
960#endif // HAVE_OPENCL
void cleanup(dt_imageio_module_format_t *self)
Definition avif.c:164
int width
Definition bilateral.h:1
int height
Definition bilateral.h:1
dt_gaussian_t * _hl_gauss_get(const int width, const int height, const int channels, const float sigma)
Definition blur.c:30
dt_gaussian_cl_t * _region_blur_handle(const int devid, const int region_w, const int region_h, const float sigma)
Definition blur.c:69
static const int row
#define dt_pixelpipe_cache_free_align(mem)
Definition darktable.h:475
#define dt_pixelpipe_cache_alloc_align_float(pixels, pipe)
Definition darktable.h:454
#define __DT_CLONE_TARGETS__
Definition darktable.h:379
static const dt_aligned_pixel_simd_t value
Definition darktable.h:599
#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 darktable.h:293
static int FCxtrans(const int row, const int col, global const unsigned char(*const xtrans)[6])
static int FC(const int row, const int col, const unsigned int filters)
static void weight(const float *c1, const float *c2, const float sharpen, dt_aligned_pixel_t weight)
Definition eaw.c:30
static float gaussian(float x, float std)
Definition filmic.c:397
void dt_gaussian_free_cl(dt_gaussian_cl_t *g)
Definition gaussian.c:353
cl_int dt_gaussian_blur_cl(dt_gaussian_cl_t *g, cl_mem dev_in, cl_mem dev_out)
Definition gaussian.c:441
void dt_gaussian_blur_4c(dt_gaussian_t *g, const float *const in, float *const out)
Definition gaussian.c:325
static float kernel(const float *x, const float *y)
static const float x
__DT_CLONE_TARGETS__ void _hl_knee_apply_interpolated(float *const restrict interpolated, const size_t npix, const dt_aligned_pixel_t clipvaln, const dt_aligned_pixel_t wb4, const _hl_knee_curve_t curves[3])
Definition knee.c:515
__DT_CLONE_TARGETS__ void _hl_knee_estimate(const float *const restrict input, const size_t width, const size_t height, const uint32_t filters, const dt_iop_roi_t *const roi_in, const uint8_t(*const xtrans)[6], const dt_aligned_pixel_t clipval_raw, _hl_knee_curve_t curves[3], const dt_dev_pixelpipe_t *pipe)
Definition knee.c:105
__DT_CLONE_TARGETS__ void _hl_knee_apply_cfa(const float *const restrict input, float *const restrict input_corr, const size_t width, const size_t height, const uint32_t filters, const dt_iop_roi_t *const roi_in, const uint8_t(*const xtrans)[6], const dt_aligned_pixel_t clipval_raw, const _hl_knee_curve_t curves[3])
Definition knee.c:553
static __DT_CLONE_TARGETS__ void _knee_blur4(const float *const planes[4], float *const outs[4], const int n_planes, const int region_w, const int region_h, const float sigma, float *const restrict pack_in, float *const restrict pack_out)
Definition knee.c:53
static int _knee_p2(const int chan_a, const int chan_b)
Definition knee.c:98
static float _knee_lift_of(const _hl_knee_curve_t *const k, const float x)
Definition knee.c:30
cl_int _hl_knee_estimate_cl(const int devid, void *gd_void, cl_mem dev_in, const size_t width, const size_t height, const uint32_t filters, const dt_iop_roi_t *const roi_in, cl_mem dev_xtrans, const int is_xtrans, const dt_aligned_pixel_t clipval_raw, _hl_knee_curve_t curves[3], const dt_dev_pixelpipe_t *pipe)
Definition knee.c:584
static float _knee_median(float *const values, const size_t count)
Definition knee.c:88
cl_int _hl_knee_apply_cfa_cl(const int devid, void *gd_void, cl_mem dev_in, cl_mem dev_out, const size_t width, const size_t height, const uint32_t filters, const dt_iop_roi_t *const roi_in, cl_mem dev_xtrans, const int is_xtrans, const dt_aligned_pixel_t clipval_raw, const _hl_knee_curve_t curves[3])
Definition knee.c:917
static int _knee_cmp_float(const void *ptr_a, const void *ptr_b)
Definition knee.c:79
float *const restrict const size_t k
float dt_aligned_pixel_t[4]
int dt_opencl_enqueue_kernel_2d(const int dev, const int kernel, const size_t *sizes)
Definition opencl.c:2164
void * dt_opencl_alloc_device_buffer(const int devid, const size_t size)
Definition opencl.c:2580
void * dt_opencl_alloc_device(const int devid, const int width, const int height, const int bpp)
Definition opencl.c:2504
int dt_opencl_write_buffer_to_device(const int devid, void *host, void *device, const size_t offset, const size_t size, const int blocking)
Definition opencl.c:2348
int dt_opencl_read_buffer_from_device(const int devid, void *host, void *device, const size_t offset, const size_t size, const int blocking)
Definition opencl.c:2337
int dt_opencl_set_kernel_arg(const int dev, const int kernel, const int num, const size_t size, const void *arg)
Definition opencl.c:2155
void dt_opencl_release_mem_object(cl_mem mem)
Definition opencl.c:2415
#define DT_OPENCL_DEFAULT_ERROR
Definition opencl.h:57
#define ROUNDUPDHT(a, b)
Definition opencl.h:82
#define ROUNDUPDWD(a, b)
Definition opencl.h:81
static cl_mem _sp_cl_upload(const int devid, const void *data, const size_t bytes)
const float sigma
#define DT_HL_KNEE_R2MIN
#define DT_HL_KNEE_DET
#define DT_HL_KNEE_LO
#define DT_HL_KNEE_FMIN
#define DT_HL_KNEE_BINS
#define DT_HL_KNEE_ENGAGE
#define DT_HL_KNEE_MINVOTES
#define DT_HL_KNEE_NSIGMA
#define DT_HL_KNEE_NSIGMAS
#define HL_PFOR(...)
Region of interest passed through the pixelpipe.
Definition imageop.h:72
#define MIN(a, b)
Definition thinplate.c:32