Merge PR #1288
# Conflicts: # docs/parity-checklist.md # docs/raw-parity.md # docs/whats-new.md
This commit is contained in:
commit
44684b7950
|
|
@ -167,25 +167,47 @@ pub(crate) fn decode_huffman(src: &[u8], h: &Huffman, w: usize, hgt: usize, bits
|
|||
Ok(out)
|
||||
}
|
||||
|
||||
/// The image area without all-dark leading/trailing columns (masked or empty), kept at even offsets.
|
||||
fn dark_trimmed(d: &[u16], w: usize, h: usize) -> Rect {
|
||||
if w < 256 || h == 0 {
|
||||
/// The image area without leading/trailing lines that hold no picture (masked or empty), kept at even offsets. With
|
||||
/// the maker-note `black` level, a line is dark when its mean is at most 1/256 of the remaining range (at least 16)
|
||||
/// above it, the tolerance `cr2::masked_columns` uses: masked lines sit at the black level plus noise, while dark
|
||||
/// real content (a night sky) is well above. Without it, below 5% of the interior's mean.
|
||||
fn dark_trimmed(d: &[u16], w: usize, h: usize, black: Option<f64>, white: f32) -> Rect {
|
||||
if w < 256 || h < 256 {
|
||||
return Rect::new(0, 0, w, h);
|
||||
}
|
||||
let step = (h / 256).max(1);
|
||||
let col_mean = |x: usize| (0..h).step_by(step).map(|y| d[y * w + x] as f64).sum::<f64>() / h.div_ceil(step) as f64;
|
||||
let (xstep, ystep) = (w / 256, h / 256);
|
||||
let at = |x: usize, y: usize| d.get(y * w + x).map_or(0.0, |&v| f64::from(v));
|
||||
let col_mean = |x: usize| (0..h).step_by(ystep).map(|y| at(x, y)).sum::<f64>() / h.div_ceil(ystep) as f64;
|
||||
let row_mean = |y: usize| (0..w).step_by(xstep).map(|x| at(x, y)).sum::<f64>() / w.div_ceil(xstep) as f64;
|
||||
let interior = (w / 4..w * 3 / 4).step_by(w / 64).map(col_mean).sum::<f64>() / 32.0;
|
||||
let dark = interior * 0.05;
|
||||
let mut left = 0;
|
||||
while left < 64 && col_mean(left) < dark {
|
||||
left += 1;
|
||||
let (base, tol) = match black {
|
||||
Some(b) => (b, ((f64::from(white) - b) / 256.0).max(16.0)),
|
||||
None => (0.0, interior * 0.05),
|
||||
};
|
||||
// a frame no brighter than masked lines (a lens-cap shot) has nothing to tell them from picture
|
||||
if interior - base <= tol {
|
||||
return Rect::new(0, 0, w, h);
|
||||
}
|
||||
let mut right = w;
|
||||
while right > w - 64 && col_mean(right - 1) < dark {
|
||||
right -= 1;
|
||||
}
|
||||
let left = left.next_multiple_of(2);
|
||||
Rect::new(left, 0, (right - left) & !1, h)
|
||||
let range = f64::from(white) - base;
|
||||
// lines from one edge that hold no picture: within `tol` of the outermost line's level when that sits a little
|
||||
// above black (K-70, K-1: 65 over BlackPoint), and saturated lines when dark ones follow (645Z)
|
||||
let run = |mean: &dyn Fn(usize) -> f64| {
|
||||
let saturated = (0..64).take_while(|&k| mean(k) - base >= range * 0.98).count();
|
||||
let outer = mean(saturated) - base;
|
||||
let level = if outer > 0.0 && outer < range * 0.01 { outer } else { 0.0 };
|
||||
match (saturated..64).take_while(|&k| mean(k) - base < level + tol).count() {
|
||||
0 => 0,
|
||||
dark => saturated + dark,
|
||||
}
|
||||
};
|
||||
let edges = |n: usize, mean: &dyn Fn(usize) -> f64| {
|
||||
let lo = run(mean).next_multiple_of(2);
|
||||
let hi = n - run(&|k| mean(n - 1 - k));
|
||||
(lo, (hi - lo) & !1)
|
||||
};
|
||||
let (left, width) = edges(w, &col_mean);
|
||||
let (top, height) = edges(h, &row_mean);
|
||||
Rect::new(left, top, width, height)
|
||||
}
|
||||
|
||||
/// Whether every strip or tile of a one-sample-per-pixel image holds exactly the bytes that its rows need when
|
||||
|
|
@ -249,9 +271,11 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
|||
};
|
||||
let RawData::U16(ref samples) = data else { return Err(RawError::Unsupported("float PEF".into())) };
|
||||
|
||||
let white = white_from_data(samples, bits);
|
||||
let black_point = mn.as_ref().and_then(|m| m.ifd.f64s(BLACK_POINT)).filter(|v| v.len() == 4);
|
||||
let active = match tagged {
|
||||
Some(r) => r,
|
||||
None => dark_trimmed(samples, w, hgt),
|
||||
None => dark_trimmed(samples, w, hgt, black_point.as_deref().map(|v| v.iter().sum::<f64>() / 4.0), white),
|
||||
};
|
||||
let cfa = match (ifd0.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), ifd0.bytes(t::CFA_PATTERN_EP)) {
|
||||
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() },
|
||||
|
|
@ -261,7 +285,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
|||
// tag keep BGGR.
|
||||
_ => cfa_from_exif(&tiff).unwrap_or_else(|| Cfa::bayer_static("BGGR")),
|
||||
};
|
||||
let black = match mn.as_ref().and_then(|m| m.ifd.f64s(BLACK_POINT)).as_deref() {
|
||||
let black = match black_point.as_deref() {
|
||||
Some(v @ [_, _, _, _]) => {
|
||||
// RGGB-ordered levels → per CFA position at the active area origin
|
||||
let a = cfa.shifted(active.x, active.y);
|
||||
|
|
@ -275,7 +299,6 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
|||
let g = (v[1] + v[2]) / 2.0;
|
||||
[(v[0] / g) as f32, 1.0, (v[3] / g) as f32]
|
||||
});
|
||||
let white = white_from_data(samples, bits);
|
||||
let mut metadata = lightcraft_meta::from_tiff(&tiff);
|
||||
metadata.width = Some(active.width as u32);
|
||||
metadata.height = Some(active.height as u32);
|
||||
|
|
@ -488,4 +511,126 @@ mod tests {
|
|||
let img = decode(&tiny_pef(4, 2, 1, 16, &[0; 16]), Mode::Full).unwrap();
|
||||
assert_eq!(img.cfa, Some(Cfa::bayer_static("BGGR")));
|
||||
}
|
||||
|
||||
/// A `w x h` big-endian 14-bit PEF of plain 16-bit words without area tags, whose maker note has only a
|
||||
/// BlackPoint of `black`, with `px(x, y)` as the samples.
|
||||
fn pef_with_black_point(w: usize, h: usize, black: u16, px: impl Fn(usize, usize) -> u16) -> Vec<u8> {
|
||||
use lightcraft_tiff::{IfdBuilder, ImageData, TiffWriter, Value};
|
||||
let strip: Vec<u8> = (0..w * h).flat_map(|i| px(i % w, i / w).to_be_bytes()).collect();
|
||||
let build = |levels_at: u32| {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(0x0100, Value::Long(vec![w as u32]));
|
||||
ifd.set(0x0101, Value::Long(vec![h as u32]));
|
||||
ifd.set(0x0102, Value::Short(vec![14]));
|
||||
ifd.set(0x0103, Value::Short(vec![1]));
|
||||
ifd.set(0x0106, Value::Short(vec![32803]));
|
||||
ifd.set(0x010f, Value::Ascii("PENTAX Corporation".into()));
|
||||
ifd.set_image(ImageData::Strips { rows_per_strip: h as u32, strips: vec![strip.clone()] });
|
||||
// AOC\0 + "MM", one entry (BlackPoint, four shorts stored after the IFD), next-IFD 0, the four levels
|
||||
let mut note = b"AOC\0MM\0\x01".to_vec();
|
||||
note.extend_from_slice(&[0x02, 0x00, 0, 3, 0, 0, 0, 4]);
|
||||
note.extend_from_slice(&levels_at.to_be_bytes());
|
||||
note.extend_from_slice(&0u32.to_be_bytes());
|
||||
(0..4).for_each(|_| note.extend_from_slice(&black.to_be_bytes()));
|
||||
let mut exif = IfdBuilder::new();
|
||||
exif.set(0x927c, Value::Undefined(note));
|
||||
ifd.set_child(0x8769, exif);
|
||||
TiffWriter::new(ByteOrder::Big, false).write(&[ifd]).unwrap()
|
||||
};
|
||||
let at = Tiff::parse(&build(0)).unwrap().exif().unwrap().get(0x927c).unwrap().offset;
|
||||
build((at + 6 + 2 + 12 + 4) as u32)
|
||||
}
|
||||
|
||||
/// The K-3 Mark III / KP / KF layout (issue #1271): masked columns and rows at the black level, an empty band
|
||||
/// at zero, around an interior less than 20 times as bright as the black level.
|
||||
#[test]
|
||||
fn masked_rows_and_columns_at_the_black_level_are_trimmed_without_crop_tags() {
|
||||
let (w, h) = (512, 384);
|
||||
let file = pef_with_black_point(w, h, 256, |x, y| {
|
||||
if x >= w - 8 {
|
||||
0
|
||||
} else if y >= h - 14 {
|
||||
15
|
||||
} else if x < 24 || x >= w - 16 || y < 28 {
|
||||
256
|
||||
} else {
|
||||
1256 + ((x * 7 + y * 13) % 64) as u16
|
||||
}
|
||||
});
|
||||
let img = decode(&file, Mode::Full).unwrap();
|
||||
assert_eq!(img.black.values, vec![256.0; 4]);
|
||||
assert_eq!(img.active_area, Rect::new(24, 28, w - 40, h - 42));
|
||||
}
|
||||
|
||||
/// A night frame (bright centre, dark sky): 20 lines of real content 100 DN above the black level inside the
|
||||
/// masked border stay in the picture, whatever the interior's brightness.
|
||||
#[test]
|
||||
fn dark_real_content_next_to_the_masked_border_is_kept() {
|
||||
let (w, h) = (512, 384);
|
||||
let file = pef_with_black_point(w, h, 256, |x, y| {
|
||||
if x >= w - 8 {
|
||||
0
|
||||
} else if y >= h - 14 {
|
||||
15
|
||||
} else if x < 24 || x >= w - 16 || y < 28 {
|
||||
256 + ((x + y) % 3) as u16
|
||||
} else if x < 44 || x >= w - 36 || y < 48 || y >= h - 34 {
|
||||
356 + ((x * 7 + y * 13) % 16) as u16
|
||||
} else {
|
||||
3256 + ((x * 7 + y * 13) % 64) as u16
|
||||
}
|
||||
});
|
||||
let img = decode(&file, Mode::Full).unwrap();
|
||||
assert_eq!(img.active_area, Rect::new(24, 28, w - 40, h - 42));
|
||||
}
|
||||
|
||||
/// The K-70 (and K-1) layout: masked lines 65 DN over BlackPoint 64, just above the tolerance over black, and
|
||||
/// empty rows at the bottom.
|
||||
#[test]
|
||||
fn masked_lines_a_little_above_the_black_level_are_trimmed() {
|
||||
let (w, h) = (320, 288);
|
||||
let file = pef_with_black_point(w, h, 64, |x, y| {
|
||||
if y >= h - 12 {
|
||||
0
|
||||
} else if x < 56 || y < 28 {
|
||||
128 + ((x + y) % 3) as u16
|
||||
} else {
|
||||
2064 + ((x * 7 + y * 13) % 64) as u16
|
||||
}
|
||||
});
|
||||
assert_eq!(decode(&file, Mode::Full).unwrap().active_area, Rect::new(56, 28, 264, 248));
|
||||
}
|
||||
|
||||
/// The 645Z layout: 12 saturated columns outside 46 masked ones on the right; they hold no picture either.
|
||||
#[test]
|
||||
fn saturated_lines_outside_masked_ones_are_trimmed() {
|
||||
let (w, h) = (400, 288);
|
||||
let file = pef_with_black_point(w, h, 64, |x, y| {
|
||||
if x >= w - 12 {
|
||||
16316
|
||||
} else if x >= w - 58 {
|
||||
64 + ((x + y) % 3) as u16
|
||||
} else {
|
||||
2064 + ((x * 7 + y * 13) % 64) as u16
|
||||
}
|
||||
});
|
||||
assert_eq!(decode(&file, Mode::Full).unwrap().active_area, Rect::new(0, 0, w - 58, h));
|
||||
}
|
||||
|
||||
/// A blown-out edge with the picture right behind it is picture: nothing is trimmed.
|
||||
#[test]
|
||||
fn a_saturated_edge_followed_by_picture_stays() {
|
||||
let (w, h) = (400, 288);
|
||||
let file = pef_with_black_point(w, h, 64, |x, y| if x >= w - 12 { 16316 } else { 2064 + ((x * 7 + y * 13) % 64) as u16 });
|
||||
assert_eq!(decode(&file, Mode::Full).unwrap().active_area, Rect::new(0, 0, w, h));
|
||||
}
|
||||
|
||||
/// A frame at the black level everywhere (a lens-cap shot) has no picture to tell masked lines from: it keeps
|
||||
/// its whole area.
|
||||
#[test]
|
||||
fn a_frame_at_the_black_level_keeps_its_whole_area() {
|
||||
let (w, h) = (320, 288);
|
||||
let file = pef_with_black_point(w, h, 64, |x, y| 64 + ((x * 7 + y * 13) % 5) as u16);
|
||||
assert_eq!(decode(&file, Mode::Full).unwrap().active_area, Rect::new(0, 0, w, h));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -135,7 +135,7 @@ checked, and Canon, the largest bucket, is the weakest of the big four.
|
|||
| ORF | uncompressed 16-bit, 12-bit packed, two-field 12-bit, 12.8 bpp | ✅ | E-1, E-400, XZ-2 corpus; #651 |
|
||||
| ORF | **compressed** (every Olympus / OM body since ~2008) | ⬜ | `orf.rs` refuses; the E-M1 and E-M10 III corpus files are `KNOWN_UNSUPPORTED` |
|
||||
| RW2 / RWL / RAW | formats 2, 4, 5, 6, 7, 8 and 16-bit words | ✅ | 118 bodies (#215) |
|
||||
| PEF | uncompressed, Huffman, packed 12-bit | ✅ | K10D, K-3, K-5 IIs |
|
||||
| PEF | uncompressed, Huffman, packed 12-bit | ✅ | K10D, K-3, K-5 IIs; files without area tags lose their masked, empty and saturated border lines (#1271: KP, KF, K-3 Mark III, K-70, K-1, K-1 Mark II, K-S1, K-S2, 645Z) |
|
||||
| PEF / DNG | Pentax pixel shift | ⬜ | frames not combined |
|
||||
| SRW | uncompressed, 12-bit packed | ✅ | EX1 corpus; NX10, NX20 |
|
||||
| SRW | compressed 32770 / 32772 / 32773 | ⬜ | refused |
|
||||
|
|
@ -271,6 +271,7 @@ Beyond decoding:
|
|||
| 2026-10-11 | minor | Leica Q3 verified by a recorded check of 38 private DNGs (no decode failure; mean ΔE to the camera JPEG 6.3–13.9): 236 verified, 941 unverified, Leica bucket on a verified body 5% → 21%, weighted shares unchanged |
|
||||
| 2026-10-12 | patch | CR2 white level from the active area, not the saturated masked rows above it (#1272: EOS 50D, 60D, 450D (Rebel XSi), 500D (Rebel T1i), 550D (Rebel T2i), 1200D (Rebel T5) and 100D (Rebel SL1)); scores unchanged |
|
||||
| 2026-10-10 | minor | Compressed ORF: pointer to the two Olympus patents that describe the method, as a clean-room starting point |
|
||||
| 2026-10-12 | minor | PEF without area tags: masked border rows and columns trimmed against the BlackPoint (issue #1271; KP, KF, K-3 Mark III, K-70, K-1, K-1 Mark II, K-S1, K-S2, 645Z); counts unchanged |
|
||||
| 2026-10-11 | minor | EOS R5 Mark II verified (CRX QP-map first literal and limited-length escape, odd-height Bayer planes; 4 CC0 files pinned, QP maps of 33,665 private files): 235 verified, 942 unverified; use-weighted 85% (42% on verified bodies); decoding 77%, RAW readiness unchanged at ~56% |
|
||||
| 2026-10-10 (later) | major | Model-by-model measurement against Adobe's published list (1,446 models: 234 verified, 943 unverified, 233 preview, 36 unsupported); `docs/raw/` data and re-measure script; use-weighted share 84% (40% on verified bodies); variant matrix; colour and lens sections with scores; ranked next cameras; RAW readiness ~56% from written parts |
|
||||
| 2026-10-10 | major | Created: formats, makers, demosaic, colour and lens checklists; target counts from the installed Lightroom 9.6 bundle listing |
|
||||
|
|
|
|||
|
|
@ -14,6 +14,13 @@
|
|||
- Blown highlights in CR2 files from the Canon EOS 50D, 60D, 450D (Rebel XSi), 500D (Rebel T1i), 550D (Rebel T2i),
|
||||
1200D (Rebel T5) and 100D (Rebel SL1) now count as clipped (issue #1272). They used to stop at 81–98% of white, so
|
||||
highlight recovery missed them and, darkened, they took a colour cast instead of going neutral grey.
|
||||
## 2026-10-12
|
||||
|
||||
### Pentax raws without black borders
|
||||
- Raws from the Pentax KP, KF, K-3 Mark III (and Monochrome), K-70, K-1, K-1 Mark II, K-S1, K-S2 and 645Z no longer
|
||||
show a black (or, on the 645Z, white) band along one or more edges (issue #1271). The sensor's masked border is
|
||||
left out, so these photos come out a little smaller, closer to the camera's own JPEG. Crops and masks on photos
|
||||
edited earlier may shift by a few pixels.
|
||||
|
||||
## 2026-10-11
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue