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