Files
DarkRoom/core/dr-denoise/src/repair.rs
T
dtourolle 0c9d564586 Repair photosites beyond 8 sigma of every neighbour before the network
The app's hot-pixel pass takes gross defects only; at ISO 6400-25600 a 6D
frame keeps 1000-2000 photosites more than 8 sigma beyond all their
same-colour and adjacent neighbours, which the network turned into specks.
The same two tests with the threshold in the photosite's own sigma, plus
the factor of two that keeps a bright point of light (where 8 sigma is a
sliver of the signal). The next model is trained behind exactly this; on
an ISO 25600 frame the Rust and training code both repair 935.
2026-10-04 07:30:09 -04:00

146 lines
5.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! TRACES: FR-DEV-3g
//! Hot and dead photosites, judged against the noise, before the network.
//!
//! The app's own pass (`dr_gpu::Demosaicer::repair_hot_pixels`) runs first and
//! takes the gross defects. At high ISO it leaves thousands of photosites per
//! 6D frame more than 8σ beyond every neighbour, which the network turns into
//! specks. This second pass uses that pass's two tests with the threshold in
//! units of the photosite's own σ from the noise model:
//!
//! - beyond every same-colour neighbour (two photosites away, the 3×3 of its
//! plane) by more than `k·σ`, and
//! - beyond every adjacent photosite, whatever its colour, by more than
//! `k·σ` **and** by a factor of two — what keeps a real point of light,
//! which lights its neighbours through the lens and the anti-aliasing
//! filter. A margin in σ alone is not enough: on a bright star 8σ is a
//! sliver of the signal, and the star would be flattened.
//!
//! A hot one becomes its brightest same-colour neighbour, a dead one its
//! darkest. The shipped model was trained on input repaired exactly so
//! (darkroom-denoise `denoise/repair.py`, `--repair-k 8`): the threshold
//! belongs to the model, and changes with it. Neighbours off the frame are
//! the nearest photosite on it, as the training code reads them.
/// The threshold the shipped model was trained with, in σ.
pub const REPAIR_K: f32 = 8.0;
/// Repair `mosaic` (`h×w`, row-major, normalised) in place; `sigma(y, x, v)`
/// is the photosite's σ. Returns how many photosites changed.
pub fn repair(
mosaic: &mut [f32],
h: usize,
w: usize,
k: f32,
sigma: &(dyn Fn(usize, usize, f32) -> f32 + Sync),
) -> usize {
let copy = mosaic.to_vec();
let original = ©
let at = |y: isize, x: isize| {
let y = y.clamp(0, h as isize - 1) as usize;
let x = x.clamp(0, w as isize - 1) as usize;
original[y * w + x]
};
let threads = std::thread::available_parallelism().map_or(1, |n| n.get());
let rows_per = h.div_ceil(threads).max(1);
let mut counts = vec![0usize; h.div_ceil(rows_per)];
std::thread::scope(|scope| {
for ((chunk, rows), count) in mosaic
.chunks_mut(rows_per * w)
.enumerate()
.zip(counts.iter_mut())
{
let at = &at;
scope.spawn(move || {
for (i, row) in rows.chunks_mut(w).enumerate() {
let y = chunk * rows_per + i;
for (x, out) in row.iter_mut().enumerate() {
let v = original[y * w + x];
let (yi, xi) = (y as isize, x as isize);
let (mut s_hi, mut s_lo) = (f32::MIN, f32::MAX);
let (mut a_hi, mut a_lo) = (f32::MIN, f32::MAX);
for dy in -1isize..=1 {
for dx in -1isize..=1 {
if dy == 0 && dx == 0 {
continue;
}
let s = at(yi + 2 * dy, xi + 2 * dx);
s_hi = s_hi.max(s);
s_lo = s_lo.min(s);
let a = at(yi + dy, xi + dx);
a_hi = a_hi.max(a);
a_lo = a_lo.min(a);
}
}
let t = k * sigma(y, x, v);
if v - s_hi > t && v - a_hi > t && a_hi < 0.5 * v {
*out = s_hi;
*count += 1;
} else if s_lo - v > t && a_lo - v > t && v < 0.5 * a_lo {
*out = s_lo;
*count += 1;
}
}
}
});
}
});
counts.iter().sum()
}
#[cfg(test)]
mod tests {
use super::*;
const N: usize = 16;
fn flat(level: f32) -> Vec<f32> {
vec![level; N * N]
}
fn run(m: &mut [f32]) -> usize {
repair(m, N, N, REPAIR_K, &|_, _, _| 0.01)
}
#[test]
fn a_hot_photosite_becomes_its_brightest_same_colour_neighbour() {
let mut m = flat(0.1);
m[8 * N + 8] = 0.5; // 40σ above everything around it
m[8 * N + 10] = 0.12; // a same-colour neighbour, a little brighter
assert_eq!(run(&mut m), 1);
assert_eq!(m[8 * N + 8], 0.12);
}
#[test]
fn a_dead_photosite_in_a_lit_area_is_repaired() {
let mut m = flat(0.5);
m[5 * N + 5] = 0.0;
assert_eq!(run(&mut m), 1);
assert_eq!(m[5 * N + 5], 0.5);
}
#[test]
fn a_point_of_real_light_is_kept() {
// Light through a lens lands on a patch: its adjacent photosites are
// lit too, so the second test refuses it.
let mut m = flat(0.1);
for dy in 0..3 {
for dx in 0..3 {
m[(7 + dy) * N + 7 + dx] = if (dy, dx) == (1, 1) { 0.9 } else { 0.6 };
}
}
let before = m.clone();
assert_eq!(run(&mut m), 0);
assert_eq!(m, before);
}
#[test]
fn noise_within_the_threshold_is_left_alone() {
let mut m: Vec<f32> = (0..N * N)
.map(|i| 0.1 + 0.005 * ((i * 7919 % 13) as f32 - 6.0) / 6.0)
.collect();
let before = m.clone();
assert_eq!(run(&mut m), 0);
assert_eq!(m, before);
}
}