mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 11:17:05 -04:00
M11.1: decode EOS R7 C-RAW (odd-height CRX QP map)
An odd-height adaptive QP map (EOS R7: 4732 sensor rows, 1183 QP rows) was rejected as unsupported, so R7 C-RAW files opened from their embedded JPEG. The map decodes to the exact end of its entropy stream; the last row of the second-level high-frequency bands has no partner row and its QP is used alone. Even-height maps never reach this rule, so the six verified M50/R100/R8 fixtures are unchanged. Checked on paired R7 RAW and C-RAW captures (ISO 100 and 6400): all six C-RAW files decode, developed images match their RAW counterparts visually, and ISO 6400 sensor means agree within 0.1%. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Gs6roPby7THvteijZwptQt
This commit is contained in:
3 files changed
+28
-9
No files matched your search
Vendored
+16
-8
@@ -9,7 +9,8 @@
|
||||
//! CC0 raw.pixls.us files against an external decoder used only as a black-box oracle.
|
||||
//!
|
||||
//! Current support: version 0x100 lossless Bayer RAW and horizontal-tile C-RAW,
|
||||
//! plus version 0x200 single-tile 14-bit C-RAW with adaptive QP quantization.
|
||||
//! plus version 0x200 single-tile 14-bit C-RAW with adaptive QP quantization
|
||||
//! (including odd-height QP maps, as on the EOS R7).
|
||||
//! Both ff01/ff02/ff03 and ff11/ff12/ff13 marker families are supported.
|
||||
//! Six full M50/R100/R8 sensor fixtures match an external decoder sample for sample.
|
||||
//! Unverified coding variants return explicit unsupported errors.
|
||||
@@ -173,9 +174,6 @@ fn supports(config: &Cr3Compression) -> Result<()> {
|
||||
if config.version == 0x200 && (config.levels != 3 || config.bit_depth != 14 || config.tile_width != config.width) {
|
||||
return Err(RawError::Unsupported("Canon CRX version 0x200 requires a single-tile 14-bit Bayer C-RAW image".into()));
|
||||
}
|
||||
if config.version == 0x200 && !config.height.div_ceil(4).is_multiple_of(2) {
|
||||
return Err(RawError::Unsupported("Canon CRX adaptive QP map with an odd height".into()));
|
||||
}
|
||||
if config.median_bit_depth.is_some() {
|
||||
return Err(RawError::Unsupported("Canon CRX extended median-bit-depth header".into()));
|
||||
}
|
||||
@@ -418,7 +416,9 @@ fn adaptive_step(map: &QpMap, band: usize, x: usize, y: usize, base: u32, gain:
|
||||
let index = row.checked_mul(map.width).and_then(|n| n.checked_add(column)).ok_or(RawError::Limit("CRX QP map index"))?;
|
||||
let first = *map.data.get(index).ok_or_else(|| corrupt("QP coordinate outside map"))?;
|
||||
let value = if band < 7 {
|
||||
let second_row = row + 1;
|
||||
// An odd-height map (R7: 4732 rows give 1183 QP rows) leaves the
|
||||
// last row of the coarser bands without a partner; it is used alone.
|
||||
let second_row = (row + 1).min(map.height - 1);
|
||||
let second = *map.data.get(second_row * map.width + column).ok_or_else(|| corrupt("QP average outside map"))?;
|
||||
(first + second) / 2
|
||||
} else {
|
||||
@@ -734,9 +734,6 @@ fn high_frequency_band(src: &[u8], width: usize, height: usize) -> Result<Vec<i3
|
||||
}
|
||||
|
||||
fn qp_map(src: &[u8], width: usize, height: usize) -> Result<QpMap> {
|
||||
if !height.is_multiple_of(2) {
|
||||
return Err(RawError::Unsupported("Canon CRX adaptive QP map with an odd height".into()));
|
||||
}
|
||||
let count = width.checked_mul(height).filter(|&n| width > 0 && n > 0 && n <= MAX_CRX_SAMPLES / 32).ok_or(RawError::Limit("CRX QP map size"))?;
|
||||
let mut data = Vec::new();
|
||||
data.try_reserve_exact(count).map_err(|_| RawError::Limit("CRX QP allocation"))?;
|
||||
@@ -1006,6 +1003,17 @@ mod tests {
|
||||
assert!(matches!(qp_map(&[0, 0, 1, 4, 0x94], 1, 4), Err(RawError::Unsupported(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_height_qp_map_uses_its_last_row_alone() {
|
||||
// EOS R7 C-RAW: 4732 sensor rows give a 1183-row QP map, so the last
|
||||
// row of the coarser high-frequency bands has no partner row.
|
||||
let map = QpMap { width: 1, height: 3, data: vec![150, 153, 151] };
|
||||
assert_eq!(adaptive_step(&map, 4, 0, 0, 0, 4).unwrap(), 7);
|
||||
// Row 2 alone is QP 151; averaging with a missing row would leave the table.
|
||||
assert_eq!(adaptive_step(&map, 4, 0, 1, 0, 4).unwrap(), adaptive_step(&map, 7, 0, 2, 0, 4).unwrap());
|
||||
assert!(adaptive_step(&map, 4, 0, 2, 0, 4).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adaptive_v200_craw_reconstructs_sensor_samples() {
|
||||
let mut config = coding();
|
||||
|
||||
+10
-1
@@ -55,7 +55,16 @@ The version `0x200` QP predictor and entropy framing were checked independently.
|
||||
Controlled input mutations establish floor averaging of paired QP rows before
|
||||
the nonlinear integer table, then multiplication, division by eight, addition
|
||||
of the subband base and a minimum step of one. QP values 131–167 are verified;
|
||||
unmeasured values and an odd QP-map height are rejected. Partial-subband and
|
||||
unmeasured values are rejected.
|
||||
|
||||
An odd QP-map height (EOS R7: 4732 sensor rows give 1183 QP rows) leaves the
|
||||
last row of the second-level high-frequency bands without a partner row; that
|
||||
row's QP is used on its own. The odd-height map decodes to the exact end of its
|
||||
entropy stream. This rule is not yet checked against a black-box reference:
|
||||
it was checked on paired R7 RAW and C-RAW captures of the same scene (ISO 100
|
||||
and 6400), whose developed images match visually and whose sensor means agree
|
||||
within 0.1% at ISO 6400. Files with even-height maps never reach this rule, so
|
||||
the six verified fixtures are unchanged. Partial-subband and
|
||||
rounded-plane coding, vertical C-RAW tiling and adaptive low-frequency gains
|
||||
remain unsupported. The decoder bounds image dimensions, sample count,
|
||||
tile count, marker headers and temporary allocations. The full sensor buffer is
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
are unchanged; photos imported before this change get it when imported again.
|
||||
- Canon CR3 raws now develop from their sensor data: lossless RAW and C-RAW, checked sample for sample on the EOS
|
||||
M50, R100 and R8. CR3 files the decoder can't read yet still open from their embedded JPEG, as before.
|
||||
- Canon EOS R7 C-RAW files develop from their sensor data too, instead of opening from their embedded JPEG. For R7
|
||||
C-RAWs imported earlier, Photo ▸ Reload from Disk picks up the raw data.
|
||||
- Canon CRW, Minolta MRW, Sigma X3F, Kodak KDC, Leaf MOS and Epson ERF files that LightCraft can't decode yet
|
||||
now import as "preview only" with their embedded JPEG instead of failing. A raw whose data is damaged but whose
|
||||
preview is intact does the same.
|
||||
|
||||
Reference in new issue
Block a user