Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
src/iop/highlights/common.h
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#ifndef DT_IOP_HIGHLIGHTS_COMMON_H
21#define DT_IOP_HIGHLIGHTS_COMMON_H
22
23#include "system/openmp.h" // HL_PFOR/__OMP_PARALLEL_FOR__
24#include "system/simd.h" // dt_aligned_pixel_t + SIMD macros
25#include "pixel/gaussian.h" // dt_gaussian_t / dt_gaussian_cl_t (in _hl_gauss_slot_t + CL protos)
26#include "develop/pixelpipe_hb.h" // dt_dev_pixelpipe_t (in _hl_region_ctx_t) + dt_dev_pixelpipe_iop_t
27#include <glib.h> // CLAMP
28#include <math.h>
29#include <stdint.h>
30
31// Shared macros and struct definitions for the highlights harmonic-transposition mode,
32// used by both the C (the CPU stages) and OpenCL (process.h / the OpenCL stages)
33// halves. This is a textual include unit of highlights.c (see the note in _cpu.h).
34
35/*
36 Harmonic transposition: Ansel's highlight-reconstruction method (2026 rebuild).
37
38 Everything specific to the DT_IOP_HIGHLIGHTS_HARMONIC mode lives here: the sensor-rolloff
39 knee inversion, the per-region segmentation, the coefficient-field colour-line transport
40 (windowed joint fits, harmonic diffusion of the model, deep-channel cascade), the sparse
41 SPD Cholesky and the direct solvers built on it (biharmonic dome, screened chroma,
42 divergence-form structure-steered chroma), the HF-band hybrid, the CPU drivers for Bayer
43 and X-Trans, and the hybrid OpenCL driver (GPU gather/remosaic around the CPU middle).
44
45 This is a textual include unit of highlights.c (not a standalone translation unit): it
46 relies on the CFA helpers, gather/scatter, buffer conventions and the global kernel
47 handles defined there. See the companion article for the method's derivation.
48*/
49
50// =====================================================================================
51// Segmented full-resolution guided-laplacian reconstruction
52//
53// Rather than reconstruct the whole downsampled frame with the a-trous wavelet stack,
54// isolate each connected clipped region and run a coarse->fine FULL-VALUE guided filter
55// at full resolution on the small rectangle enclosing it (plus padding that gives the
56// colour-line fit a valid rim). This recovers the clipped channel's magnitude (the
57// intercept carries the DC), not merely its texture, and only touches clipped
58// neighbourhoods. See doc/ and the companion article for the derivation.
59//
60// NOTE: this first stage ports the guided-filter "bug fix" only. The confidence blend,
61// the all-clipped joint core and the uncertainty regulariser (all needing a small
62// per-region biharmonic solve) land in a follow-up; all-clipped cores are meanwhile
63// left to the surrounding fill.
64// =====================================================================================
65
66// DEBUG TOGGLE: set to 0 to disable the biharmonic + chroma refinement (self-dome, joint
67// all-clipped core, flat-colour) and run ONLY the guided-filter stage (increment 1), to
68// validate the segmentation + guided path in isolation.
69#define DT_HL_BIHARMONIC 1
70
71// BAND OVERRIDE: for channels whose sensor rolloff ENGAGED (knee), extend the clip detection
72// down to this fraction of the threshold. The knee's value-map restores the band's level but
73// cannot restore a slope the sensor compressed away; the colour-line model, anchored on the
74// truly linear data below the band, can -- and each band pixel's knee-lifted measurement acts
75// as its per-pixel saturation floor, so the override can only raise, never lose data. On the
76// rolloff bench scene this removes the residual contour arc and cuts the zone RMSE 5x
77// (0.012 -> 0.0025); on hard-clipping channels (knee not engaged) detection is unchanged and
78// the output is bit-identical. Values in [0.7, 1.0); 1.0 disables.
79#define DT_HL_BAND_OVR 0.9f
80
81#define HL_PFOR(...) __OMP_PARALLEL_FOR__(__VA_ARGS__)
82
83// Clip-asymmetry gate for the chromaticity-preserving floor family. The per-channel saturation
84// floors imprint the CLIP LEVELS' chromaticity on multi-clip pixels wherever the solver
85// under-predicts; whether that imprint is plausible scene content depends only on the clip levels
86// themselves: with NEAR-EQUAL per-channel clips (unit WB -- every synthetic bench case) the imprint
87// is neutral and usually near the truth of a bright core, with strongly UNEQUAL clips (real cameras:
88// clips = WB coefficients) the imprint is the inverse-WB magenta, never a plausible emitter. Gate
89// the joint (hue-preserving) floor family by the clip asymmetry A = max_c/min_c.
90// The ramp starts at 1.25, NOT 1.0: clips inherit processed_maximum, which carries a ~10% non-WB
91// wiggle from the input profile handling even at unit white balance (measured A = 1.145 on the
92// unit-WB article-bench DNGs, AsShotNeutral = (1,1,1)) -- that regime must keep the approved
93// per-channel behavior exactly (g = 0). Real-camera white balance sits at A ~ 2..2.6 (the Bayer
94// green photosite is ~2x more sensitive than red/blue), so g = 1 from A >= 2 covers every real
95// raw while the dead zone below 1.25 absorbs profile wiggle.
96static inline float _hl_floor_gate(const float clips[4])
97{
98 const float mn = fminf(clips[0], fminf(clips[1], clips[2]));
99 const float mx = fmaxf(clips[0], fmaxf(clips[1], clips[2]));
100 const float asym = (mn > 1e-9f) ? mx / mn : 1.f;
101 const float t = CLAMP((asym - 1.25f) / 0.75f, 0.f, 1.f);
102 return t * t * (3.f - 2.f * t);
103}
104
105// Trusted-ring validation of the flat-mean colour prior -- the surround-importing refinements'
106// own scene assumption ("blown-core colour ~ bright-surround mean colour"). At 1-clip pixels
107// (exactly one clipped channel: reconstructed from TWO measured guides, measured
108// chromaticity-correct), compare the RING MEAN chromaticity shares to cmean's shares, normalized
109// by the ring's own dispersion: t = |ring_mean - cmean|_L1 / max(ring_std_sum, 0.02),
110// vote = exp(-(t/5)^2). A self-coloured emitter shifts the whole ring COHERENTLY (measured
111// magentasun: bias 0.21 over dispersion 0.007 -> t ~ 10 floored, vote ~ 0), while real scenes
112// scatter the ring by noise/texture AROUND the mean (measured MAC/sunrise: t = 0.3..1.5,
113// vote ~ 1). A per-pixel agreement average fails here: real-ring spread caps it at ~0.3
114// regardless of tolerance, and any tolerance wide enough for real content re-opens the emitter.
115// Mirrors py_flat_mean_vote (validate.py); the cgrad ring gate answers a DIFFERENT question
116// (continuation of the extended gradient field) and may disagree (MAC: cgrad closes, this opens).
117// Serial double accumulation on purpose: deterministic run-to-run; the ring is a small subset.
118static inline float _hl_ring_flat_mean_vote(const float *const restrict estimate,
119 const float *const restrict valid,
120 const dt_aligned_pixel_t cmean, const size_t region_pixels)
121{
122 const float cmean_sum = fmaxf(cmean[0] + cmean[1] + cmean[2], 1e-9f);
123 const float cmean_share[3]
124 = { cmean[0] / cmean_sum, cmean[1] / cmean_sum, cmean[2] / cmean_sum };
125 double share_sum[3] = { 0.0, 0.0, 0.0 };
126 double share_sq[3] = { 0.0, 0.0, 0.0 };
127 double ring_count = 0.0;
128 for(size_t i = 0; i < region_pixels; i++)
129 {
130 const int n_clipped = (valid[i * 4 + 0] < 0.5f) + (valid[i * 4 + 1] < 0.5f) + (valid[i * 4 + 2] < 0.5f);
131 if(n_clipped != 1) continue;
132 const float sum = fmaxf(estimate[i * 4 + 0] + estimate[i * 4 + 1] + estimate[i * 4 + 2], 1e-9f);
133 for(int c = 0; c < 3; c++)
134 {
135 const double share = (double)(estimate[i * 4 + c] / sum);
136 share_sum[c] += share;
137 share_sq[c] += share * share;
138 }
139 ring_count += 1.0;
140 }
141 if(ring_count <= 0.0) return 0.f; // no trusted ring: the prior stays unproven
142 float bias = 0.f;
143 float dispersion = 0.f;
144 for(int c = 0; c < 3; c++)
145 {
146 const double mean = share_sum[c] / ring_count;
147 bias += fabsf((float)mean - cmean_share[c]);
148 dispersion += sqrtf(fmaxf((float)(share_sq[c] / ring_count - mean * mean), 0.f));
149 }
150 const float t = bias / fmaxf(dispersion, 0.02f);
151 const float arg = t / 5.f;
152 return expf(-arg * arg);
153}
154
155// Per-thread cache of dt_gaussian handles keyed on (width, height, channels, sigma).
156// dt_gaussian_init allocates its recursion temporaries on every call, and the region stages
157// fire dozens of same-shaped blurs per region (the knee, over a hundred per image): reusing
158// the handle removes pure allocation churn. The cache is __thread and the blur calls happen
159// serially on the caller's thread (parallelism lives INSIDE dt_gaussian_blur), so no locking.
160// Drivers flush it on exit (_hl_gauss_cache_flush) so nothing leaks across pipeline runs.
161typedef struct
162{
163 int width;
166 float sigma;
169
170#define HL_GAUSS_SLOTS 4
171
172// GUIDE-SELECTION TUNABLES -- traced from magenta regressions on real amber/orange highlights.
173// DT_HL_PAIR_VARIANCE: 1 = score guides by the pair-restricted variance already computed for the fit
174// where valid pairs are scarce -- e.g. the thin valid surround of a saturated highlight).
175// DT_HL_GUIDE_GATE: may a CLIPPED channel guide through its clip value? On amber/orange highlights
176// the only surviving channel is a LOW blue -- a poor guide that collapses green to magenta -- while
177// the clipped red at its clip level pulls green back up. But allowing that everywhere magentas the
178// shallow annulus (there a clipped guide's clip value is wrong). So gate it by DEPTH:
179// 2 = DEPTH-GATED (default): clipped guide allowed only where dep >= 0.5 * region radius (the
180// saturated core/inner ring -> recovers amber); the shallow annulus keeps valid-only (accurate).
181// 1 = never (prototype: annulus good, core magenta/dark); 0 = always (core good, annulus magenta).
182#define DT_HL_PAIR_VARIANCE 1
183#define DT_HL_GUIDE_GATE 2
184
185// DT_HL_CLIP_FLOOR: a clipped channel SATURATED, so its true value is >= its clip level. If the
186// colour-line fit (from a low surviving guide) comes out below that, floor it back up. Physical,
187// parameter-free, and monotone (only raises below-clip values -> no overshoot, no per-pixel guide
188// switching -> no patchwork). Fixes the amber core/ring collapsing to a dark magenta without the
189// clipped-guide hacks (DT_HL_GUIDE_GATE 0/2), which magenta the annulus / stain the core.
190#define DT_HL_CLIP_FLOOR 1
191
192// Max unknowns in the dense-Cholesky biharmonic dome before it downsamples (O(N^3)). Larger = finer
193// dome grid (ds->1 = exact full-res) at more cost -- raise it to test if the coarse solve matters.
194// Below this many clipped input pixels there is nothing worth reconstructing: process()/process_cl()
195// copy the input through instead of running a mode. Same idea as filmicrgb's mask_clipped_pixels()
196// bail-out, one order of magnitude more generous because the reconstruction modes here cost far more
197// per frame (region segmentation, sparse solves) than filmic's wavelet pass.
198// Width, in full-resolution pixels, of the collar around the clip contour where the
199// chromaticity-gradient stage's value-continuation pass (a3) is allowed to act. That pass erases the
200// seam the saturation floor prints at the contour, and is only sound close to it -- see the comment
201// at its hole test. Scaled by the pipe scale so a preview matches the full-resolution render.
202// Range scale of the coefficient-field's edge-aware fit windows, as a fraction of the blown zone's
203// plateau luminance: a guide step of this size halves the transport across it. Small enough to stop
204// a silhouette, large enough that a smooth sky gradient is still one window.
205#define CF_EDGE_RANGE 0.15f
206
207// Pre-smoothing of that guide, in pixels: enough to take sensor noise out of the warp rate, far
208// below the scale of any silhouette it has to stop.
209#define CF_EDGE_GUIDE_SIGMA 3.f
210
211// L1 share distance at which the joint floor's chromaticity rescue is worth its noise cost in full
212// (one just-noticeable hue step -- the same scale as the cgrad content gate's tolerance). Below it
213// the per-channel floor is kept: it clamps the fit's noise and meets the surround at the clip level.
214#define CF_JOINT_TAU 0.10f
215
216
217
218
219// A REGION-level decision, not a per-pixel one. Both floors are legitimate whole treatments, but
220// they cannot be mixed spatially: the per-channel floor clamps each clipped channel at its own c0,
221// which on WB'd clips imprints the inverse-WB chromaticity -- magenta. So wherever the per-channel
222// side dominates a spatial blend it PAINTS that magenta, and a joint core ringed by a per-channel
223// annulus reads as the worst of both (observed directly: shrinking the yellow core by depth and
224// handing the surround back to per-channel produced magenta spots around it, worse than either
225// endpoint). The choice must therefore be uniform across a blown zone -- blown zones are separated
226// by measured pixels, so a different choice per zone prints no seam.
227//
228// The statistic: what the joint form buys the region AS A WHOLE, i.e. the L1 distance between the
229// two candidates' AGGREGATE chromaticity over the region's clipped pixels, measured BEFORE the floor
230// runs (measuring it after the floor gives a different, far less separable quantity -- 3x instead of
231// the 14x below, which is how the first version of these thresholds came out inert).
232// Measured pre-floor: PK1_3540's blown sky 0.0033 -- the joint form buys it almost nothing and its
233// scatter is a bad trade. MAC25640's lamps 0.045 / 0.048 / 0.089 / 0.288 -- badly floor-imprinted,
234// the rescue is the whole point. Two of MAC's smaller regions sit at 0.0042 and 0.0019, i.e. inside
235// PK1's range: they lose the rescue, which is the price of deciding per region rather than per pixel
236// (a per-pixel or per-depth blend cannot be used at all -- see above).
237#define CF_REGION_LO 0.005f // at or below: the region gains too little to pay the scatter
238#define CF_REGION_HI 0.015f // at or above: the region needs the rescue in full
239
240static inline float _hl_region_worth(const float region_benefit)
241{
242 const float t = CLAMP((region_benefit - CF_REGION_LO) / (CF_REGION_HI - CF_REGION_LO), 0.f, 1.f);
243 return t * t * (3.f - 2.f * t);
244}
245
246// How much the joint floor's chromaticity rescue is WORTH at this pixel, in [0,1]. The joint form
247// keeps the fit (and the fit's scatter with it); the per-channel floor clamps each clipped channel
248// at its own c0, which suppresses that scatter and meets the surround at the level the neighbouring
249// measured pixels sit at. Measured cost of keeping the fit: +56% grain on MAC25640, +58% on
250// PK1_3540 -- the same penalty on both. What differs is the benefit: MAC's clipped subsets are
251// badly floor-imprinted and the joint form moves G/((R+B)/2) 0.300 -> 0.635 (2.1x, the magenta
252// rescue it exists for), while PK1's blown sky moves 0.755 -> 0.773 (2.3%) -- the same noise bought
253// for almost nothing, which prints as a flat, grainy core. So pay the noise in proportion to the
254// chromaticity actually rescued: the L1 distance between the two candidates' shares, ramped over
255// one just-noticeable hue step. Where they agree the per-channel floor is kept verbatim, so a scene
256// the joint form was not changing cannot regress.
257static inline float _hl_floor_worth(const float chroma_gain)
258{
259 const float t = CLAMP(chroma_gain / CF_JOINT_TAU, 0.f, 1.f);
260 return t * t * (3.f - 2.f * t);
261}
262
263// A 1-clip channel whose model predicted below its own saturation level comes out pinned AT the
264// floor, carrying the floor's chroma instead of the material's. This is the lift, relative to
265// clip0, by which such a pixel is considered to have been reconstructed on its own merits: at 1.0
266// the fit said nothing, by this value it has spoken and is left alone. Ramped, never a threshold.
267#define CF_AUTHORED_RAMP 1.12f
268
269#define DT_HL_A3_COLLAR_PX 24.f
270
271#define DT_HL_MIN_CLIPPED_PIXELS 25
272
273#define DT_HL_DOME_NMAX 2000
274
275// With the sparse direct solver the dome grid can be MUCH finer at less cost than the dense
276// O(N^3) solve ever allowed: coarse-grid unknowns cap for the sparse path (ds -> 1 up to this).
277#define DT_HL_DOME_NMAX_SPARSE 8192
278
279// REFINEMENT stages inside the biharmonic block. The joint core (all-clip magnitude dome + diffused
280// chroma) is ALWAYS on -- it recovers the sun disc. The per-channel SELF-DOME and SEAM-REG were
281// disabled after they turned a correctly-guided amber annulus magenta. That was traced NOT to the
282// per-channel method (the RGB prototype validates it: it never magentas, even on a 59%-clipped amber
283// sun) but to three C-only divergences from the prototype, now fixed:
284// 1. the self-dome domed each channel at its OWN downsample factor -> per-channel-inconsistent
285// approximation -> chroma drift. Now all three share ONE ds (sized from the union hole).
286// 2. the seam regulariser ran only "iterations" (~30) CG steps on an ill-conditioned biharmonic;
287// each channel stopped at a different point -> chroma drift. Now runs the full CG budget.
288// 3. the confidence R^2 was gated to 0 in low-valid-weight regions, starving the seam reg of data
289// fidelity (Wd = R^4 -> 0) at the all-clip core. Gate removed (matches the prototype).
290// Also: the saturation floor is now re-asserted AFTER the self dome (the prototype floors there).
291// Re-enabled for real-image A/B; flip either to 0 to isolate. They help genuinely decorrelated
292// content (rare in natural images) and are near no-ops on correlated content.
293#define DT_HL_SELF_DOME 1
294
295// Structure-steered chroma post-pass: keep the ladder's MAGNITUDE (norm), re-diffuse the clipped
296// channels' RATIOS along the isophotes of the recovered luminance, coarse-to-fine so the diffusion
297// seeds the entire hole (unreached interior would keep its magenta ratios). Fixes the guide-flip /
298// scale hand-off chroma patches without touching the recovery. See _aniso_tensor/_aniso_iterate.
299#define DT_HL_ANISO_CHROMA 1
300
301// R9: blind sensor-rolloff (knee) pre-correction. Real sensors compress the last few percent below
302// the clip level (saturation rolloff), so the near-clip BAND holds values biased LOW. Every
303// reconstruction hand-off against that biased data seams (R2-R8 lesson: seam energy = estimator
304// disagreement, and no weighting can hide it) -- the only zero-seam fix is to DEBIAS THE BAND DATA
305// ITSELF. A windowed colour-line fitted on fully-trusted pixels predicts what each band value should
306// be; binned robust medians of (measured, predicted) pairs trace the knee inverse, which is applied
307// to the band before reconstruction. On hard-clipped (unbiased) data the fitted curve is the
308// identity, so the correction has a NO-OP GUARANTEE. Estimation is global per channel (the knee is
309// a sensor property) and runs on a downsampled copy; like the Laplacian normalization it is local
310// to the tile being processed.
311#define DT_HL_KNEE 1
312
313// COEFFICIENT-FIELD reconstruction (replaces the guided ladder when 1). The PK1 step study
314// showed the ladder's coarse scales write FLAT fills into the deep zone (heterogeneous windows
315// attenuate the fitted slope toward the window mean) in depth-level-set annuli whose boundaries
316// are the visible hard arcs (scale hand-offs between disagreeing estimators). Values
317// extrapolated from far are unstable -- but the local colour-line COEFFICIENTS are smooth by
318// nature. So: fit est_c = a*g1 + b*g2 + d in windows where channel c is trusted, harmonically
319// diffuse the coefficient fields (a, b, d) across the clipped zone, and evaluate against the
320// MEASURED valid guides at every pixel. The clipped channel inherits the guides' true structure;
321// no scales, no depth gates, no level-set writes -> no seams by construction.
322//
323// Around the core fit, four hand-off-free safeguards (each validated on the synthetic bench):
324// - the fit quality R^2 is diffused alongside the coefficients and scales the estimate's
325// HIGH FREQUENCIES (a weak colour-line must not print the guides' fine texture);
326// - a DEPTH-GATED per-channel self-dome takes over where the model is doubtful (low R^2) AND
327// the dome is trustworthy (shallow: biharmonic extrapolation degrades with distance) --
328// depth is the only reliable arbiter between correlated transfer and decorrelated printing,
329// which is undecidable from rim statistics alone;
330// - the clip floor is SOFT (rounded over ~2% of the clip level), so a prediction oscillating
331// around saturation does not print the binding contour;
332// - the all-clip core rebuild is FEATHERED into the surrounding reconstruction over a blurred
333// mask instead of a hard hand-off. All-clip pixels have no guides: they stay at the clip
334// floor and the joint core rebuilds them, with the aniso chroma pass restricted to them
335// (coefficient-field results act as anchors).
336#define DT_HL_COEFF_FIELD 1
337
338// Laplacian-band (HF) guiding, hybridized: the detail band gets its OWN windowed colour-line
339// (R^2-shrunk gains -- on a zero-mean band shrinkage is the correct estimator), and the
340// reconstruction's high frequencies are blended between this guided resynthesis and the
341// R^2-damped transfer by QUADRATIC MIN-ENERGY odds: a mixed-window gain misfiring at an object
342// edge shows up as an HF energy spike, so the failure detects itself and the damped path takes
343// over there. Restores the 2021 method's namesake where it is measurably right (texture whose
344// detail-band correlation is real) at no cost where it is not.
345#define DT_HL_HF_GUIDE 1
346
347// The coefficient-field pipeline is CFA-agnostic (it works on the interpolated RGB planes and
348// masks): Bayer and X-Trans share the whole reconstruction and differ only in the gather
349// (bilinear interpolation), the scatter (remosaic) and the knee's raw-mosaic access.
350
351typedef struct
352{
353 int x0, y0, x1, y1; // inclusive bbox of the clipped pixels
354 int rx0, ry0, rx1, ry1; // padded read box (clamped to image), context for the guide
355 int pad; // padding width = ceil(radius)
356 float radius; // reconstruction radius = deepest clip-to-valid distance in this region
358
359// Anisotropic transport of the coefficient planes (production default, CPU + OpenCL, parity-
360// tested by the HL_FILLCL_TEST aniso leg). The coefficient fills are steered by the measured
361// guide structure through a variance-adaptive tensor (_cf_adaptive_tensor below): where a HARD
362// EDGE crosses the blown zone, transport runs along the isophotes (a boundary means the content
363// beyond follows another colour-line -- do not mix models across it); on a clean halo ramp it
364// runs along the steepest gradient (the model lives on the rim and must travel radially inward).
365// Validated on the 6 ground-truth scenes (never worse than the isotropic fill; pk1synth -7%,
366// occluded -2% RMSE at equal convergence) and on natural-raw A/B (visually structureless).
367//
368// DT_HL_CF_K = the relative-std threshold of the edge detector. Fine-sweep optimum: every
369// ground-truth scene at or below the isotropic RMSE across k in [0.14, 0.25]; the occluded
370// scene improves monotonically toward low k (the isophote lean engages earlier on the boundary)
371// while the correlated scene's radial gain evaporates below ~0.12 -- 0.15 takes the boundary
372// win with margin from that frontier.
373#define DT_HL_CF_K 0.15f
374
375// max planes sharing one anchor mask in the fused GPU harmonic fill
376#define DT_HL_FILL_CL_MAXP 3
377
378// exact sparse SPD Cholesky direct solvers (dome, region PDE); iterative fallback kept.
379// The factor structs and the solvers themselves live in the reusable libraries
380// common/solvers/sparse_cholesky.h (CPU) and common/solvers/sparse_cholesky_cl.h (GPU).
381#define DT_HL_SPARSE_SOLVE 1
382
383// cap on the direct-solve size (number of hole unknowns) for the full-resolution diffusion
384// systems; beyond it the iterative conjugate-gradient fallback runs (the factor's memory
385// grows as O(N log N) and its arithmetic as O(N^1.5))
386#define DT_HL_SPARSE_MAX (1 << 14)
387
388// anisotropic chroma solver selector (2 = divergence-form exact/pyramid; see _cpu.h options)
389#define DT_HL_ANISO_SOLVER 2
390
391// Fusing the planes matters: the mask pyramid, the tensor and the edge weights depend only on
392// (hole, steer, geometry), so np planes share ONE build and ONE sweep pass reads the weights
393// once per cell for np accumulations. Per plane, the arithmetic is identical to np separate
394// fills (same weights, same accumulation order).
395#define DT_HL_FILL_MAXP 4
396
397// whose fixed cost dwarfs the arithmetic on small windows -- a 24x29 region measured 22 ms on
398// device vs <1 ms on host. The window crosses the bus once in each direction (pack kernel +
399// one readback, one upload + unpack kernel), so the traffic is 9*rn floats down, 4*rn up.
400// Threshold overridable via HL_CL_CPU_PX for tuning.
401#define DT_HL_CL_CPU_REGION_PX (1u << 20)
402
403// Device working set of one region on the device path (_region_guided_filter_cl), in floats per pixel
404// of its padded window. Measured 256.4 to 257.2 bytes per pixel on windows of 2.8 to 16.5 Mpx
405// (doc/highlights-reconstruction.md), so it is linear in the window. A window that does not fit what
406// the device has left takes the CPU offload, which needs 9 floats per pixel on the device.
407#define DT_HL_CL_REGION_FLOATS_PER_PX 65.f
408
409#define DT_HL_KNEE_LO 0.80f // trust threshold: values below are assumed strictly linear
410#define DT_HL_KNEE_DET 0.995f // clip-detection threshold in clip units
411#define DT_HL_KNEE_BINS 24 // curve resolution over the band
412#define DT_HL_KNEE_FMIN 0.02f // minimum trusted mass a stats window must hold
413#define DT_HL_KNEE_R2MIN 0.25f // minimum colour-line fit quality for a pair to vote
414#define DT_HL_KNEE_MINVOTES 100 // minimum votes per bin: no evidence -> identity (safe default)
415#define DT_HL_KNEE_NSIGMA 2.0f // lift must exceed NSIGMA * standard error of the bin median
416#define DT_HL_KNEE_ENGAGE 0.005f // curves lifting less than this are noise: stay identity
417#define DT_HL_KNEE_NSIGMAS 5 // multi-scale stats windows, finest with trusted mass wins
418
419typedef struct _hl_knee_curve_t
420{
421 int engaged; // 0 = identity (no correction for this channel)
422 float lift[DT_HL_KNEE_BINS]; // additive lift per bin center, clip-normalized units
424
425// ---------------------------------------------------------------------------------------------
426// Per-region working-set handle shared by the CPU reconstruction stages (region.h et al.).
427// MATHS/FLOW BRIDGE -- per-region reconstruction (article §"The algorithm", steps 3-8), the whole
428// second half of the mermaid flowchart run once per merged region Omega on its PADDED read window
429// (article §"The C production code": each region is cropped to region->rx0..ry1, reconstructed in a
430// contiguous rw x rh buffer, then scattered back -- so the cost is linear in the padded area, not the
431// image, article §"Linear in the padded area"). The stages compose as:
432// 3 colour-line coefficient field (_region fit+transport+eval block below; minimizes E_affine per
433// pixel, then transports the coefficients by E_transport) ->
434// 4 HF refit (re-fits the high-frequency detail band on the same colour line) ->
435// 5-6 soft floors + self-dome (5: clip-level floor so a fit can only RAISE a saturated channel;
436// 6: depth-gated blend of the guided estimate with a per-channel
437// biharmonic self-dome, weight We = R^4, minimizing E_bihar where
438// the colour line is weak) ->
439// 7 all-clip luminance dome + chroma (E_bihar luminance dome shared by R,G,B, times an E_chrominance
440// screened-Poisson chromaticity fill, for pixels where NO channel
441// survives) ->
442// 8 anisotropic chroma coherence (final E_chrominance diffusion ironing the core<->annulus seam) ->
443// composite (scatter the reconstructed CLIPPED channels back, floored at 0).
444// The region radius R (deepest clip-to-valid depth, from _segment_clipped_regions) sets the reach: the
445// coarsest guided scale and the coefficient-field window sigma = clip(R/6, 8, 64) are both derived from
446// it below, so the +-3 sigma window just reaches the deepest pixel and no farther. The internal step
447// sections are annotated in place; this header only ties them together.
448// Per-region working set shared by the reconstruction stages of _region_guided_filter.
449// Holds the padded-window buffers (allocated once by the driver) and the region geometry so
450// each stage operates on the same arrays through a single handle instead of a ~35-argument
451// call. Buffer reuse across stages is intentional and documented at each site (e.g.
452// valid_variance carries the CF fit coefficients, then the dome-gate weight Wc; prev_scale
453// carries blur moments, then the anisotropic anchor validity). Do not "tidy up" the reuse.
454typedef struct _hl_region_ctx_t
455{
456 // full-resolution I/O (indexed with `width` stride at the region's rx0/ry0 offset)
457 float *interp;
458 const float *mask;
459 const float *depth;
460 int width;
461 // region geometry + tunables
467 float scale; // dt_dev_get_module_scale(): full-resolution pixels per buffer pixel
468 float epsilon;
472 float floor_gate; // clip-asymmetry gate g in [0,1]: 0 = per-channel floors (unit-WB clips, the
473 // approved behavior, bit-exact), 1 = joint chromaticity-preserving floors +
474 // surround-chroma refinements (real-camera WB'd clips). See _hl_floor_gate().
475 float region_worth; // ONE decision per blown zone: does this region need the joint floor's
476 // chromaticity rescue enough to pay its scatter? Computed once in
477 // _cf_reconstruct's first pass and reused by every floor site, because the two
478 // floors cannot be mixed spatially -- see _hl_region_worth().
479 // group-A padded-window buffers (live for the whole region)
481 float *plane1, *plane2, *plane3;
483 // group-B solver working set (freed after the all-clip / anisotropic stages)
484 uint8_t *hole;
488
489// ---------------------------------------------------------------------------------------------
490// Highlights module parameters + per-module OpenCL global data. Defined here (rather than in
491// highlights.c) so every per-stage module TU can see them; highlights.c keeps only its GUI
492// data struct. The params struct carries the introspection $DEFAULT/$DESCRIPTION annotations.
493
495{
496 DT_IOP_HIGHLIGHTS_CLIP = 0, // $DESCRIPTION: "clip highlights"
497 DT_IOP_HIGHLIGHTS_LCH = 1, // $DESCRIPTION: "reconstruct in LCh"
498 DT_IOP_HIGHLIGHTS_INPAINT = 2, // $DESCRIPTION: "reconstruct color"
499 DT_IOP_HIGHLIGHTS_LAPLACIAN = 3, //$DESCRIPTION: "guided laplacians"
500 DT_IOP_HIGHLIGHTS_HARMONIC = 4, //$DESCRIPTION: "harmonic transposition"
502
504{
505 WAVELETS_1_SCALE = 0, // $DESCRIPTION: "2 px"
506 WAVELETS_2_SCALE = 1, // $DESCRIPTION: "4 px"
507 WAVELETS_3_SCALE = 2, // $DESCRIPTION: "8 px"
508 WAVELETS_4_SCALE = 3, // $DESCRIPTION: "16 px"
509 WAVELETS_5_SCALE = 4, // $DESCRIPTION: "32 px"
510 WAVELETS_6_SCALE = 5, // $DESCRIPTION: "64 px"
511 WAVELETS_7_SCALE = 6, // $DESCRIPTION: "128 px (slow)"
512 WAVELETS_8_SCALE = 7, // $DESCRIPTION: "256 px (slow)"
513 WAVELETS_9_SCALE = 8, // $DESCRIPTION: "512 px (very slow)"
514 WAVELETS_10_SCALE = 9, // $DESCRIPTION: "1024 px (very slow)"
515 WAVELETS_11_SCALE = 10, // $DESCRIPTION: "2048 px (insanely slow)"
516 WAVELETS_12_SCALE = 11, // $DESCRIPTION: "4096 px (insanely slow)"
518
520{
521 // params of v1
522 dt_iop_highlights_mode_t mode; // $DEFAULT: DT_IOP_HIGHLIGHTS_HARMONIC $DESCRIPTION: "method"
523 float blendL; // unused $DEFAULT: 1.0
524 float blendC; // unused $DEFAULT: 0.0
525 float blendh; // unused $DEFAULT: 0.0
526 // params of v2
527 float clip; // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 1.0 $DESCRIPTION: "clipping threshold"
528 // params of v3
529 float noise_level; // $MIN: 0. $MAX: 1.0 $DEFAULT: 0.00 $DESCRIPTION: "noise level"
530 int iterations; // $MIN: 1 $MAX: 512 $DEFAULT: 30 $DESCRIPTION: "iterations"
531 dt_atrous_wavelets_scales_t scales; // $DEFAULT: 8 $DESCRIPTION: "diameter of reconstruction"
532 float reconstructing; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.4 $DESCRIPTION: "cast balance"
533 float combine; // $MIN: 0.0 $MAX: 10.0 $DEFAULT: 2.0 $DESCRIPTION: "combine segments"
535 // params of v4
536 float solid_color; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "inpaint a flat color"
538
540
542{
675
680
683
684// ---------------------------------------------------------------------------------------------
685// Guided-laplacian (2021 a-trous) shared constants + scale enums. Used by the laplacian mode TU
686// and by highlights.c's tiling_callback; kept here so both see one definition.
687
688#define MAX_NUM_SCALES 12
689#define REDUCESIZE 64
690#define DS_FACTOR 4
691#define SQRT3 1.7320508075688772935274463415058723669L
692#define SQRT12 3.4641016151377545870548926830117447339L // 2*SQRT3
699{
700 ANY_SCALE = 1 << 0, // any wavelets scale : reconstruct += HF
701 FIRST_SCALE = 1 << 1, // first wavelets scale : reconstruct = 0
702 LAST_SCALE = 1 << 2, // last wavelets scale : reconstruct += residual
703};
704#endif // DT_IOP_HIGHLIGHTS_COMMON_H
const int t
DT_ALIGNED_PIXEL float dt_aligned_pixel_t[4]
Definition simd.h:53
static float _hl_ring_flat_mean_vote(const float *const restrict estimate, const float *const restrict valid, const dt_aligned_pixel_t cmean, const size_t region_pixels)
static float _hl_floor_gate(const float clips[4])
@ DT_IOP_HIGHLIGHTS_HARMONIC
@ DT_IOP_HIGHLIGHTS_INPAINT
@ DT_IOP_HIGHLIGHTS_LAPLACIAN
#define CF_REGION_LO
#define DT_HL_KNEE_BINS
#define CF_JOINT_TAU
diffuse_reconstruct_variant_t
@ DIFFUSE_RECONSTRUCT_RGB
@ DIFFUSE_RECONSTRUCT_CHROMA
dt_iop_highlights_params_t dt_iop_highlights_data_t
static float _hl_floor_worth(const float chroma_gain)
static float _hl_region_worth(const float region_benefit)
#define CF_REGION_HI
const _hl_region_t * region
const dt_dev_pixelpipe_t * pipe
dt_atrous_wavelets_scales_t scales
typedef double((*spd)(unsigned long int wavelength, double TempK))