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