Files
DarkRoom/core/dr-denoise/examples/denoise_raw.rs
T
dtourolle 9cba420fd5 Denoise a whole frame in one call where the GPU takes any size
A fixed 1408 tile is exact only in its centre, and Best keeps 896 of
every 1408 it computes: 2.47 photosites of work for each one kept. The
tiler now takes a network of any size as well as a square one, and
plans the frame as the fewest equal tiles under the rung's limit --
one tile, the whole frame and its reflected border, whenever it fits.
If the first call of a plan fails, as a GPU out of memory does, the
kept centre is halved and the frame planned again.

Each shipped network names its any-size sibling (mosaic-best.onnx
beside mosaic-best-1408.onnx). OnnxNet::open takes it where the engine
runs whole frames and the file is installed, and the 1408 tiles
otherwise; open_tiled forces the tiles, and denoise_raw's DR_PLAN=tiles
uses it to compare. The cache key stays on the fixed model: the output
is the same network's. Tests hold any-size tiles, a grid of them and a
plan rebuilt after a failure to the square tiles' answer in every Bayer
phase.
2026-10-06 21:47:19 -04:00

161 lines
5.9 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.
//! Denoise one RAW file end to end, as develop will, and time it.
//!
//! ```sh
//! DARKROOM_ORT_DIR=~/.local/share/darkroom/runtime \
//! cargo run --release -p dr-denoise --features native --example denoise_raw -- IMG.CR2 out [fast|medium|best]
//! ```
//!
//! Decode, the app's hot-pixel pass, the frame's noise from its best source,
//! then one of the shipped networks (`best` unless named) under the inference engine on whatever rung this
//! machine probes to. Writes `out.npy` — the active area, `h×w×3` f32 linear
//! camera RGB — for comparison with the training repo's own path
//! (`tools/compare_rust.py` in darkroom-denoise). `DARKROOM_ORT_DIR` points
//! at an ONNX Runtime build; the engine's cache goes to `DR_ENGINE_CACHE` or
//! a temporary directory. The whole-frame network (`mosaic-best.onnx` beside
//! the fixed file) runs where the rung takes any size; `DR_PLAN=tiles` keeps
//! the 1408² tiles anyway, to compare the two.
use std::path::PathBuf;
use std::time::{Duration, Instant};
use dr_denoise::onnx::OnnxNet;
use dr_inference_engine::{Config, Role};
fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
let mut args = std::env::args().skip(1);
let (Some(input), Some(out)) = (args.next(), args.next()) else {
eprintln!("usage: denoise_raw RAW OUT_PREFIX [fast|medium|best]");
std::process::exit(2);
};
let shipped = match args.next().as_deref() {
None | Some("best") => dr_denoise::BEST,
Some("medium") => dr_denoise::MEDIUM,
Some("fast") => dr_denoise::FAST,
Some(other) => {
eprintln!("no network called {other}: fast, medium or best");
std::process::exit(2);
}
};
let model = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../models/denoise")
.join(shipped.file);
let whole = model.with_file_name(shipped.whole);
let tiles_only = std::env::var("DR_PLAN").is_ok_and(|p| p == "tiles");
let mut models = vec![(Role::Denoiser, model.clone())];
if whole.is_file() && !tiles_only {
models.push((Role::WholeDenoiser, whole));
}
let cache = std::env::var_os("DR_ENGINE_CACHE")
.map(PathBuf::from)
.unwrap_or_else(|| std::env::temp_dir().join("dr-denoise-engines"));
let started = Instant::now();
dr_inference_engine::init(Config {
runtime_dirs: std::env::var_os("DARKROOM_ORT_DIR")
.map(PathBuf::from)
.into_iter()
.collect(),
cache_dir: cache,
models,
embedded: Vec::new(),
ceiling: None,
threads: 0,
decay: Duration::ZERO,
});
// Wait for the probe and the engine build, so the timing below is the
// rung this machine settles on, not the fallback used while it compiles.
// The probe starts on its own thread; give it a moment to say so.
std::thread::sleep(Duration::from_secs(1));
loop {
let s = dr_inference_engine::status();
if !s.probing && s.engines.0 >= s.engines.1 {
println!(
"engine {} ({:.1} s to settle)",
s.line(),
started.elapsed().as_secs_f64()
);
break;
}
std::thread::sleep(Duration::from_millis(200));
}
let bytes = std::fs::read(&input).expect("read raw");
let t = Instant::now();
let mut raw = dr_decode::decode(&bytes).expect("decode");
let meta = dr_decode::metadata(&bytes).expect("metadata");
let decode = t.elapsed();
let t = Instant::now();
let ctx =
pollster::block_on(dr_gpu::GpuContext::new_headless()).expect("GPU for the hot-pixel pass");
let repaired = dr_gpu::Demosaicer::new(&ctx)
.expect("demosaicer")
.repair_hot_pixels(&mut raw)
.expect("repair");
let repair = t.elapsed();
let noise = dr_denoise::noise::for_frame(&raw, &bytes, meta.iso)
.expect("no noise source for this frame");
println!(
"frame {} {} ISO {:?}, {}×{}, {:?}, {repaired} hot photosites repaired",
raw.make, raw.model, meta.iso, raw.crop.width, raw.crop.height, raw.cfa_pattern
);
println!(
"noise {} — σ at 10 % grey (G) {:.5}, read {:.5}, row {:.5}, col {:.5}",
noise.source.label(),
noise.sigma(1, 0.1),
noise.o[1].sqrt(),
noise.row,
noise.col
);
let mut net = if tiles_only {
OnnxNet::open_tiled(&model, shipped)
} else {
OnnxNet::open(&model, shipped)
}
.expect("model");
println!(
"rung {} · {}",
net.rung().map(|r| r.label()).unwrap_or("?"),
if net.whole_frame() {
"whole frame"
} else {
"1408² tiles"
}
);
let t = Instant::now();
let rgb = dr_denoise::denoise(&raw, &noise, &mut net, &mut |done, total| {
eprint!("\rtile {done}/{total}");
true
})
.expect("denoise")
.expect("not cancelled");
let run = t.elapsed();
eprintln!();
println!(
"time decode {:.2} s · hot pixels {:.2} s · network {:.2} s ({:.1} MP)",
decode.as_secs_f64(),
repair.as_secs_f64(),
run.as_secs_f64(),
(raw.crop.width * raw.crop.height) as f64 / 1e6
);
let (h, w) = (raw.crop.height as usize, raw.crop.width as usize);
let mut npy = Vec::with_capacity(rgb.len() * 4 + 128);
let mut header =
format!("{{'descr': '<f4', 'fortran_order': False, 'shape': ({h}, {w}, 3), }}");
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 &rgb {
npy.extend_from_slice(&v.to_le_bytes());
}
std::fs::write(format!("{out}.npy"), npy).expect("write");
println!("wrote {out}.npy");
}