Raw: measure the Nikon D90 image area (4288x2848 at (10, 10)) (#4323)

Aligning the decoded D90 sensor plane with the camera's own JPEG puts the
image at (10, 10) in the 4352x2868 sensor plane, giving the camera's
4288x2848 frame (10-pixel margins; the right border reads the camera's flat
255 from column 4310). Add the measured ImageArea so a D90 NEF opens on the
camera's frame instead of the uncropped plane, and make a measured profile
leave a file that carries its own colour calibration (a DNG) untouched.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Vikram Bhaskaran
2026-10-15 00:22:48 -04:00
committed by GitHub
co-authored by Brandon Thomas
parent 17c85e4175
commit 7edc7aa03d
4 changed files with 48 additions and 2 deletions
+2 -1
View File
@@ -123,7 +123,8 @@ smallest preview at least 512 pixels long, fits and develops in one go.
Camera Raw's tuned import fits at the default exposure and as-shot white balance first,
then applies the user's Exposure, Temperature and Tint to that same rendering.
The D5100's measured image area is 4928 × 3264 at (8, 8) in a 4992 × 3280
sensor plane; it removes the border without changing levels, WB or calibration.
sensor plane; the D90's is 4288 × 2848 at (10, 10) in a 4352 × 2868 plane; each
removes the border without changing levels, WB or calibration.
Measured image areas preserve an explicit file crop.
For a slightly tighter camera JPEG, fitting can align the reference proxy using a bounded
+40
View File
@@ -127,6 +127,10 @@ const PROFILES: &[Profile] = &[Profile {
/// 4992 x 3280 sensor data, giving the camera JPEG's 4928 x 3264 frame.
/// * **ILCE-7** (2 files): 12 x 12 pixels in from the left and top of the
/// 6048 x 4024 cRAW data (36 and 12 pixels of margin on the right and bottom).
/// * **D90** (1 file): 10 x 10 pixels in from the left and top of the
/// 4352 x 2868 sensor data, giving the camera JPEG's 4288 x 2848 frame
/// (10 pixels of margin on the right and bottom; the right border reads the
/// camera's flat 255 from column 4310 on).
struct ImageArea {
make: &'static str,
model: &'static str,
@@ -137,6 +141,7 @@ struct ImageArea {
}
const IMAGE_AREAS: &[ImageArea] = &[
ImageArea { make: "NIKON CORPORATION", model: "NIKON D90", size: (4352, 2868), crop: (10, 10, 4288, 2848) },
ImageArea { make: "NIKON CORPORATION", model: "NIKON D5100", size: (4992, 3280), crop: (8, 8, 4928, 3264) },
ImageArea { make: "NIKON CORPORATION", model: "NIKON D800E", size: (7424, 4924), crop: (8, 6, 7360, 4912) },
ImageArea { make: "SONY", model: "ILCE-7", size: (6048, 4024), crop: (12, 12, 6000, 4000) },
@@ -152,6 +157,11 @@ pub(crate) fn apply(s: &mut Sensor) {
s.crop = Rect::new(x, y, w, h);
}
let Some(p) = PROFILES.iter().find(|p| make == p.make && model == p.model && (s.width, s.height) == p.size) else { return };
// A raw with its own colour calibration (a DNG) keeps it: only the measured image area
// above applies. The measured profile stands in for files that carry no matrix themselves.
if !s.color.calibrations.is_empty() {
return;
}
let (x, y, w, h) = p.crop;
s.crop = Rect::new(x, y, w, h);
s.white = [p.white; 3];
@@ -265,6 +275,36 @@ mod tests {
assert_eq!(s.crop, Rect::new(0, 0, 7424, 4924));
}
#[test]
fn d90_image_area_matches_the_camera_jpeg() {
let mut s = nef("NIKON D90", 4352, 2868);
apply(&mut s);
assert_eq!(s.crop, Rect::new(10, 10, 4288, 2848));
// Only the image area is measured: levels, colour and tone stay untouched.
assert!(s.color.calibrations.is_empty() && s.tone_curve.is_empty());
assert_eq!(s.white, [16383.0; 3]);
// An explicit crop in the file wins over the measured area.
s.crop = Rect::new(0, 0, 4288, 2848);
apply(&mut s);
assert_eq!(s.crop, Rect::new(0, 0, 4288, 2848));
}
#[test]
fn a_file_with_its_own_calibration_keeps_it() {
// A file that carries its own colour calibration (a DNG, or any calibrated raw) keeps
// it: the measured profile must not override it (only the image area would apply).
let mut s = nef("NIKON D4", 4992, 3292);
s.color = ColorInfo {
calibrations: vec![Calibration { temperature: 6504.0, color_matrix: IDENTITY, forward_matrix: None, camera_calibration: IDENTITY }],
..ColorInfo::default()
};
apply(&mut s);
assert_eq!(s.color.calibrations.len(), 1);
assert_eq!(s.color.calibrations[0].temperature, 6504.0, "the file's own calibration stands");
assert!(s.tone_curve.is_empty() && s.black_point == 0.0);
assert_eq!(s.crop, Rect::new(0, 0, 4992, 3292), "the D4 has no measured image area");
}
#[test]
fn other_cameras_and_sizes_are_left_alone() {
for (model, w, h) in [("NIKON D4S", 4992, 3292), ("NIKON D4", 2496, 1646), ("NIKON D800", 7424, 4924)] {
+1 -1
View File
@@ -66,7 +66,7 @@ supports many more, including CR3).
| DNG (incl. LJPEG and JPEG XL tiles) | yes; lossy DNG no | FILE-215-11 |
| Canon CR2 | yes | |
| Canon CR3 | detected, unsupported | `crates/raw/src/lib.rs:219` |
| Nikon NEF/NRW (lossless, lossy) | yes; "lossy after split" no | #50; D5100 image area measured on three 14-bit lossy NEFs; [validation](nikon-nef-development.md) |
| Nikon NEF/NRW (lossless, lossy) | yes; "lossy after split" no | #50; D5100 and D90 image areas measured (14-bit NEFs); [validation](nikon-nef-development.md) |
| Sony ARW/SR2/SRF (incl. compressed) | yes | |
| Panasonic RW2, Pentax PEF | yes | |
| Olympus ORF | uncompressed only | |
+5
View File
@@ -129,6 +129,10 @@ what an agent can't finish alone.
tuning retains the fitted look and applies user gains afterwards. Three D5100 NEFs
establish a guarded 4928 × 3264 image area. This is an estimated rendering, not a
measured D5100 colour calibration; [validation](nikon-nef-development.md).
- **Progress (2026-10-13):** the D90's image area (4288 × 2848 at (10, 10) in a
4352 × 2868 plane) is measured by aligning the decoded plane with the camera's own
JPEG, so a D90 NEF opens on the camera's frame instead of the uncropped sensor plane;
`crates/raw/src/cameras.rs` (`IMAGE_AREAS`).
- **Estimate:** 120–200 h.
### G15. AI and generative features
@@ -263,6 +267,7 @@ what an agent can't finish alone.
| Date | Change | Summary |
|---|---|---|
| 2026-10-13 | minor | G14: D90 image area measured (4288 × 2848 at (10, 10)); gap remains open |
| 2026-10-14 | minor | Corpus floors raised to the measured results of the CMYK, Lab, 32-bit, Levels, Threshold, Posterize, feather and pass-through fixes landed together: Photoshop-authored 165 → 185 of 258, io 146 → 148 of 170, psd-tools 242 → 249 of 309 |
| 2026-10-13 | minor | G1: CMYK Hue/Saturation and Color Balance work on C, M and Y as inverted RGB with black kept, as Photoshop does; cmyk hue-saturation 100% → 0.4–0.7% bad pixels, color-balance 74% → 25%; floor unchanged |
| 2026-10-13 | minor | G1: CMYK Posterize works on the stored values (1 − ink); psd-tools posterize_cmyk passes; psd-tools floor → 242 of 309 |