Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
rawdenoiseai.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18
19/*
20 * Neural raw (CFA-domain) denoiser.
21 *
22 * Runs a sigma-map-conditioned U-Net (common/nn_model.{h,c}) on the mosaiced
23 * buffer, before demosaicing, where sensor noise is still per-sensel
24 * independent. The network was trained on synthetic Poisson-Gaussian noise
25 * drawn from the community noise-profile database, conditioned on the
26 * per-pixel noise standard deviation — so one set of weights serves every
27 * profiled camera, Bayer and X-Trans alike, and a newly profiled camera is
28 * supported without retraining. Training pipeline:
29 * https://github.com/aurelienpierreeng/ansel-denoise
30 *
31 * The sigma map is computed from the per-channel variance line
32 * Var(x) = a*x + b of the matched noise profile at the image ISO, applied in
33 * the post-rawprepare normalized domain — the exact domain the profiles were
34 * fitted in and the training used. Do NOT copy denoiseprofile's white-balance
35 * adjustments here: those compensate its post-demosaic, post-WB position.
36 *
37 * User parameters:
38 * - "strength": opacity of the correction, an alpha blend of the inferred
39 * noise residual: out = in + strength * (denoised - in).
40 * - model version / size (large|half|quarter network width) / variant (single-scale
41 * or multiscale) select the weights file
42 * denoise-<size>-<single|multi>-<version>.anselnn.
43 * - "global correction" and the per-channel R/G/B corrections scale the
44 * sigma map (see the calibration note above the params struct).
45 *
46 * Weights are loaded once per session from the user config dir (override for
47 * testing) or <datadir>/. Without a weights file the module stays disabled.
48 * The training counterpart of every inference step lives in the
49 * ansel-denoise repository: _k_assemble() <-> dataset.py,
50 * _k_bin_planes() <-> cfa.bin_mosaic_torch()/bin_sigma_torch(),
51 * the coarse->fine guide flow <-> train.ms_forward(), and
52 * _apply_low_band_anchor() <-> cfa.fuse_low_bands(). Keep them in sync.
53 */
54
55#ifdef HAVE_CONFIG_H
56#include "config.h"
57#endif
58#include "widgets/bauhaus.h"
59#include "system/macros.h"
60#include "system/openmp.h"
62#include "system/mem_alloc.h"
63#include "common/logging.h"
65#include <json-glib/json-glib.h>
68#include "common/imagebuf.h"
69#include "common/nn_model.h"
71#include "common/opencl.h"
72#include "develop/imageop.h"
73#include "develop/imageop_gui.h"
75#include "develop/tiling.h"
76#include "iop/iop_api.h"
77
78#include <gtk/gtk.h>
79#include <stdlib.h>
81#include "widgets/label.h"
82
83#define DT_RAWDENOISEAI_MODEL_LEN 128
84
86
87/* Model version: pins which trained network a history entry uses, so results
88 * stay reproducible across app updates. A new release that retrains the net
89 * appends a value here (and ships the matching weights file) instead of
90 * replacing v1 — old edits keep rendering with the model they were made on. */
95
96/* Model size: same architecture family, different width of the FINE net
97 * (32 / 16 / 8; the coarse chroma net of a multiscale model stays at 32 for
98 * every size — it runs on the superpixel-binned image and costs a few
99 * percent). large is the reference quality (practical with OpenCL), half is
100 * ~4x cheaper for the CPU path, quarter ~4x cheaper again for very weak
101 * hardware and near-realtime editing. The outputs are NOT interchangeable,
102 * hence a user parameter rather than a silent runtime choice. */
104{
105 DT_RAWDENOISEAI_LARGE = 0, // $DESCRIPTION: "large"
106 DT_RAWDENOISEAI_HALF = 1, // $DESCRIPTION: "half"
107 DT_RAWDENOISEAI_QUARTER = 2, // $DESCRIPTION: "quarter"
109
110/* Model variant: single-scale (the fine mosaic net alone — fast, no
111 * low-frequency chroma handling) vs multiscale (coarse chroma net guiding
112 * the fine net, plus the hybrid low-band fusion — high quality). */
114{
115 DT_RAWDENOISEAI_SINGLE = 0, // $DESCRIPTION: "single-scale"
116 DT_RAWDENOISEAI_MULTI = 1, // $DESCRIPTION: "multiscale"
118
119#define DT_RAWDENOISEAI_NUM_VERSIONS 1
120#define DT_RAWDENOISEAI_NUM_SIZES 3
121#define DT_RAWDENOISEAI_NUM_SCALES 2
122
123// filename components per enum value; the weights file is
124// denoise-<size>-<single|multi>-<version>.anselnn
125static const char *const _version_tag[DT_RAWDENOISEAI_NUM_VERSIONS] = { "v1" };
126static const char *const _size_tag[DT_RAWDENOISEAI_NUM_SIZES] = { "large", "half", "quarter" };
127static const char *const _scale_tag[DT_RAWDENOISEAI_NUM_SCALES] = { "single", "multi" };
128
129/* The shipped noise profiles understate the true mosaic-domain sigma by an
130 * exact factor 2 before any demosaic effect: tools/noise/noiseprofile.c
131 * estimates sigma as MAD/0.6745 of the HH band of a decimated lifting Haar
132 * whose normalization is HH = (x00 - x01 - x10 + x11)/4, so std(HH) = sigma/2
133 * for iid noise (the orthonormal Haar assumed by the MAD rule divides by 2).
134 * The gnuplot fit in ansel-gen-noiseprofile squares that std into (a, b)
135 * without correction, so every profile carries 1/4 of the physical variance.
136 * Historical consumers (denoiseprofile) were tuned end to end around these
137 * units; this module is the first to treat (a, b) as absolute physical
138 * variance, so the correction lives here — the shared profile database must
139 * stay consistent with a decade of fits and cannot change.
140 *
141 * The remaining, channel-dependent part of the deviation (the profiles are
142 * fitted on demosaiced pixels, and interpolation averages away high-frequency
143 * noise — most on the dense green lattice) is exposed as the per-channel
144 * corrections below, calibrated by measuring flat-region noise on raw mosaics
145 * against the profile prediction: 253 profiled cameras (one raw.pixls.us
146 * sample each) plus 64 images across ISO 64-12800 on three local bodies.
147 * Cross-camera medians (estimator-bias corrected): R 1.41, G 1.97, B 1.48
148 * after the factor 2 above; the deviation is ISO-stable.
149 *
150 * The whole correction is carried by the three GUI sliders and nothing
151 * else: what the user sees is exactly what multiplies the profile sigma
152 * (times the global correction). No hidden constants, no model-side
153 * multiplication — a model's cfg may document the sigma convention it was
154 * trained under, but the module never applies it behind the user's back
155 * (a hidden factor stacked with visible sliders is how the calibration got
156 * silently applied twice — the yellow-cast field bug). The slider defaults
157 * are the calibration itself: 2 x the sweep medians above. */
158
160{
161 float strength; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.85 $DESCRIPTION: "strength"
162 dt_iop_rawdenoiseai_version_t version; // $DEFAULT: DT_RAWDENOISEAI_V1 $DESCRIPTION: "model version"
163 dt_iop_rawdenoiseai_size_t size; // $DEFAULT: DT_RAWDENOISEAI_QUARTER $DESCRIPTION: "model size"
164 float noise_level; // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 1.0 $DESCRIPTION: "global correction"
165 float sigma_red; // $MIN: 0.5 $MAX: 8.0 $DEFAULT: 2.82 $DESCRIPTION: "red correction"
166 float sigma_green; // $MIN: 0.5 $MAX: 8.0 $DEFAULT: 3.94 $DESCRIPTION: "green correction"
167 float sigma_blue; // $MIN: 0.5 $MAX: 8.0 $DEFAULT: 2.96 $DESCRIPTION: "blue correction"
168 dt_iop_rawdenoiseai_scale_t scale_variant; // $DEFAULT: DT_RAWDENOISEAI_MULTI $DESCRIPTION: "model variant"
169 /* Empty: use the shipped model selected by (version, size, scale) above.
170 * Otherwise the basename of a .anselnn in the user config dir, which
171 * overrides all three. Stored by NAME, never by list position: the set of
172 * files on disk changes between sessions, and an index would silently
173 * re-point every history entry that used it (same reason colorin stores
174 * its ICC filename). */
177
192
194{
195 float strength; // opacity of the correction: out = in + strength * (denoised - in)
196 float noise_level; // scales the sigma map fed to the network (1.0 = trust the profile)
197 float sigma_scale[3]; // per-channel demosaic-bias correction, applied on top of noise_level
198 float a[3], b[3]; // noise variance line per RGB channel, normalized domain
199 dt_nn_model_t *model; // resolved from (version, variant); NULL disables the piece
201
203{
204 // lazily loaded, cached for the session; guarded by lock. tried[][] records
205 // a load attempt so a missing file is probed only once.
208 // user models from the config dir, keyed by basename; a NULL value records
209 // a failed load so a broken file is probed once, like tried[][] above
211 dt_pthread_mutex_t lock;
212#ifdef HAVE_OPENCL
213 dt_nn_cl_t *nn_cl; // U-Net kernel handles, from rawdenoiseai.cl
214 // device-resident glue kernels: the whole tile runs dev_in -> dev_out with
215 // no mid-tile host round-trip (command-queue syncs dominate GPU cost)
218#endif
220
221/* Thread-safe lazy loader: returns the model for (version, size, scale),
222 * loading it on first request from
223 * <configdir>/denoise-<size>-<single|multi>-<version>.anselnn (user
224 * override) or <datadir>/ (shipped). NULL if the file is absent or invalid.
225 * Runs on the pipeline thread via commit_params, hence the mutex. */
228{
229 if((int)ver < 0 || (int)ver >= DT_RAWDENOISEAI_NUM_VERSIONS || (int)sz < 0
230 || (int)sz >= DT_RAWDENOISEAI_NUM_SIZES || (int)sc < 0 || (int)sc >= DT_RAWDENOISEAI_NUM_SCALES)
231 return NULL;
232
234 if(!gd->tried[ver][sz][sc])
235 {
236 gd->tried[ver][sz][sc] = TRUE;
237 char name[64];
238 snprintf(name, sizeof(name), "denoise-%s-%s-%s.anselnn", _size_tag[sz], _scale_tag[sc], _version_tag[ver]);
239
240 char dir[PATH_MAX] = { 0 };
241 char path[PATH_MAX] = { 0 };
242 char err[256] = "";
243 dt_loc_get_user_config_dir(dir, sizeof(dir));
244 snprintf(path, sizeof(path), "%s/%s", dir, name);
245 gd->models[ver][sz][sc] = dt_nn_model_load(path, err, sizeof(err));
246 if(!gd->models[ver][sz][sc])
247 {
248 dt_loc_get_datadir(dir, sizeof(dir));
249 snprintf(path, sizeof(path), "%s/%s", dir, name);
250 gd->models[ver][sz][sc] = dt_nn_model_load(path, err, sizeof(err));
251 }
252 if(gd->models[ver][sz][sc])
253 dt_print(DT_DEBUG_ALWAYS, "[rawdenoiseai] loaded %s\n", path);
254 else
255 dt_print(DT_DEBUG_ALWAYS, "[rawdenoiseai] %s unavailable (%s)\n", name, err);
256 }
257 dt_nn_model_t *m = gd->models[ver][sz][sc];
259 return m;
260}
261
262/* A user model: any .anselnn dropped in the config dir under a name that is
263 * not one of the shipped ones. Cached by basename for the session, with a
264 * NULL entry recording a failed load so a broken file is probed once. Same
265 * mutex as the shipped matrix — this also runs on the pipeline thread. */
267{
268 if(!base || !*base || strchr(base, '/') || strchr(base, '\\')) return NULL;
269
271 if(!gd->custom) gd->custom = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
273 if(!g_hash_table_lookup_extended(gd->custom, base, NULL, (gpointer *)&m))
274 {
275 char dir[PATH_MAX] = { 0 };
276 char path[PATH_MAX] = { 0 };
277 char err[256] = "";
278 dt_loc_get_user_config_dir(dir, sizeof(dir));
279 snprintf(path, sizeof(path), "%s/%s", dir, base);
280 m = dt_nn_model_load(path, err, sizeof(err));
281 g_hash_table_insert(gd->custom, g_strdup(base), m);
282 if(m)
283 dt_print(DT_DEBUG_ALWAYS, "[rawdenoiseai] loaded user model %s\n", path);
284 else
285 dt_print(DT_DEBUG_ALWAYS, "[rawdenoiseai] user model %s unusable (%s)\n", base, err);
286 }
288 return m;
289}
290
291/* Basenames of every .anselnn in the config dir, sorted, shipped names
292 * excluded (those are overrides of the matrix, already reachable through the
293 * size/variant combos). Caller frees with g_list_free_full(l, g_free). */
295{
296 char dir[PATH_MAX] = { 0 };
297 dt_loc_get_user_config_dir(dir, sizeof(dir));
298 GDir *d = g_dir_open(dir, 0, NULL);
299 if(!d) return NULL;
300 GList *out = NULL;
301 const gchar *fn;
302 while((fn = g_dir_read_name(d)))
303 {
304 if(!g_str_has_suffix(fn, ".anselnn")) continue;
305 gboolean shipped = FALSE;
306 for(int v = 0; v < DT_RAWDENOISEAI_NUM_VERSIONS && !shipped; v++)
307 for(int z = 0; z < DT_RAWDENOISEAI_NUM_SIZES && !shipped; z++)
308 for(int c = 0; c < DT_RAWDENOISEAI_NUM_SCALES && !shipped; c++)
309 {
310 char name[64];
311 snprintf(name, sizeof(name), "denoise-%s-%s-%s.anselnn", _size_tag[z], _scale_tag[c], _version_tag[v]);
313 }
315 }
316 g_dir_close(d);
318}
319
320const char *name()
321{
322 return _("raw denoise (AI)");
323}
324
325const char **description(struct dt_iop_module_t *self)
326{
327 return dt_iop_set_description(self,
328 _("denoise the raw picture with a neural network conditioned "
329 "on the camera noise profile"),
330 _("corrective"), _("linear, raw, scene-referred"), _("linear, raw"),
331 _("linear, raw, scene-referred"));
332}
333
338
340{
341 return IOP_GROUP_REPAIR;
342}
343
345{
346 return IOP_CS_RAW;
347}
348
351{
352 default_input_format(self, pipe, piece, dsc);
353 dsc->channels = 1;
355}
356
357/* Route the executor's scratch through the pixelpipe cache arena, per the
358 * project rule that pixel buffers never come from bare malloc. nn_model
359 * cannot include darktable.h (it is deliberately pipeline-free), so the
360 * arena is injected here, once, before any pipeline runs. */
361/* Per-tile scratch REGION: the whole working set of one process() call —
362 * input planes, output plane and every executor tensor — is reserved from the
363 * pixelpipe arena as ONE allocation, up front, sized by the executor's exact
364 * ledger plus an internal-fragmentation margin. The executor's alloc/free
365 * churn then happens inside the region through the sub-allocator below and is
366 * invisible to the arena: no interleaving with cache entries, no fragmenting
367 * of the arena's free runs, and the tiling engine's largest-free-run cap
368 * guarantees the single reservation fits by construction. Memory is planned,
369 * reserved, then executed within — never discovered at runtime; if the
370 * reservation itself fails, the tile fails BEFORE any compute.
371 *
372 * The sub-allocator is a trivial first-fit block list: at most a dozen live
373 * tensors exist at once, all sized in whole planes. The region pointer is
374 * thread-local because the darkroom's full and preview pipes may run
375 * process() concurrently. */
376#define NN_REGION_MAX_BLOCKS 32
377// the two-ended layout (churn bottom-up, skips top-down) achieves the
378// ledger's live peak exactly; the slack only covers per-block 64-byte
379// alignment crumbs
380#define NN_REGION_SLACK 1.02f
381
382typedef struct nn_region_t
383{
384 char *base;
385 size_t size;
386 struct
387 {
388 size_t off, len;
392
394
395static void *_region_alloc(nn_region_t *r, size_t bytes, int long_lived)
396{
397 bytes = (bytes + 63) & ~(size_t)63; // keep 64-byte alignment inside the region
398 if(r->n_blocks >= NN_REGION_MAX_BLOCKS) return NULL;
399 size_t off = (size_t)-1;
400 int at = 0;
401 if(!long_lived)
402 {
403 // churn packs bottom-up, first fit; blocks are kept sorted by offset
404 size_t gap = 0;
405 for(int i = 0; i <= r->n_blocks; i++)
406 {
407 const size_t end = (i < r->n_blocks) ? r->blocks[i].off : r->size;
408 if(end - gap >= bytes)
409 {
410 off = gap;
411 at = i;
412 break;
413 }
414 if(i == r->n_blocks) break;
415 gap = r->blocks[i].off + r->blocks[i].len;
416 }
417 }
418 else
419 {
420 // long-lived blocks (skip connections) pack top-down, last fit, so they
421 // never split the churn area mid-region — this is what lets the region be
422 // sized at exactly planes + the ledger's live peak, no slack
423 size_t gap_end = r->size;
424 for(int i = r->n_blocks; i >= 0; i--)
425 {
426 const size_t gap_start = (i > 0) ? r->blocks[i - 1].off + r->blocks[i - 1].len : 0;
427 if(gap_end - gap_start >= bytes)
428 {
429 off = gap_end - bytes;
430 at = i;
431 break;
432 }
433 if(i > 0) gap_end = r->blocks[i - 1].off;
434 }
435 }
436 if(off == (size_t)-1)
437 {
438 size_t live = 0, largest_gap = 0, gap = 0;
439 for(int i = 0; i <= r->n_blocks; i++)
440 {
441 const size_t end = (i < r->n_blocks) ? r->blocks[i].off : r->size;
442 if(end - gap > largest_gap) largest_gap = end - gap;
443 if(i == r->n_blocks) break;
444 live += r->blocks[i].len;
445 gap = r->blocks[i].off + r->blocks[i].len;
446 }
448 "[rawdenoiseai] region alloc failed: %" G_GSIZE_FORMAT " bytes (%s), region %" G_GSIZE_FORMAT
449 ", live %" G_GSIZE_FORMAT " in %d blocks, largest gap %" G_GSIZE_FORMAT "\n",
450 bytes, long_lived ? "long-lived" : "churn", r->size, live, r->n_blocks, largest_gap);
451 return NULL;
452 }
453 for(int i = r->n_blocks; i > at; i--) r->blocks[i] = r->blocks[i - 1];
454 r->blocks[at].off = off;
455 r->blocks[at].len = bytes;
456 r->n_blocks++;
457 return r->base + off;
458}
459
460static void _region_free(nn_region_t *r, void *p)
461{
462 const size_t off = (size_t)((char *)p - r->base);
463 for(int i = 0; i < r->n_blocks; i++)
464 if(r->blocks[i].off == off)
465 {
466 for(int j = i; j < r->n_blocks - 1; j++) r->blocks[j] = r->blocks[j + 1];
467 r->n_blocks--;
468 return;
469 }
470}
471
477
478static void _nn_arena_free(void *p)
479{
481 if(r && (char *)p >= r->base && (char *)p < r->base + r->size)
482 {
483 _region_free(r, p);
484 return;
485 }
487}
488
490{
494#ifdef HAVE_OPENCL
495 gd->nn_cl = dt_nn_cl_create(39); // rawdenoiseai.cl, from programs.conf
496 gd->k_assemble = dt_opencl_create_kernel(39, "nn_assemble");
497 gd->k_bin_planes = dt_opencl_create_kernel(39, "nn_bin_planes");
498 gd->k_residual = dt_opencl_create_kernel(39, "nn_residual");
499 gd->k_upsample_n = dt_opencl_create_kernel(39, "nn_upsample_n");
500 gd->k_bin16_mdv = dt_opencl_create_kernel(39, "nn_bin16_mdv");
501 gd->k_avg2x2 = dt_opencl_create_kernel(39, "nn_avg2x2");
502 gd->k_floor_fuse = dt_opencl_create_kernel(39, "nn_floor_fuse");
503 gd->k_fuse_step = dt_opencl_create_kernel(39, "nn_fuse_step");
504 gd->k_bilerp_add = dt_opencl_create_kernel(39, "nn_bilerp_add");
505 gd->k_blend_crop = dt_opencl_create_kernel(39, "nn_blend_crop");
506#endif
507 module->data = gd;
508 // models are loaded lazily on first use per (version, variant)
509}
510
512{
513 // the hooks point into this module's code: leaving them set after dlclose
514 // would leave the core library with dangling function pointers
517 if(gd)
518 {
519 for(int v = 0; v < DT_RAWDENOISEAI_NUM_VERSIONS; v++)
520 for(int sz = 0; sz < DT_RAWDENOISEAI_NUM_SIZES; sz++)
521 for(int sc = 0; sc < DT_RAWDENOISEAI_NUM_SCALES; sc++) dt_nn_model_free(gd->models[v][sz][sc]);
522#ifdef HAVE_OPENCL
523 dt_nn_cl_destroy(gd->nn_cl);
524 dt_opencl_free_kernel(gd->k_assemble);
525 dt_opencl_free_kernel(gd->k_bin_planes);
526 dt_opencl_free_kernel(gd->k_residual);
527 dt_opencl_free_kernel(gd->k_upsample_n);
528 dt_opencl_free_kernel(gd->k_bin16_mdv);
529 dt_opencl_free_kernel(gd->k_avg2x2);
530 dt_opencl_free_kernel(gd->k_floor_fuse);
531 dt_opencl_free_kernel(gd->k_fuse_step);
532 dt_opencl_free_kernel(gd->k_bilerp_add);
533 dt_opencl_free_kernel(gd->k_blend_crop);
534#endif
535 if(gd->custom)
536 {
538 gpointer k, v;
539 g_hash_table_iter_init(&it, gd->custom);
540 while(g_hash_table_iter_next(&it, &k, &v))
542 g_hash_table_destroy(gd->custom);
543 gd->custom = NULL;
544 }
546 }
547 free(module->data);
548 module->data = NULL;
549}
550
551/* The size a new history entry gets, from what the machine can actually run:
552 * half where OpenCL will carry it, quarter on CPU alone — the only size that
553 * stays interactive there. The variant is multiscale either way; the coarse
554 * chroma pass is worth most exactly where capacity is scarce (it buys quarter
555 * ~2.6 dB of low-frequency chroma error and large almost nothing), so the
556 * hardware picks the width, not the variant. */
561
562/* Supported when the input is mosaiced and the model a new entry would DEFAULT
563 * to can be loaded (i.e. weights are installed). Probing that exact model and
564 * not a fixed one is what keeps the enable button honest on an installation
565 * carrying only part of the matrix. A specific (version, size, variant) the
566 * user selects that turns out to be missing is handled per-piece by
567 * copy-through. */
574
576{
577 /* Pick the default size from what the machine can actually run, because a
578 * user who enables the module without knowing what it is must get a result
579 * in seconds — not a frozen application. The variant is multiscale in both
580 * cases: it is what keeps the smaller networks free of low-frequency chroma
581 * blotches, and that matters most exactly where capacity is scarce (see
582 * doc/rawdenoiseai.md — the coarse pass buys quarter ~2.6 dB of chroma
583 * error and large almost nothing).
584 *
585 * OpenCL present: half, ~4x the quarter cost but clearly better.
586 * CPU only: quarter, the only size that stays interactive on a CPU.
587 *
588 * dt_opencl_is_enabled() reflects both the build and the user's preference,
589 * and is a static stub returning 0 without HAVE_OPENCL. Users who want a
590 * different trade-off still pick any size by hand; this only seeds a NEW
591 * history entry, so existing edits keep whatever they were created with. */
593 d->size = _default_size();
594 d->scale_variant = DT_RAWDENOISEAI_MULTI;
595
596 module->hide_enable_button = !_rawdenoiseai_supported(module);
597 module->default_enabled = 0;
598}
599
600gboolean force_enable(struct dt_iop_module_t *self, const gboolean current_state)
601{
602 // history sanitization: an entry pasted onto a non-mosaic image, or loaded
603 // without a model available, is forced off at history-read time
605}
606
607/* Fill d->a/d->b from the best noise profile for this image: exact ISO match,
608 * interpolation between the bracketing profiled ISOs, clamping outside the
609 * profiled range, generic Poissonian when the camera has no profiles. Mirrors
610 * the training-time ProfileSampler semantics (clamped interpolation). */
612{
615 const float iso = self->dev->image_storage.exif_iso;
616
617 if(profiles)
618 {
619 dt_noiseprofile_t *first = (dt_noiseprofile_t *)profiles->data;
620 dt_noiseprofile_t *last = NULL;
621 interpolated = *first; // clamp below the profiled range
622 for(GList *iter = profiles; iter; iter = g_list_next(iter))
623 {
624 dt_noiseprofile_t *current = (dt_noiseprofile_t *)iter->data;
625 if(current->iso == iso)
626 {
627 interpolated = *current;
628 break;
629 }
630 if(last && last->iso < iso && current->iso > iso)
631 {
632 interpolated.iso = iso;
633 dt_noiseprofile_interpolate(last, current, &interpolated);
634 break;
635 }
636 interpolated = *current; // clamp above the profiled range
637 last = current;
638 }
639 }
640 for(int k = 0; k < 3; k++)
641 {
642 d->a[k] = interpolated.a[k];
643 d->b[k] = interpolated.b[k];
644 }
646}
647
650{
653
654 d->strength = p->strength;
655 d->noise_level = p->noise_level;
656 d->sigma_scale[0] = p->sigma_red;
657 d->sigma_scale[1] = p->sigma_green;
658 d->sigma_scale[2] = p->sigma_blue;
659 // neural inference is among the most expensive nodes of the pipe: always
660 // materialize this piece's output in the RAM cache (same policy as
661 // diffuse/atrous), so interactive edits downstream never re-run it
662 piece->cache_output_on_ram = TRUE;
663 _fetch_noise_profile(self, d);
664
666 /* A named user model wins over the shipped matrix. If it has gone missing
667 * since the edit was made, the piece is disabled rather than silently
668 * rendered through a DIFFERENT network: the two are not interchangeable,
669 * and quietly substituting one would change the picture without telling
670 * anyone. The GUI keeps showing the name so the situation is legible. */
671 const gboolean want_custom = gd && p->custom_model[0];
672 d->model = want_custom ? _get_custom_model(gd, p->custom_model)
673 : gd ? _get_model(gd, p->version, p->size, p->scale_variant)
674 : NULL;
675
677 "[rawdenoiseai] commit: camera '%s' iso %.0f model %s-%s-%s%s -> "
678 "a=(%.3g %.3g %.3g) b=(%.3g %.3g %.3g) strength %.2f noise level %.2f "
679 "channel correction (%.2f %.2f %.2f)\n",
682 _scale_tag[CLAMP(p->scale_variant, 0, DT_RAWDENOISEAI_NUM_SCALES - 1)],
684 d->model ? (want_custom ? " (user model)" : "") : " (missing)",
685 d->a[0], d->a[1], d->a[2], d->b[0], d->b[1], d->b[2], p->strength, p->noise_level, p->sigma_red,
686 p->sigma_green, p->sigma_blue);
687
688 // plane-layout contract with the training repo: a model that does not
689 // match what _assemble_planes builds must be treated as missing, not fed
690 if(d->model)
691 {
692 const int fine_in = dt_nn_model_in_channels(d->model);
693 const int c_out = dt_nn_model_coarse_out_channels(d->model);
694 const gboolean ok = c_out > 0 ? (dt_nn_model_coarse_in_channels(d->model) == 6 && c_out == 3 && fine_in == 8)
695 : (fine_in == 5);
696 if(!ok)
697 {
699 "[rawdenoiseai] model plane layout unsupported "
700 "(fine_in %d coarse_out %d) — module disabled\n",
701 fine_in, c_out);
702 d->model = NULL;
703 }
704 }
705 if(!d->model || !(p->strength > 0.0f)) piece->enabled = 0;
706}
707
708static unsigned _align_lcm(const unsigned a, const unsigned b)
709{
710 if(a == 0 || b == 0) return a > b ? a : b;
711 unsigned x = a, y = b;
712 while(y)
713 {
714 const unsigned t = x % y;
715 x = y;
716 y = t;
717 }
718 return a / x * b;
719}
720
721void tiling_callback(struct dt_iop_module_t *self, const struct dt_dev_pixelpipe_t *pipe,
722 const struct dt_dev_pixelpipe_iop_t *piece, struct dt_develop_tiling_t *tiling)
723{
725 // per input pixel (4 bytes): 5 input planes + 1 output plane + network
726 // scratch, all float32. factor is relative to the unit buffer size.
727 float extra = 6.0f;
728 float extra_cl = 6.0f;
729 unsigned overlap = 0;
730 if(d && d->model)
731 {
732 // input planes + output plane (in_ch is 8 for a multi-scale model), the
733 // coarse buffers at 1/bin^2, and the executor scratch (which already
734 // includes the coarse net's share for unet-ms)
735 const gboolean xtrans_cfa = piece->dsc_in.filters == 9u;
736 const int bin = dt_nn_model_bin(d->model, xtrans_cfa);
737 const float planes = (float)(dt_nn_model_in_channels(d->model) + 1)
738 + (bin > 1 ? 12.0f / (float)(bin * bin) : 0.0f);
739 /* Host: process() reserves the tile's whole working set as ONE arena
740 * region — the (in_ch + 1) planes plus the executor's ledger peak times
741 * the sub-allocator's fragmentation margin. The factor declares exactly
742 * that reservation, so the tiling plan, the reservation and the execution
743 * are the same number (the coarse-stage module buffers, 12/bin^2, are the
744 * only separate short-lived arena entries). Expressed as a factor of the
745 * input image size — floats per input pixel — never absolute bytes. */
746 extra = planes + NN_REGION_SLACK * dt_nn_unet_scratch_per_px(d->model);
747 /* Device: no region — buffers are device-side. The CL executor's sequence
748 * differs (it materializes the decoder concat), and the CL path adds
749 * three device planes of its own (dev_den, plus the buffer-format copies
750 * dev_in_buf/dev_out_buf) and the fusion grids (~0.13 plane); count all
751 * of it explicitly rather than letting it ride inside factor_cl's
752 * padding headroom. */
753 extra_cl = planes + dt_nn_unet_scratch_per_px_cl(d->model) + 3.2f;
754
755 // The theoretical receptive field of the depth-4 U-Net is ~110 px, but the
756 // measured impulse response of the trained model decays to 1.4e-6 of peak
757 // at radius 32 (3 orders below 8-bit visibility) and to exactly zero at
758 // 96. 48 px of overlap is generous against seams while wasting far less
759 // redundant tile area than the theoretical bound would.
760 // the coarse guide widens the receptive field: 96 px covers 2x the fine
761 // net's measured impulse support plus the coarse levels carrying nearly
762 // all of its measured energy (see doc/rawdenoiseai.md); leaks beyond it
763 // are sub-visibility smooth offsets since the guide is conditioning, not
764 // additive output. 96 is a multiple of both CFA alignments. Fast mode
765 // (chroma pass off) keeps the historical 48.
766 overlap = (bin > 1) ? 96 : 48;
767 }
768 tiling->factor = 2.0f + extra;
769 /* The GPU budget must be more conservative than the CPU one, for two
770 * reasons the tiling engine cannot see. First, every buffer here is
771 * allocated at the alignment-padded tile size, up to (align-1) larger per
772 * axis than the tile the engine budgeted — only for the ragged last tile of
773 * a row or column now that xalign below equals the alignment, so an interior
774 * tile is padded by nothing at all and this term is pure headroom. Second,
775 * the engine hands out up to 100 % of the device's reported memory, and a
776 * budget that spends all of VRAM never allocates in practice (display,
777 * driver and allocator overhead) — CPU RAM overcommits, VRAM does not,
778 * and the U-Net scratch dominates this factor so the whole budget is
779 * exact where other modules' estimates carry implicit slack. 1.4 covers
780 * the worst realistic padding inflation (~1.15) times an allocation
781 * headroom (~1.2); the cost of overshooting is a smaller tile, the cost
782 * of undershooting is CL_MEM_OBJECT_ALLOCATION_FAILURE and a silent
783 * fallback of the whole tile to CPU. */
784 tiling->factor_cl = 2.0f + extra_cl * 1.4f;
785 tiling->maxbuf = 1.0f;
786 // device-resident weight blob (up to ~38 MB) plus fixed slack
787 tiling->overhead = 64u * 1024u * 1024u;
788 tiling->overlap = overlap;
789 /* Tiles must preserve the CFA phase — and, for a multi-scale model, every
790 * other LATTICE this module lays over its input: the superpixel binning of
791 * the coarse stage (period `bin`) and the low-band fusion pyramid (period
792 * DT_NN_FUSION_COARSEST). All of them are anchored to the tile's own origin,
793 * because that is the only origin process() is handed. The tiling engine
794 * places tile origins on multiples of lcm(xalign, yalign), so anything short
795 * of the full lattice period lets two different tile grids bin the same
796 * sensels into differently-phased superpixels — which changes the coarse
797 * guide, and with it the result, EVERYWHERE inside the tile rather than only
798 * near its seams. No amount of overlap fixes that; only aligning the origins
799 * does. It is why the same edit rendered differently on CPU and GPU: the two
800 * budget their memory differently, so they tile differently, so they binned
801 * on different lattices.
802 *
803 * dt_nn_model_alignment() is the one function that owns those periods (the
804 * fine net's stride pyramid, the coarse net's binned one, and the fusion
805 * bands) — ask it rather than re-deriving any of them here, or the next
806 * lattice added to the model silently reopens this bug. Aligning to the FULL
807 * value also means an interior tile needs no reflect padding at all, so no
808 * mirrored data is folded into the fusion's per-tile statistics. */
809 const gboolean xtrans = piece->dsc_in.filters == 9u;
810 unsigned align = xtrans ? 6u : 2u;
811 if(d && d->model) align = _align_lcm(align, (unsigned)dt_nn_model_alignment(d->model));
812 tiling->xalign = align;
813 tiling->yalign = align;
814}
815
816// mirror-reflect coordinate into [0, n): same row/column parity is preserved
817// for even n-1 steps; the CFA color planes are computed from the SOURCE
818// sensel, so the network always sees consistent (value, color) pairs even
819// where reflection breaks the periodic layout (X-Trans borders).
820static inline __attribute__((always_inline)) int _reflect(int v, int n)
821{
822 if(n == 1) return 0;
823 while(v < 0 || v >= n)
824 {
825 if(v < 0) v = -v;
826 if(v >= n) v = 2 * n - 2 - v;
827 }
828 return v;
829}
830
831/* Total sigma scale per CFA color: global correction x the per-channel
832 * slider, nothing else — the GUI values ARE the conditioning (see the
833 * calibration comment at the top of this file). */
834static void _sigma_scale(const dt_iop_rawdenoiseai_data_t *d, float scale[3])
835{
836 for(int c = 0; c < 3; c++) scale[c] = d->noise_level * d->sigma_scale[c];
837}
838
839/* Build the coarse stage's 6 input planes [R, G, B, sigmaR, sigmaG, sigmaB]
840 * from the assembled fine planes 0-3 (count-weighted superpixel means; the
841 * binning itself is dt_nn_bin_planes, the bit-exact contract with the
842 * training repo). Coarse sigma is the analytic sigma of the mean of n
843 * sensels: scale[c] * sqrt((a*x + b) / n). Shared verbatim by the CPU and
844 * OpenCL paths. */
846static void _k_bin_planes(const float *const nn_in, float *const coarse_in, float *const cnt,
847 const int pw, const int ph, const int bin,
848 const dt_iop_rawdenoiseai_data_t *const d)
849{
850 const int cw = pw / bin, chh = ph / bin;
851 const size_t cplane = (size_t)cw * chh;
853 float scale[3];
854 _sigma_scale(d, scale);
856 for(int c = 0; c < 3; c++)
857 {
858 const float *const mean = coarse_in + (size_t)c * cplane;
859 const float *const n_c = cnt + (size_t)c * cplane;
860 float *const sigma = coarse_in + (size_t)(3 + c) * cplane;
861 for(size_t i = 0; i < cplane; i++)
862 {
863 const float n = n_c[i] > 1.0f ? n_c[i] : 1.0f;
864 const float var = (d->a[c] * MAX(mean[i], 0.0f) + d->b[c]) / n;
865 sigma[i] = scale[c] * sqrtf(MAX(var, 1e-12f));
866 }
867 }
868}
869
870/* Build the network's 5 input planes [mosaic, R, G, B one-hot, sigma] from the
871 * mosaic, reflect-padded from (width, height) to (pw, ph). Shared verbatim by
872 * the CPU and OpenCL paths so both feed the network identical data. */
874static void _k_assemble(const float *const in, float *const nn_in, const int width, const int height,
875 const int pw, const int ph, const dt_iop_rawdenoiseai_data_t *const d,
876 const uint32_t filters, const uint8_t (*const xtrans)[6],
877 const dt_iop_roi_t *const roi)
878{
879 const size_t plane = (size_t)pw * ph;
880 float scale[3];
881 _sigma_scale(d, scale);
883 for(int y = 0; y < ph; y++)
884 {
885 const int sy = _reflect(y, height);
886 const float *const irow = in + (size_t)sy * width;
887 float *const mosaic = nn_in + (size_t)y * pw;
888 float *const onehot_r = nn_in + plane + (size_t)y * pw;
889 float *const onehot_g = nn_in + 2 * plane + (size_t)y * pw;
890 float *const onehot_b = nn_in + 3 * plane + (size_t)y * pw;
891 float *const sigma = nn_in + 4 * plane + (size_t)y * pw;
892 for(int x = 0; x < pw; x++)
893 {
894 const int sx = _reflect(x, width);
895 const float v = irow[sx];
896 int c = (filters == 9u) ? FCxtrans(sy, sx, roi, xtrans) : (int)FC(sy, sx, filters);
897 if(c < 0 || c > 2) c = 1; // both greens share the G statistics; clamp junk CFA data
898 mosaic[x] = v;
899 onehot_r[x] = (c == 0) ? 1.0f : 0.0f;
900 onehot_g[x] = (c == 1) ? 1.0f : 0.0f;
901 onehot_b[x] = (c == 2) ? 1.0f : 0.0f;
902 const float var = d->a[c] * MAX(v, 0.0f) + d->b[c];
903 sigma[x] = scale[c] * sqrtf(MAX(var, 1e-12f));
904 }
905 }
906}
907
908
909/* ---- CPU kernels ------------------------------------------------------
910 * One function per OpenCL kernel in rawdenoiseai.cl, same name, same
911 * arguments in the same order, so process() and process_cl() read as the same
912 * procedure and a change to one has an obvious counterpart in the other.
913 * Every pixel loop in this module lives in one of these. */
914
915/* Per-channel Bayer densities: the fraction of a block's sensels carrying each
916 * colour. Shared with the OpenCL twins, which take them as dens0/dens1/dens2.
917 * Bayer values are used for both CFA families, matching the torch reference. */
918static const float DT_NN_FUSION_DENS[3] = { 0.25f, 0.5f, 0.25f };
919
920/* chi^2-quantile guard: the local mean of a squared noise term over a 3x3 cell
921 * neighbourhood has ~9 effective samples, so pure-noise cells fluctuate up to
922 * ~2x their expectation. Must equal the literal in nn_floor_fuse/nn_fuse_step. */
923#define DT_NN_FUSION_T_CHI2 2.5
924
925/* mirrors nn_residual: the network predicts what to REMOVE, so the denoised
926 * signal is input minus head. Applied by the caller on BOTH devices — the
927 * executor writes the raw head output either way. */
929static void _k_residual(const float *const in, const float *const head, float *const out, const size_t n)
930{
932 for(size_t i = 0; i < n; i++) out[i] = in[i] - head[i];
933}
934
935/* mirrors nn_blend_crop: strength is the opacity of the correction, so the
936 * output lerps between the original mosaic and the denoised one while dropping
937 * the alignment padding. */
939static void _k_blend_crop(const float *const in, const float *const den, float *const out, const int width,
940 const int height, const int pw, const float strength)
941{
943 for(int y = 0; y < height; y++)
944 {
945 const float *const src_in = in + (size_t)y * width;
946 const float *const src_nn = den + (size_t)y * pw;
947 float *const dst = out + (size_t)y * width;
948 for(int x = 0; x < width; x++) dst[x] = src_in[x] + strength * (src_nn[x] - src_in[x]);
949 }
950}
951
952/* mirrors nn_bin16_mdv: count-weighted per-channel mean of the mosaic, the
953 * denoised plane and sigma^2 over each 16x16 block. */
955static void _k_bin16_mdv(const float *const nn_in, const float *const denoised, float *const M,
956 float *const D, float *const V, const int pw, const int ph)
957{
958 const size_t plane = (size_t)pw * ph;
959 const float *const sig = nn_in + 4 * plane;
960 const int cw = pw / DT_NN_FUSION_FINEST, chh = ph / DT_NN_FUSION_FINEST;
961 const size_t p0 = (size_t)cw * chh;
962 for(int c = 0; c < 3; c++)
963 {
964 const float *const oh = nn_in + (size_t)(1 + c) * plane;
966 for(int cy = 0; cy < chh; cy++)
967 for(int cx = 0; cx < cw; cx++)
968 {
969 float sm = 0.f, sd = 0.f, sv = 0.f, cnt = 0.f;
970 for(int y = cy * DT_NN_FUSION_FINEST; y < (cy + 1) * DT_NN_FUSION_FINEST; y++)
971 for(int x = cx * DT_NN_FUSION_FINEST; x < (cx + 1) * DT_NN_FUSION_FINEST; x++)
972 {
973 const size_t i = (size_t)y * pw + x;
974 sm += nn_in[i] * oh[i];
975 sd += denoised[i] * oh[i];
976 sv += sig[i] * sig[i] * oh[i];
977 cnt += oh[i];
978 }
979 const float n = cnt > 1.f ? cnt : 1.f;
980 const size_t o = (size_t)c * p0 + (size_t)cy * cw + cx;
981 M[o] = sm / n;
982 D[o] = sd / n;
983 V[o] = sv / n;
984 }
985 }
986}
987
988/* mirrors nn_avg2x2: 2x2 average pooling of a 3-plane grid. Per-channel counts
989 * are uniform at these scales, so a plain mean of four children equals
990 * re-binning from full resolution. */
992static void _k_avg2x2(const float *const in, float *const out, const int sw, const int sh, const size_t p0)
993{
994 const int w2 = sw / 2, h2 = sh / 2;
995 for(int c = 0; c < 3; c++)
996 {
997 const float *const src = in + (size_t)c * p0;
998 float *const dst = out + (size_t)c * p0;
999 for(int y = 0; y < h2; y++)
1000 for(int x = 0; x < w2; x++)
1001 dst[(size_t)y * w2 + x]
1002 = 0.25f
1003 * (src[(size_t)(2 * y) * sw + 2 * x] + src[(size_t)(2 * y) * sw + 2 * x + 1]
1004 + src[(size_t)(2 * y + 1) * sw + 2 * x] + src[(size_t)(2 * y + 1) * sw + 2 * x + 1]);
1005 }
1006}
1007
1008/* Hybrid low-band fusion (mirrors cfa.fuse_low_bands in the training repo):
1009 * per-band self-calibrated Wiener weights at scales 16/32, pure measurement
1010 * at the coarsest band. All band corrections are upsampled BILINEARLY
1011 * (align_corners=false, matching torch F.interpolate): nearest upsampling
1012 * turned the per-block measurement noise (sigma/sqrt(n), non-negligible on
1013 * very noisy images) into visible checkers of colored squares. The finest
1014 * fusion band is 16 px for the same reason. The coarsest band is the
1015 * n-averaged measurement outright, so the hallucination-free guarantee
1016 * holds. The pyramid is always 16/32/64 — the same bands the training
1017 * reference fuses — because dt_nn_model_alignment() guarantees a padded tile
1018 * that divides by the coarsest one. Bayer channel densities are used for both
1019 * CFA families, matching the torch reference. */
1020static inline __attribute__((always_inline)) float _bilerp_tap(const float *const p, const int w,
1021 const int h, const float fx,
1022 const float fy)
1023{
1024 const float cx = fx < 0.f ? 0.f : (fx > w - 1.f ? w - 1.f : fx);
1025 const float cy = fy < 0.f ? 0.f : (fy > h - 1.f ? h - 1.f : fy);
1026 const int x0 = (int)cx, y0 = (int)cy;
1027 const int x1 = x0 + 1 < w ? x0 + 1 : x0;
1028 const int y1 = y0 + 1 < h ? y0 + 1 : y0;
1029 const float ax = cx - x0, ay = cy - y0;
1030 const float top = p[(size_t)y0 * w + x0] * (1.f - ax) + p[(size_t)y0 * w + x1] * ax;
1031 const float bot = p[(size_t)y1 * w + x0] * (1.f - ax) + p[(size_t)y1 * w + x1] * ax;
1032 return top * (1.f - ay) + bot * ay;
1033}
1034
1035// dst (fw x fh) = bilinear upsample of src (sw x sh) by integer factor f
1037static void _upsample_bilinear(const float *const src, const int sw, const int sh, const int f, float *const dst)
1038{
1039 const int fw = sw * f, fh = sh * f;
1040 for(int y = 0; y < fh; y++)
1041 {
1042 const float sy = (y + 0.5f) / f - 0.5f;
1043 for(int x = 0; x < fw; x++) dst[(size_t)y * fw + x] = _bilerp_tap(src, sw, sh, (x + 0.5f) / f - 0.5f, sy);
1044 }
1045}
1046
1047/* Number of pyramid levels between the finest and the coarsest fusion band.
1048 * Constant by construction — both are fixed by the training reference. */
1049static inline int _fusion_levels(void)
1050{
1051 int n = 1;
1052 for(int s = DT_NN_FUSION_FINEST; s < DT_NN_FUSION_COARSEST; s *= 2) n++;
1053 return n;
1054}
1055
1056/* Returns 0 on success, non-zero if the fusion could not run — the caller must
1057 * treat that as a failed tile, exactly as process_cl() does when a device
1058 * buffer for the same pyramid cannot be allocated. Rendering the tile with the
1059 * fine network's raw low band instead would be a silent, tile-shaped quality
1060 * regression, and would differ from whatever the other device did. */
1062/* mirrors nn_floor_fuse: structure-gated blend — the measurement owns every
1063 * cell whose own mean-removed local energy is noise-sized (the dilution
1064 * guarantee), the model owns structured cells, because a box average across an
1065 * edge mixes both sides into a saturated outline. The gate reads the
1066 * MEASUREMENT, not the model discrepancy: D-M cannot tell a real edge from the
1067 * model drifting on flat content. REQUIRES a model trained with the
1068 * DC-ownership loss; one from the older fused loss drifts in deep shadows. */
1070static void _k_floor_fuse(const float *const M, const float *const D, const float *const V,
1071 float *const fused, const int sw, const int sh, const size_t p0, const int S)
1072{
1073 for(int c = 0; c < 3; c++)
1074 {
1075 const double vscale = 1.0 / (DT_NN_FUSION_DENS[c] * S * S);
1076 const float *const Mp = M + (size_t)c * p0;
1077 const float *const Dp = D + (size_t)c * p0;
1078 const float *const Vp = V + (size_t)c * p0;
1079 float *const fs = fused + (size_t)c * p0;
1080 for(int y = 0; y < sh; y++)
1081 for(int x = 0; x < sw; x++)
1082 {
1083 // structure = blur3((M - blur3(M))^2): insensitive to smooth gradients,
1084 // unlike a plain window variance
1085 double structure = 0.0;
1086 for(int ny = -1; ny <= 1; ny++)
1087 for(int nx = -1; nx <= 1; nx++)
1088 {
1089 const int cy2 = CLAMP(y + ny, 0, sh - 1), cx2 = CLAMP(x + nx, 0, sw - 1);
1090 double mean = 0.0;
1091 for(int dy = -1; dy <= 1; dy++)
1092 for(int dx = -1; dx <= 1; dx++)
1093 {
1094 const int yy = CLAMP(cy2 + dy, 0, sh - 1), xx = CLAMP(cx2 + dx, 0, sw - 1);
1095 mean += Mp[(size_t)yy * sw + xx];
1096 }
1097 const double mloc = Mp[(size_t)cy2 * sw + cx2] - mean / 9.0;
1098 structure += mloc * mloc;
1099 }
1100 const size_t i = (size_t)y * sw + x;
1101 // this cell's own mean sigma^2, not the tile's (cfa.fuse_low_bands)
1102 const double vn = (double)Vp[i] * vscale;
1104 if(structure < 0.0) structure = 0.0;
1105 const float w = (float)(structure / (structure + vn + 1e-20));
1106 fs[i] = w * Dp[i] + (1.f - w) * Mp[i];
1107 }
1108 }
1109}
1110
1111/* mirrors nn_fuse_step: upsample the running fusion and add the finer band,
1112 * weighted per cell by a Wiener gain on the band discrepancy. `ups` is scratch
1113 * for two upsampled planes. */
1115static void _k_fuse_step(const float *const fused_c, const float *const Mf, const float *const Df,
1116 const float *const Vf, const float *const Mc, const float *const Dc,
1117 float *const fused_f, float *const ups, const int sw, const int sh,
1118 const size_t p0, const int sc)
1119{
1120 const int fw = sw * 2, fh = sh * 2;
1121 for(int c = 0; c < 3; c++)
1122 {
1123 const float *const Dfp = Df + (size_t)c * p0;
1124 const float *const Mfp = Mf + (size_t)c * p0;
1125 const float *const Vfp = Vf + (size_t)c * p0;
1126 float *const upD = ups, *const upM = ups + p0;
1127 _upsample_bilinear(Dc + (size_t)c * p0, sw, sh, 2, upD);
1128 _upsample_bilinear(Mc + (size_t)c * p0, sw, sh, 2, upM);
1129 /* Var(mean_s - up(mean_2s)) = Var(mean_s) * 3/4 once the covariance with the
1130 * 2x2 parent is folded in, which is what this reciprocal difference is; so
1131 * the scale-s cell mean is the right sigma^2 for the whole term. */
1132 const double vscale
1133 = 1.0 / (DT_NN_FUSION_DENS[c] * sc * sc) - 1.0 / (DT_NN_FUSION_DENS[c] * 4.0 * sc * sc);
1134 _upsample_bilinear(fused_c + (size_t)c * p0, sw, sh, 2, fused_f + (size_t)c * p0);
1135 float *const fs = fused_f + (size_t)c * p0;
1136 for(int y = 0; y < fh; y++)
1137 for(int x = 0; x < fw; x++)
1138 {
1139 // per-cell Wiener weight from the 3x3-smoothed band discrepancy
1140 double acc = 0.0;
1141 int n = 0;
1142 for(int dy = -1; dy <= 1; dy++)
1143 for(int dx = -1; dx <= 1; dx++)
1144 {
1145 const int yy = CLAMP(y + dy, 0, fh - 1), xx = CLAMP(x + dx, 0, fw - 1);
1146 const size_t j = (size_t)yy * fw + xx;
1147 const double d = (double)(Dfp[j] - upD[j]) - (double)(Mfp[j] - upM[j]);
1148 acc += d * d;
1149 n++;
1150 }
1151 const size_t i = (size_t)y * fw + x;
1152 const double vn = (double)Vfp[i] * vscale;
1153 double vm = acc / n - DT_NN_FUSION_T_CHI2 * vn;
1154 if(vm < 0.0) vm = 0.0;
1155 const float w = (float)(vn / (vn + vm + 1e-20));
1156 fs[i] += w * (Dfp[i] - upD[i]) + (1.f - w) * (Mfp[i] - upM[i]);
1157 }
1158 }
1159}
1160
1161/* mirrors nn_bilerp_add: scatter (fused - D16) bilinearly upsampled from the
1162 * level-0 grid onto the denoised plane, on whichever colour plane owns each
1163 * sensel. `scratch` is reused as the correction plane. */
1165static void _k_bilerp_add(const float *const fused, const float *const D16, float *const scratch,
1166 const float *const nn_in, float *const denoised, const int pw, const int ph,
1167 const size_t p0, const int cw0, const int ch0)
1168{
1169 const size_t plane = (size_t)pw * ph;
1170 const float inv = 1.f / (float)DT_NN_FUSION_FINEST;
1171 for(int c = 0; c < 3; c++)
1172 {
1173 float *const cs = scratch + (size_t)c * p0;
1174 const float *const fa = fused + (size_t)c * p0;
1175 const float *const d0 = D16 + (size_t)c * p0;
1176 for(size_t i = 0; i < p0; i++) cs[i] = fa[i] - d0[i];
1177 }
1179 for(int y = 0; y < ph; y++)
1180 {
1181 const float sy = (y + 0.5f) * inv - 0.5f;
1182 for(int x = 0; x < pw; x++)
1183 {
1184 const size_t i = (size_t)y * pw + x;
1185 for(int c = 0; c < 3; c++)
1186 if(nn_in[(size_t)(1 + c) * plane + i] > 0.0f)
1187 {
1188 denoised[i] += _bilerp_tap(scratch + (size_t)c * p0, cw0, ch0, (x + 0.5f) * inv - 0.5f, sy);
1189 break;
1190 }
1191 }
1192 }
1193}
1194
1195static int _apply_low_band_anchor(const float *const nn_in, float *const denoised, const int pw, const int ph,
1196 const int scale)
1197{
1198 if(scale <= 0) return 0;
1199 // The pyramid is 16/32/64, fixed by the training reference. The padded tile
1200 // divides by 64 because dt_nn_model_alignment() folds DT_NN_FUSION_COARSEST
1201 // in for any model that declares an anchor; deriving the number of levels
1202 // from the tile size instead is what used to make the render depend on how
1203 // the pipe happened to tile (i.e. on the machine, and on CPU vs GPU).
1204 const int S = DT_NN_FUSION_COARSEST;
1205 if(pw % S || ph % S) return 0;
1206 const int cw0 = pw / 16, ch0 = ph / 16; // level-0 grid (scale 16)
1207 const size_t p0 = (size_t)cw0 * ch0;
1208 // slots: M/D/V per level (16, 32, 64) + fused ping-pong + upsample scratch
1209 float *const buf = dt_pixelpipe_cache_alloc_align_float_cache(p0 * 3 * (3 * 3 + 3), 0);
1210 if(IS_NULL_PTR(buf)) return 1;
1211 float *lv[9]; // lv[3 * level + {0: mosaic, 1: denoised, 2: sigma^2}]
1212 for(int k = 0; k < 9; k++) lv[k] = buf + (size_t)k * p0 * 3;
1213 float *fusedA = buf + p0 * 3 * 9, *fusedB = fusedA + p0 * 3, *ups = fusedB + p0 * 3;
1214
1215 _k_bin16_mdv(nn_in, denoised, lv[0], lv[1], lv[2], pw, ph);
1216 const int nlev = _fusion_levels();
1217 int w_l = cw0, h_l = ch0;
1218 for(int k = 1; k < nlev; k++)
1219 {
1220 for(int md = 0; md < 3; md++) _k_avg2x2(lv[3 * (k - 1) + md], lv[3 * k + md], w_l, h_l, p0);
1221 w_l /= 2;
1222 h_l /= 2;
1223 }
1224
1225 // Coarse-to-fine fusion with LOCAL weights (mirrors cfa.fuse_low_bands).
1226 // Two distinct gates (see the training repo for the full rationale):
1227 // - floor band: anchor to the measurement wherever the MEASUREMENT itself
1228 // is smooth at this scale (local variance vs noise); the model-vs-
1229 // measurement discrepancy cannot distinguish a real edge from the model
1230 // drifting on flat content;
1231 // - soft bands: per-cell Wiener on the band discrepancy with a chi^2
1232 // guard (T = 2.5, ~9 effective samples per 3x3 cell neighbourhood).
1233 int sw = w_l, sh = h_l;
1234 // FLOOR: structure-gated blend (mirrors cfa.fuse_low_bands) — the
1235 // measurement owns every cell where its own mean-removed local energy is
1236 // noise-sized (the dilution guarantee), the model owns structured cells
1237 // (a box average across an edge mixes both sides -> saturated outline).
1238 // REQUIRES a model trained with the DC-ownership loss: models from the
1239 // older fused loss drift in deep shadows and must not be used with this
1240 // code.
1241 _k_floor_fuse(lv[3 * (nlev - 1)], lv[3 * (nlev - 1) + 1], lv[3 * (nlev - 1) + 2], fusedA, sw, sh, p0,
1242 DT_NN_FUSION_FINEST << (nlev - 1));
1243 for(int k = nlev - 2; k >= 0; k--)
1244 {
1245 _k_fuse_step(fusedA, lv[3 * k], lv[3 * k + 1], lv[3 * k + 2], lv[3 * (k + 1)], lv[3 * (k + 1) + 1], fusedB,
1246 ups, sw, sh, p0, DT_NN_FUSION_FINEST << k);
1247 float *tmp = fusedA;
1248 fusedA = fusedB;
1249 fusedB = tmp;
1250 sw *= 2;
1251 sh *= 2;
1252 }
1253
1256 return 0;
1257}
1258
1260int process(struct dt_iop_module_t *self, const dt_dev_pixelpipe_t *pipe, const dt_dev_pixelpipe_iop_t *piece,
1261 const void *const ivoid, void *const ovoid)
1262{
1263 const dt_iop_roi_t *const roi = &piece->roi_in;
1265 dt_nn_model_t *const model = d->model;
1266
1267 const int width = roi->width, height = roi->height;
1268 const float *const in = (const float *)ivoid;
1269 float *const out = (float *)ovoid;
1270
1271 if(!model || !(d->strength > 0.0f))
1272 {
1274 return 0;
1275 }
1276
1277 /* The Bayer CFA lookup below is tile-local (FC() has no ROI awareness), so it
1278 * needs the word already rotated to this ROI's phase — see the CFA-phase rule
1279 * in CLAUDE.md. The X-Trans branch stays self-correcting: it takes the raw
1280 * table plus `roi` and adds the offset itself, and dt_dev_get_roi_filters()
1281 * no-ops on X-Trans, so `filters == 9u` still identifies it. */
1282 const uint32_t filters = dt_dev_get_roi_filters(piece, roi);
1283 const uint8_t(*const xtrans)[6] = (const uint8_t(*const)[6])piece->dsc_in.xtrans;
1284
1285 // reflect-pad to the network alignment
1286 const int align = dt_nn_model_alignment(model);
1287 const int pw = (width + align - 1) / align * align;
1288 const int ph = (height + align - 1) / align * align;
1289 const size_t plane = (size_t)pw * ph;
1290
1291 /* The tile's whole working set — input planes, output plane, executor
1292 * scratch — is ONE arena reservation, made before any compute, sized by the
1293 * executor's exact ledger plus the sub-allocator's fragmentation margin.
1294 * This is the same quantity tiling_callback declares, so the plan, the
1295 * reservation and the execution are one number. */
1296 const int in_ch = dt_nn_model_in_channels(model);
1297 nn_region_t region = { 0 };
1298 region.size = ((plane * (in_ch + 1) * sizeof(float) + 63) & ~(size_t)63)
1299 + (size_t)(NN_REGION_SLACK * (float)dt_nn_unet_scratch_bytes(model, pw, ph));
1300 region.size = (region.size + 63) & ~(size_t)63;
1302 if(IS_NULL_PTR(region.base))
1303 {
1304 dt_print(DT_DEBUG_ALWAYS, "[rawdenoiseai] region alloc failed for %dx%d tile (%.1f MB)\n", pw, ph,
1305 region.size / 1048576.0);
1307 return 1;
1308 }
1309 _nn_region = &region;
1310 float *const nn_in = _region_alloc(&region, plane * in_ch * sizeof(float), 0);
1311 float *const nn_out = _region_alloc(&region, plane * sizeof(float), 0);
1312 // by construction these cannot fail: the region was sized for them
1313
1314 _k_assemble(in, nn_in, width, height, pw, ph, d, filters, xtrans, roi);
1315
1316 int rc = 0;
1317 const int bin = dt_nn_model_bin(model, filters == 9u);
1318 if(bin > 1)
1319 {
1320 // coarse (low-frequency chroma) pass: denoise the superpixel-binned RGB
1321 // and inject the nearest-upsampled result as guide planes 5-7 of the
1322 // fine network's input (mirrors ms_forward() in ansel-denoise train.py)
1323 const int cw = pw / bin, chh = ph / bin;
1324 const size_t cplane = (size_t)cw * chh;
1329 rc = 1;
1330 else
1331 {
1334 // the coarse head predicts the correction to the binned RGB planes
1336 if(!rc) dt_nn_upsample_nearest(coarse_out, 3, cw, chh, bin, nn_in + plane * 5);
1337 }
1341 }
1342
1343 // 3. fine pass writes the RAW noise prediction; the residual is a kernel of
1344 // ours, sequenced exactly as process_cl() sequences nn_residual
1345 if(!rc) rc = dt_nn_unet_apply_stage(model, 0, nn_in, nn_out, pw, ph, 0);
1346 if(!rc) _k_residual(nn_in, nn_out, nn_out, plane);
1348 if(rc)
1349 {
1350 dt_print(DT_DEBUG_ALWAYS, "[rawdenoiseai] inference failed (%d) on %dx%d tile, scratch %.1f MB\n", rc, pw, ph,
1351 dt_nn_unet_scratch_bytes(model, pw, ph) / 1048576.0);
1353 }
1354 else
1355 {
1356 _k_blend_crop(in, nn_out, out, width, height, pw, d->strength);
1357 }
1358
1359 _nn_region = NULL;
1361 return rc;
1362}
1363
1364#ifdef HAVE_OPENCL
1365/* GPU path. Step for step the same procedure as process(), each _k_* function
1366 * there having the kernel of the same name here; the whole tile runs
1367 * dev_in -> dev_out with no mid-tile host round-trip, since command-queue syncs
1368 * dominate GPU cost. Returns FALSE on any failure so the pipeline falls back
1369 * to CPU. */
1370int process_cl(struct dt_iop_module_t *self, const dt_dev_pixelpipe_t *pipe, const dt_dev_pixelpipe_iop_t *piece,
1371 cl_mem dev_in, cl_mem dev_out)
1372{
1373 const dt_iop_roi_t *const roi = &piece->roi_in;
1376 dt_nn_model_t *const model = d->model;
1377 const int devid = pipe->devid;
1378
1379 if(!model || !gd || !gd->nn_cl || !(d->strength > 0.0f)) return FALSE;
1380
1381 const int width = roi->width, height = roi->height;
1382 // pre-shifted for the kernel's tile-local Bayer branch, exactly as in process()
1383 const uint32_t filters = dt_dev_get_roi_filters(piece, roi);
1384 const int is_xtrans = filters == 9u;
1385
1386 const int align = dt_nn_model_alignment(model);
1387 const int pw = (width + align - 1) / align * align;
1388 const int ph = (height + align - 1) / align * align;
1389 const size_t plane = (size_t)pw * ph;
1390 const int in_ch = dt_nn_model_in_channels(model);
1391 const int bin = dt_nn_model_bin(model, is_xtrans);
1392 const int cw = pw / bin, chh = ph / bin;
1393 const size_t cplane = (size_t)cw * chh;
1394 const int anchor = dt_nn_model_anchor(model);
1395 const int gw = pw / 16, gh = ph / 16;
1396 const size_t p16 = (size_t)gw * gh;
1397
1398 float scale[3];
1399 _sigma_scale(d, scale);
1400
1401 /* The whole tile runs on the device: assembly, binning, both network
1402 * stages, residuals, low-band fusion and the final blend are enqueued as
1403 * one chain with no mid-tile readback — command-queue syncs dominate GPU
1404 * cost, not arithmetic. The CPU path (process()) is the bit-parity
1405 * reference for every step. */
1406 gboolean success = FALSE;
1407 int err = CL_SUCCESS;
1408 cl_mem dev_planes = dt_opencl_alloc_device_buffer(devid, plane * in_ch * sizeof(float));
1409 cl_mem dev_noise = dt_opencl_alloc_device_buffer(devid, plane * sizeof(float));
1410 cl_mem dev_den = dt_opencl_alloc_device_buffer(devid, plane * sizeof(float));
1411 cl_mem dev_xtrans = dt_opencl_alloc_device_buffer(devid, 36);
1412 /* The pipeline hands modules their I/O as image2d objects (CL_R float here),
1413 * but every kernel in this chain addresses plain float buffers — passing an
1414 * image where a buffer is declared makes clSetKernelArg fail with
1415 * CL_INVALID_MEM_OBJECT and the enqueue with CL_INVALID_KERNEL_ARGS. Convert
1416 * at the endpoints only: one device-side image->buffer copy of the input, one
1417 * buffer->image copy of the blended result. The interior chain is untouched
1418 * and stays device-resident. */
1419 cl_mem dev_in_buf = dt_opencl_alloc_device_buffer(devid, (size_t)width * height * sizeof(float));
1420 cl_mem dev_out_buf = dt_opencl_alloc_device_buffer(devid, (size_t)width * height * sizeof(float));
1421 cl_mem dev_cin = NULL, dev_chead = NULL, dev_cden = NULL;
1422 // M/D/V per level (16, 32, 64) then the fused ping-pong pair
1423 cl_mem grids[11] = { NULL };
1424 if(!dev_planes || !dev_noise || !dev_den || !dev_xtrans || !dev_in_buf || !dev_out_buf) goto cleanup;
1425 {
1426 size_t origin[3] = { 0, 0, 0 };
1427 size_t region[3] = { (size_t)width, (size_t)height, 1 };
1429 if(err != CL_SUCCESS) goto cleanup;
1430 }
1431 if(bin > 1)
1432 {
1433 dev_cin = dt_opencl_alloc_device_buffer(devid, cplane * 6 * sizeof(float));
1434 dev_chead = dt_opencl_alloc_device_buffer(devid, cplane * 3 * sizeof(float));
1435 dev_cden = dt_opencl_alloc_device_buffer(devid, cplane * 3 * sizeof(float));
1436 if(!dev_cin || !dev_chead || !dev_cden) goto cleanup;
1437 }
1438 // same gate as the CPU path's _apply_low_band_anchor(), so both devices fuse
1439 // or skip together — the alignment guarantees the modulo holds
1440 const int do_anchor = anchor > 0 && pw % DT_NN_FUSION_COARSEST == 0 && ph % DT_NN_FUSION_COARSEST == 0;
1441 if(do_anchor)
1442 {
1443 for(int k = 0; k < 11; k++)
1444 {
1445 grids[k] = dt_opencl_alloc_device_buffer(devid, p16 * 3 * sizeof(float));
1446 if(!grids[k]) goto cleanup;
1447 }
1448 }
1449
1450 {
1451 unsigned char xtrans_host[36]; // staging copy: the CL API takes a non-const pointer
1452 memcpy(xtrans_host, piece->dsc_in.xtrans, sizeof(xtrans_host));
1454 goto cleanup;
1455 }
1456
1457 // 1. assemble the base planes (reflect pad + one-hot + sigma)
1458 {
1459 const int K = gd->k_assemble;
1460 const unsigned int f = filters;
1461 dt_opencl_set_kernel_arg(devid, K, 0, sizeof(cl_mem), &dev_in_buf);
1462 dt_opencl_set_kernel_arg(devid, K, 1, sizeof(cl_mem), &dev_planes);
1463 dt_opencl_set_kernel_arg(devid, K, 2, sizeof(int), &width);
1464 dt_opencl_set_kernel_arg(devid, K, 3, sizeof(int), &height);
1465 dt_opencl_set_kernel_arg(devid, K, 4, sizeof(int), &pw);
1466 dt_opencl_set_kernel_arg(devid, K, 5, sizeof(int), &ph);
1467 dt_opencl_set_kernel_arg(devid, K, 6, sizeof(unsigned int), &f);
1468 dt_opencl_set_kernel_arg(devid, K, 7, sizeof(cl_mem), &dev_xtrans);
1469 dt_opencl_set_kernel_arg(devid, K, 8, sizeof(int), &is_xtrans);
1470 dt_opencl_set_kernel_arg(devid, K, 9, sizeof(int), &roi->x);
1471 dt_opencl_set_kernel_arg(devid, K, 10, sizeof(int), &roi->y);
1472 for(int c = 0; c < 3; c++) dt_opencl_set_kernel_arg(devid, K, 11 + c, sizeof(float), &d->a[c]);
1473 for(int c = 0; c < 3; c++) dt_opencl_set_kernel_arg(devid, K, 14 + c, sizeof(float), &d->b[c]);
1474 for(int c = 0; c < 3; c++) dt_opencl_set_kernel_arg(devid, K, 17 + c, sizeof(float), &scale[c]);
1475 size_t sizes[3] = { ROUNDUPDWD(pw, devid), ROUNDUPDHT(ph, devid), 1 };
1477 if(err != CL_SUCCESS) goto cleanup;
1478 }
1479
1480 // 2. coarse chroma pass (multiscale models)
1481 if(bin > 1)
1482 {
1483 const int K = gd->k_bin_planes;
1484 dt_opencl_set_kernel_arg(devid, K, 0, sizeof(cl_mem), &dev_planes);
1485 dt_opencl_set_kernel_arg(devid, K, 1, sizeof(cl_mem), &dev_cin);
1486 dt_opencl_set_kernel_arg(devid, K, 2, sizeof(int), &pw);
1487 dt_opencl_set_kernel_arg(devid, K, 3, sizeof(int), &ph);
1488 dt_opencl_set_kernel_arg(devid, K, 4, sizeof(int), &bin);
1489 for(int c = 0; c < 3; c++) dt_opencl_set_kernel_arg(devid, K, 5 + c, sizeof(float), &d->a[c]);
1490 for(int c = 0; c < 3; c++) dt_opencl_set_kernel_arg(devid, K, 8 + c, sizeof(float), &d->b[c]);
1491 for(int c = 0; c < 3; c++) dt_opencl_set_kernel_arg(devid, K, 11 + c, sizeof(float), &scale[c]);
1492 size_t sizes[3] = { ROUNDUPDWD(cw, devid), ROUNDUPDHT(chh * 3, devid), 1 };
1494 if(err != CL_SUCCESS) goto cleanup;
1495
1496 err = dt_nn_unet_apply_stage_cl(model, 1, gd->nn_cl, devid, dev_cin, dev_chead, cw, chh);
1497 if(err != CL_SUCCESS) goto cleanup;
1498 const int KR = gd->k_residual;
1499 const int n3 = (int)(cplane * 3);
1500 dt_opencl_set_kernel_arg(devid, KR, 0, sizeof(cl_mem), &dev_cin);
1501 dt_opencl_set_kernel_arg(devid, KR, 1, sizeof(cl_mem), &dev_chead);
1502 dt_opencl_set_kernel_arg(devid, KR, 2, sizeof(cl_mem), &dev_cden);
1503 dt_opencl_set_kernel_arg(devid, KR, 3, sizeof(int), &n3);
1504 size_t sz[3] = { ROUNDUPDWD(n3, devid), 1, 1 };
1506 if(err != CL_SUCCESS) goto cleanup;
1507 cl_mem guide_src = dev_cden;
1508 const int KU = gd->k_upsample_n;
1509 const int three = 3;
1510 const cl_long dst_off = (cl_long)plane * 5;
1511 dt_opencl_set_kernel_arg(devid, KU, 0, sizeof(cl_mem), &guide_src);
1512 dt_opencl_set_kernel_arg(devid, KU, 1, sizeof(cl_mem), &dev_planes);
1513 dt_opencl_set_kernel_arg(devid, KU, 2, sizeof(int), &cw);
1514 dt_opencl_set_kernel_arg(devid, KU, 3, sizeof(int), &chh);
1515 dt_opencl_set_kernel_arg(devid, KU, 4, sizeof(int), &bin);
1516 dt_opencl_set_kernel_arg(devid, KU, 5, sizeof(int), &three);
1517 dt_opencl_set_kernel_arg(devid, KU, 6, sizeof(cl_long), &dst_off);
1518 size_t sizes_u[3] = { ROUNDUPDWD(pw, devid), ROUNDUPDHT(ph * 3, devid), 1 };
1520 if(err != CL_SUCCESS) goto cleanup;
1521 }
1522
1523 // 3. fine pass (raw noise) + residual -> denoised plane
1524 err = dt_nn_unet_apply_stage_cl(model, 0, gd->nn_cl, devid, dev_planes, dev_noise, pw, ph);
1525 if(err != CL_SUCCESS)
1526 {
1527 dt_print(DT_DEBUG_OPENCL, "[rawdenoiseai] GPU inference failed on %dx%d tile, falling back to CPU\n", pw, ph);
1528 goto cleanup;
1529 }
1530 {
1531 const int KR = gd->k_residual;
1532 const int n1 = (int)plane;
1533 dt_opencl_set_kernel_arg(devid, KR, 0, sizeof(cl_mem), &dev_planes);
1534 dt_opencl_set_kernel_arg(devid, KR, 1, sizeof(cl_mem), &dev_noise);
1535 dt_opencl_set_kernel_arg(devid, KR, 2, sizeof(cl_mem), &dev_den);
1536 dt_opencl_set_kernel_arg(devid, KR, 3, sizeof(int), &n1);
1537 size_t sz[3] = { ROUNDUPDWD(n1, devid), 1, 1 };
1539 if(err != CL_SUCCESS) goto cleanup;
1540 }
1541
1542 // 4. hybrid low-band fusion, entirely on device
1543 if(do_anchor)
1544 {
1545 {
1546 const int K = gd->k_bin16_mdv;
1547 dt_opencl_set_kernel_arg(devid, K, 0, sizeof(cl_mem), &dev_planes);
1548 dt_opencl_set_kernel_arg(devid, K, 1, sizeof(cl_mem), &dev_den);
1549 dt_opencl_set_kernel_arg(devid, K, 2, sizeof(cl_mem), &grids[0]);
1550 dt_opencl_set_kernel_arg(devid, K, 3, sizeof(cl_mem), &grids[1]);
1551 dt_opencl_set_kernel_arg(devid, K, 4, sizeof(cl_mem), &grids[2]);
1552 dt_opencl_set_kernel_arg(devid, K, 5, sizeof(int), &pw);
1553 dt_opencl_set_kernel_arg(devid, K, 6, sizeof(int), &ph);
1554 size_t sizes[3] = { ROUNDUPDWD(gw, devid), ROUNDUPDHT(gh * 3, devid), 1 };
1556 if(err != CL_SUCCESS) goto cleanup;
1557 }
1558 const int nlev = _fusion_levels();
1559 int lw = gw, lh = gh;
1560 for(int k = 1; k < nlev; k++)
1561 {
1562 for(int md = 0; md < 3; md++)
1563 {
1564 const int K = gd->k_avg2x2;
1565 dt_opencl_set_kernel_arg(devid, K, 0, sizeof(cl_mem), &grids[3 * (k - 1) + md]);
1566 dt_opencl_set_kernel_arg(devid, K, 1, sizeof(cl_mem), &grids[3 * k + md]);
1567 dt_opencl_set_kernel_arg(devid, K, 2, sizeof(int), &lw);
1568 dt_opencl_set_kernel_arg(devid, K, 3, sizeof(int), &lh);
1569 size_t sizes[3] = { ROUNDUPDWD(lw / 2, devid), ROUNDUPDHT((lh / 2) * 3, devid), 1 };
1571 if(err != CL_SUCCESS) goto cleanup;
1572 }
1573 lw /= 2;
1574 lh /= 2;
1575 }
1576 // floor band: structure-gated blend (see the CPU comment)
1577 int fA = 9, fB = 10; // fused ping-pong, past the 3 x 3 level grids
1578 {
1579 const int K = gd->k_floor_fuse;
1580 const int S = 16 << (nlev - 1);
1581 dt_opencl_set_kernel_arg(devid, K, 0, sizeof(cl_mem), &grids[3 * (nlev - 1)]);
1582 dt_opencl_set_kernel_arg(devid, K, 1, sizeof(cl_mem), &grids[3 * (nlev - 1) + 1]);
1583 dt_opencl_set_kernel_arg(devid, K, 2, sizeof(cl_mem), &grids[fA]);
1584 dt_opencl_set_kernel_arg(devid, K, 3, sizeof(cl_mem), &grids[3 * (nlev - 1) + 2]);
1585 dt_opencl_set_kernel_arg(devid, K, 4, sizeof(int), &lw);
1586 dt_opencl_set_kernel_arg(devid, K, 5, sizeof(int), &lh);
1587 dt_opencl_set_kernel_arg(devid, K, 6, sizeof(int), &S);
1588 for(int c = 0; c < 3; c++) dt_opencl_set_kernel_arg(devid, K, 7 + c, sizeof(float), &DT_NN_FUSION_DENS[c]);
1589 size_t sizes[3] = { ROUNDUPDWD(lw, devid), ROUNDUPDHT(lh * 3, devid), 1 };
1591 if(err != CL_SUCCESS) goto cleanup;
1592 }
1593 for(int k = nlev - 2; k >= 0; k--)
1594 {
1595 const int fw = lw * 2, fh = lh * 2, sc = 16 << k;
1596 {
1597 const int K = gd->k_fuse_step;
1598 dt_opencl_set_kernel_arg(devid, K, 0, sizeof(cl_mem), &grids[fA]);
1599 dt_opencl_set_kernel_arg(devid, K, 1, sizeof(cl_mem), &grids[3 * k]);
1600 dt_opencl_set_kernel_arg(devid, K, 2, sizeof(cl_mem), &grids[3 * k + 1]);
1601 dt_opencl_set_kernel_arg(devid, K, 3, sizeof(cl_mem), &grids[3 * (k + 1)]);
1602 dt_opencl_set_kernel_arg(devid, K, 4, sizeof(cl_mem), &grids[3 * (k + 1) + 1]);
1603 dt_opencl_set_kernel_arg(devid, K, 5, sizeof(cl_mem), &grids[fB]);
1604 dt_opencl_set_kernel_arg(devid, K, 6, sizeof(cl_mem), &grids[3 * k + 2]);
1605 dt_opencl_set_kernel_arg(devid, K, 7, sizeof(int), &fw);
1606 dt_opencl_set_kernel_arg(devid, K, 8, sizeof(int), &fh);
1607 dt_opencl_set_kernel_arg(devid, K, 9, sizeof(int), &sc);
1608 for(int c = 0; c < 3; c++) dt_opencl_set_kernel_arg(devid, K, 10 + c, sizeof(float), &DT_NN_FUSION_DENS[c]);
1609 size_t sizes[3] = { ROUNDUPDWD(fw, devid), ROUNDUPDHT(fh * 3, devid), 1 };
1611 if(err != CL_SUCCESS) goto cleanup;
1612 }
1613 const int t = fA;
1614 fA = fB;
1615 fB = t;
1616 lw = fw;
1617 lh = fh;
1618 }
1619 {
1620 const int K = gd->k_bilerp_add;
1621 dt_opencl_set_kernel_arg(devid, K, 0, sizeof(cl_mem), &grids[fA]);
1622 dt_opencl_set_kernel_arg(devid, K, 1, sizeof(cl_mem), &grids[1]);
1623 dt_opencl_set_kernel_arg(devid, K, 2, sizeof(cl_mem), &dev_planes);
1624 dt_opencl_set_kernel_arg(devid, K, 3, sizeof(cl_mem), &dev_den);
1625 dt_opencl_set_kernel_arg(devid, K, 4, sizeof(int), &pw);
1626 dt_opencl_set_kernel_arg(devid, K, 5, sizeof(int), &ph);
1627 size_t sizes[3] = { ROUNDUPDWD(pw, devid), ROUNDUPDHT(ph, devid), 1 };
1629 if(err != CL_SUCCESS) goto cleanup;
1630 }
1631 }
1632
1633 // 5. strength blend + crop straight into the pipeline output
1634 {
1635 const int K = gd->k_blend_crop;
1636 dt_opencl_set_kernel_arg(devid, K, 0, sizeof(cl_mem), &dev_in_buf);
1637 dt_opencl_set_kernel_arg(devid, K, 1, sizeof(cl_mem), &dev_den);
1638 dt_opencl_set_kernel_arg(devid, K, 2, sizeof(cl_mem), &dev_out_buf);
1639 dt_opencl_set_kernel_arg(devid, K, 3, sizeof(int), &width);
1640 dt_opencl_set_kernel_arg(devid, K, 4, sizeof(int), &height);
1641 dt_opencl_set_kernel_arg(devid, K, 5, sizeof(int), &pw);
1642 dt_opencl_set_kernel_arg(devid, K, 6, sizeof(float), &d->strength);
1643 size_t sizes[3] = { ROUNDUPDWD(width, devid), ROUNDUPDHT(height, devid), 1 };
1645 if(err != CL_SUCCESS) goto cleanup;
1646 }
1647 {
1648 // hand the result back in the pipeline's format (see the entry copy above)
1649 size_t origin[3] = { 0, 0, 0 };
1650 size_t region[3] = { (size_t)width, (size_t)height, 1 };
1652 if(err != CL_SUCCESS) goto cleanup;
1653 }
1654 success = TRUE;
1655
1656cleanup:
1666 for(int k = 0; k < 11; k++)
1668 return success;
1669}
1670#endif // HAVE_OPENCL
1671
1676
1678{
1680 piece->data_size = sizeof(dt_iop_rawdenoiseai_data_t);
1681}
1682
1684{
1685 dt_free_align(piece->data);
1686 piece->data = NULL;
1687}
1688
1689/* The combo lists "(shipped model)" then whatever .anselnn files the config
1690 * dir holds. The VALUE written to params is the basename, never the index —
1691 * see the params comment. gui_update re-derives the index from the name and
1692 * appends an entry for a name that is no longer on disk, so an edit made with
1693 * a since-removed model still shows what it wants instead of silently
1694 * displaying the wrong one. */
1696{
1697 if(dt_gui_widgets_suppressed()) return;
1700 const int idx = dt_bauhaus_combobox_get(w);
1701 const char *txt = idx > 0 ? dt_bauhaus_combobox_get_text(w) : NULL;
1702 if(txt)
1703 g_strlcpy(p->custom_model, txt, sizeof(p->custom_model));
1704 else
1705 p->custom_model[0] = '\0';
1706 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
1707}
1708
1710{
1713 dt_bauhaus_combobox_clear(g->custom_model);
1714 dt_bauhaus_combobox_add(g->custom_model, _("(shipped model)"));
1715 GList *files = _list_custom_models();
1716 int sel = 0, i = 0;
1717 for(GList *l = files; l; l = g_list_next(l))
1718 {
1719 dt_bauhaus_combobox_add(g->custom_model, (const char *)l->data);
1720 i++;
1721 if(!g_strcmp0((const char *)l->data, p->custom_model)) sel = i;
1722 }
1723 // the edit names a file that is not there any more: keep it visible
1724 if(p->custom_model[0] && !sel)
1725 {
1726 dt_bauhaus_combobox_add(g->custom_model, p->custom_model);
1727 sel = i + 1;
1728 }
1729 g_list_free_full(files, g_free);
1730 dt_bauhaus_combobox_set(g->custom_model, sel);
1731 // the shipped-matrix combos are meaningless while a user model is selected
1732 const gboolean shipped = !p->custom_model[0];
1735 gtk_widget_set_sensitive(g->scale_variant, shipped);
1736}
1737
1739{
1743 gtk_stack_set_visible_child_name(GTK_STACK(self->gui->widget), self->hide_enable_button ? "unsupported" : "raw");
1744
1745 // rescan on every panel update: the user may have dropped a file in since
1747
1748 // two-line status: the selected model (warn if its weights are missing) and
1749 // the noise profile the sigma map will use
1750 const gboolean have_model = gd && (p->custom_model[0] ? _get_custom_model(gd, p->custom_model)
1751 : _get_model(gd, p->version, p->size, p->scale_variant));
1753 gchar *prof = profiles
1754 ? g_strdup_printf(_("noise profile: %s at ISO %d"), self->dev->image_storage.camera_makermodel,
1755 (int)self->dev->image_storage.exif_iso)
1756 : g_strdup(_("no noise profile for this camera — using the generic profile"));
1757 gchar *label = have_model
1758 ? g_strdup(prof)
1759 : g_strdup_printf(_("selected model (%s %s, %s) is not installed — module inactive\n%s"),
1761 _scale_tag[CLAMP(p->scale_variant, 0, DT_RAWDENOISEAI_NUM_SCALES - 1)],
1763 gtk_label_set_text(GTK_LABEL(g->profile_label), label);
1765 g_free(label);
1766 g_free(prof);
1768}
1769
1771{
1773
1775
1776 g->strength = dt_bauhaus_slider_from_params(self, "strength");
1777 dt_bauhaus_slider_set_digits(g->strength, 3);
1778 dt_bauhaus_slider_set_format(g->strength, "%");
1779 gtk_widget_set_tooltip_text(g->strength, _("opacity of the noise removal: blends between the original\n"
1780 "image (0%) and the fully denoised result (100%).\n"
1781 "lower it to keep some residual grain"));
1782
1783 g->version = dt_bauhaus_combobox_from_params(self, "version");
1784 gtk_widget_set_tooltip_text(g->version, _("neural model version. Older edits keep their original\n"
1785 "version so their result never changes across updates."));
1786
1787 g->size = dt_bauhaus_combobox_from_params(self, "size");
1788 gtk_widget_set_tooltip_text(g->size, _("network width. large: reference quality, practical on GPU\n"
1789 "(OpenCL) and the default there. half: ~4x faster.\n"
1790 "quarter: ~4x faster again, the default without OpenCL\n"
1791 "and the choice for weak hardware or near-realtime editing."));
1792
1793 g->scale_variant = dt_bauhaus_combobox_from_params(self, "scale_variant");
1794 gtk_widget_set_tooltip_text(g->scale_variant, _("single-scale: the fine full-resolution pass only — fast,\n"
1795 "no low-frequency chroma handling. multiscale: adds the\n"
1796 "coarse chroma pass and the low-band fusion — high quality,\n"
1797 "recommended for high ISO."));
1798
1800 dt_bauhaus_widget_set_label(g->custom_model, N_("custom model"));
1801 gtk_box_pack_start(GTK_BOX(box_raw), g->custom_model, TRUE, TRUE, 0);
1802 gtk_widget_set_tooltip_text(g->custom_model,
1803 _("use a neural model of your own instead of the shipped ones.\n"
1804 "drop a .anselnn file into your Ansel config directory and it\n"
1805 "appears here; the edit records the file NAME, so it keeps\n"
1806 "pointing at the same model as the folder changes."));
1807 g_signal_connect(G_OBJECT(g->custom_model), "value-changed", G_CALLBACK(_custom_model_callback), self);
1808
1809 gtk_box_pack_start(GTK_BOX(box_raw), dt_ui_section_label_new(_("noise profile correction")), FALSE, FALSE, 0);
1810
1811 g->profile_label = dt_ui_label_new("");
1812 gtk_label_set_line_wrap(GTK_LABEL(g->profile_label), TRUE);
1813 gtk_box_pack_start(GTK_BOX(box_raw), g->profile_label, FALSE, FALSE, 0);
1814
1815 g->noise_level = dt_bauhaus_slider_from_params(self, "noise_level");
1816 dt_bauhaus_slider_set_digits(g->noise_level, 3);
1817 dt_bauhaus_slider_set_format(g->noise_level, "%");
1818 gtk_widget_set_tooltip_text(g->noise_level, _("global scale on the assumed noise amplitude, relative to the\n"
1819 "calibrated noise for this camera at this ISO (100% trusts the\n"
1820 "calibration). raise it if noise remains, lower it if fine\n"
1821 "detail is eaten"));
1822
1823 dt_gui_new_collapsible_section(&g->cs, "plugins/darkroom/rawdenoiseai/expand_channel",
1824 _("per-channel corrections"), GTK_BOX(box_raw), GTK_PACK_START);
1825 self->gui->widget = GTK_WIDGET(g->cs.container); // sliders below pack into the section
1826
1827 const char *sigma_tooltip = _("per-channel correction of the camera noise profile, applied on top\n"
1828 "of the global correction. Profiles are measured after demosaicing,\n"
1829 "which averages away part of the noise — most on the dense green\n"
1830 "lattice — while this module sees the raw sensor noise at full\n"
1831 "strength. The defaults are calibrated against raw-mosaic\n"
1832 "measurements over 253 cameras.");
1833 g->sigma_red = dt_bauhaus_slider_from_params(self, "sigma_red");
1834 dt_bauhaus_slider_set_digits(g->sigma_red, 3);
1835 dt_bauhaus_slider_set_format(g->sigma_red, "%");
1837
1838 g->sigma_green = dt_bauhaus_slider_from_params(self, "sigma_green");
1839 dt_bauhaus_slider_set_digits(g->sigma_green, 3);
1840 dt_bauhaus_slider_set_format(g->sigma_green, "%");
1842
1843 g->sigma_blue = dt_bauhaus_slider_from_params(self, "sigma_blue");
1844 dt_bauhaus_slider_set_digits(g->sigma_blue, 3);
1845 dt_bauhaus_slider_set_format(g->sigma_blue, "%");
1847
1848 self->gui->widget = box_raw; // done packing into the collapsible section
1849
1850 GtkWidget *label_unsupported = dt_ui_label_new(_("AI raw denoising needs a mosaiced raw image\n"
1851 "and an installed rawdenoiseai model file."));
1852
1853 self->gui->widget = gtk_stack_new();
1855 gtk_stack_add_named(GTK_STACK(self->gui->widget), label_unsupported, "unsupported");
1857}
1858
1860{
1862}
1863// clang-format off
1864// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
1865// vim: shiftwidth=2 expandtab tabstop=2 cindent
1866// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
1867// clang-format on
static double * inv
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
void cleanup(dt_imageio_module_format_t *self)
Definition avif.c:170
#define m
Definition basecurve.c:282
void dt_bauhaus_slider_set_digits(GtkWidget *widget, int val)
Definition bauhaus.c:3338
void dt_bauhaus_combobox_clear(GtkWidget *widget)
Definition bauhaus.c:1992
int dt_bauhaus_combobox_get(GtkWidget *widget)
Definition bauhaus.c:2131
const char * dt_bauhaus_combobox_get_text(GtkWidget *widget)
Definition bauhaus.c:1965
void dt_bauhaus_combobox_set(GtkWidget *widget, const int pos)
Definition bauhaus.c:2085
void dt_bauhaus_widget_set_label(GtkWidget *widget, const char *label)
Definition bauhaus.c:1504
GtkWidget * dt_bauhaus_combobox_new(dt_bauhaus_t *bh, dt_gui_module_t *self)
Definition bauhaus.c:1693
void dt_bauhaus_slider_set_format(GtkWidget *widget, const char *format)
Definition bauhaus.c:3402
void dt_bauhaus_combobox_add(GtkWidget *widget, const char *text)
Definition bauhaus.c:1819
void dt_gui_new_collapsible_section(dt_gui_collapsible_section_t *cs, const char *confname, const char *label, GtkBox *parent, GtkPackType pack)
Create a collapsible section and pack it into the parent box.
void dt_gui_update_collapsible_section(dt_gui_collapsible_section_t *cs)
@ IOP_CS_RAW
static const float x
const float f
const int t
const float v
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
const dt_colormatrix_t dt_aligned_pixel_t out
const float top
static const dt_colormatrix_t M
static float strength(float value, float strength)
Definition colorzones.c:430
#define S(V, params)
gboolean dt_image_needs_demosaic(const dt_image_t *img)
struct dt_bauhaus_t * dt_bauhaus_get_global(void)
Definition darktable.c:688
static int FCxtrans(const int row, const int col, global const unsigned char(*const xtrans)[6])
static int FC(const int row, const int col, const unsigned int filters)
#define dt_dev_add_history_item(dev, module, enable, redraw)
void dt_iop_params_t
Definition dev_history.h:43
void default_input_format(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece, dt_iop_buffer_dsc_t *dsc)
static int dt_pthread_mutex_unlock(dt_pthread_mutex_t *mutex) RELEASE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:385
static int dt_pthread_mutex_init(dt_pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
Definition dtpthread.h:370
static int dt_pthread_mutex_destroy(dt_pthread_mutex_t *mutex)
Definition dtpthread.h:390
static int dt_pthread_mutex_lock(dt_pthread_mutex_t *mutex) ACQUIRE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:375
void dt_loc_get_datadir(char *datadir, size_t bufsize)
void dt_loc_get_user_config_dir(char *configdir, size_t bufsize)
#define DT_GUI_MODULE(x)
static void dt_iop_image_copy_by_size(float *const __restrict__ out, const float *const __restrict__ in, const size_t width, const size_t height, const size_t ch)
Definition imagebuf.h:91
uint32_t dt_dev_get_roi_filters(const dt_dev_pixelpipe_iop_t *const piece, const dt_iop_roi_t *const roi_in)
Definition imageop.c:139
void dt_iop_default_init(dt_iop_module_t *module)
Definition imageop.c:308
const char ** dt_iop_set_description(dt_iop_module_t *module, const char *main_text, const char *purpose, const char *input, const char *process, const char *output)
Definition imageop.c:1624
void dt_iop_request_focus(dt_iop_module_t *module)
@ IOP_FLAGS_SUPPORTS_BLENDING
Definition imageop.h:180
@ IOP_FLAGS_ALLOW_TILING
Definition imageop.h:182
@ IOP_GROUP_REPAIR
Definition imageop.h:153
GtkWidget * dt_bauhaus_slider_from_params(dt_iop_module_t *self, const char *param)
GtkWidget * dt_bauhaus_combobox_from_params(dt_iop_module_t *self, const char *param)
#define IOP_GUI_FREE
Definition imageop_gui.h:96
static dt_iop_gui_data_t * dt_iop_gui_data(const struct dt_iop_module_t *m)
The module's GUI data blob, NULL-safe for headless callers: IOP process() implementations read it for...
Definition imageop_gui.h:81
#define IOP_GUI_ALLOC(module)
Definition imageop_gui.h:93
void *const ovoid
const char * model
GtkWidget * dt_ui_section_label_new(const gchar *str)
Definition label.c:101
GtkWidget * dt_ui_label_new(const gchar *str)
Definition label.c:112
#define w2
Definition lmmse.c:60
@ DT_DEBUG_OPENCL
Definition logging.h:43
@ DT_DEBUG_PARAMS
Definition logging.h:57
@ DT_DEBUG_ALWAYS
Definition logging.h:34
void dt_print(dt_debug_thread_t thread, const char *msg,...) __attribute__((format(printf
float *const restrict const size_t k
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:65
#define dt_free_align(ptr)
Definition mem_alloc.h:122
static void * dt_calloc_align(size_t size)
Definition mem_alloc.h:129
uint32_t width
Definition mipmap_cache.c:0
uint32_t height
Definition mipmap_cache.c:1
#define DT_MODULE_INTROSPECTION(MODVER, PARAMSTYPE)
static float gh(const float f)
void dt_nn_cl_destroy(dt_nn_cl_t *cl)
Definition nn_model.c:1098
int dt_nn_unet_apply_stage_cl(const dt_nn_model_t *m, int stage, dt_nn_cl_t *cl, int devid, cl_mem dev_in, cl_mem dev_out, int width, int height)
Definition nn_model.c:1338
int dt_nn_model_anchor(const dt_nn_model_t *m)
Definition nn_model.c:442
void dt_nn_model_free(dt_nn_model_t *m)
Definition nn_model.c:405
int dt_nn_model_in_channels(const dt_nn_model_t *m)
Definition nn_model.c:416
int dt_nn_model_coarse_in_channels(const dt_nn_model_t *m)
Definition nn_model.c:432
dt_nn_cl_t * dt_nn_cl_create(int program)
Definition nn_model.c:1088
size_t dt_nn_unet_scratch_bytes(const dt_nn_model_t *m, int width, int height)
Definition nn_model.c:736
int dt_nn_unet_apply_stage(const dt_nn_model_t *m, int stage, const float *in, float *out, int width, int height, int apply_residual)
Definition nn_model.c:1009
float dt_nn_unet_scratch_per_px(const dt_nn_model_t *m)
Definition nn_model.c:707
dt_nn_model_t * dt_nn_model_load(const char *path, char *err, size_t err_len)
Definition nn_model.c:237
__DT_CLONE_TARGETS__ void dt_nn_upsample_nearest(const float *in, int ch, int w, int h, int factor, float *out)
Definition nn_model.c:1058
int dt_nn_model_coarse_out_channels(const dt_nn_model_t *m)
Definition nn_model.c:437
float dt_nn_unet_scratch_per_px_cl(const dt_nn_model_t *m)
Definition nn_model.c:712
int dt_nn_model_alignment(const dt_nn_model_t *m)
Definition nn_model.c:460
__DT_CLONE_TARGETS__ void dt_nn_bin_planes(const float *planes, int pw, int ph, int bin, float *out_rgb, float *out_cnt)
Definition nn_model.c:1022
int dt_nn_model_bin(const dt_nn_model_t *m, const int is_xtrans)
Definition nn_model.c:426
void dt_nn_set_allocator(dt_nn_alloc_f alloc_fn, dt_nn_free_f free_fn)
Definition nn_model.c:77
#define DT_NN_FUSION_COARSEST
Definition nn_model.h:98
#define DT_NN_FUSION_FINEST
Definition nn_model.h:97
const dt_noiseprofile_t dt_noiseprofile_generic
void dt_noiseprofile_interpolate(const dt_noiseprofile_t *const p1, const dt_noiseprofile_t *const p2, dt_noiseprofile_t *out)
void dt_noiseprofile_free(gpointer data)
GList * dt_noiseprofile_get_matching(const dt_image_t *cimg)
int dt_opencl_enqueue_kernel_2d(const int dev, const int kernel, const size_t *sizes)
Definition opencl.c:2276
void * dt_opencl_alloc_device_buffer(const int devid, const size_t size)
Definition opencl.c:2692
int dt_opencl_enqueue_copy_buffer_to_image(const int devid, cl_mem src_buffer, cl_mem dst_image, size_t offset, size_t *origin, size_t *region)
Definition opencl.c:2424
int dt_opencl_create_kernel(const int prog, const char *name)
Definition opencl.c:2170
int dt_opencl_write_buffer_to_device(const int devid, void *host, void *device, const size_t offset, const size_t size, const int blocking)
Definition opencl.c:2460
int dt_opencl_is_enabled(void)
Definition opencl.c:2947
void dt_opencl_free_kernel(const int kernel)
Definition opencl.c:2213
int dt_opencl_set_kernel_arg(const int dev, const int kernel, const int num, const size_t size, const void *arg)
Definition opencl.c:2267
int dt_opencl_enqueue_copy_image_to_buffer(const int devid, cl_mem src_image, cl_mem dst_buffer, size_t *origin, size_t *region, size_t offset)
Definition opencl.c:2412
void dt_opencl_release_mem_object(cl_mem mem)
Definition opencl.c:2527
#define ROUNDUPDHT(a, b)
Definition opencl.h:83
#define ROUNDUPDWD(a, b)
Definition opencl.h:82
#define __OMP_PARALLEL_FOR__(...)
Definition openmp.h:60
#define PATH_MAX
Definition paths.h:45
void dt_iop_buffer_dsc_update_bpp(dt_iop_buffer_dsc_t *dsc)
#define dt_pixelpipe_cache_alloc_align_float_cache(pixels, id)
#define dt_pixelpipe_cache_alloc_align_cache(size, id)
#define dt_pixelpipe_cache_free_align(mem)
static dt_nn_model_t * _get_custom_model(dt_iop_rawdenoiseai_global_data_t *gd, const char *base)
void init(dt_iop_module_t *module)
const char ** description(struct dt_iop_module_t *self)
int default_group()
static const char *const _scale_tag[2]
__DT_CLONE_TARGETS__ int process(struct dt_iop_module_t *self, const dt_dev_pixelpipe_t *pipe, const dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid)
static void _region_free(nn_region_t *r, void *p)
#define DT_NN_FUSION_T_CHI2
static __DT_CLONE_TARGETS__ void _k_bin16_mdv(const float *const nn_in, const float *const denoised, float *const M, float *const D, float *const V, const int pw, const int ph)
void reload_defaults(dt_iop_module_t *module)
static int _apply_low_band_anchor(const float *const nn_in, float *const denoised, const int pw, const int ph, const int scale)
static void _sigma_scale(const dt_iop_rawdenoiseai_data_t *d, float scale[3])
void commit_params(struct dt_iop_module_t *self, dt_iop_params_t *params, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
#define NN_REGION_MAX_BLOCKS
dt_iop_rawdenoiseai_scale_t
@ DT_RAWDENOISEAI_MULTI
@ DT_RAWDENOISEAI_SINGLE
void gui_update(dt_iop_module_t *self)
Refresh GUI controls from current params and configuration.
#define NN_REGION_SLACK
static __DT_CLONE_TARGETS__ void _upsample_bilinear(const float *const src, const int sw, const int sh, const int f, float *const dst)
static void _nn_arena_free(void *p)
static __DT_CLONE_TARGETS__ void _k_residual(const float *const in, const float *const head, float *const out, const size_t n)
void init_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
static gboolean _rawdenoiseai_supported(dt_iop_module_t *module)
static GList * _list_custom_models(void)
const char * name()
void gui_init(dt_iop_module_t *self)
static void _custom_model_populate(dt_iop_module_t *self)
static __DT_CLONE_TARGETS__ void _k_bin_planes(const float *const nn_in, float *const coarse_in, float *const cnt, const int pw, const int ph, const int bin, const dt_iop_rawdenoiseai_data_t *const d)
#define DT_RAWDENOISEAI_MODEL_LEN
static __DT_CLONE_TARGETS__ void _k_assemble(const float *const in, float *const nn_in, const int width, const int height, const int pw, const int ph, const dt_iop_rawdenoiseai_data_t *const d, const uint32_t filters, const uint8_t(*const xtrans)[6], const dt_iop_roi_t *const roi)
static __thread nn_region_t * _nn_region
void tiling_callback(struct dt_iop_module_t *self, const struct dt_dev_pixelpipe_t *pipe, const struct dt_dev_pixelpipe_iop_t *piece, struct dt_develop_tiling_t *tiling)
static __DT_CLONE_TARGETS__ void _k_bilerp_add(const float *const fused, const float *const D16, float *const scratch, const float *const nn_in, float *const denoised, const int pw, const int ph, const size_t p0, const int cw0, const int ch0)
void gui_cleanup(dt_iop_module_t *self)
static void _fetch_noise_profile(dt_iop_module_t *self, dt_iop_rawdenoiseai_data_t *d)
void cleanup_global(dt_iop_module_so_t *module)
__DT_CLONE_TARGETS__ static __DT_CLONE_TARGETS__ void _k_floor_fuse(const float *const M, const float *const D, const float *const V, float *const fused, const int sw, const int sh, const size_t p0, const int S)
static void _custom_model_callback(GtkWidget *w, dt_iop_module_t *self)
int default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, const dt_dev_pixelpipe_iop_t *piece)
void input_format(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece, dt_iop_buffer_dsc_t *dsc)
static const char *const _version_tag[1]
int flags()
dt_iop_rawdenoiseai_size_t
@ DT_RAWDENOISEAI_LARGE
@ DT_RAWDENOISEAI_QUARTER
@ DT_RAWDENOISEAI_HALF
static const char *const _size_tag[3]
static void * _nn_arena_alloc(size_t bytes, int long_lived)
gboolean force_enable(struct dt_iop_module_t *self, const gboolean current_state)
void cleanup_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
static unsigned _align_lcm(const unsigned a, const unsigned b)
static void * _region_alloc(nn_region_t *r, size_t bytes, int long_lived)
#define DT_RAWDENOISEAI_NUM_SCALES
void init_global(dt_iop_module_so_t *module)
static __DT_CLONE_TARGETS__ void _k_avg2x2(const float *const in, float *const out, const int sw, const int sh, const size_t p0)
static __DT_CLONE_TARGETS__ void _k_blend_crop(const float *const in, const float *const den, float *const out, const int width, const int height, const int pw, const float strength)
dt_iop_rawdenoiseai_version_t
@ DT_RAWDENOISEAI_V1
int process_cl(struct dt_iop_module_t *self, const dt_dev_pixelpipe_t *pipe, const dt_dev_pixelpipe_iop_t *piece, cl_mem dev_in, cl_mem dev_out)
#define DT_RAWDENOISEAI_NUM_SIZES
static dt_nn_model_t * _get_model(dt_iop_rawdenoiseai_global_data_t *gd, dt_iop_rawdenoiseai_version_t ver, dt_iop_rawdenoiseai_size_t sz, dt_iop_rawdenoiseai_scale_t sc)
static int _fusion_levels(void)
static const float DT_NN_FUSION_DENS[3]
static __DT_CLONE_TARGETS__ void _k_fuse_step(const float *const fused_c, const float *const Mf, const float *const Df, const float *const Vf, const float *const Mc, const float *const Dc, float *const fused_f, float *const ups, const int sw, const int sh, const size_t p0, const int sc)
#define DT_RAWDENOISEAI_NUM_VERSIONS
static dt_iop_rawdenoiseai_size_t _default_size(void)
#define lw
Definition retouch.c:1070
float dt_aligned_pixel_simd_t __attribute__((vector_size(16), aligned(16)))
Apply one channel's tone curve to each of the three colour channels, or pass the channel through unto...
Definition simd.h:55
const float sigma
const float r
dt_iop_buffer_dsc_t dsc_in
struct dt_iop_module_t *void * data
dt_image_t image_storage
Definition develop.h:226
char camera_makermodel[128]
Definition image.h:358
float exif_iso
Definition image.h:346
uint32_t filters
Definition format.h:89
unsigned int channels
Definition format.h:83
uint8_t xtrans[6][6]
Definition format.h:99
GtkWidget * widget
Definition imageop_gui.h:47
int32_t hide_enable_button
Definition imageop.h:261
struct dt_iop_module_gui_t * gui
Definition imageop.h:326
struct dt_develop_t * dev
Definition imageop.h:302
dt_iop_global_data_t * global_data
Definition imageop.h:317
dt_iop_params_t * params
Definition imageop.h:313
dt_nn_model_t * models[1][3][2]
dt_gui_collapsible_section_t cs
dt_iop_rawdenoiseai_version_t version
dt_iop_rawdenoiseai_size_t size
dt_iop_rawdenoiseai_scale_t scale_variant
Region of interest passed through the pixelpipe.
Definition format.h:49
int width
Definition format.h:50
int height
Definition format.h:50
dt_aligned_pixel_t a
dt_aligned_pixel_t b
struct nn_region_t::@45 blocks[32]
#define __DT_CLONE_TARGETS__
typedef double((*spd)(unsigned long int wavelength, double TempK))
#define MAX(a, b)
Definition thinplate.c:29
gboolean dt_gui_widgets_suppressed(void)
#define DT_GUI_BOX_SPACING