Offer three denoise networks and a method to choose between them
AI Denoise's Apply switch becomes Method: Bilinear, Fast, Medium, Best, default Best, so an untouched raw writes nothing and develops through the mixture. `apply` is still read and never written: 0 is Bilinear, 1 keeps a network already chosen. - Best is the mixture of a flat and an edge expert with a learned gate; Medium and Fast are students distilled from it. 2.48 s, 0.79 s and 0.57 s for a 20 MP frame on TensorRT fp16. - Each network carries its own tile border (256 for the mixture, 192 for the students) through `dr_denoise::Shipped` and `TileNet::halo`. - The file is hashed once at open and each network keys its own cached result; Bilinear keeps the result in memory for the way back. - Each has an .a16w16 sibling for the Hexagon: 0.00 dB on the 6D gate, at most 0.11 dB with the noise scaled x0.5 to x4. - APK BUNDLED 19 -> 23; the PKGBUILD installs all three.
This commit is contained in:
@@ -338,7 +338,7 @@ fn unpack_bundled_models(app: &slint::android::AndroidApp) {
|
||||
// sibling when the probe chose that rung and ignores it otherwise. The
|
||||
// segmenter's and XFeat's forms are compiled into the binary instead,
|
||||
// beside their f32 graphs.
|
||||
const BUNDLED: [(&std::ffi::CStr, &str); 19] = [
|
||||
const BUNDLED: [(&std::ffi::CStr, &str); 23] = [
|
||||
(c"models/scrfd_500m_640.onnx", "scrfd_500m_640.onnx"),
|
||||
(
|
||||
c"models/scrfd_500m_640.a16w8.onnx",
|
||||
@@ -372,8 +372,23 @@ fn unpack_bundled_models(app: &slint::android::AndroidApp) {
|
||||
// The panorama border filler (FR-MRG-4); MIT, 28 MB.
|
||||
(c"models/migan-512.onnx", "migan-512.onnx"),
|
||||
(c"models/migan-512.a16w16.onnx", "migan-512.a16w16.onnx"),
|
||||
(c"models/mosaic-1408.onnx", "mosaic-1408.onnx"),
|
||||
(c"models/mosaic-1408.a16w16.onnx", "mosaic-1408.a16w16.onnx"),
|
||||
// The learned demosaic and denoise, one network per method
|
||||
// (FR-DEV-3g), each with the 16-bit form the Hexagon runs.
|
||||
(c"models/mosaic-fast-1408.onnx", "mosaic-fast-1408.onnx"),
|
||||
(
|
||||
c"models/mosaic-fast-1408.a16w16.onnx",
|
||||
"mosaic-fast-1408.a16w16.onnx",
|
||||
),
|
||||
(c"models/mosaic-medium-1408.onnx", "mosaic-medium-1408.onnx"),
|
||||
(
|
||||
c"models/mosaic-medium-1408.a16w16.onnx",
|
||||
"mosaic-medium-1408.a16w16.onnx",
|
||||
),
|
||||
(c"models/mosaic-best-1408.onnx", "mosaic-best-1408.onnx"),
|
||||
(
|
||||
c"models/mosaic-best-1408.a16w16.onnx",
|
||||
"mosaic-best-1408.a16w16.onnx",
|
||||
),
|
||||
];
|
||||
|
||||
let dir = dr_ui::shared_face_models_dir();
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
//!
|
||||
//! ```sh
|
||||
//! DARKROOM_ORT_DIR=~/.local/share/darkroom/runtime \
|
||||
//! cargo run --release -p dr-denoise --features native --example denoise_raw -- IMG.CR2 out
|
||||
//! 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 the shipped network under the inference engine on whatever rung this
|
||||
//! 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
|
||||
@@ -23,11 +23,21 @@ 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");
|
||||
eprintln!("usage: denoise_raw RAW OUT_PREFIX [fast|medium|best]");
|
||||
std::process::exit(2);
|
||||
};
|
||||
let model =
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../models/denoise/mosaic-1408.onnx");
|
||||
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 cache = std::env::var_os("DR_ENGINE_CACHE")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| std::env::temp_dir().join("dr-denoise-engines"));
|
||||
@@ -91,7 +101,7 @@ fn main() {
|
||||
noise.col
|
||||
);
|
||||
|
||||
let mut net = OnnxNet::from_path(&model).expect("model");
|
||||
let mut net = OnnxNet::from_path(&model, shipped.halo).expect("model");
|
||||
println!(
|
||||
"rung {}",
|
||||
net.rung().map(|r| r.label()).unwrap_or("?")
|
||||
|
||||
@@ -27,6 +27,33 @@ use dr_decode::RawImage;
|
||||
pub use noise::{NoiseModel, Source};
|
||||
pub use tile::{TileNet, HALO};
|
||||
|
||||
/// TRACES: FR-DEV-3g
|
||||
/// A network the app ships in `models/denoise/`: its file, and the context
|
||||
/// it needs past a tile's kept centre (docs/dev/denoise.md §13).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Shipped {
|
||||
pub file: &'static str,
|
||||
pub halo: usize,
|
||||
}
|
||||
|
||||
/// The smallest student: 0.9 M parameters, 11 GMAC a megapixel.
|
||||
pub const FAST: Shipped = Shipped {
|
||||
file: "mosaic-fast-1408.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",
|
||||
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.
|
||||
pub const BEST: Shipped = Shipped {
|
||||
file: "mosaic-best-1408.onnx",
|
||||
halo: 256,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DenoiseError {
|
||||
#[error("the network cannot take this photograph: {0}")]
|
||||
|
||||
@@ -21,15 +21,19 @@ pub const TILE: usize = 1408;
|
||||
pub struct OnnxNet {
|
||||
model: Model,
|
||||
tile: usize,
|
||||
halo: usize,
|
||||
}
|
||||
|
||||
impl OnnxNet {
|
||||
pub fn from_path(path: &std::path::Path) -> Result<Self, DenoiseError> {
|
||||
/// The network at `path`, which needs `halo` photosites of context
|
||||
/// ([`crate::Shipped::halo`]).
|
||||
pub fn from_path(path: &std::path::Path, halo: usize) -> Result<Self, DenoiseError> {
|
||||
let (path, form) = dr_inference_engine::resolve_model(Role::Denoiser, path);
|
||||
let bytes = std::fs::read(&path)?;
|
||||
Ok(OnnxNet {
|
||||
model: dr_inference_engine::open(Role::Denoiser, form, &bytes)?,
|
||||
tile: TILE,
|
||||
halo,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,6 +48,10 @@ impl TileNet for OnnxNet {
|
||||
self.tile
|
||||
}
|
||||
|
||||
fn halo(&self) -> usize {
|
||||
self.halo
|
||||
}
|
||||
|
||||
fn run(
|
||||
&mut self,
|
||||
mosaic: Vec<f32>,
|
||||
|
||||
+18
-11
@@ -15,7 +15,9 @@
|
||||
|
||||
use dr_decode::CfaPattern;
|
||||
|
||||
/// Photosites of context beyond a tile's kept centre, on every side.
|
||||
/// Photosites of context beyond a tile's kept centre, on every side, for a
|
||||
/// single network; a mixture reaches further and says so through
|
||||
/// [`TileNet::halo`].
|
||||
pub const HALO: usize = 192;
|
||||
|
||||
/// A fixed-shape network: `mosaic` and `sigma`, `n×n` RGGB, in; `3×n×n`
|
||||
@@ -28,6 +30,11 @@ pub const HALO: usize = 192;
|
||||
pub trait TileNet {
|
||||
/// The edge `n` of the square tile the network takes.
|
||||
fn tile(&self) -> usize;
|
||||
/// Photosites of context it needs past a tile's kept centre: at least
|
||||
/// its receptive field. [`HALO`] unless the network says otherwise.
|
||||
fn halo(&self) -> usize {
|
||||
HALO
|
||||
}
|
||||
fn run(
|
||||
&mut self,
|
||||
mosaic: Vec<f32>,
|
||||
@@ -78,13 +85,13 @@ pub fn run_tiled(
|
||||
let (dy, dx) = rggb_offset(pattern).ok_or_else(|| {
|
||||
crate::DenoiseError::Unsupported(format!("{pattern:?} is not a Bayer pattern"))
|
||||
})?;
|
||||
let n = net.tile();
|
||||
if n <= 2 * HALO || !(n - 2 * HALO).is_multiple_of(2) {
|
||||
let (n, halo) = (net.tile(), net.halo());
|
||||
if n <= 2 * halo || !(n - 2 * halo).is_multiple_of(2) {
|
||||
return Err(crate::DenoiseError::Model(format!(
|
||||
"tile {n} leaves no even centre past a {HALO} halo"
|
||||
"tile {n} leaves no even centre past a {halo} halo"
|
||||
)));
|
||||
}
|
||||
let core = n - 2 * HALO;
|
||||
let core = n - 2 * halo;
|
||||
// In unified coordinates the frame spans u ∈ [dy, dy + h), v ∈ [dx, dx + w).
|
||||
let (uh, uw) = (h + dy, w + dx);
|
||||
let (ty, tx) = (uh.div_ceil(core), uw.div_ceil(core));
|
||||
@@ -108,11 +115,11 @@ pub fn run_tiled(
|
||||
scope.spawn(move || {
|
||||
for (i, (mrow, srow)) in m.chunks_mut(n).zip(s.chunks_mut(n)).enumerate() {
|
||||
let r = chunk * rows_per + i;
|
||||
// Unified row u = u0 + r − HALO; frame row y = u − dy, reflected.
|
||||
let u = u0 as isize + r as isize - HALO as isize;
|
||||
// Unified row u = u0 + r − halo; frame row y = u − dy, reflected.
|
||||
let u = u0 as isize + r as isize - halo as isize;
|
||||
let y = reflect(u - dy as isize, h);
|
||||
for c in 0..n {
|
||||
let v = v0 as isize + c as isize - HALO as isize;
|
||||
let v = v0 as isize + c as isize - halo as isize;
|
||||
let x = reflect(v - dx as isize, w);
|
||||
let val = at(y, x);
|
||||
mrow[c] = val;
|
||||
@@ -169,10 +176,10 @@ pub fn run_tiled(
|
||||
scope.spawn(move || {
|
||||
for (i, row) in block.chunks_mut(w * 3).enumerate() {
|
||||
let y = y_lo + chunk * per + i;
|
||||
// Tile row of frame row y: u = y + dy = u0 + r − HALO.
|
||||
let r = y + dy + HALO - u0;
|
||||
// Tile row of frame row y: u = y + dy = u0 + r − halo.
|
||||
let r = y + dy + halo - u0;
|
||||
for x in x_lo..x_hi {
|
||||
let c = x + dx + HALO - v0;
|
||||
let c = x + dx + halo - v0;
|
||||
for ch in 0..3 {
|
||||
row[x * 3 + ch] = rgb[ch * n * n + r * n + c];
|
||||
}
|
||||
|
||||
@@ -176,10 +176,10 @@ pub struct EditGraph {
|
||||
/// lens profile, so not in the state; it only decides whether the
|
||||
/// switch below is offered.
|
||||
denoise_available: bool,
|
||||
/// Whether the learned denoise replaces the demosaic. An edit: published
|
||||
/// as [`crate::learned_denoise`], captured, stored and undone with the
|
||||
/// rest (FR-DEV-3c).
|
||||
denoise_applied: bool,
|
||||
/// Which demosaic develops the photograph: a network, or the classical
|
||||
/// one. An edit: published as [`crate::learned_denoise`], captured,
|
||||
/// stored and undone with the rest (FR-DEV-3c).
|
||||
denoise_method: crate::learned_denoise::Method,
|
||||
/// How strongly to denoise, 0–100; what is not taken goes back as grain.
|
||||
denoise_strength: f32,
|
||||
}
|
||||
@@ -239,7 +239,7 @@ impl EditGraph {
|
||||
lens_profile: None,
|
||||
lens_profile_applied: true,
|
||||
denoise_available: false,
|
||||
denoise_applied: true,
|
||||
denoise_method: crate::learned_denoise::Method::DEFAULT,
|
||||
denoise_strength: 100.0,
|
||||
}
|
||||
}
|
||||
@@ -425,7 +425,13 @@ impl EditGraph {
|
||||
/// photograph that cannot take it is harmless and does nothing, as a
|
||||
/// lens switch with no profile does.
|
||||
pub fn denoise_applied(&self) -> bool {
|
||||
self.denoise_applied
|
||||
self.denoise_method.learned()
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3g
|
||||
/// Which demosaic is asked for.
|
||||
pub fn denoise_method(&self) -> crate::learned_denoise::Method {
|
||||
self.denoise_method
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3g
|
||||
@@ -625,7 +631,7 @@ impl EditGraph {
|
||||
OpCapability {
|
||||
id: desc.id,
|
||||
label: desc.label,
|
||||
active: self.denoise_applied,
|
||||
active: self.denoise_applied(),
|
||||
params: desc
|
||||
.params
|
||||
.iter()
|
||||
@@ -743,7 +749,7 @@ impl EditGraph {
|
||||
// Derived from the file, like the profile above.
|
||||
denoise_available: _,
|
||||
// Edits, in the state through `capabilities` like the lens switch.
|
||||
denoise_applied: _,
|
||||
denoise_method: _,
|
||||
denoise_strength: _,
|
||||
masks,
|
||||
film,
|
||||
@@ -818,7 +824,19 @@ impl EditGraph {
|
||||
pub fn set_param(&mut self, op: OpId, param: ParamId, value: f32) {
|
||||
if op == crate::learned_denoise::ID {
|
||||
match param {
|
||||
p if p == crate::learned_denoise::APPLY => self.denoise_applied = value != 0.0,
|
||||
p if p == crate::learned_denoise::METHOD => {
|
||||
self.denoise_method = crate::learned_denoise::Method::from_index(value)
|
||||
}
|
||||
// 0.21 and 0.22's switch (see `APPLY`): off is the classical
|
||||
// demosaic, on is a network — the one already chosen, if any.
|
||||
p if p == crate::learned_denoise::APPLY => {
|
||||
use crate::learned_denoise::Method;
|
||||
if value == 0.0 {
|
||||
self.denoise_method = Method::Bilinear;
|
||||
} else if !self.denoise_method.learned() {
|
||||
self.denoise_method = Method::DEFAULT;
|
||||
}
|
||||
}
|
||||
p if p == crate::learned_denoise::STRENGTH => {
|
||||
self.denoise_strength = value.clamp(0.0, 100.0)
|
||||
}
|
||||
@@ -889,8 +907,9 @@ impl EditGraph {
|
||||
pub fn param(&self, op: OpId, param: ParamId) -> Option<f32> {
|
||||
if op == crate::learned_denoise::ID {
|
||||
return match param {
|
||||
p if p == crate::learned_denoise::METHOD => Some(self.denoise_method.index()),
|
||||
p if p == crate::learned_denoise::APPLY => {
|
||||
Some(if self.denoise_applied { 1.0 } else { 0.0 })
|
||||
Some(if self.denoise_applied() { 1.0 } else { 0.0 })
|
||||
}
|
||||
p if p == crate::learned_denoise::STRENGTH => Some(self.denoise_strength),
|
||||
p if p == crate::learned_denoise::GRAIN => Some(100.0 - self.denoise_strength),
|
||||
@@ -943,9 +962,9 @@ impl EditGraph {
|
||||
// a reset does not change which lens took the photograph. What returns
|
||||
// to default is the answer to whether to use it, which is on.
|
||||
self.set_lens_profile_applied(true);
|
||||
// The learned denoise returns to off; whether it is available is the
|
||||
// file's and stays.
|
||||
self.denoise_applied = true;
|
||||
// The learned denoise returns to its default network; whether it is
|
||||
// available is the file's and stays.
|
||||
self.denoise_method = crate::learned_denoise::Method::DEFAULT;
|
||||
self.denoise_strength = 100.0;
|
||||
}
|
||||
|
||||
@@ -2106,9 +2125,14 @@ mod tests {
|
||||
.find(|c| c.id == learned_denoise::ID)
|
||||
.expect("offered");
|
||||
assert!(cap.active, "on by default");
|
||||
assert_eq!(g.denoise_method(), learned_denoise::Method::Best);
|
||||
assert_eq!(g.denoise_grain(), 0.0, "at full strength");
|
||||
assert_eq!(cap.id, g.capabilities()[0].id, "and first in the panel");
|
||||
g.set_param(learned_denoise::ID, learned_denoise::APPLY, 0.0);
|
||||
g.set_param(
|
||||
learned_denoise::ID,
|
||||
learned_denoise::METHOD,
|
||||
learned_denoise::Method::Bilinear.index(),
|
||||
);
|
||||
g.set_param(learned_denoise::ID, learned_denoise::STRENGTH, 70.0);
|
||||
assert!(!g.denoise_applied());
|
||||
assert!((g.denoise_grain() - 0.3).abs() < 1e-6);
|
||||
@@ -2117,6 +2141,58 @@ mod tests {
|
||||
assert_eq!(g.denoise_grain(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_untouched_raw_writes_nothing_and_develops_through_the_best() {
|
||||
// TRACES: FR-DEV-3g
|
||||
use crate::learned_denoise::{self, Method};
|
||||
let mut g = EditGraph::default_chain();
|
||||
g.set_denoise_available(true);
|
||||
assert_eq!(g.denoise_method(), Method::Best);
|
||||
let stored = |g: &EditGraph| {
|
||||
crate::Preset::capture_params(g)
|
||||
.params()
|
||||
.keys()
|
||||
.any(|(op, _)| op == learned_denoise::ID.0)
|
||||
};
|
||||
assert!(!stored(&g), "the default is not written");
|
||||
g.set_param(
|
||||
learned_denoise::ID,
|
||||
learned_denoise::METHOD,
|
||||
Method::Fast.index(),
|
||||
);
|
||||
assert!(stored(&g), "a choice is");
|
||||
assert!(
|
||||
!crate::Preset::capture_params(&g).params().contains_key(&(
|
||||
learned_denoise::ID.0.into(),
|
||||
learned_denoise::APPLY.0.into()
|
||||
)),
|
||||
"and the old switch never is"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_edit_saved_with_the_switch_keeps_its_look() {
|
||||
// TRACES: FR-DEV-3g
|
||||
// 0.21 and 0.22 stored on or off; off is the classical demosaic, and
|
||||
// on keeps a network already chosen.
|
||||
use crate::learned_denoise::{self, Method};
|
||||
let mut g = EditGraph::default_chain();
|
||||
g.set_param(learned_denoise::ID, learned_denoise::APPLY, 0.0);
|
||||
assert_eq!(g.denoise_method(), Method::Bilinear);
|
||||
g.set_param(learned_denoise::ID, learned_denoise::APPLY, 1.0);
|
||||
assert_eq!(g.denoise_method(), Method::DEFAULT);
|
||||
g.set_param(
|
||||
learned_denoise::ID,
|
||||
learned_denoise::METHOD,
|
||||
Method::Fast.index(),
|
||||
);
|
||||
g.set_param(learned_denoise::ID, learned_denoise::APPLY, 1.0);
|
||||
assert_eq!(g.denoise_method(), Method::Fast);
|
||||
// A number from a newer build with more methods is the default.
|
||||
g.set_param(learned_denoise::ID, learned_denoise::METHOD, 9.0);
|
||||
assert_eq!(g.denoise_method(), Method::DEFAULT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_edit_saved_with_grain_keeps_its_look() {
|
||||
// TRACES: FR-DEV-3g
|
||||
@@ -2137,11 +2213,16 @@ mod tests {
|
||||
let mut g = EditGraph::default_chain();
|
||||
g.set_denoise_available(true);
|
||||
g.set_param(learned_denoise::ID, learned_denoise::STRENGTH, 60.0);
|
||||
g.set_param(
|
||||
learned_denoise::ID,
|
||||
learned_denoise::METHOD,
|
||||
learned_denoise::Method::Medium.index(),
|
||||
);
|
||||
let state = g.state();
|
||||
let mut h = EditGraph::default_chain();
|
||||
h.set_denoise_available(true);
|
||||
let _ = h.set_state(&state);
|
||||
assert!(h.denoise_applied());
|
||||
assert_eq!(h.denoise_method(), learned_denoise::Method::Medium);
|
||||
assert!((h.denoise_grain() - 0.4).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! TRACES: FR-DEV-3g
|
||||
//! The learned denoise's settings: whether to use it, and how much grain to
|
||||
//! keep.
|
||||
//! The learned denoise's settings: which network develops the photograph,
|
||||
//! if any, and how much grain to keep.
|
||||
//!
|
||||
//! Not an [`crate::operation::Operation`]: the learned stage replaces the
|
||||
//! demosaic and runs once per photograph, off the render path
|
||||
@@ -17,6 +17,12 @@ use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, ParamDescriptor,
|
||||
use crate::{OpId, ParamId};
|
||||
|
||||
pub const ID: OpId = OpId("learned_denoise");
|
||||
/// TRACES: FR-DEV-3g
|
||||
/// Which demosaic develops the photograph, a [`Method`] by index.
|
||||
pub const METHOD: ParamId = ParamId("method");
|
||||
/// What 0.21 and 0.22 stored instead of [`METHOD`]: on or off. Still read —
|
||||
/// off is [`Method::Bilinear`], on is the default network — so an edit saved
|
||||
/// by those releases keeps its look; never written, and not offered.
|
||||
pub const APPLY: ParamId = ParamId("apply");
|
||||
/// TRACES: FR-DEV-3g
|
||||
/// How strongly to denoise, 0–100: 100 is the network's result as it is, and
|
||||
@@ -27,9 +33,52 @@ pub const STRENGTH: ParamId = ParamId("strength");
|
||||
/// written, and not offered as a control.
|
||||
pub const GRAIN: ParamId = ParamId("grain");
|
||||
|
||||
/// On by default, at full strength: every Bayer raw is developed from the
|
||||
/// learned demosaic, and the switch and the slider are there to take it back
|
||||
/// or ease it off. It costs seconds per photograph the first time, while
|
||||
/// 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.
|
||||
#[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.
|
||||
Fast,
|
||||
/// One network the size of the first release's.
|
||||
Medium,
|
||||
/// Two experts, one for flat areas and one for edges, and a gate.
|
||||
Best,
|
||||
}
|
||||
|
||||
impl Method {
|
||||
pub const ALL: [Method; 4] = [Method::Bilinear, Method::Fast, Method::Medium, Method::Best];
|
||||
pub const DEFAULT: Method = Method::Best;
|
||||
|
||||
/// The sidecar's number for it.
|
||||
pub fn index(self) -> f32 {
|
||||
Self::ALL.iter().position(|m| *m == self).unwrap_or(0) as f32
|
||||
}
|
||||
|
||||
/// The method a stored number names; out of range is the default, as
|
||||
/// from a newer build with more of them.
|
||||
pub fn from_index(value: f32) -> Method {
|
||||
let i = value.round();
|
||||
if i >= 0.0 && (i as usize) < Self::ALL.len() {
|
||||
Self::ALL[i as usize]
|
||||
} else {
|
||||
Self::DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a network runs at all.
|
||||
pub fn learned(self) -> bool {
|
||||
self != Method::Bilinear
|
||||
}
|
||||
}
|
||||
|
||||
/// The best network by default, at full strength: every Bayer raw is
|
||||
/// developed from the learned demosaic, and the choice and the slider are
|
||||
/// there to take it back, trade it for time, or ease it off. It costs seconds per photograph the first time, while
|
||||
/// the classical demosaic shows; the result is cached, so a photograph
|
||||
/// reopened or exported does not pay again (docs/dev/denoise.md §7).
|
||||
pub(crate) static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||
@@ -37,7 +86,17 @@ pub(crate) static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||
id: ID,
|
||||
label: LocalizedKey("op.learned_denoise"),
|
||||
params: vec![
|
||||
ParamDescriptor::switch_on("apply", "param.learned_denoise.apply"),
|
||||
ParamDescriptor::choice(
|
||||
"method",
|
||||
"param.learned_denoise.method",
|
||||
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"),
|
||||
],
|
||||
)
|
||||
.with_default(Method::DEFAULT.index()),
|
||||
ParamDescriptor::scalar(
|
||||
"strength",
|
||||
"param.learned_denoise.strength",
|
||||
|
||||
@@ -457,3 +457,72 @@ under Detail. So:
|
||||
What it costs: every raw opened runs the network once, with the classical demosaic shown until the
|
||||
result lands, and a first export of an unopened raw runs it too. Every raw renders differently from
|
||||
0.21.0 unless switched off.
|
||||
|
||||
## 13. Three networks and a method (after 0.22.0)
|
||||
|
||||
The photographer asked for a choice between quality and time. `Method` replaces the Apply switch:
|
||||
`Bilinear`, `Fast`, `Medium`, `Best`, by index in that order, default `Best`. An untouched raw
|
||||
writes nothing and develops through `Best`. `apply` is still read and never written: 0 is
|
||||
`Bilinear`, 1 keeps a network already chosen or is the default. A number past the list, from a newer
|
||||
build, reads as the default. A build before this one ignores `method` and develops through its own
|
||||
network, which is the most an older peer can do.
|
||||
|
||||
**The networks** (darkroom-denoise, every one trained on the same data and noise as §11, plus 1,201
|
||||
further frames cropped from the library and 6,000 drawn scenes — polygons, lines of one to four
|
||||
photosites, text, gratings — rendered at 4× through a random affine and smooth displacement, so
|
||||
edges fall off the photosite grid):
|
||||
|
||||
| Method | File | Network | Parameters | GMAC / MP | Halo |
|
||||
|---|---|---|---|---|---|
|
||||
| Best | `mosaic-best-1408.onnx` | two U-Nets of §11's shape (a flat expert from `m2`, an edge expert from the ×100 edge-weighted run) and a 128 k-parameter gate that blends them per photosite | 6.4 M | 110 | 256 |
|
||||
| Medium | `mosaic-medium-1408.onnx` | §11's U-Net, distilled from Best (75 % its output, 25 % the truth) | 3.2 M | 48 | 192 |
|
||||
| Fast | `mosaic-fast-1408.onnx` | widths 16-32-64-128, blocks 1-1-1-2, distilled the same way | 0.93 M | 11 | 192 |
|
||||
|
||||
The gate learned on its own to trust the edge expert at 0.77–0.88 on edges and not at all on flat
|
||||
areas. The mixture's receptive field is the experts' plus the gate's, so it keeps the centre of a
|
||||
1408 tile past a 256 halo, where the single networks keep 1024 past 192. `dr_denoise::Shipped`
|
||||
carries each file's halo, and `TileNet::halo` hands it to the tiler.
|
||||
|
||||
**Quality.** PSNR after the display transform on 1,842 held-out crops, and the width of a hard
|
||||
edge on the drawn chart at ISO 6400 (truth 0.80 photosites; lower is sharper):
|
||||
|
||||
| | ISO 400 | 1600 | 6400 | 25600 | Edge width |
|
||||
|---|---|---|---|---|---|
|
||||
| §11's network | 40.61 | 39.77 | 38.43 | 36.67 | 1.77 |
|
||||
| Best | 40.69 | 39.84 | 38.47 | 36.71 | 0.82 |
|
||||
| Medium | 40.59 | 39.75 | 38.40 | 36.65 | 1.30 |
|
||||
| Fast | 39.90 | 39.06 | 37.54 | 35.33 | 1.84 |
|
||||
| Bilinear | 36.15 | 33.26 | 28.72 | 23.83 | 2.15 |
|
||||
|
||||
On photographs the three are close; on hard edges Best is half as wide as §11's network and
|
||||
Medium most of the way there. Fast costs a dB at high ISO and edges as soft as §11's.
|
||||
|
||||
**Speed**, a whole 20 MP 6D frame, the network alone, TensorRT fp16 on the laptop's RTX 3050
|
||||
(uncapped: memory at 5 GHz), engine already built: Best 2.48 s, Medium 0.79 s, Fast 0.57 s. Decode
|
||||
and the hot-pixel pass add about 0.5 s. The first build of each TensorRT engine takes 80 s (Fast) to
|
||||
190 s (Best), in the background at first launch, cached after.
|
||||
|
||||
**Before the network, two passes changed since §11.**
|
||||
|
||||
- *A noise-aware repair* (`dr_denoise::repair`) after the app's hot-pixel pass: a photosite more
|
||||
than 8σ beyond every same-colour neighbour *and* every adjacent photosite, and more than twice
|
||||
each adjacent one, is clamped to the brightest of its same-colour neighbours; a dead one, to the
|
||||
darkest. The ratio test is what spares a point of light, whose neighbours are lit too. The networks
|
||||
were trained behind the same pass (the Python and Rust agree: 935 repairs on an ISO 25600 frame).
|
||||
- *The tiler feeds the network without waiting*: tiles are gathered on every core by a producer
|
||||
thread one tile ahead, and the output is written back in parallel from the runtime's own buffer.
|
||||
0.14 s of tiler for a frame, which is what keeps Fast under a second.
|
||||
|
||||
**The Hexagon.** Each network has an `.a16w16.onnx` sibling made by `tools/quantise-models.sh
|
||||
--ranges`, the ranges from darkroom-3e's gate (96 training tiles, a third at noise ×2 and ×4). On
|
||||
the 6D gate A16W16 loses 0.00 dB for all three; with the noise scaled ×0.5–×4 at most 0.11 dB.
|
||||
A16W8 holds the gate (≤ 0.27 dB) but loses 0.63 dB on Medium at ×4, so A16W16 stays the form.
|
||||
|
||||
**Cache.** Each network keys its own results (§7.1 keys on the model's file name), and the file is
|
||||
hashed once at open, so changing the method never re-reads it. Choosing `Bilinear` keeps the
|
||||
network's result in memory for the way back; changing to another network drops it, and coming back
|
||||
reads the cache.
|
||||
|
||||
**Packaging.** All six files in the APK (`BUNDLED`, 23 entries, +44.6 MB, ~41 MB compressed); the
|
||||
three f32 networks in the Arch package and the Windows installer, which stage `models/denoise` by
|
||||
directory.
|
||||
|
||||
+16
-16
File diff suppressed because one or more lines are too long
+9
-4
@@ -134,11 +134,16 @@ declined, and the InsightFace grant of D13).
|
||||
|
||||
| File | Source | Trained on | Used by |
|
||||
|---|---|---|---|
|
||||
| `denoise/mosaic-1408.onnx` | trained from scratch in the `darkroom-denoise` repository (2026-10-03, run `m2`, 60 000 steps) | 427 of the maintainer's own base-ISO Canon EOS 6D raws, with the 6D's measured noise added | the learned demosaic and denoise (FR-DEV-3g) |
|
||||
| `denoise/mosaic-best-1408.onnx` | trained in the `darkroom-denoise` repository (2026-10-04, run `final`, 30 000 steps, from the experts of runs `m2` and `edges-100`) | 1,701 of the maintainer's own base-ISO raws and 6,000 synthetic scenes the repository draws itself, with the Canon EOS 6D's measured noise added | the learned demosaic and denoise, Best (FR-DEV-3g) |
|
||||
| `denoise/mosaic-medium-1408.onnx` | distilled from `final` in the same repository (2026-10-04, run `student-m`, 20 000 steps, from `m2`) | the same | Medium |
|
||||
| `denoise/mosaic-fast-1408.onnx` | distilled from `final` (2026-10-04, run `student-s`, 30 000 steps, from scratch) | the same | Fast |
|
||||
|
||||
A U-Net of plain 3×3 convolutions, ReLU, strided and transposed
|
||||
U-Nets of plain 3×3 convolutions, ReLU, strided and transposed
|
||||
convolutions and additive skips — no third-party architecture code or
|
||||
weights — at a fixed `1×1×1408×1408` for `mosaic` and `sigma`, exported by
|
||||
`python -m denoise.export` in `darkroom-denoise`. Trained only on
|
||||
weights — and, for Best, two of them blended per photosite by a small gate
|
||||
network of the same parts. Each at a fixed `1×1×1408×1408` for `mosaic`
|
||||
and `sigma`, exported by `python -m denoise.export` in `darkroom-denoise`;
|
||||
the `.a16w16.onnx` siblings are the same networks quantised for the
|
||||
Hexagon by `tools/quantise-models.sh`. Trained only on
|
||||
photographs the maintainer owns, so the weights carry no grant but the
|
||||
project's own: GPL-3.0-or-later, like the code (denoise.md §10).
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11
-8
@@ -124,12 +124,15 @@ package() {
|
||||
fi
|
||||
install -Dm644 "${_src}" "${pkgdir}/usr/share/darkroom/models/migan-512.onnx"
|
||||
|
||||
# The learned demosaic and denoise (the project's own weights, GPL —
|
||||
# models/LICENCE.md). Same pointer check, same directory.
|
||||
_src="models/denoise/mosaic-1408.onnx"
|
||||
if [[ "$(stat -c%s "${_src}")" -lt 100000 ]]; then
|
||||
echo "error: the denoise model is an LFS pointer — run: git lfs pull" >&2
|
||||
return 1
|
||||
fi
|
||||
install -Dm644 "${_src}" "${pkgdir}/usr/share/darkroom/models/mosaic-1408.onnx"
|
||||
# The learned demosaic and denoise, one network per method (the project's
|
||||
# own weights, GPL — models/LICENCE.md). Same pointer check, same
|
||||
# directory.
|
||||
for _net in fast medium best; do
|
||||
_src="models/denoise/mosaic-${_net}-1408.onnx"
|
||||
if [[ "$(stat -c%s "${_src}")" -lt 100000 ]]; then
|
||||
echo "error: the ${_net} denoise model is an LFS pointer — run: git lfs pull" >&2
|
||||
return 1
|
||||
fi
|
||||
install -Dm644 "${_src}" "${pkgdir}/usr/share/darkroom/models/mosaic-${_net}-1408.onnx"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -41,7 +41,11 @@ TABLE = {
|
||||
"migan-512": dict(dir="inpaint", form="a16w16", feed="migan"),
|
||||
"xfeat-1024": dict(dir="keypoints", form="int8", feed="xfeat", rewrites=["unfold", "resize"]),
|
||||
"xfeat-768": dict(dir="keypoints", form="int8", feed="xfeat", rewrites=["unfold", "resize"]),
|
||||
"mosaic-1408": dict(dir="denoise", form="a16w16", feed=None, rewrites=["bayer"]),
|
||||
"mosaic-fast-1408": dict(dir="denoise", form="a16w16", feed=None, rewrites=["bayer"]),
|
||||
"mosaic-medium-1408": dict(dir="denoise", form="a16w16", feed=None, rewrites=["bayer"]),
|
||||
# Exported with its packs as SpaceToDepth already; the rewrite finds
|
||||
# nothing to do.
|
||||
"mosaic-best-1408": dict(dir="denoise", form="a16w16", feed=None, rewrites=["bayer"]),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Produce the Hexagon's form of each model (docs/dev/inference.md §1.5, §5).
|
||||
#
|
||||
# ./tools/quantise-models.sh PHOTO_DIR [MODEL ...]
|
||||
# ./tools/quantise-models.sh --ranges RANGES.json mosaic-1408
|
||||
# ./tools/quantise-models.sh --ranges RANGES.json mosaic-medium-1408
|
||||
#
|
||||
# Writes `<stem>.<form>.onnx` beside each canonical file under models/: a QDQ
|
||||
# graph from QNN's own quantisation config, per-channel weights, in the form
|
||||
|
||||
+107
-43
@@ -18,6 +18,7 @@ use std::sync::Arc;
|
||||
|
||||
use dr_decode::RawImage;
|
||||
use dr_gpu::{DemosaicedImage, GrainBlend};
|
||||
use dr_pipeline::learned_denoise::Method;
|
||||
|
||||
use super::session::DevelopSession;
|
||||
|
||||
@@ -41,9 +42,12 @@ pub(crate) struct DenoiseState {
|
||||
failed: Option<String>,
|
||||
/// Where the noise figures came from, for the panel.
|
||||
source: Option<dr_denoise::Source>,
|
||||
/// The result's key in the on-disk cache: the file's bytes and the model
|
||||
/// (`denoise_cache::key`). `None` where there is no model to key on.
|
||||
cache_key: Option<String>,
|
||||
/// The file's bytes, hashed for the on-disk cache, which keys each
|
||||
/// network's result on them and the model (`denoise_cache::FileHash`).
|
||||
file_hash: Option<super::denoise_cache::FileHash>,
|
||||
/// The method the result, the job and the failure above are for. A
|
||||
/// different one asked for discards them.
|
||||
method: Option<Method>,
|
||||
}
|
||||
|
||||
struct Job {
|
||||
@@ -93,10 +97,10 @@ impl DevelopSession {
|
||||
self.denoise.profile = dr_decode::noise_profile(bytes);
|
||||
self.denoise.iso = meta.iso;
|
||||
// TRACES: FR-DEV-3g
|
||||
// The bytes are only here now, so the cache key is made now: a
|
||||
// reopened or exported photograph finds its result on disk.
|
||||
self.denoise.cache_key = crate::library::denoise_model()
|
||||
.map(|model| super::denoise_cache::key(bytes, &model));
|
||||
// The bytes are only here now, so they are hashed now: a
|
||||
// reopened or exported photograph finds its result on disk,
|
||||
// under whichever method it asks for.
|
||||
self.denoise.file_hash = Some(super::denoise_cache::FileHash::of(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +109,7 @@ impl DevelopSession {
|
||||
/// Cheap when nothing changed; the develop view calls it after every
|
||||
/// change to the edit, whatever made it — a slider, undo, a version.
|
||||
pub fn reconcile_denoise(&mut self) {
|
||||
self.forget_other_method();
|
||||
let wanted = self.graph.denoise_applied() && self.denoise.mosaic.is_some();
|
||||
if !wanted {
|
||||
if let Some(job) = self.denoise.job.take() {
|
||||
@@ -120,21 +125,11 @@ impl DevelopSession {
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(model) = crate::library::denoise_model() else {
|
||||
self.denoise.failed = Some("the denoise model is not installed".into());
|
||||
let Some(work) = self.work(Arc::new(AtomicBool::new(false))) else {
|
||||
return;
|
||||
};
|
||||
let cancel = work.cancel.clone();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let cancel = Arc::new(AtomicBool::new(false));
|
||||
let work = Work {
|
||||
ctx: self.ctx.clone(),
|
||||
raw: self.denoise.mosaic.clone().expect("checked above"),
|
||||
profile: self.denoise.profile.clone(),
|
||||
iso: self.denoise.iso,
|
||||
model,
|
||||
cancel: cancel.clone(),
|
||||
cache_key: self.denoise.cache_key.clone(),
|
||||
};
|
||||
crate::executors::spawn(crate::executors::Executor::Decode, "denoise", move || {
|
||||
let result = work.run(&mut |done, total| {
|
||||
let _ = tx.send(Msg::Progress(done, total));
|
||||
@@ -144,6 +139,51 @@ impl DevelopSession {
|
||||
self.denoise.job = Some(Job { rx, cancel });
|
||||
}
|
||||
|
||||
/// Drop what was computed, or is being computed, for a network the edit
|
||||
/// no longer asks for — a choice in the panel, an undo, a version.
|
||||
/// Each network's result stays in the on-disk cache, so going back to
|
||||
/// one is a read, not a run. The classical demosaic asks for no network
|
||||
/// and drops nothing: the result is kept for the way back.
|
||||
fn forget_other_method(&mut self) {
|
||||
let asked = self.graph.denoise_method();
|
||||
if !asked.learned() || self.denoise.method == Some(asked) {
|
||||
return;
|
||||
}
|
||||
if self.denoise.method.is_none() {
|
||||
// The first network asked for: nothing computed is another's.
|
||||
self.denoise.method = Some(asked);
|
||||
return;
|
||||
}
|
||||
if let Some(job) = self.denoise.job.take() {
|
||||
job.cancel.store(true, Ordering::Relaxed);
|
||||
}
|
||||
self.denoise.result = None;
|
||||
self.denoise.blended = None;
|
||||
self.denoise.failed = None;
|
||||
self.denoise.source = None;
|
||||
self.denoise.method = Some(asked);
|
||||
}
|
||||
|
||||
/// The job for the method the edit asks for, or `None` — with the reason
|
||||
/// kept as the failure — where its network is not installed.
|
||||
fn work(&mut self, cancel: Arc<AtomicBool>) -> Option<Work> {
|
||||
let raw = self.denoise.mosaic.clone()?;
|
||||
let Some((model, net)) = crate::library::denoise_model(self.graph.denoise_method()) else {
|
||||
self.denoise.failed = Some("the denoise model is not installed".into());
|
||||
return None;
|
||||
};
|
||||
Some(Work {
|
||||
ctx: self.ctx.clone(),
|
||||
raw,
|
||||
profile: self.denoise.profile.clone(),
|
||||
iso: self.denoise.iso,
|
||||
cache_key: self.denoise.file_hash.as_ref().map(|h| h.key(&model)),
|
||||
model,
|
||||
halo: net.halo,
|
||||
cancel,
|
||||
})
|
||||
}
|
||||
|
||||
/// Collect what the job sent since the last poll.
|
||||
pub fn poll_denoise(&mut self) -> DenoiseStatus {
|
||||
let Some(job) = &self.denoise.job else {
|
||||
@@ -182,9 +222,10 @@ impl DevelopSession {
|
||||
if !self.graph.denoise_applied() || self.denoise.result.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(raw) = self.denoise.mosaic.clone() else {
|
||||
self.forget_other_method();
|
||||
if self.denoise.mosaic.is_none() {
|
||||
return Ok(());
|
||||
};
|
||||
}
|
||||
// Already under way: wait for it rather than start again.
|
||||
if let Some(job) = self.denoise.job.take() {
|
||||
for msg in job.rx.iter() {
|
||||
@@ -193,15 +234,8 @@ impl DevelopSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
let model = crate::library::denoise_model().ok_or("the denoise model is not installed")?;
|
||||
let work = Work {
|
||||
ctx: self.ctx.clone(),
|
||||
raw,
|
||||
profile: self.denoise.profile.clone(),
|
||||
iso: self.denoise.iso,
|
||||
model,
|
||||
cancel: Arc::new(AtomicBool::new(false)),
|
||||
cache_key: self.denoise.cache_key.clone(),
|
||||
let Some(work) = self.work(Arc::new(AtomicBool::new(false))) else {
|
||||
return Err(self.denoise.failed.clone().unwrap_or_default());
|
||||
};
|
||||
let finished = work.run(&mut |_, _| {})?;
|
||||
self.land(finished)
|
||||
@@ -282,6 +316,8 @@ struct Work {
|
||||
profile: Option<Vec<(f32, f32)>>,
|
||||
iso: Option<u32>,
|
||||
model: std::path::PathBuf,
|
||||
/// The context `model` needs past a tile's kept centre.
|
||||
halo: usize,
|
||||
cancel: Arc<AtomicBool>,
|
||||
cache_key: Option<String>,
|
||||
}
|
||||
@@ -315,8 +351,8 @@ impl Work {
|
||||
.map_err(|e| e.to_string())?;
|
||||
let noise = dr_denoise::noise::for_frame_with(&raw, self.profile.as_deref(), self.iso)
|
||||
.ok_or("this photograph gives no way to measure its noise")?;
|
||||
let mut net =
|
||||
dr_denoise::onnx::OnnxNet::from_path(&self.model).map_err(|e| e.to_string())?;
|
||||
let mut net = dr_denoise::onnx::OnnxNet::from_path(&self.model, self.halo)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rung = net
|
||||
.rung()
|
||||
.map(|r| r.label().to_string())
|
||||
@@ -426,12 +462,18 @@ mod tests {
|
||||
|
||||
// On by default, at full strength: the network's result as it is.
|
||||
assert!(same(&s.developed_source(), &result));
|
||||
// Off: the classical demosaic, result or no result.
|
||||
s.graph
|
||||
.set_param(learned_denoise::ID, learned_denoise::APPLY, 0.0);
|
||||
// Bilinear: the classical demosaic, result or no result.
|
||||
s.graph.set_param(
|
||||
learned_denoise::ID,
|
||||
learned_denoise::METHOD,
|
||||
Method::Bilinear.index(),
|
||||
);
|
||||
assert!(same(&s.developed_source(), &classical));
|
||||
s.graph
|
||||
.set_param(learned_denoise::ID, learned_denoise::APPLY, 1.0);
|
||||
s.graph.set_param(
|
||||
learned_denoise::ID,
|
||||
learned_denoise::METHOD,
|
||||
Method::DEFAULT.index(),
|
||||
);
|
||||
// Less strength: a blend, made once per value and reused until it
|
||||
// moves.
|
||||
s.graph
|
||||
@@ -455,19 +497,41 @@ mod tests {
|
||||
let mut s =
|
||||
DevelopSession::open_owned(&ctx, bayer(64, 64), dr_types::Orientation::NORMAL).unwrap();
|
||||
s.denoise.failed = Some("no model".into());
|
||||
s.graph
|
||||
.set_param(learned_denoise::ID, learned_denoise::APPLY, 1.0);
|
||||
s.reconcile_denoise();
|
||||
assert!(
|
||||
s.denoise.job.is_none(),
|
||||
"a failure is not retried while the switch stays on"
|
||||
"a failure is not retried while the method stays"
|
||||
);
|
||||
s.graph.set_param(
|
||||
learned_denoise::ID,
|
||||
learned_denoise::METHOD,
|
||||
Method::Bilinear.index(),
|
||||
);
|
||||
s.graph
|
||||
.set_param(learned_denoise::ID, learned_denoise::APPLY, 0.0);
|
||||
s.reconcile_denoise();
|
||||
assert!(
|
||||
s.denoise.failed.is_none(),
|
||||
"toggling off is how a failure is retried"
|
||||
"choosing bilinear is how a failure is retried"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_network_discards_the_result_and_bilinear_keeps_it() {
|
||||
let Some(ctx) = headless() else { return };
|
||||
let mut s =
|
||||
DevelopSession::open_owned(&ctx, bayer(64, 64), dr_types::Orientation::NORMAL).unwrap();
|
||||
s.denoise.method = Some(Method::DEFAULT);
|
||||
landed(&mut s);
|
||||
let to = |s: &mut DevelopSession, m: Method| {
|
||||
s.graph
|
||||
.set_param(learned_denoise::ID, learned_denoise::METHOD, m.index());
|
||||
s.forget_other_method();
|
||||
};
|
||||
to(&mut s, Method::Bilinear);
|
||||
assert!(s.denoise.result.is_some(), "kept for the way back");
|
||||
to(&mut s, Method::DEFAULT);
|
||||
assert!(s.denoise.result.is_some());
|
||||
to(&mut s, Method::Fast);
|
||||
assert!(s.denoise.result.is_none(), "another network's picture");
|
||||
assert_eq!(s.denoise.method, Some(Method::Fast));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,17 +47,29 @@ pub fn dir() -> PathBuf {
|
||||
.unwrap_or_else(|| PathBuf::from("denoise-cache"))
|
||||
}
|
||||
|
||||
/// The key for a file's bytes under a model.
|
||||
pub fn key(bytes: &[u8], model: &Path) -> String {
|
||||
let mut h = Sha256::new();
|
||||
h.update(bytes);
|
||||
if let Some(name) = model.file_name() {
|
||||
h.update(name.to_string_lossy().as_bytes());
|
||||
/// A file's bytes, hashed once at open: each method's network keys its
|
||||
/// result from this, so changing the method does not read the file again.
|
||||
#[derive(Clone)]
|
||||
pub struct FileHash(Sha256);
|
||||
|
||||
impl FileHash {
|
||||
pub fn of(bytes: &[u8]) -> Self {
|
||||
let mut h = Sha256::new();
|
||||
h.update(bytes);
|
||||
FileHash(h)
|
||||
}
|
||||
|
||||
/// The key for these bytes under a model.
|
||||
pub fn key(&self, model: &Path) -> String {
|
||||
let mut h = self.0.clone();
|
||||
if let Some(name) = model.file_name() {
|
||||
h.update(name.to_string_lossy().as_bytes());
|
||||
}
|
||||
let size = std::fs::metadata(model).map(|m| m.len()).unwrap_or(0);
|
||||
h.update(size.to_le_bytes());
|
||||
let digest = h.finalize();
|
||||
digest.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
let size = std::fs::metadata(model).map(|m| m.len()).unwrap_or(0);
|
||||
h.update(size.to_le_bytes());
|
||||
let digest = h.finalize();
|
||||
digest.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
fn path_in(dir: &Path, key: &str) -> PathBuf {
|
||||
@@ -204,11 +216,11 @@ mod tests {
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let model = dir.join("m.onnx");
|
||||
std::fs::write(&model, b"weights").unwrap();
|
||||
let a = key(b"photo", &model);
|
||||
assert_eq!(a, key(b"photo", &model));
|
||||
assert_ne!(a, key(b"photo2", &model));
|
||||
let a = FileHash::of(b"photo").key(&model);
|
||||
assert_eq!(a, FileHash::of(b"photo").key(&model));
|
||||
assert_ne!(a, FileHash::of(b"photo2").key(&model));
|
||||
std::fs::write(&model, b"other weights").unwrap();
|
||||
assert_ne!(a, key(b"photo", &model));
|
||||
assert_ne!(a, FileHash::of(b"photo").key(&model));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,10 @@ pub fn init(runtime_dirs: Vec<PathBuf>) {
|
||||
(Role::EyeClassifier, crate::library::EYE_MODEL),
|
||||
(Role::EyeClassifier, crate::library::SUNGLASSES_MODEL),
|
||||
(Role::Inpainter, crate::library::INPAINT_MODEL),
|
||||
(Role::Denoiser, crate::library::DENOISE_MODEL),
|
||||
]);
|
||||
wanted.extend(
|
||||
[dr_denoise::FAST, dr_denoise::MEDIUM, dr_denoise::BEST].map(|n| (Role::Denoiser, n.file)),
|
||||
);
|
||||
let models: Vec<(Role, PathBuf)> = wanted
|
||||
.into_iter()
|
||||
.filter_map(|(role, name)| Some((role, crate::library::shared_model(name)?)))
|
||||
|
||||
@@ -223,6 +223,12 @@ fn catalogued(key: &str) -> Option<&'static str> {
|
||||
// panel under its own name — see `rows_filtered`.
|
||||
"param.lens_profile.apply" => "Apply",
|
||||
"param.learned_denoise.apply" => "Apply",
|
||||
// Which demosaic: the classical one, or a network by how long it takes.
|
||||
"param.learned_denoise.method" => "Method",
|
||||
"param.learned_denoise.method.bilinear" => "Bilinear",
|
||||
"param.learned_denoise.method.fast" => "Fast",
|
||||
"param.learned_denoise.method.medium" => "Medium",
|
||||
"param.learned_denoise.method.best" => "Best",
|
||||
// How strongly: 100 % is the network's result, and less puts the
|
||||
// removed noise's brightness back as grain.
|
||||
"param.learned_denoise.strength" => "Strength",
|
||||
|
||||
@@ -390,13 +390,28 @@ pub fn inpaint_model() -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3g
|
||||
/// The learned demosaic and denoise, as shipped in `models/denoise/`.
|
||||
pub const DENOISE_MODEL: &str = "mosaic-1408.onnx";
|
||||
/// The network a denoise method runs, as shipped in `models/denoise/`;
|
||||
/// `None` for the classical demosaic, which runs none.
|
||||
pub fn denoise_network(
|
||||
method: dr_pipeline::learned_denoise::Method,
|
||||
) -> Option<dr_denoise::Shipped> {
|
||||
use dr_pipeline::learned_denoise::Method;
|
||||
match method {
|
||||
Method::Bilinear => None,
|
||||
Method::Fast => Some(dr_denoise::FAST),
|
||||
Method::Medium => Some(dr_denoise::MEDIUM),
|
||||
Method::Best => Some(dr_denoise::BEST),
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3g
|
||||
/// Where the denoise model is, by the border filler's search.
|
||||
pub fn denoise_model() -> Option<PathBuf> {
|
||||
shared_model(DENOISE_MODEL)
|
||||
/// Where a method's network is, by the border filler's search, and the
|
||||
/// context it needs.
|
||||
pub fn denoise_model(
|
||||
method: dr_pipeline::learned_denoise::Method,
|
||||
) -> Option<(PathBuf, dr_denoise::Shipped)> {
|
||||
let net = denoise_network(method)?;
|
||||
Some((shared_model(net.file)?, net))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user