Run the app's hot-pixel pass alone, and dump through it

The learned demosaic replaces the classical one and takes its input, the
mosaic hot_pixels.wgsl leaves (denoise.md §2), so its training data and its
input in the app must come through that pass and not a lookalike. The pass
was recorded inline in Demosaicer::run; it is now built by hot_pass and
recorded by record_hot_pass, which run still uses unchanged, and
Demosaicer::repair_hot_pixels runs it on its own and reads the mosaic back.

mosaic_dump moves to dr-gpu to call it, records how many photosites changed,
and keeps --unrepaired for a raw readout.
This commit is contained in:
2026-10-03 10:25:28 -04:00
parent 1f266a4478
commit d4248bc0dd
4 changed files with 230 additions and 77 deletions
-99
View File
@@ -1,99 +0,0 @@
//! Dump RAW files' mosaics for training the learned denoise (FR-DEV-3g).
//!
//! The training repo must read photosites the way the app reads them —
//! same black and white levels, same active area, same CFA phase — or a
//! network trained on one phase runs on another and paints moiré everywhere
//! (denoise.md §4.4). So it reads this, not LibRaw.
//!
//! Reads `input<TAB>output-prefix` lines on stdin and writes, per line,
//! `prefix.npy` (the whole readout, masked border included, `u16`, row-major)
//! and `prefix.json` (what `decode` and `metadata` say about it). The border
//! is kept because its optically black photosites are a dark frame for free:
//! read noise and row noise at that ISO.
//!
//! ```sh
//! printf 'IMG_0001.CR2\tout/IMG_0001\n' |
//! cargo run --release -p dr-decode --example mosaic_dump
//! ```
use std::io::{BufRead, Write};
fn main() {
let mut failed = 0;
for line in std::io::stdin().lock().lines() {
let line = line.expect("stdin");
let Some((input, prefix)) = line.split_once('\t') else {
continue;
};
match dump(input, prefix) {
Ok(()) => println!("ok\t{input}"),
Err(e) => {
failed += 1;
println!("fail\t{input}\t{e}");
}
}
std::io::stdout().flush().ok();
}
std::process::exit(if failed > 0 { 1 } else { 0 });
}
fn dump(input: &str, prefix: &str) -> Result<(), String> {
let bytes = std::fs::read(input).map_err(|e| e.to_string())?;
let raw = dr_decode::decode(&bytes).map_err(|e| e.to_string())?;
if raw.samples_per_pixel != 1 {
return Err("linear DNG: no photosites".into());
}
let meta = dr_decode::metadata(&bytes).map_err(|e| e.to_string())?;
let mut npy = Vec::with_capacity(raw.data.len() * 2 + 128);
let mut header = format!(
"{{'descr': '<u2', 'fortran_order': False, 'shape': ({}, {}), }}",
raw.height, raw.width
);
// The header, its magic and length are padded to a multiple of 64.
while (10 + header.len() + 1) % 64 != 0 {
header.push(' ');
}
header.push('\n');
npy.extend_from_slice(b"\x93NUMPY\x01\x00");
npy.extend_from_slice(&(header.len() as u16).to_le_bytes());
npy.extend_from_slice(header.as_bytes());
for v in &raw.data {
npy.extend_from_slice(&v.to_le_bytes());
}
std::fs::write(format!("{prefix}.npy"), npy).map_err(|e| e.to_string())?;
let opt = |v: Option<f32>| v.map_or("null".to_string(), |v| v.to_string());
let matrix = raw
.color_matrix
.map_or("null".to_string(), |m| format!("{m:?}"));
let json = format!(
concat!(
"{{\"source\": {:?}, \"make\": {:?}, \"model\": {:?}, ",
"\"width\": {}, \"height\": {}, ",
"\"crop\": [{}, {}, {}, {}], \"cfa\": {:?}, ",
"\"black\": {:?}, \"white\": {}, \"wb\": {:?}, \"cam_to_srgb\": {}, ",
"\"iso\": {}, \"shutter\": {}, \"aperture\": {}, \"captured_at\": {}}}\n"
),
input,
raw.make,
raw.model,
raw.width,
raw.height,
raw.crop.x,
raw.crop.y,
raw.crop.width,
raw.crop.height,
format!("{:?}", raw.cfa_pattern),
raw.black_level,
raw.white_level,
raw.wb_coeffs,
matrix,
meta.iso.map_or("null".to_string(), |v| v.to_string()),
opt(meta.shutter),
opt(meta.aperture),
meta.captured_at
.map_or("null".to_string(), |v| v.to_string()),
);
std::fs::write(format!("{prefix}.json"), json).map_err(|e| e.to_string())
}