From 9e4f5c997c2e0344f6ae3c46d6c38abd3c939eb2 Mon Sep 17 00:00:00 2001 From: Andrii Ryzhkov Date: Sat, 27 Jun 2026 14:32:11 +0300 Subject: [PATCH 1/2] Fix neural restore rejecting sRaw images as unsupported --- src/common/ai/restore.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/ai/restore.c b/src/common/ai/restore.c index c5e850059be..8f8c907f044 100644 --- a/src/common/ai/restore.c +++ b/src/common/ai/restore.c @@ -427,7 +427,7 @@ dt_restore_context_t *dt_restore_load_denoise(dt_restore_env_t *env) dt_restore_sensor_class_t dt_restore_classify_sensor(const dt_image_t *img) { - if(!img || !(img->flags & DT_IMAGE_RAW)) + if(!img || !(img->flags & (DT_IMAGE_RAW | DT_IMAGE_S_RAW))) return DT_RESTORE_SENSOR_CLASS_UNSUPPORTED; if(img->flags & (DT_IMAGE_MONOCHROME | DT_IMAGE_MONOCHROME_BAYER)) return DT_RESTORE_SENSOR_CLASS_UNSUPPORTED; From cb64285351066ab37f62e5ea3235d970ea77d530 Mon Sep 17 00:00:00 2001 From: Andrii Ryzhkov Date: Sat, 27 Jun 2026 16:20:31 +0300 Subject: [PATCH 2/2] Fix neural restore preview crash and white cast on LINEAR images --- src/common/ai/restore_raw_linear.c | 65 +++++++++++++++++++---------- src/libs/neural_restore.c | 67 +++++++++++++++++++++++------- 2 files changed, 95 insertions(+), 37 deletions(-) diff --git a/src/common/ai/restore_raw_linear.c b/src/common/ai/restore_raw_linear.c index 1ef4441b593..5d19c5bf40c 100644 --- a/src/common/ai/restore_raw_linear.c +++ b/src/common/ai/restore_raw_linear.c @@ -1061,19 +1061,20 @@ int dt_restore_raw_linear_preview_piped(dt_restore_context_t *ctx, g_free(tile_out); return 1; } - const size_t pixel_sz = is_uint16 ? 2 : 4; + // classify (not filters==0) so 4BAYER routes as LINEAR like upstream + const uint32_t mip_channels = img->buf_dsc.channels; + const gboolean is_linear = + (dt_restore_classify_sensor(img) == DT_RESTORE_SENSOR_CLASS_LINEAR); + const size_t pixel_sz = mip_channels * (is_uint16 ? 2 : 4); const size_t total_bytes = (size_t)raw_w * raw_h * pixel_sz; - // rawprepare's normalisation: pipe will do (value - sub) / div where - // sub is per-CFA-site black level and div is (white - black). to get - // back to raw ADC space we compute per-site (value * range[idx]) + black[idx] - // NOTE: raw_black_level_separate is indexed by CFA position k in 0..3 - // (even if X-Trans has 6 colours, darktable's per-sensel black is 4- - // entry; typical cameras use one value for all positions anyway) + // black[]/range[] are CFA-parity indexed; LINEAR uses scalar pair float black[4], range[4], white; _compute_cfa_black_range(img, black, range, &white); + const float lin_black = (float)img->raw_black_level; + const float lin_range + = (white - lin_black > 0.0f) ? (white - lin_black) : 1.0f; - // build patched CFA: copy original, overwrite crop with re-mosaiced denoised void *patched = g_try_malloc(total_bytes); if(!patched) { @@ -1087,7 +1088,7 @@ int dt_restore_raw_linear_preview_piped(dt_restore_context_t *ctx, // buffer extent) rather than just the display crop. this gives the // pipe's geometry chain ~tile-size/2 pixels of slop on each side so // any residual coordinate drift falls inside denoised data instead of - // showing original CFA at the preview edge + // showing original at the preview edge const int patch_x0 = (inf_x < 0) ? 0 : inf_x; const int patch_y0 = (inf_y < 0) ? 0 : inf_y; const int patch_x1 = (inf_x + T > width) ? width : inf_x + T; @@ -1114,20 +1115,40 @@ int dt_restore_raw_linear_preview_piped(dt_restore_context_t *ctx, if(cam[c] < 0.0f) cam[c] = 0.0f; if(cam[c] > 1.0f) cam[c] = 1.0f; } - // re-mosaic: pick the single colour that the X-Trans pattern - // wants at this sensor position, scaled back to raw ADC range. - // FCxtrans uses raw-sensor parity (since xtrans[6][6] is aligned - // with the raw, not the post-crop buffer) - const int ch = FCxtrans(sr_raw, sc_raw, NULL, img->buf_dsc.xtrans); - const int bl_idx = ((sr_raw & 1) << 1) | (sc_raw & 1); - const float adc = cam[ch] * range[bl_idx] + black[bl_idx]; - const float clipped - = adc < 0.0f ? 0.0f : (adc > white ? white : adc); - const size_t idx = (size_t)sr_raw * raw_w + sc_raw; - if(is_uint16) - ((uint16_t *)patched)[idx] = (uint16_t)(clipped + 0.5f); + const size_t pix = (size_t)sr_raw * raw_w + sc_raw; + + if(is_linear) + { + // float mipmaps are already in [0, 1]; uint16 needs un-prepare + for(int c = 0; c < 3; c++) + { + const size_t off = pix * mip_channels + c; + if(is_uint16) + { + const float adc = cam[c] * lin_range + lin_black; + const float clipped + = adc < 0.0f ? 0.0f : (adc > white ? white : adc); + ((uint16_t *)patched)[off] = (uint16_t)(clipped + 0.5f); + } + else + { + ((float *)patched)[off] = cam[c]; + } + } + } else - ((float *)patched)[idx] = clipped; + { + // X-Trans: re-mosaic + const int ch = FCxtrans(sr_raw, sc_raw, NULL, img->buf_dsc.xtrans); + const int bl_idx = ((sr_raw & 1) << 1) | (sc_raw & 1); + const float adc = cam[ch] * range[bl_idx] + black[bl_idx]; + const float clipped + = adc < 0.0f ? 0.0f : (adc > white ? white : adc); + if(is_uint16) + ((uint16_t *)patched)[pix] = (uint16_t)(clipped + 0.5f); + else + ((float *)patched)[pix] = clipped; + } } } diff --git a/src/libs/neural_restore.c b/src/libs/neural_restore.c index 03432595113..1f622c9cbca 100644 --- a/src/libs/neural_restore.c +++ b/src/libs/neural_restore.c @@ -316,6 +316,9 @@ typedef struct dt_lib_neural_restore_t // X-Trans / linear) so re-picking a new crop on the same image skips // the slow load + demosaic; freed on imgid or sensor-type change. dt_imgid_t preview_raw_imgid; + // imgid we last showed the "pre-demosaiced" warning for, so the + // toast doesn't fire every patch-click move during preview + dt_imgid_t preview_linear_warned_imgid; dt_restore_sensor_class_t preview_raw_sensor_class; float *preview_full_cfa; // Bayer: full sensor (w*h floats) int preview_full_w; @@ -391,6 +394,10 @@ typedef struct dt_neural_job_t char *icc_filename; // only used when icc_type == DT_COLORSPACE_FILE // when TRUE, wide-gamut pixels pass through unchanged on denoise gboolean preserve_wide_gamut; + // raw denoise only: TRUE after we've toasted the "pre-demosaiced" + // caveat for any LINEAR image in this batch; avoids one warning + // per file when the user kicks off a multi-image job + gboolean linear_warned; } dt_neural_job_t; typedef struct dt_neural_format_params_t @@ -1356,6 +1363,16 @@ static int _process_raw_denoise_one(dt_neural_job_t *j, return _process_raw_denoise_linear(j, imgid, out_filename, src_path, &img_meta); case DT_RESTORE_SENSOR_CLASS_LINEAR: + // most LINEAR-class inputs are computational raws (Apple + // ProRAW, Google HDR+, etc.) that have already been denoised + // on-device; the model will run but adds little; Canon/Nikon + // sRAW is the exception; warn once per job and proceed + if(!j->linear_warned) + { + j->linear_warned = TRUE; + dt_control_log(_("raw denoise: this image is already pre-demosaiced " + "(sRaw / computational raw); results may be limited")); + } return _process_raw_denoise_linear(j, imgid, out_filename, src_path, &img_meta); default: @@ -2676,24 +2693,42 @@ static gpointer _preview_thread_raw(gpointer data) const uint32_t filters = img_meta.buf_dsc.filters; const dt_restore_sensor_class_t cls = dt_restore_classify_sensor(&img_meta); - const gboolean is_xtrans = (cls == DT_RESTORE_SENSOR_CLASS_XTRANS); - if(cls != DT_RESTORE_SENSOR_CLASS_BAYER - && cls != DT_RESTORE_SENSOR_CLASS_XTRANS) + // today x-trans piggybacks on the linear pipeline because we don't + // ship a dedicated x-trans denoise model yet. when one lands, drop + // XTRANS from use_linear and add an x-trans branch alongside the + // use_linear and bayer paths (buffer fetch, preview pipe, max_disp) + const gboolean use_linear = (cls == DT_RESTORE_SENSOR_CLASS_XTRANS + || cls == DT_RESTORE_SENSOR_CLASS_LINEAR); + if(cls != DT_RESTORE_SENSOR_CLASS_BAYER && !use_linear) { dt_print(DT_DEBUG_AI, - "[neural_restore] raw preview: imgid %d is not bayer/xtrans " + "[neural_restore] raw preview: imgid %d unsupported " "(filters=0x%x class=%d)", pd->imgid, filters, cls); bail_err = DT_NR_PREVIEW_ERR_UNSUPPORTED; goto cleanup; } + const char *cls_name = (cls == DT_RESTORE_SENSOR_CLASS_XTRANS) ? "x-trans" + : use_linear ? "linear" + : "bayer"; dt_print(DT_DEBUG_AI, "[neural_restore] raw preview: imgid=%d %s patch=(%.3f,%.3f) " "widget=%dx%d filters=0x%x", - pd->imgid, is_xtrans ? "x-trans" : "bayer", + pd->imgid, cls_name, pd->patch_center[0], pd->patch_center[1], pd->preview_w, pd->preview_h, filters); + // same caveat as the batch path: LINEAR is usually a computational + // raw that's already been denoised on-device. warn once per imgid + // so picking a different patch doesn't re-toast + if(cls == DT_RESTORE_SENSOR_CLASS_LINEAR + && d->preview_linear_warned_imgid != pd->imgid) + { + d->preview_linear_warned_imgid = pd->imgid; + dt_control_log(_("raw denoise: this image is already pre-demosaiced " + "(sRaw / computational raw); results may be limited")); + } + // 2. ensure the right ctx is loaded (matches batch logic in // _ensure_raw_ctx). reload if cached_task is wrong or if the // cached sensor class doesn't match this image. @@ -2715,6 +2750,9 @@ static gpointer _preview_thread_raw(gpointer data) case DT_RESTORE_SENSOR_CLASS_XTRANS: d->cached_ctx = dt_restore_load_rawdenoise_xtrans(pd->env); break; + case DT_RESTORE_SENSOR_CLASS_LINEAR: + d->cached_ctx = dt_restore_load_rawdenoise_linear(pd->env); + break; default: d->cached_ctx = NULL; break; @@ -2753,12 +2791,12 @@ static gpointer _preview_thread_raw(gpointer data) const gboolean cache_matches = d->preview_raw_imgid == pd->imgid && d->preview_raw_sensor_class == cls - && ((is_xtrans && d->preview_full_lin) - || (!is_xtrans && d->preview_full_cfa)); + && ((use_linear && d->preview_full_lin) + || (!use_linear && d->preview_full_cfa)); if(cache_matches) { - if(is_xtrans) + if(use_linear) { full_lin_use = d->preview_full_lin; full_w = d->preview_lin_w; @@ -2771,7 +2809,7 @@ static gpointer _preview_thread_raw(gpointer data) full_h = d->preview_full_h; } } - else if(is_xtrans) + else if(use_linear) { if(dt_restore_raw_linear_prepare(ctx, pd->imgid, &take_full_lin, &full_w, &full_h) != 0 @@ -2901,7 +2939,7 @@ static gpointer _preview_thread_raw(gpointer data) // crop in sensor pixels: // bayer: 2*T - 4*overlap_packed = 2*T - 128 (for OVERLAP_PACKED=32) // linear: T - 2*overlap_linear = T - 64 (for OVERLAP_LINEAR=32) - const int max_disp = is_xtrans ? (T - 64) : (2 * T - 128); + const int max_disp = use_linear ? (T - 64) : (2 * T - 128); // the raw buffer is always landscape (sensor layout), but the preview // thumbnail the user clicks on is oriented per EXIF. un-rotate the @@ -2913,7 +2951,7 @@ static gpointer _preview_thread_raw(gpointer data) int crop_w = MIN(swap_xy ? pd->preview_h : pd->preview_w, max_disp); int crop_h = MIN(swap_xy ? pd->preview_w : pd->preview_h, max_disp); // Bayer: snap to mod 2 (CFA grid) - if(!is_xtrans) + if(!use_linear) { crop_w = (crop_w / 2) * 2; crop_h = (crop_h / 2) * 2; @@ -2943,7 +2981,7 @@ static gpointer _preview_thread_raw(gpointer data) int crop_y = (int)sy - crop_h / 2; crop_x = CLAMP(crop_x, 0, full_w - crop_w); crop_y = CLAMP(crop_y, 0, full_h - crop_h); - if(!is_xtrans) + if(!use_linear) { crop_x = (crop_x / 2) * 2; crop_y = (crop_y / 2) * 2; @@ -2954,8 +2992,7 @@ static gpointer _preview_thread_raw(gpointer data) "patch_center=(%.3f,%.3f) -> sensor=(%d,%d %dx%d) %s", full_w, full_h, (unsigned)ori, pd->patch_center[0], pd->patch_center[1], - crop_x, crop_y, crop_w, crop_h, - is_xtrans ? "linear" : "bayer"); + crop_x, crop_y, crop_w, crop_h, cls_name); // 5. inference // Bayer path uses the _piped variant which runs darktable's full @@ -2968,7 +3005,7 @@ static gpointer _preview_thread_raw(gpointer data) float *denoised_rgb = NULL; int actual_w = 0, actual_h = 0; int err; - if(is_xtrans) + if(use_linear) err = dt_restore_raw_linear_preview_piped(ctx, &img_meta, pd->imgid, full_lin_use, full_w, full_h,