mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
LR-IMP-FORMATS: decode EOS R6 Mark II C-RAW with adaptive QP up to 175
EOS R6 Mark II C-RAW QP maps reach 172 and 175; crx.rs accepted 131-168,
so those files opened as their preview ("Canon CRX adaptive QP value 172").
Adaptive QP now goes to 175 (quantization value 51, step 228), the highest
seen in any file; uniform subband values still stop at 44.
Checked black-box against the camera JPEGs on private files: 144 R6 II /
R7 C-RAW files decode to identical hashes where they decoded before, the 3
failing ones now decode (green follows the JPEG at r 0.98), fine detail per
QP shows no jump at 172/175, 100 % crops are clean, and 1,492 files sampled
from a 35,796-file library decode with max QP 175. No third-party decoder
source consulted. Bumps RENDER_CACHE_VERSION to 63; cr3.md and What's New.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSGFEf87ba1zKJZb9uzddK
This commit is contained in:
@@ -62,7 +62,7 @@ impl SettingsHashes {
|
||||
}
|
||||
|
||||
/// Bump when the pipeline's output changes, to invalidate cached thumbnails.
|
||||
pub const RENDER_CACHE_VERSION: u64 = 62;
|
||||
pub const RENDER_CACHE_VERSION: u64 = 63;
|
||||
|
||||
/// Thumbnails render at one of these long edges (so window/cell size changes reuse the cache).
|
||||
pub const THUMB_SIZES: [usize; 4] = [128, 256, 384, 512];
|
||||
|
||||
Vendored
+39
-13
@@ -30,6 +30,12 @@ const MAX_CRX_SAMPLES: usize = 200_000_000;
|
||||
const MAX_CRX_EDGE: u32 = 65_536;
|
||||
/// Unary QP-map prefix length that announces an 8-bit escaped symbol.
|
||||
const QP_ESCAPE_ZEROS: u32 = 23;
|
||||
/// Highest uniform (per-subband) quantization value with a measured step.
|
||||
const MAX_UNIFORM_QUANT: u8 = 44;
|
||||
/// Highest adaptive quantization value (QP − 124) seen in real files: QP 175 on the EOS R6 Mark II.
|
||||
/// See [`quant_step`].
|
||||
const MAX_ADAPTIVE_QUANT: u8 = 51;
|
||||
const MAX_QP: i32 = 124 + MAX_ADAPTIVE_QUANT as i32;
|
||||
const RUN_EXPONENT: [u8; 32] = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 9, 10, 11, 12, 13, 14, 15];
|
||||
|
||||
fn corrupt(reason: &str) -> RawError {
|
||||
@@ -207,7 +213,7 @@ fn validate_planes(tiles: &[Tile], config: &Cr3Compression) -> Result<()> {
|
||||
if config.levels != 0 {
|
||||
match band.quant {
|
||||
Quantization::Uniform(value) => {
|
||||
quant_step(value)?;
|
||||
quant_step(value, MAX_UNIFORM_QUANT)?;
|
||||
}
|
||||
Quantization::Adaptive { gain, .. } => {
|
||||
if index < 4 && gain != 0 {
|
||||
@@ -342,12 +348,14 @@ fn tile_qp(sample: &[u8], tile: &Tile) -> Result<Option<QpMap>> {
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn quant_step(value: u8) -> Result<i32> {
|
||||
fn quant_step(value: u8, max: u8) -> Result<i32> {
|
||||
// Measured independently with an external decoder after modifying only the
|
||||
// quantValue field: all forty values 4..43 follow this integer sequence. Value 44 (QP 168)
|
||||
// continues it (step 102); it was checked against a measured step of about 100 on an EOS R50
|
||||
// C-RAW file. Nothing above 44 has been seen, so it stays rejected.
|
||||
if !(4..=44).contains(&value) {
|
||||
// C-RAW file. Uniform subband values stop there (`MAX_UNIFORM_QUANT`). Adaptive QP maps reach
|
||||
// QP 175 (value 51) on the EOS R6 Mark II; those steps (114..228) continue the same sequence and
|
||||
// were checked against the camera's own JPEGs (docs/cr3.md). Anything higher is still rejected.
|
||||
if !(4..=max).contains(&value) {
|
||||
return Err(RawError::Unsupported(format!("Canon CRX quantization value {value}")));
|
||||
}
|
||||
let scale = [40i32, 45, 51, 57, 64, 72].get(usize::from(value % 6)).copied().ok_or_else(|| corrupt("quantization table index"))?;
|
||||
@@ -403,7 +411,7 @@ fn wavelet_plane(sample: &[u8], plane: &Plane, width: usize, height: usize, edge
|
||||
};
|
||||
for (position, value) in data.iter_mut().enumerate() {
|
||||
let step = match band.quant {
|
||||
Quantization::Uniform(value) => quant_step(value)?,
|
||||
Quantization::Uniform(value) => quant_step(value, MAX_UNIFORM_QUANT)?,
|
||||
Quantization::Adaptive { base, gain } => {
|
||||
adaptive_step(qp.ok_or_else(|| corrupt("missing QP map"))?, i, position % w, position / w, base, gain)?
|
||||
}
|
||||
@@ -443,7 +451,7 @@ fn adaptive_step(map: &QpMap, band: usize, x: usize, y: usize, base: u32, gain:
|
||||
// QP rows are averaged before looking up the integer step. R8 field
|
||||
// mutations distinguish this order from averaging dequantization steps.
|
||||
let quant = u8::try_from(value - 124).map_err(|_| corrupt("QP value outside quantization table"))?;
|
||||
u64::from(base) + u64::from(gain) * quant_step(quant)? as u64 / 8
|
||||
u64::from(base) + u64::from(gain) * quant_step(quant, MAX_ADAPTIVE_QUANT)? as u64 / 8
|
||||
};
|
||||
i32::try_from(step.max(1)).map_err(|_| corrupt("adaptive quantization step outside precision"))
|
||||
}
|
||||
@@ -775,7 +783,7 @@ fn qp_map(src: &[u8], width: usize, height: usize) -> Result<QpMap> {
|
||||
// The map opens with its first symbol as a 32-bit literal; every later one is Rice coded.
|
||||
let symbol = if x == 0 && y == 0 { (bits.take(16)? << 16) | bits.take(16)? } else { bits.qp_rice(k)? };
|
||||
let value = median(left, above, corner).checked_add(signed(symbol)).ok_or_else(|| corrupt("QP prediction overflow"))?;
|
||||
if !(131..=168).contains(&value) {
|
||||
if !(131..=MAX_QP).contains(&value) {
|
||||
return Err(RawError::Unsupported(format!("Canon CRX adaptive QP value {value}")));
|
||||
}
|
||||
*current.get_mut(x).ok_or_else(|| corrupt("QP outside row"))? = value;
|
||||
@@ -990,10 +998,19 @@ mod tests {
|
||||
#[test]
|
||||
fn measured_quantization_and_allocation_limits() {
|
||||
for (quant, expected) in [(4, 1), (9, 1), (10, 2), (16, 4), (22, 8), (26, 12), (32, 25), (36, 40), (41, 72), (42, 80), (43, 90), (44, 102)] {
|
||||
assert_eq!(quant_step(quant).unwrap(), expected);
|
||||
assert_eq!(quant_step(quant, MAX_UNIFORM_QUANT).unwrap(), expected);
|
||||
}
|
||||
assert!(matches!(quant_step(3), Err(RawError::Unsupported(_))));
|
||||
assert!(matches!(quant_step(45), Err(RawError::Unsupported(_))));
|
||||
assert!(matches!(quant_step(3, MAX_UNIFORM_QUANT), Err(RawError::Unsupported(_))));
|
||||
assert!(matches!(quant_step(45, MAX_UNIFORM_QUANT), Err(RawError::Unsupported(_))));
|
||||
// Adaptive QP 169..=175 (EOS R6 Mark II C-RAW) continue the sequence; QP 176 does not decode.
|
||||
for (quant, expected) in [(45, 114), (46, 128), (47, 144), (48, 160), (49, 180), (50, 204), (51, 228)] {
|
||||
assert_eq!(quant_step(quant, MAX_ADAPTIVE_QUANT).unwrap(), expected);
|
||||
}
|
||||
assert!(matches!(quant_step(52, MAX_ADAPTIVE_QUANT), Err(RawError::Unsupported(_))));
|
||||
let high = QpMap { width: 1, height: 2, data: vec![172, 175] };
|
||||
// floor((172+175)/2) = 173 → value 49, step 180; gain 8/8 plus base 1.
|
||||
assert_eq!(adaptive_step(&high, 4, 0, 0, 1, 8).unwrap(), 181);
|
||||
assert_eq!(adaptive_step(&high, 7, 0, 1, 0, 8).unwrap(), 228);
|
||||
for (width, height) in [(20_000, 20_000), (65_538, 2), (2, 65_538)] {
|
||||
let mut config = coding();
|
||||
config.width = width;
|
||||
@@ -1032,9 +1049,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qp_168_is_accepted_and_169_is_not() {
|
||||
assert_eq!(qp_map(&[0, 0, 1, 0x50, 0x94], 1, 4).unwrap().data, [168; 4]);
|
||||
assert!(matches!(qp_map(&[0, 0, 1, 0x52, 0x94], 1, 4), Err(RawError::Unsupported(_))));
|
||||
fn qp_limit_is_accepted_and_one_above_is_not() {
|
||||
// The first symbol is a 32-bit literal: signed(2·QP) = QP from a prediction of 0. QP 172 and 175
|
||||
// occur in EOS R6 Mark II maps (3 of 40 sample files failed with them before).
|
||||
let map = |qp: u32| {
|
||||
let symbol = 2 * qp;
|
||||
let mut src = vec![0, 0, (symbol >> 8) as u8, symbol as u8];
|
||||
src.push(0x94);
|
||||
qp_map(&src, 1, 4)
|
||||
};
|
||||
assert_eq!(map(168).unwrap().data, [168; 4]);
|
||||
assert_eq!(map(MAX_QP as u32).unwrap().data, [MAX_QP; 4]);
|
||||
assert!(matches!(map(MAX_QP as u32 + 1), Err(RawError::Unsupported(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+17
-1
@@ -93,7 +93,23 @@ 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 are rejected.
|
||||
168–175 continue the same integer sequence (below); higher values are rejected.
|
||||
|
||||
EOS R6 Mark II C-RAW maps reach QP 172 and 175, which the decoder used to reject ("Canon CRX adaptive QP
|
||||
value 172"; the file opened as its preview). Quantization values 45–51 (QP 169–175) give steps 114, 128,
|
||||
144, 160, 180, 204 and 228, continuing the 2^(1/6) ladder of the measured values. No external decoder was
|
||||
run; the check is black-box against the camera, on a user's private files:
|
||||
|
||||
- 144 C-RAW files (EOS R6 Mark II and R7): every file that decoded before decodes to an identical sensor
|
||||
hash; the 3 that failed (QP maps 168–175) now decode, and their green mosaic follows the camera's
|
||||
embedded JPEG at Pearson r 0.980–0.984 (24 × 16 cells, as in the corpus test), within the set's range;
|
||||
- per QP value, the ratio of fine detail in the decode to fine detail in the camera JPEG over the same
|
||||
16 × 4-pixel QP cells (pooled over the files) is 6.2 at QP 172 and 12.2 at QP 175, inside the
|
||||
2.8–15.5 spread of the already-accepted QPs 150–165; a wrong step at those QPs would show as a jump;
|
||||
- 100 % crops round the highest-QP cells of six files show no blocking, banding, ringing or seams, and
|
||||
no change in noise between high-QP areas and their surroundings;
|
||||
- 1,492 files sampled from a 35,796-file library (1,094 R7, 298 R6 Mark II, 100 R100) decode with no
|
||||
errors; the highest QP is 175 (3 of the 298 R6 Mark II files go above 167; the R7 stays at or below 164).
|
||||
|
||||
The QP map is coded as follows (established on the EOS R5 Mark II). Its first symbol is a 32-bit
|
||||
big-endian literal, mapped like every other symbol and added to a prediction of zero. Every later
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
## 2026-10-14
|
||||
|
||||
### More Canon EOS R6 Mark II C-RAW files decode
|
||||
- Some EOS R6 Mark II C-RAW files (about 1 in 50 in one photographer's library) opened as their preview with
|
||||
"Canon CRX adaptive QP value 172" or "175". They now decode from the sensor data. Photos imported before this
|
||||
switch over with Photo ▸ Reload from Disk.
|
||||
|
||||
### Hard-edged Heal and Clone spots
|
||||
- A spot with Feather at 0 no longer draws black pixels exactly on its radius. This also fixes spots at 0% Opacity
|
||||
changing pixels along their edge. The hard edge now leaves pixels on the radius unchanged.
|
||||
|
||||
Reference in New Issue
Block a user