Develop every raw through the AI denoise by default, with a strength, cached
The learned demosaic was an option under Detail, off by default. It is now how a Bayer raw is developed: on by default at full strength on every device — which hardware runs it is the inference engine's choice — and first in the Adjust panel, since it decides what every control below is applied to. Strength (0-100, default 100) replaces Keep grain: grain = 100 - strength, the same luminance-only blend, so moving it is one GPU pass and never a re-run. 0.21.0's sidecars stored grain; it is still read, as the inverse, and never written. With it on for every photograph, the result is now kept on disk (denoise.md §7.1, §12): the network's output as half floats, keyed on a SHA-256 of the file's bytes and the model, oldest first past a 5 GB budget, beside the inference engine's cache. A reopened photograph and an export of one already developed read it back instead of running the network again; a damaged entry is a miss.
This commit is contained in:
@@ -41,6 +41,9 @@ 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>,
|
||||
}
|
||||
|
||||
struct Job {
|
||||
@@ -89,6 +92,11 @@ impl DevelopSession {
|
||||
if self.denoise.mosaic.is_some() {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +133,7 @@ impl DevelopSession {
|
||||
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| {
|
||||
@@ -192,6 +201,7 @@ impl DevelopSession {
|
||||
iso: self.denoise.iso,
|
||||
model,
|
||||
cancel: Arc::new(AtomicBool::new(false)),
|
||||
cache_key: self.denoise.cache_key.clone(),
|
||||
};
|
||||
let finished = work.run(&mut |_, _| {})?;
|
||||
self.land(finished)
|
||||
@@ -273,11 +283,30 @@ struct Work {
|
||||
iso: Option<u32>,
|
||||
model: std::path::PathBuf,
|
||||
cancel: Arc<AtomicBool>,
|
||||
cache_key: Option<String>,
|
||||
}
|
||||
|
||||
impl Work {
|
||||
fn run(self, progress: &mut dyn FnMut(usize, usize)) -> Result<Finished, String> {
|
||||
let started = std::time::Instant::now();
|
||||
// TRACES: FR-DEV-3g
|
||||
// A result computed before — this photograph opened earlier, or
|
||||
// developed and now exported — is read back rather than recomputed.
|
||||
let cache_dir = super::denoise_cache::dir();
|
||||
if let Some(hit) = self
|
||||
.cache_key
|
||||
.as_deref()
|
||||
.and_then(|key| super::denoise_cache::load(&cache_dir, key))
|
||||
{
|
||||
return Ok(Finished {
|
||||
rgb: hit.rgb,
|
||||
width: hit.width,
|
||||
height: hit.height,
|
||||
source: hit.source,
|
||||
rung: "the cache".into(),
|
||||
seconds: started.elapsed().as_secs_f64(),
|
||||
});
|
||||
}
|
||||
// The app's own hot-pixel pass, on a copy: the classical source was
|
||||
// repaired by the same pass inside `Demosaicer::run`.
|
||||
let mut raw = (*self.raw).clone();
|
||||
@@ -299,14 +328,24 @@ impl Work {
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or("stopped")?;
|
||||
Ok(Finished {
|
||||
let finished = Finished {
|
||||
rgb,
|
||||
width: raw.crop.width,
|
||||
height: raw.crop.height,
|
||||
source: noise.source,
|
||||
rung,
|
||||
seconds: started.elapsed().as_secs_f64(),
|
||||
})
|
||||
};
|
||||
if let Some(key) = self.cache_key.as_deref() {
|
||||
let entry = super::denoise_cache::Entry {
|
||||
rgb: finished.rgb.clone(),
|
||||
width: finished.width,
|
||||
height: finished.height,
|
||||
source: finished.source,
|
||||
};
|
||||
super::denoise_cache::store(&cache_dir, key, &entry, super::denoise_cache::BUDGET);
|
||||
}
|
||||
Ok(finished)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,15 +424,18 @@ mod tests {
|
||||
let result = s.denoise.result.clone().unwrap();
|
||||
let same = |a: &Arc<DemosaicedImage>, b: &Arc<DemosaicedImage>| Arc::ptr_eq(a, b);
|
||||
|
||||
// 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);
|
||||
assert!(same(&s.developed_source(), &classical));
|
||||
// On, no grain: the network's result as it is.
|
||||
s.graph
|
||||
.set_param(learned_denoise::ID, learned_denoise::APPLY, 1.0);
|
||||
assert!(same(&s.developed_source(), &result));
|
||||
// Grain: a blend, made once per value and reused until it moves.
|
||||
// Less strength: a blend, made once per value and reused until it
|
||||
// moves.
|
||||
s.graph
|
||||
.set_param(learned_denoise::ID, learned_denoise::GRAIN, 40.0);
|
||||
.set_param(learned_denoise::ID, learned_denoise::STRENGTH, 60.0);
|
||||
let blended = s.developed_source();
|
||||
assert!(!same(&blended, &result) && !same(&blended, &classical));
|
||||
assert!(
|
||||
@@ -401,7 +443,7 @@ mod tests {
|
||||
"the same grain must not blend again"
|
||||
);
|
||||
s.graph
|
||||
.set_param(learned_denoise::ID, learned_denoise::GRAIN, 60.0);
|
||||
.set_param(learned_denoise::ID, learned_denoise::STRENGTH, 40.0);
|
||||
assert!(!same(&s.developed_source(), &blended));
|
||||
// The sensor's own reading stays the classical one throughout.
|
||||
assert!(same(&s.demosaiced, &classical));
|
||||
|
||||
Reference in New Issue
Block a user