Make Best one network, and retire Medium and the mixture

Best was a mixture of two experts and a gate, 110 GMAC a megapixel;
Medium a single network at 48 that was softer on real edges. fb-combo
(darkroom-denoise, 20 000 steps from fb-edges2, taught by the mixture
with a quarter of its crops from the edge-rich parts of the frames) is
Medium's shape and holds the mixture's edges on real photographs:
edge PSNR within 0.04-0.06 dB at ISO 1600/6400/25600, more sharpness
kept at all three, the chart's edge 0.89 photosites wide against 0.82.
It is 0.27 dB short on smooth areas at ISO 25600. It becomes Best, and
the methods are Bilinear, Fast and Best.

Saved edits keep their numbers: 2, which was Medium, is now Best, and
3, which was Best, is past the end and reads as the default, Best.
The network ships as mosaic-hq, a new name: the result cache keys a
model by name and size, and this one is byte for byte the old Medium's
size. Its tablet form (A16W16) lost 0.00 dB in simulated QDQ at every
ISO and at most 0.09 dB across the noise bracket.
This commit is contained in:
2026-10-07 06:58:24 -04:00
parent 4b71ef0947
commit a9271c4850
25 changed files with 74 additions and 78 deletions
+4 -5
View File
@@ -2,7 +2,7 @@
//!
//! ```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]
//! cargo run --release -p dr-denoise --features native --example denoise_raw -- IMG.CR2 out [fast|best]
//! ```
//!
//! Decode, the app's hot-pixel pass, the frame's noise from its best source,
@@ -11,7 +11,7 @@
//! 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
//! a temporary directory. The whole-frame network (`mosaic-hq.onnx` beside
//! the fixed file) runs where the rung takes any size; `DR_PLAN=tiles` keeps
//! the 1408² tiles anyway, to compare the two.
@@ -25,15 +25,14 @@ 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]");
eprintln!("usage: denoise_raw RAW OUT_PREFIX [fast|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");
eprintln!("no network called {other}: fast or best");
std::process::exit(2);
}
};
+8 -12
View File
@@ -44,19 +44,15 @@ pub const FAST: Shipped = Shipped {
whole: "mosaic-fast.onnx",
halo: HALO,
};
/// A student of the mixture with the first release's shape: 3.2 M
/// parameters, 48 GMAC a megapixel.
pub const MEDIUM: Shipped = Shipped {
file: "mosaic-medium-1408.onnx",
whole: "mosaic-medium.onnx",
halo: HALO,
};
/// The mixture: a flat expert, an edge expert and the gate that blends them.
/// It reaches further than either, so it keeps a smaller centre of each tile.
/// One network of the first release's shape, 3.2 M parameters and 48 GMAC a
/// megapixel, taught by the mixture of experts that was Best until 0.24:
/// its edges at a third of its work (denoise.md §15). A new file name, not
/// the old Medium's or Best's: the result cache keys a model by its name
/// and size, and this one is byte for byte the old Medium's size.
pub const BEST: Shipped = Shipped {
file: "mosaic-best-1408.onnx",
whole: "mosaic-best.onnx",
halo: 256,
file: "mosaic-hq-1408.onnx",
whole: "mosaic-hq.onnx",
halo: HALO,
};
#[derive(Debug, thiserror::Error)]
+4 -4
View File
@@ -11,11 +11,11 @@
//! with the Bayer packing spelled `SpaceToDepth`, which QNN can hold and the
//! 6-D reshape it replaces it cannot.
//!
//! Each network also ships with any height and width (`mosaic-best.onnx`
//! beside `mosaic-best-1408.onnx`, darkroom-denoise `tools/export_whole.py`,
//! Each network also ships with any height and width (`mosaic-hq.onnx`
//! beside `mosaic-hq-1408.onnx`, darkroom-denoise `tools/export_whole.py`,
//! identical to the fixed file at 1408²). Where the rung takes any size, the
//! frame runs whole instead of in tiles whose borders are thrown away —
//! Best computes 2.47 photosites for every one it keeps in 1408² tiles
//! frame runs whole instead of in tiles whose borders are thrown away — a
//! 1408² tile keeps 1024², 1.89 photosites computed for each one kept
//! (denoise.md §14).
use crate::tile::{Sizes, TileNet};
+2 -2
View File
@@ -2216,13 +2216,13 @@ mod tests {
g.set_param(
learned_denoise::ID,
learned_denoise::METHOD,
learned_denoise::Method::Medium.index(),
learned_denoise::Method::Fast.index(),
);
let state = g.state();
let mut h = EditGraph::default_chain();
h.set_denoise_available(true);
let _ = h.set_state(&state);
assert_eq!(h.denoise_method(), learned_denoise::Method::Medium);
assert_eq!(h.denoise_method(), learned_denoise::Method::Fast);
assert!((h.denoise_grain() - 0.4).abs() < 1e-6);
}
}
+29 -9
View File
@@ -35,23 +35,27 @@ pub const GRAIN: ParamId = ParamId("grain");
/// TRACES: FR-DEV-3g
/// The demosaics a photograph can be developed with, in the order the
/// sidecar numbers them. Three networks that trade time for quality — the
/// same training, distilled into smaller students (docs/dev/denoise.md §13)
/// — and the classical demosaic, which is no network at all.
/// sidecar numbers them. Two networks that trade time for quality
/// (docs/dev/denoise.md §15) and the classical demosaic, which is no network
/// at all.
///
/// Until 0.24 there were four — Bilinear, Fast, Medium, Best — and the
/// sidecar keeps their numbers: 2, which was Medium, is now Best, and 3,
/// which was Best, is past the end and reads as the default, which is
/// Best. Both land on the network that replaced them, with no migration.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Method {
/// The classical demosaic: the noise stays.
Bilinear,
/// The smallest student: a quarter of the medium network's work.
/// The smallest student: a quarter of Best's work.
Fast,
/// One network the size of the first release's.
Medium,
/// Two experts, one for flat areas and one for edges, and a gate.
/// One network of the first release's size, taught by the mixture of
/// experts it replaced: the mixture's edges at a third of its work.
Best,
}
impl Method {
pub const ALL: [Method; 4] = [Method::Bilinear, Method::Fast, Method::Medium, Method::Best];
pub const ALL: [Method; 3] = [Method::Bilinear, Method::Fast, Method::Best];
pub const DEFAULT: Method = Method::Best;
/// The sidecar's number for it.
@@ -92,7 +96,6 @@ pub(crate) static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
vec![
LocalizedKey("param.learned_denoise.method.bilinear"),
LocalizedKey("param.learned_denoise.method.fast"),
LocalizedKey("param.learned_denoise.method.medium"),
LocalizedKey("param.learned_denoise.method.best"),
],
)
@@ -117,3 +120,20 @@ pub(crate) static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
pub fn descriptor() -> Arc<OpDescriptor> {
DESCRIPTOR.clone()
}
#[cfg(test)]
mod tests {
use super::*;
/// An edit saved before 0.24 stored Medium as 2 and Best as 3. Both
/// now name the network that replaced them, and nothing reads as Fast
/// or Bilinear that did not before.
#[test]
fn the_retired_methods_read_as_best() {
assert_eq!(Method::from_index(0.0), Method::Bilinear);
assert_eq!(Method::from_index(1.0), Method::Fast);
assert_eq!(Method::from_index(2.0), Method::Best, "Medium, before 0.24");
assert_eq!(Method::from_index(3.0), Method::Best, "Best, before 0.24");
assert_eq!(Method::Best.index(), 2.0);
}
}