Files
dtourolle 8f9e59b9fa Find hot photosites without repairing them, and measure a sensor's aging
The hot-pixel pass could only repair: it returned how many photosites it
changed and threw away which. find_hot_pixels runs the same pass and
returns them as sensor coordinates, leaving the frame alone, so a sensor's
defects can be tracked across frames.

sensor_scan prints each frame's candidates, and with --probe reads a list
of coordinates back out of every frame. Run over 53 6D raws from 2015 to
2026, it found 32 persistent defects, 2 in 2015 and 32 by 2026, and showed
what a defect map has to account for: a frame that does not flag a
photosite proves nothing unless its neighbourhood is dark, and the 6D
hides some of its defects itself above ISO 5000. docs/dev/sensor-health.md
records the findings and the design they argue for.
2026-10-04 02:38:48 -04:00

118 lines
4.5 KiB
Rust

//! List each RAW file's hot and dead photosite candidates (docs/dev/sensor-health.md).
//!
//! Reads paths on stdin and prints one JSON line per file: its capture
//! conditions and every photosite [`Demosaicer::find_hot_pixels`] flags, as
//! `[x, y, value, hot]` in sensor coordinates. Which candidates are defects
//! is a question across frames, so this answers nothing on its own.
//!
//! ```sh
//! find ~/Pictures -name '*.CR2' | cargo run --release -p dr-gpu --example sensor_scan
//! ```
use std::io::{BufRead, Write};
use dr_gpu::{Demosaicer, GpuContext};
fn main() {
if let Some(list) = std::env::args().skip_while(|a| a != "--probe").nth(1) {
return probe(&list);
}
let ctx = pollster::block_on(GpuContext::new_headless()).expect("a GPU for the hot-pixel pass");
let demosaicer = Demosaicer::new(&ctx).expect("demosaicer");
for line in std::io::stdin().lock().lines() {
let path = line.expect("stdin");
match scan(&path, &demosaicer) {
Ok(json) => println!("{json}"),
Err(e) => eprintln!("fail\t{path}\t{e}"),
}
std::io::stdout().flush().ok();
}
}
fn scan(path: &str, demosaicer: &Demosaicer) -> Result<String, String> {
let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
let raw = dr_decode::decode(&bytes).map_err(|e| e.to_string())?;
let meta = dr_decode::metadata(&bytes).map_err(|e| e.to_string())?;
let sites = demosaicer
.find_hot_pixels(&raw)
.map_err(|e| e.to_string())?;
let opt = |v: Option<f64>| v.map_or("null".to_string(), |v| v.to_string());
let list: Vec<String> = sites
.iter()
.map(|s| {
let v = raw.data[(s.y * raw.width + s.x) as usize];
format!("[{},{},{},{}]", s.x, s.y, v, u8::from(s.hot))
})
.collect();
Ok(format!(
"{{\"path\":{:?},\"model\":{:?},\"captured\":{},\"iso\":{},\"shutter\":{},\"white\":{},\"black\":{:?},\"crop\":[{},{},{},{}],\"sites\":[{}]}}",
path,
format!("{} {}", raw.make, raw.model),
meta.captured_at.map_or("null".to_string(), |t| t.to_string()),
opt(meta.iso.map(f64::from)),
opt(meta.shutter.map(f64::from)),
raw.white_level,
raw.black_level,
raw.crop.x,
raw.crop.y,
raw.crop.width,
raw.crop.height,
list.join(","),
))
}
/// `--probe COORDS`: for each path on stdin, each `x y` line of COORDS as
/// `[value, same-colour neighbour max, median]` over black, on the CPU. A
/// probe asks whether a photosite stood out in a frame where it would have
/// been visible, which the scan's verdict cannot say: a frame that does not
/// flag a defect may only have been too bright around it.
fn probe(list: &str) {
let coords: Vec<(u32, u32)> = std::fs::read_to_string(list)
.expect("coords")
.lines()
.filter_map(|l| {
let mut it = l.split_whitespace().map(|v| v.parse().ok());
Some((it.next()??, it.next()??))
})
.collect();
for line in std::io::stdin().lock().lines() {
let path = line.expect("stdin");
let Ok(bytes) = std::fs::read(&path) else {
continue;
};
let (Ok(raw), Ok(meta)) = (dr_decode::decode(&bytes), dr_decode::metadata(&bytes)) else {
continue;
};
let w = raw.width as i64;
let at = |x: i64, y: i64| {
let cell = (((y - raw.crop.y as i64) & 1) * 2 + ((x - raw.crop.x as i64) & 1)) as usize;
raw.data[(y * w + x) as usize].saturating_sub(raw.black_level[cell])
};
let rows: Vec<String> = coords
.iter()
.map(|&(x, y)| {
let (x, y) = (x as i64, y as i64);
let mut n: Vec<u16> = Vec::new();
for dy in [-2i64, 0, 2] {
for dx in [-2i64, 0, 2] {
if (dx, dy) != (0, 0) {
n.push(at(x + dx, y + dy));
}
}
}
n.sort_unstable();
format!("[{},{},{}]", at(x, y), n[n.len() - 1], n[n.len() / 2])
})
.collect();
println!(
"{{\"path\":{:?},\"captured\":{},\"iso\":{},\"shutter\":{},\"range\":{},\"p\":[{}]}}",
path,
meta.captured_at.unwrap_or(0),
meta.iso.unwrap_or(0),
meta.shutter.unwrap_or(0.0),
raw.white_level - raw.black_level[0],
rows.join(","),
);
}
}