Files
DarkRoom/core/dr-pipeline/src/learned_denoise.rs
T
dtourolle a9271c4850 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.
2026-10-07 06:58:24 -04:00

140 lines
5.5 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
//! 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
//! (docs/dev/denoise.md §2, §7), and the grain is a blend of its result with
//! the classical one, done where the source is chosen. But what a
//! photographer sets travels the one road every setting travels — the
//! capability list feeds the panel, [`crate::Preset`] captures it, the
//! sidecar stores it, the undo stack replays it (FR-DEV-3c) — so it is
//! published as a capability, like the lens profile switch.
use std::sync::{Arc, LazyLock};
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, ParamDescriptor, Scale, Unit};
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
/// lower puts the removed noise's brightness back as grain.
pub const STRENGTH: ParamId = ParamId("strength");
/// What 0.21.0 stored instead of [`STRENGTH`]: the grain kept, its inverse.
/// Still read, so an edit saved by that release keeps its look; never
/// written, and not offered as a control.
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. 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 Best's work.
Fast,
/// 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; 3] = [Method::Bilinear, Method::Fast, 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(|| {
Arc::new(OpDescriptor {
id: ID,
label: LocalizedKey("op.learned_denoise"),
params: vec![
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.best"),
],
)
.with_default(Method::DEFAULT.index()),
ParamDescriptor::scalar(
"strength",
"param.learned_denoise.strength",
0.0,
100.0,
100.0,
Unit::Percent,
Scale::Linear,
0,
),
],
// With the classical noise reduction, which is what a photographer
// looks for it beside.
attributes: vec![Attribute::Detail],
})
});
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);
}
}