mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-11 16:38:24 -06:00
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:
co-authored by
Brandon Thomas
parent
17c85e4175
commit
7edc7aa03d
@@ -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
|
||||
|
||||
@@ -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)] {
|
||||
|
||||
@@ -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 | |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user