Files
DarkRoom/core/dr-segment/examples/detect.rs
T
dtourolleandClaude Opus 5 c75849040c Format the tree the way the gate asks for it
`cargo fmt --check` is a required step and had drifted across 45 files. Most of
it arrived this week: several operations were written in parallel worktrees and
merged by hand, and a hand-merge resolves conflicts without ever running the
formatter over the result.

No behaviour changes — this is `cargo fmt --all` and nothing else, kept as its
own commit so the next reader can skip it wholesale rather than search it for
one that matters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 21:16:34 +02:00

148 lines
5.2 KiB
Rust

//! Run the semantic arm over a JPEG and write what it found.
//!
//! The point of S15 step 2 applied to arm B: no amount of unit testing settles
//! whether the decode is right, because a transposed axis or an off-by-one in
//! the letterbox produces perfectly plausible numbers and a mask sitting six
//! pixels to the left. Looking at the overlay settles it in one glance.
//!
//! ```sh
//! cargo run -p dr-segment --example detect --release -- photo.jpg
//! cargo run -p dr-segment --example detect --release -- photo.jpg out 0.25 tiled
//! ```
//!
//! Writes `<prefix>-overlay.ppm` — the image with each instance tinted by a
//! per-instance colour — and prints the detection list. PPM for the same
//! reason the other examples use it: no encoder dependency, and every viewer
//! reads it.
use dr_segment::semantic::{SemanticModel, SemanticOptions, Tiling};
fn main() {
env_logger::init();
let mut args = std::env::args().skip(1);
let Some(path) = args.next() else {
eprintln!("usage: detect <photo.jpg> [out-prefix] [confidence] [tiled]");
std::process::exit(2);
};
let prefix = args.next().unwrap_or_else(|| "detect".into());
let confidence = args
.next()
.and_then(|s| s.parse().ok())
.unwrap_or(SemanticOptions::default().confidence);
let tiled = args.next().is_some_and(|s| s == "tiled");
let (rgb, width, height) = load_jpeg(&path);
println!("image {width}x{height}");
let options = SemanticOptions {
confidence,
tiling: if tiled {
Tiling::Grid { overlap: 0.25 }
} else {
Tiling::Whole
},
..SemanticOptions::default()
};
println!(
"tiling {}",
if tiled {
"grid, 25% overlap"
} else {
"whole frame"
}
);
let t0 = std::time::Instant::now();
let mut model = SemanticModel::embedded().expect("load embedded model");
println!("load {:.0} ms", t0.elapsed().as_secs_f32() * 1000.0);
let t1 = std::time::Instant::now();
let instances = model
.detect(&rgb, width, height, &options)
.expect("inference");
println!("detect {:.0} ms", t1.elapsed().as_secs_f32() * 1000.0);
println!("found {} instances", instances.len());
for (i, inst) in instances.iter().enumerate() {
let covered = inst.mask.iter().filter(|&&m| m >= 0.5).count();
println!(
" [{i:2}] {:<14} {:.2} box ({:.0},{:.0})-({:.0},{:.0}) {:.1}% of frame",
inst.class_name,
inst.score,
inst.bbox.0,
inst.bbox.1,
inst.bbox.2,
inst.bbox.3,
100.0 * covered as f32 / (width * height) as f32,
);
}
// Tint each instance and write the composite. A mask in the wrong place is
// obvious here and invisible in the numbers above.
let mut out = vec![0u8; width * height * 3];
for (p, px) in out.chunks_exact_mut(3).enumerate() {
for c in 0..3 {
px[c] = (rgb[p * 3 + c].clamp(0.0, 1.0) * 255.0) as u8;
}
}
for (i, inst) in instances.iter().enumerate() {
let tint = colour(i);
for (p, &m) in inst.mask.iter().enumerate() {
if m < 0.5 {
continue;
}
let px = &mut out[p * 3..p * 3 + 3];
for c in 0..3 {
px[c] = ((px[c] as f32) * 0.45 + tint[c] as f32 * 0.55) as u8;
}
}
}
let file = format!("{prefix}-overlay.ppm");
write_ppm(&file, &out, width, height);
println!("wrote {file}");
}
/// A distinct colour per instance index — the same golden-angle walk the
/// watershed example uses, so the two overlays are read the same way.
fn colour(i: usize) -> [u8; 3] {
let h = (i as f32 * 137.508) % 360.0;
let (c, x) = (255.0, 255.0 * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs()));
let (r, g, b) = match (h / 60.0) as u32 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
[r as u8, g as u8, b as u8]
}
fn load_jpeg(path: &str) -> (Vec<f32>, usize, usize) {
let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("read {path}: {e}"));
let mut decoder = zune_jpeg::JpegDecoder::new(&bytes);
let pixels = decoder.decode().expect("decode jpeg");
let info = decoder.info().expect("jpeg info");
let (w, h) = (info.width as usize, info.height as usize);
// The model was trained on gamma-encoded sRGB, so the JPEG's own values go
// through unlinearised — this is one of the few places in the codebase
// where *not* linearising is the correct thing to do.
let rgb = match pixels.len() / (w * h) {
3 => pixels.iter().map(|&v| v as f32 / 255.0).collect(),
1 => pixels.iter().flat_map(|&v| [v as f32 / 255.0; 3]).collect(),
n => panic!("unexpected {n} components per pixel"),
};
(rgb, w, h)
}
fn write_ppm(path: &str, rgb: &[u8], width: usize, height: usize) {
use std::io::Write;
let mut f = std::io::BufWriter::new(std::fs::File::create(path).expect("create ppm"));
write!(f, "P6\n{width} {height}\n255\n").expect("ppm header");
f.write_all(rgb).expect("ppm body");
}