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#pragma once
20
21#include "common/darktable.h" // HL_PFOR/__OMP_PARALLEL_FOR__, CLAMP, dt_get_wtime, SIMD macros
22#include "common/gaussian.h" // dt_gaussian_t / dt_gaussian_cl_t (in _hl_gauss_slot_t + CL protos)
23#include "develop/pixelpipe_hb.h" // dt_dev_pixelpipe_t (in _hl_region_ctx_t) + dt_dev_pixelpipe_iop_t
24#include <stdint.h>
25
26// Shared macros and struct definitions for the highlights harmonic-transposition mode,
27// used by both the C (the CPU stages) and OpenCL (process.h / the OpenCL stages)
28// halves. This is a textual include unit of highlights.c (see the note in _cpu.h).
29
30/*
31 Harmonic transposition: Ansel's highlight-reconstruction method (2026 rebuild).
32
33 Everything specific to the DT_IOP_HIGHLIGHTS_HARMONIC mode lives here: the sensor-rolloff
34 knee inversion, the per-region segmentation, the coefficient-field colour-line transport
35 (windowed joint fits, harmonic diffusion of the model, deep-channel cascade), the sparse
36 SPD Cholesky and the direct solvers built on it (biharmonic dome, screened chroma,
37 divergence-form structure-steered chroma), the HF-band hybrid, the CPU drivers for Bayer
38 and X-Trans, and the hybrid OpenCL driver (GPU gather/remosaic around the CPU middle).
39
40 This is a textual include unit of highlights.c (not a standalone translation unit): it
41 relies on the CFA helpers, gather/scatter, buffer conventions and the global kernel
42 handles defined there. See the companion article for the method's derivation.
43*/
44
45// =====================================================================================
46// Segmented full-resolution guided-laplacian reconstruction
47//
48// Rather than reconstruct the whole downsampled frame with the a-trous wavelet stack,
49// isolate each connected clipped region and run a coarse->fine FULL-VALUE guided filter
50// at full resolution on the small rectangle enclosing it (plus padding that gives the
51// colour-line fit a valid rim). This recovers the clipped channel's magnitude (the
52// intercept carries the DC), not merely its texture, and only touches clipped
53// neighbourhoods. See doc/ and the companion article for the derivation.
54//
55// NOTE: this first stage ports the guided-filter "bug fix" only. The confidence blend,
56// the all-clipped joint core and the uncertainty regulariser (all needing a small
57// per-region biharmonic solve) land in a follow-up; all-clipped cores are meanwhile
58// left to the surrounding fill.
59// =====================================================================================
60
61// DEBUG TOGGLE: set to 0 to disable the biharmonic + chroma refinement (self-dome, joint
62// all-clipped core, flat-colour) and run ONLY the guided-filter stage (increment 1), to
63// validate the segmentation + guided path in isolation.
64#define DT_HL_BIHARMONIC 1
65
66// BAND OVERRIDE: for channels whose sensor rolloff ENGAGED (knee), extend the clip detection
67// down to this fraction of the threshold. The knee's value-map restores the band's level but
68// cannot restore a slope the sensor compressed away; the colour-line model, anchored on the
69// truly linear data below the band, can -- and each band pixel's knee-lifted measurement acts
70// as its per-pixel saturation floor, so the override can only raise, never lose data. On the
71// rolloff bench scene this removes the residual contour arc and cuts the zone RMSE 5x
72// (0.012 -> 0.0025); on hard-clipping channels (knee not engaged) detection is unchanged and
73// the output is bit-identical. Values in [0.7, 1.0); 1.0 disables.
74#define DT_HL_BAND_OVR 0.9f
75
76#define HL_PFOR(...) __OMP_PARALLEL_FOR__(__VA_ARGS__)
77
78// Per-thread cache of dt_gaussian handles keyed on (width, height, channels, sigma).
79// dt_gaussian_init allocates its recursion temporaries on every call, and the region stages
80// fire dozens of same-shaped blurs per region (the knee, over a hundred per image): reusing
81// the handle removes pure allocation churn. The cache is __thread and the blur calls happen
82// serially on the caller's thread (parallelism lives INSIDE dt_gaussian_blur), so no locking.
83// Drivers flush it on exit (_hl_gauss_cache_flush) so nothing leaks across pipeline runs.
84typedef struct
85{
86 int width;
87 int height;
89 float sigma;
92
93#define HL_GAUSS_SLOTS 4
94
95// GUIDE-SELECTION TUNABLES -- traced from magenta regressions on real amber/orange highlights.
96// DT_HL_PAIR_VARIANCE: 1 = score guides by the pair-restricted variance already computed for the fit
97// where valid pairs are scarce -- e.g. the thin valid surround of a saturated highlight).
98// DT_HL_GUIDE_GATE: may a CLIPPED channel guide through its clip value? On amber/orange highlights
99// the only surviving channel is a LOW blue -- a poor guide that collapses green to magenta -- while
100// the clipped red at its clip level pulls green back up. But allowing that everywhere magentas the
101// shallow annulus (there a clipped guide's clip value is wrong). So gate it by DEPTH:
102// 2 = DEPTH-GATED (default): clipped guide allowed only where dep >= 0.5 * region radius (the
103// saturated core/inner ring -> recovers amber); the shallow annulus keeps valid-only (accurate).
104// 1 = never (prototype: annulus good, core magenta/dark); 0 = always (core good, annulus magenta).
105#define DT_HL_PAIR_VARIANCE 1
106#define DT_HL_GUIDE_GATE 2
107
108// DT_HL_CLIP_FLOOR: a clipped channel SATURATED, so its true value is >= its clip level. If the
109// colour-line fit (from a low surviving guide) comes out below that, floor it back up. Physical,
110// parameter-free, and monotone (only raises below-clip values -> no overshoot, no per-pixel guide
111// switching -> no patchwork). Fixes the amber core/ring collapsing to a dark magenta without the
112// clipped-guide hacks (DT_HL_GUIDE_GATE 0/2), which magenta the annulus / stain the core.
113#define DT_HL_CLIP_FLOOR 1
114
115// Max unknowns in the dense-Cholesky biharmonic dome before it downsamples (O(N^3)). Larger = finer
116// dome grid (ds->1 = exact full-res) at more cost -- raise it to test if the coarse solve matters.
117#define DT_HL_DOME_NMAX 2000
118
119// With the sparse direct solver the dome grid can be MUCH finer at less cost than the dense
120// O(N^3) solve ever allowed: coarse-grid unknowns cap for the sparse path (ds -> 1 up to this).
121#define DT_HL_DOME_NMAX_SPARSE 8192
122
123// REFINEMENT stages inside the biharmonic block. The joint core (all-clip magnitude dome + diffused
124// chroma) is ALWAYS on -- it recovers the sun disc. The per-channel SELF-DOME and SEAM-REG were
125// disabled after they turned a correctly-guided amber annulus magenta. That was traced NOT to the
126// per-channel method (the RGB prototype validates it: it never magentas, even on a 59%-clipped amber
127// sun) but to three C-only divergences from the prototype, now fixed:
128// 1. the self-dome domed each channel at its OWN downsample factor -> per-channel-inconsistent
129// approximation -> chroma drift. Now all three share ONE ds (sized from the union hole).
130// 2. the seam regulariser ran only "iterations" (~30) CG steps on an ill-conditioned biharmonic;
131// each channel stopped at a different point -> chroma drift. Now runs the full CG budget.
132// 3. the confidence R^2 was gated to 0 in low-valid-weight regions, starving the seam reg of data
133// fidelity (Wd = R^4 -> 0) at the all-clip core. Gate removed (matches the prototype).
134// Also: the saturation floor is now re-asserted AFTER the self dome (the prototype floors there).
135// Re-enabled for real-image A/B; flip either to 0 to isolate. They help genuinely decorrelated
136// content (rare in natural images) and are near no-ops on correlated content.
137#define DT_HL_SELF_DOME 1
138
139// Structure-steered chroma post-pass: keep the ladder's MAGNITUDE (norm), re-diffuse the clipped
140// channels' RATIOS along the isophotes of the recovered luminance, coarse-to-fine so the diffusion
141// seeds the entire hole (unreached interior would keep its magenta ratios). Fixes the guide-flip /
142// scale hand-off chroma patches without touching the recovery. See _aniso_tensor/_aniso_iterate.
143#define DT_HL_ANISO_CHROMA 1
144
145// R9: blind sensor-rolloff (knee) pre-correction. Real sensors compress the last few percent below
146// the clip level (saturation rolloff), so the near-clip BAND holds values biased LOW. Every
147// reconstruction hand-off against that biased data seams (R2-R8 lesson: seam energy = estimator
148// disagreement, and no weighting can hide it) -- the only zero-seam fix is to DEBIAS THE BAND DATA
149// ITSELF. A windowed colour-line fitted on fully-trusted pixels predicts what each band value should
150// be; binned robust medians of (measured, predicted) pairs trace the knee inverse, which is applied
151// to the band before reconstruction. On hard-clipped (unbiased) data the fitted curve is the
152// identity, so the correction has a NO-OP GUARANTEE. Estimation is global per channel (the knee is
153// a sensor property) and runs on a downsampled copy; like the Laplacian normalization it is local
154// to the tile being processed.
155#define DT_HL_KNEE 1
156
157// COEFFICIENT-FIELD reconstruction (replaces the guided ladder when 1). The PK1 step study
158// showed the ladder's coarse scales write FLAT fills into the deep zone (heterogeneous windows
159// attenuate the fitted slope toward the window mean) in depth-level-set annuli whose boundaries
160// are the visible hard arcs (scale hand-offs between disagreeing estimators). Values
161// extrapolated from far are unstable -- but the local colour-line COEFFICIENTS are smooth by
162// nature. So: fit est_c = a*g1 + b*g2 + d in windows where channel c is trusted, harmonically
163// diffuse the coefficient fields (a, b, d) across the clipped zone, and evaluate against the
164// MEASURED valid guides at every pixel. The clipped channel inherits the guides' true structure;
165// no scales, no depth gates, no level-set writes -> no seams by construction.
166//
167// Around the core fit, four hand-off-free safeguards (each validated on the synthetic bench):
168// - the fit quality R^2 is diffused alongside the coefficients and scales the estimate's
169// HIGH FREQUENCIES (a weak colour-line must not print the guides' fine texture);
170// - a DEPTH-GATED per-channel self-dome takes over where the model is doubtful (low R^2) AND
171// the dome is trustworthy (shallow: biharmonic extrapolation degrades with distance) --
172// depth is the only reliable arbiter between correlated transfer and decorrelated printing,
173// which is undecidable from rim statistics alone;
174// - the clip floor is SOFT (rounded over ~2% of the clip level), so a prediction oscillating
175// around saturation does not print the binding contour;
176// - the all-clip core rebuild is FEATHERED into the surrounding reconstruction over a blurred
177// mask instead of a hard hand-off. All-clip pixels have no guides: they stay at the clip
178// floor and the joint core rebuilds them, with the aniso chroma pass restricted to them
179// (coefficient-field results act as anchors).
180#define DT_HL_COEFF_FIELD 1
181
182// Laplacian-band (HF) guiding, hybridized: the detail band gets its OWN windowed colour-line
183// (R^2-shrunk gains -- on a zero-mean band shrinkage is the correct estimator), and the
184// reconstruction's high frequencies are blended between this guided resynthesis and the
185// R^2-damped transfer by QUADRATIC MIN-ENERGY odds: a mixed-window gain misfiring at an object
186// edge shows up as an HF energy spike, so the failure detects itself and the damped path takes
187// over there. Restores the 2021 method's namesake where it is measurably right (texture whose
188// detail-band correlation is real) at no cost where it is not.
189#define DT_HL_HF_GUIDE 1
190
191// The coefficient-field pipeline is CFA-agnostic (it works on the interpolated RGB planes and
192// masks): Bayer and X-Trans share the whole reconstruction and differ only in the gather
193// (bilinear interpolation), the scatter (remosaic) and the knee's raw-mosaic access.
194
195typedef struct
196{
197 int x0, y0, x1, y1; // inclusive bbox of the clipped pixels
198 int rx0, ry0, rx1, ry1; // padded read box (clamped to image), context for the guide
199 int pad; // padding width = ceil(radius)
200 float radius; // reconstruction radius = deepest clip-to-valid distance in this region
202
203// Anisotropic transport of the coefficient planes (production default, CPU + OpenCL, parity-
204// tested by the HL_FILLCL_TEST aniso leg). The coefficient fills are steered by the measured
205// guide structure through a variance-adaptive tensor (_cf_adaptive_tensor below): where a HARD
206// EDGE crosses the blown zone, transport runs along the isophotes (a boundary means the content
207// beyond follows another colour-line -- do not mix models across it); on a clean halo ramp it
208// runs along the steepest gradient (the model lives on the rim and must travel radially inward).
209// Validated on the 6 ground-truth scenes (never worse than the isotropic fill; pk1synth -7%,
210// occluded -2% RMSE at equal convergence) and on natural-raw A/B (visually structureless).
211//
212// DT_HL_CF_K = the relative-std threshold of the edge detector. Fine-sweep optimum: every
213// ground-truth scene at or below the isotropic RMSE across k in [0.14, 0.25]; the occluded
214// scene improves monotonically toward low k (the isophote lean engages earlier on the boundary)
215// while the correlated scene's radial gain evaporates below ~0.12 -- 0.15 takes the boundary
216// win with margin from that frontier.
217#define DT_HL_CF_K 0.15f
218
219// max planes sharing one anchor mask in the fused GPU harmonic fill
220#define DT_HL_FILL_CL_MAXP 3
221
222// exact sparse SPD Cholesky direct solvers (dome, region PDE); iterative fallback kept.
223// The factor structs and the solvers themselves live in the reusable libraries
224// common/solvers/sparse_cholesky.h (CPU) and common/solvers/sparse_cholesky_cl.h (GPU).
225#define DT_HL_SPARSE_SOLVE 1
226
227// cap on the direct-solve size (number of hole unknowns) for the full-resolution diffusion
228// systems; beyond it the iterative conjugate-gradient fallback runs (the factor's memory
229// grows as O(N log N) and its arithmetic as O(N^1.5))
230#define DT_HL_SPARSE_MAX (1 << 14)
231
232// anisotropic chroma solver selector (2 = divergence-form exact/pyramid; see _cpu.h options)
233#define DT_HL_ANISO_SOLVER 2
234
235// Fusing the planes matters: the mask pyramid, the tensor and the edge weights depend only on
236// (hole, steer, geometry), so np planes share ONE build and ONE sweep pass reads the weights
237// once per cell for np accumulations. Per plane, the arithmetic is identical to np separate
238// fills (same weights, same accumulation order).
239#define DT_HL_FILL_MAXP 4
240
241// whose fixed cost dwarfs the arithmetic on small windows -- a 24x29 region measured 22 ms on
242// device vs <1 ms on host. The window crosses the bus once in each direction (pack kernel +
243// one readback, one upload + unpack kernel), so the traffic is 9*rn floats down, 4*rn up.
244// Threshold overridable via HL_CL_CPU_PX for tuning.
245#define DT_HL_CL_CPU_REGION_PX (1u << 20)
246
247#define DT_HL_KNEE_LO 0.80f // trust threshold: values below are assumed strictly linear
248#define DT_HL_KNEE_DET 0.995f // clip-detection threshold in clip units
249#define DT_HL_KNEE_BINS 24 // curve resolution over the band
250#define DT_HL_KNEE_FMIN 0.02f // minimum trusted mass a stats window must hold
251#define DT_HL_KNEE_R2MIN 0.25f // minimum colour-line fit quality for a pair to vote
252#define DT_HL_KNEE_MINVOTES 100 // minimum votes per bin: no evidence -> identity (safe default)
253#define DT_HL_KNEE_NSIGMA 2.0f // lift must exceed NSIGMA * standard error of the bin median
254#define DT_HL_KNEE_ENGAGE 0.005f // curves lifting less than this are noise: stay identity
255#define DT_HL_KNEE_NSIGMAS 5 // multi-scale stats windows, finest with trusted mass wins
256
257typedef struct _hl_knee_curve_t
258{
259 int engaged; // 0 = identity (no correction for this channel)
260 float lift[DT_HL_KNEE_BINS]; // additive lift per bin center, clip-normalized units
262
263// ---------------------------------------------------------------------------------------------
264// Per-region working-set handle shared by the CPU reconstruction stages (region.h et al.).
265// MATHS/FLOW BRIDGE -- per-region reconstruction (article §"The algorithm", steps 3-8), the whole
266// second half of the mermaid flowchart run once per merged region Omega on its PADDED read window
267// (article §"The C production code": each region is cropped to region->rx0..ry1, reconstructed in a
268// contiguous rw x rh buffer, then scattered back -- so the cost is linear in the padded area, not the
269// image, article §"Linear in the padded area"). The stages compose as:
270// 3 colour-line coefficient field (_region fit+transport+eval block below; minimizes E_affine per
271// pixel, then transports the coefficients by E_transport) ->
272// 4 HF refit (re-fits the high-frequency detail band on the same colour line) ->
273// 5-6 soft floors + self-dome (5: clip-level floor so a fit can only RAISE a saturated channel;
274// 6: depth-gated blend of the guided estimate with a per-channel
275// biharmonic self-dome, weight We = R^4, minimizing E_bihar where
276// the colour line is weak) ->
277// 7 all-clip luminance dome + chroma (E_bihar luminance dome shared by R,G,B, times an E_chrominance
278// screened-Poisson chromaticity fill, for pixels where NO channel
279// survives) ->
280// 8 anisotropic chroma coherence (final E_chrominance diffusion ironing the core<->annulus seam) ->
281// composite (scatter the reconstructed CLIPPED channels back, floored at 0).
282// The region radius R (deepest clip-to-valid depth, from _segment_clipped_regions) sets the reach: the
283// coarsest guided scale and the coefficient-field window sigma = clip(R/6, 8, 64) are both derived from
284// it below, so the +-3 sigma window just reaches the deepest pixel and no farther. The internal step
285// sections are annotated in place; this header only ties them together.
286// Per-region working set shared by the reconstruction stages of _region_guided_filter.
287// Holds the padded-window buffers (allocated once by the driver) and the region geometry so
288// each stage operates on the same arrays through a single handle instead of a ~35-argument
289// call. Buffer reuse across stages is intentional and documented at each site (e.g.
290// valid_variance carries the CF fit coefficients, then the dome-gate weight Wc; prev_scale
291// carries blur moments, then the anisotropic anchor validity). Do not "tidy up" the reuse.
292typedef struct _hl_region_ctx_t
293{
294 // full-resolution I/O (indexed with `width` stride at the region's rx0/ry0 offset)
295 float *interp;
296 const float *mask;
297 const float *depth;
298 int width;
299 // region geometry + tunables
305 float epsilon;
309 // group-A padded-window buffers (live for the whole region)
311 float *plane1, *plane2, *plane3;
313 // group-B solver working set (freed after the all-clip / anisotropic stages)
314 uint8_t *hole;
318
319// ---------------------------------------------------------------------------------------------
320// Highlights module parameters + per-module OpenCL global data. Defined here (rather than in
321// highlights.c) so every per-stage module TU can see them; highlights.c keeps only its GUI
322// data struct. The params struct carries the introspection $DEFAULT/$DESCRIPTION annotations.
323
325{
326 DT_IOP_HIGHLIGHTS_CLIP = 0, // $DESCRIPTION: "clip highlights"
327 DT_IOP_HIGHLIGHTS_LCH = 1, // $DESCRIPTION: "reconstruct in LCh"
328 DT_IOP_HIGHLIGHTS_INPAINT = 2, // $DESCRIPTION: "reconstruct color"
329 DT_IOP_HIGHLIGHTS_LAPLACIAN = 3, //$DESCRIPTION: "guided laplacians"
330 DT_IOP_HIGHLIGHTS_HARMONIC = 4, //$DESCRIPTION: "harmonic transposition"
332
334{
335 WAVELETS_1_SCALE = 0, // $DESCRIPTION: "2 px"
336 WAVELETS_2_SCALE = 1, // $DESCRIPTION: "4 px"
337 WAVELETS_3_SCALE = 2, // $DESCRIPTION: "8 px"
338 WAVELETS_4_SCALE = 3, // $DESCRIPTION: "16 px"
339 WAVELETS_5_SCALE = 4, // $DESCRIPTION: "32 px"
340 WAVELETS_6_SCALE = 5, // $DESCRIPTION: "64 px"
341 WAVELETS_7_SCALE = 6, // $DESCRIPTION: "128 px (slow)"
342 WAVELETS_8_SCALE = 7, // $DESCRIPTION: "256 px (slow)"
343 WAVELETS_9_SCALE = 8, // $DESCRIPTION: "512 px (very slow)"
344 WAVELETS_10_SCALE = 9, // $DESCRIPTION: "1024 px (very slow)"
345 WAVELETS_11_SCALE = 10, // $DESCRIPTION: "2048 px (insanely slow)"
346 WAVELETS_12_SCALE = 11, // $DESCRIPTION: "4096 px (insanely slow)"
348
350{
351 // params of v1
352 dt_iop_highlights_mode_t mode; // $DEFAULT: DT_IOP_HIGHLIGHTS_CLIP $DESCRIPTION: "method"
353 float blendL; // unused $DEFAULT: 1.0
354 float blendC; // unused $DEFAULT: 0.0
355 float blendh; // unused $DEFAULT: 0.0
356 // params of v2
357 float clip; // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 1.0 $DESCRIPTION: "clipping threshold"
358 // params of v3
359 float noise_level; // $MIN: 0. $MAX: 1.0 $DEFAULT: 0.00 $DESCRIPTION: "noise level"
360 int iterations; // $MIN: 1 $MAX: 512 $DEFAULT: 30 $DESCRIPTION: "iterations"
361 dt_atrous_wavelets_scales_t scales; // $DEFAULT: 8 $DESCRIPTION: "diameter of reconstruction"
362 float reconstructing; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.4 $DESCRIPTION: "cast balance"
363 float combine; // $MIN: 0.0 $MAX: 10.0 $DEFAULT: 2.0 $DESCRIPTION: "combine segments"
365 // params of v4
366 float solid_color; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.5 $DESCRIPTION: "inpaint a flat color"
368
370
372{
476
481
484
485// ---------------------------------------------------------------------------------------------
486// Guided-laplacian (2021 a-trous) shared constants + scale enums. Used by the laplacian mode TU
487// and by highlights.c's tiling_callback; kept here so both see one definition.
488
489#define MAX_NUM_SCALES 12
490#define REDUCESIZE 64
491#define DS_FACTOR 4
492#define SQRT3 1.7320508075688772935274463415058723669L
493#define SQRT12 3.4641016151377545870548926830117447339L // 2*SQRT3
500{
501 ANY_SCALE = 1 << 0, // any wavelets scale : reconstruct += HF
502 FIRST_SCALE = 1 << 1, // first wavelets scale : reconstruct = 0
503 LAST_SCALE = 1 << 2, // last wavelets scale : reconstruct += residual
504};
@ DT_IOP_HIGHLIGHTS_HARMONIC
@ DT_IOP_HIGHLIGHTS_INPAINT
@ DT_IOP_HIGHLIGHTS_LAPLACIAN
#define DT_HL_KNEE_BINS
diffuse_reconstruct_variant_t
@ DIFFUSE_RECONSTRUCT_RGB
@ DIFFUSE_RECONSTRUCT_CHROMA
dt_iop_highlights_params_t dt_iop_highlights_data_t
const _hl_region_t * region
const dt_dev_pixelpipe_t * pipe
dt_atrous_wavelets_scales_t scales