LR-IMP-CAMERA-COVERAGE: measured image area for Nikon bodies without CropArea

Nikon bodies from before ~2016 record no CropArea, so the decoder kept
their whole sensor data, less trailing dark columns. Where that data
holds masked lines above black or optical black, they were developed as
a coloured or dark bar on that edge: the D7000 and D5100's 36 columns at
~600 (#976), the D800's 14 (#914, the top in portrait), the D600 / D610's
8 (#1236), the D3200's 46, and others.

MEASURED_BODIES (crates/raw/src/vendor/nef.rs) now gives 44 more model /
data size / depth groups the frame of their camera JPEG inside the data,
next to the D4, D7100 and D90 entries already there: D7000, D5100, D800,
D800E, Df, the D4's 12-bit mode, D3, D3S, D700, D300, D300S, D600, D610,
D810, D4S, D5600, Coolpix A, D3000, D40X, D60, D80, D2Xs, D200, D3200
and Nikon 1 V1; and, on best-guess frames short of the evidence rule,
each with a comment naming its evidence, the D3100 (3 of 4 clean files
agree), D5000 (4 of 5, and the D90's sensor), D40 (2 of 2), D50 and D70
(one file each, agreeing) and D70s (next to them, between pixels).
Every best-guess frame keeps the masked lines out by several pixels.
An entry matches model, data size and depth exactly. Bodies whose data
holds only picture beyond the camera frame get no entry and render as
before.

`cargo xtask raw-frame FILE...` (crates/engine/examples/raw_frame.rs)
measures a body: it aligns each raw's embedded camera JPEG with the
sensor data (half-size luminance, gradient correlation, sub-pixel peak;
weak, ambiguous or between-pixel peaks are flagged), counts the edge
lines that hold no picture and reports what the decoder's crop keeps
beyond the camera frame. A masked line is flat and its two CFA phases
agree within the noise or within 0.4% of white, so filler with a small
fixed offset between them (D90, D5000) counts. It prints entries only
with evidence: at least 3 clean files agreeing exactly, of the model
(12- and 14-bit pooled) or, with at least one of its own, with sibling
models of the same readout (data and JPEG size) that agree on the same
frame. On the D7500, which records its CropArea, it finds that area
exactly; it declines the Z f, whose JPEG is distortion-corrected. It
works for any make whose raw embeds a full-size camera JPEG: on Sony,
Canon CR2 / CR3, Pentax and Samsung files it finds the decoder's own
crop (relative to the active area).

Every Nikon raw on raw.pixls.us without CropArea was surveyed this way
(its 138 CC0 samples, plus 124 other publicly posted sample files; only
the measurements are committed). Bodies whose data holds only picture
beyond the frame need no entry (D750, D5300, D5500, D3300, D2X, D2Hs,
D3X, D5200, Coolpix P6000, Nikon 1 AW1 / J3 / V2 / V3, the D800's crop
modes); raw-parity.md lists them, and no longer claims every NEF
coding but HE decodes. measured_bodies_are_consistent checks every entry lies
inside its data and is listed once; measured_older_dx_bodies_image_area
covers the D200, D3200, Nikon 1 V1 and best-guess entries.

Fixes #976
Fixes #914
Fixes #1236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
robmdunnandClaude Opus 5.5 committed 2026-10-14 22:40:06 -04:00
1 parent af4c5e102c
commit 2b3b6e7871
7 files changed
+700 -9

No files matched your search

+20
View File
@@ -37,6 +37,11 @@ commands:
corpus [--download]
show where test corpora live; --download fetches PngSuite and CC0 raw samples (raw.pixls.us) into corpus/ and checks their sha256
stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`)
raw-frame [--json] FILE…
measure where each raw's embedded camera JPEG sits in its sensor data, which edge bands hold no
picture and what the decoder's crop keeps beyond the camera frame (any make with a full-size
embedded JPEG); print image-area entries for Nikon bodies that record no crop (MEASURED_BODIES in
crates/raw/src/vendor/nef.rs)
";
fn main() -> ExitCode {
@@ -54,6 +59,7 @@ fn main() -> ExitCode {
Some("ci") => cmd_ci(),
Some("corpus") => cmd_corpus(rest.contains(&"--download")),
Some("stats") => stats::run(&root(), rest.contains(&"--exact")),
Some("raw-frame") => cmd_raw_frame(&rest),
Some("-h" | "--help" | "help") | None => {
print!("{USAGE}");
Ok(())
@@ -69,6 +75,20 @@ fn main() -> ExitCode {
}
}
/// `cargo xtask raw-frame FILE…`: runs the `raw_frame` example of `lightcraft-engine` on the files.
fn cmd_raw_frame(files: &[&str]) -> Result<(), String> {
if files.is_empty() {
return Err("raw-frame needs one or more raw files (several per camera, with detail across the frame)".into());
}
eprintln!("$ cargo run --release -p lightcraft-engine --example raw_frame -- {}", files.join(" "));
let status = cargo()
.args(["run", "--release", "-q", "-p", "lightcraft-engine", "--example", "raw_frame", "--"])
.args(files)
.status()
.map_err(|e| format!("raw_frame: {e}"))?;
if status.success() { Ok(()) } else { Err("raw_frame failed".into()) }
}
/// Workspace root (parent of the xtask crate).
pub fn root() -> PathBuf {
// `cargo run` sets it at run time; prefer that over the compile-time value, which goes stale