mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-02 06:27:53 -06:00
LR-EDIT-OPTICS-PROFILE: apply A7CR embedded distortion for corrected full-frame raws
Use the corrected-radius table convention for full-frame Bayer ILCE-7CR files with distortion Auto or forced Auto. Preserve Off files, unvalidated modes and the existing ILCE-7RM4A conversion. Add mapping, setting, scope, padding, orientation, resizing and DNG checks. Record camera-JPEG geometry and color measurements, including the remaining wide-end edge residual. Refresh stored lens data with LOOK_VERSION 11 and rendered results with RENDER_CACHE_VERSION 68. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0da998f24d
commit
1be3b3d743
@@ -34,7 +34,8 @@ use lightcraft_raw::{
|
||||
/// 7: the sensor proxy follows the file's lens warp when the camera JPEG is lens-corrected.
|
||||
/// 8: the bundled NIKON Z 6_2 profile.
|
||||
/// 9: the bundled NIKON Z 8 and NIKON D5300 profiles.
|
||||
pub const LOOK_VERSION: u32 = 9;
|
||||
/// 11: validated A7CR distortion, including the lens data stored with each smart preview.
|
||||
pub const LOOK_VERSION: u32 = 11;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct CameraLook {
|
||||
|
||||
@@ -1658,6 +1658,49 @@ pub(crate) mod tests {
|
||||
Some((best.1, best.0))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corpus_a7cr_warp_keeps_coordinates_after_orientation_and_resizing() {
|
||||
use lightcraft_geom::{Orientation, Point};
|
||||
use lightcraft_pipeline::optics::Warp;
|
||||
let root = std::env::var_os("LIGHTCRAFT_CORPUS")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus"));
|
||||
for name in ["arw-sony-a7cr-compressed.arw", "arw-sony-a7cr-lossless-l.arw"] {
|
||||
let Ok(mut bytes) = std::fs::read(root.join("raw").join(name)) else { continue };
|
||||
let off = lightcraft_raw::probe_info(&bytes).unwrap();
|
||||
assert!(embedded_lens(&off).is_none());
|
||||
let tiff = lightcraft_tiff::Tiff::parse(&bytes).unwrap();
|
||||
let at = tiff.find(0x7036).unwrap().offset as usize;
|
||||
let mut enabled = Vec::new();
|
||||
tiff.order.put_u16(&mut enabled, 17);
|
||||
bytes[at..at + 2].copy_from_slice(&enabled);
|
||||
let mut raw = lightcraft_raw::probe_info(&bytes).unwrap();
|
||||
raw.orientation = Orientation::Normal;
|
||||
let mut settings = lightcraft_develop::DevelopSettings::default();
|
||||
settings.optics.lens_profile = true;
|
||||
let base_lens = embedded_lens(&raw).unwrap();
|
||||
let base = Warp::from_settings(9504.0, 6336.0, &settings, Some(&base_lens));
|
||||
for orientation in 1..=8 {
|
||||
let o = Orientation::from_exif(orientation);
|
||||
raw.orientation = o;
|
||||
let lens = embedded_lens(&raw).unwrap();
|
||||
for edge in [1200.0, 2560.0, 9504.0] {
|
||||
let scale = edge / 9504.0;
|
||||
let (w, h) = if o.swaps_axes() { (6336.0 * scale, edge) } else { (edge, 6336.0 * scale) };
|
||||
let oriented = Warp::from_settings(w, h, &settings, Some(&lens));
|
||||
for (x, y) in [(0.05, 0.1), (0.5, 0.5), (0.9, 0.8)] {
|
||||
let point = Point::new(x * 9504.0, y * 6336.0);
|
||||
let source = base.corrected_to_source(point, 1);
|
||||
let (px, py) = o.map(point.x, point.y, 9504.0, 6336.0);
|
||||
let (sx, sy) = o.map(source.x, source.y, 9504.0, 6336.0);
|
||||
let got = oriented.corrected_to_source(Point::new(px * scale, py * scale), 1);
|
||||
assert!((got.x - sx * scale).hypot(got.y - sy * scale) < 1e-8, "{name} orientation {orientation} edge {edge}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Issue #256: public Panasonic / Leica raws (skipped without the corpus) are corrected for distortion the way the
|
||||
/// camera corrected its own JPEG (tag 0x0119): at the corners of the frame, the render with "Enable Profile
|
||||
/// Corrections" lines up with the embedded JPEG within 2 px at 640 px, while the uncorrected render of the wide
|
||||
|
||||
@@ -62,7 +62,7 @@ impl SettingsHashes {
|
||||
}
|
||||
|
||||
/// Bump when the pipeline's output changes, to invalidate cached thumbnails.
|
||||
pub const RENDER_CACHE_VERSION: u64 = 64;
|
||||
pub const RENDER_CACHE_VERSION: u64 = 68;
|
||||
|
||||
/// 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];
|
||||
|
||||
@@ -137,7 +137,7 @@ pub enum SonySettingMeaning {
|
||||
/// Whether a component is applied, and if not, why.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum SonyLensStatus {
|
||||
/// Converted into the image's `OpcodeList3` (today: ILCE-7RM4A and ILCE-7RM6 Bayer distortion in native 3:2 framing).
|
||||
/// Converted into the image's `OpcodeList3` (ILCE-7RM4A, ILCE-7RM6 or eligible full-frame ILCE-7CR Bayer distortion).
|
||||
Applied,
|
||||
/// No usable table.
|
||||
NoTable,
|
||||
@@ -149,6 +149,8 @@ pub enum SonyLensStatus {
|
||||
/// Decoded only: the model is validated, but not this coding or framing (linear YCbCr, a non-3:2 crop) or a
|
||||
/// table found only in the `SR2SubIFD`.
|
||||
NotValidatedMode,
|
||||
/// A7CR distortion is Off, unavailable, missing or unknown. Its uncorrected JPEG and render keep the same geometry.
|
||||
CameraCorrectionDisabled,
|
||||
/// Validated scope, but the table is all zeros: a valid no-op, nothing to apply.
|
||||
Neutral,
|
||||
/// Validated scope, but the conversion refused this table (values outside the validated range, a degenerate
|
||||
|
||||
Vendored
+241
-9
@@ -1,6 +1,6 @@
|
||||
//! Sony's in-file lens-correction records (shading, lateral chromatic aberration, distortion): bounded decoding of
|
||||
//! the raw image IFD's fields and their encrypted `SR2SubIFD` copies into [`SonyLensCorrections`], and the one
|
||||
//! validated conversion, the ILCE-7RM4A's distortion table, into a DNG warp for the shared optics pipeline.
|
||||
//! the raw image IFD's fields and their encrypted `SR2SubIFD` copies into [`SonyLensCorrections`], and validated
|
||||
//! ILCE-7RM4A and ILCE-7CR distortion tables into DNG warps for the shared optics pipeline.
|
||||
//!
|
||||
//! Tag identity/layout: https://exiftool.org/TagNames/EXIF.html (`0x7031` to `0x7037`, signed shorts) and
|
||||
//! https://exiftool.org/TagNames/Sony.html#SR2SubIFD (`0x797d`, `0x7980`, `0x7982`). The `SR2SubIFD` setting tags
|
||||
@@ -34,13 +34,18 @@ const DISTORTION: Tags = Tags { setting: (0x7036, 0x7981), table: (0x7037, 0x798
|
||||
/// TIFF field types.
|
||||
const SHORT: u16 = 3;
|
||||
const SSHORT: u16 = 8;
|
||||
/// Camera models whose raw-IFD distortion table was independently validated. Exact names: ILCE-7RM4 and ILCE-7RM4A
|
||||
/// are distinct models, not interchangeable aliases.
|
||||
/// Models using the validated A7R IVA conversion. The A7CR uses a separate conversion.
|
||||
/// ILCE-7RM4 and ILCE-7RM4A are distinct models, not interchangeable aliases.
|
||||
const DISTORTION_MODELS: &[&str] = &["ILCE-7RM4A"];
|
||||
/// Most directory entries scanned: a classic TIFF directory's largest count, also bounding a BigTIFF one.
|
||||
const MAX_ENTRIES: usize = 1 << 16;
|
||||
const SAMPLES: usize = 1024;
|
||||
const MAX_ERROR: f64 = 0.0005;
|
||||
/// Empirical A7CR unit, fixed on four FE 24-50mm files before checking the other files and lenses.
|
||||
/// The camera-JPEG measurements and remaining coverage limits are in docs/sony-lens-corrections.md.
|
||||
const A7CR_UNIT: f64 = 15559.300457825264;
|
||||
const A7CR_FRAME: (usize, usize) = (9504, 6336);
|
||||
const A7CR_APPROXIMATION_PX: f64 = 0.25;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Tags {
|
||||
@@ -307,11 +312,17 @@ pub(super) fn corrections(
|
||||
// Only the validated scope reaches the conversion: the raw IFD's table, as the TIFF reader parsed it, of a Bayer
|
||||
// image whose image crop and reference are native 3:2 (DefaultCrop can keep native geometry while Sony's tags
|
||||
// select an aspect crop).
|
||||
let validated_model = DISTORTION_MODELS.contains(&model);
|
||||
let in_mode = !geometry.linear_ycbcr && geometry.native_3_2 && distortion_table.source == Some(SonySource::RawIfd);
|
||||
let a7cr = model == "ILCE-7CR";
|
||||
let validated_model = DISTORTION_MODELS.contains(&model) || a7cr;
|
||||
let in_mode = !geometry.linear_ycbcr
|
||||
&& geometry.native_3_2
|
||||
&& distortion_table.source == Some(SonySource::RawIfd)
|
||||
&& (!a7cr || ((geometry.reference.width, geometry.reference.height) == A7CR_FRAME && geometry.image_crop == geometry.reference));
|
||||
let a7cr_auto = distortion_setting.value.is_some_and(|s| matches!(s.meaning, SonySettingMeaning::Auto | SonySettingMeaning::ForcedAuto));
|
||||
let (w, h) = (geometry.storage_width, geometry.storage_height);
|
||||
let warp = match &distortion_table.value {
|
||||
Some(table) if in_mode => distortion(model, table, Rect::new(0, 0, w, h), geometry.reference),
|
||||
Some(table) if in_mode && a7cr && a7cr_auto => a7cr_distortion(table, Rect::new(0, 0, w, h), geometry.reference),
|
||||
Some(table) if in_mode && !a7cr => distortion(model, table, Rect::new(0, 0, w, h), geometry.reference),
|
||||
_ => None,
|
||||
};
|
||||
let distortion_status = match &distortion_table.value {
|
||||
@@ -319,6 +330,7 @@ pub(super) fn corrections(
|
||||
Some(_) if warp.is_some() => SonyLensStatus::Applied,
|
||||
Some(_) if !validated_model => SonyLensStatus::NotValidatedModel,
|
||||
Some(_) if !in_mode => SonyLensStatus::NotValidatedMode,
|
||||
Some(_) if a7cr && !a7cr_auto => SonyLensStatus::CameraCorrectionDisabled,
|
||||
Some(table) if table.iter().all(|v| *v == 0) => SonyLensStatus::Neutral,
|
||||
Some(_) => SonyLensStatus::Rejected,
|
||||
};
|
||||
@@ -334,6 +346,94 @@ pub(super) fn corrections(
|
||||
(Some(record), warp)
|
||||
}
|
||||
|
||||
/// A7CR camera-JPEG convention, measured independently of the ILCE-7RM4A conversion below.
|
||||
/// For output radius rho, the corrected radius is rho / zoom. The table gives source radius at that
|
||||
/// corrected radius. Its knots use the full-frame diagonal, not storage padding. Off/unknown settings
|
||||
/// and unvalidated capture modes are excluded by the caller.
|
||||
fn a7cr_distortion(table: &[i16; 16], active: Rect, crop: Rect) -> Option<Opcode> {
|
||||
if (crop.width, crop.height) != A7CR_FRAME
|
||||
|| crop != crop.clipped(active.width, active.height)
|
||||
|| table.iter().all(|v| *v == 0)
|
||||
|| table.iter().any(|v| !(-4096..=4096).contains(v))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let (cw, ch) = (crop.width as f64, crop.height as f64);
|
||||
let spacing = cw.hypot(ch) / 30.0;
|
||||
let source = |r: f64| {
|
||||
let at = r / spacing;
|
||||
let i = (at.floor() as usize).min(14);
|
||||
let (a, b) = (f64::from(table[i]), f64::from(table[i + 1]));
|
||||
r * (1.0 + (a + (b - a) * (at - i as f64)) / A7CR_UNIT)
|
||||
};
|
||||
let border_min = cw.min(ch) / 2.0;
|
||||
let border_max = cw.hypot(ch) / 2.0;
|
||||
let border_ratio = |zoom: f64| {
|
||||
(0..=4096)
|
||||
.map(|j| {
|
||||
let r = border_min + (border_max - border_min) * j as f64 / 4096.0;
|
||||
source(r / zoom) / r
|
||||
})
|
||||
.fold(0.0, f64::max)
|
||||
};
|
||||
let (mut lo, mut hi) = (0.5, 2.0);
|
||||
if border_ratio(lo) <= 1.0 || border_ratio(hi) > 1.0 {
|
||||
return None;
|
||||
}
|
||||
for _ in 0..60 {
|
||||
let mid = (lo + hi) / 2.0;
|
||||
if border_ratio(mid) > 1.0 {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
let zoom = (lo + hi) / 2.0;
|
||||
let radius = (cw - 1.0).hypot(ch - 1.0) / 2.0;
|
||||
let basis = |r: f64| {
|
||||
let r2 = r * r;
|
||||
[r, r * r2, r * r2 * r2, r * r2 * r2 * r2]
|
||||
};
|
||||
let mut matrix = [[0.0; 4]; 4];
|
||||
let mut rhs = [0.0; 4];
|
||||
let mut previous = -1.0;
|
||||
for j in 0..=SAMPLES {
|
||||
let r = j as f64 / SAMPLES as f64;
|
||||
let mapped = source(r * radius / zoom) / radius;
|
||||
if !mapped.is_finite() || mapped <= previous {
|
||||
return None;
|
||||
}
|
||||
previous = mapped;
|
||||
let terms = basis(r);
|
||||
for (i, &a) in terms.iter().enumerate() {
|
||||
rhs[i] += a * mapped;
|
||||
for (k, &b) in terms.iter().enumerate() {
|
||||
matrix[i][k] += a * b;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut k = solve4(matrix, rhs)?;
|
||||
// Check every displayed radius, separately from the empirical camera-JPEG validation.
|
||||
for j in 0..=32768 {
|
||||
let r = j as f64 / 32768.0;
|
||||
let fit = basis(r).iter().zip(k).map(|(a, b)| a * b).sum::<f64>();
|
||||
let r2 = r * r;
|
||||
let derivative = k[0] + r2 * (3.0 * k[1] + r2 * (5.0 * k[2] + r2 * 7.0 * k[3]));
|
||||
let error_px = (fit * radius - source(r * radius / zoom)).abs() * 1440.0 / cw;
|
||||
if !error_px.is_finite() || error_px > A7CR_APPROXIMATION_PX || derivative <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let (ax, ay) = (active.width as f64 - 1.0, active.height as f64 - 1.0);
|
||||
let (cx, cy) = (crop.x as f64 + (cw - 1.0) / 2.0, crop.y as f64 + (ch - 1.0) / 2.0);
|
||||
let farthest = cx.max(ax - cx).hypot(cy.max(ay - cy));
|
||||
let ratio2 = (farthest / radius).powi(2);
|
||||
for (i, coefficient) in k.iter_mut().enumerate() {
|
||||
*coefficient *= ratio2.powi(i as i32);
|
||||
}
|
||||
Some(Opcode::WarpRectilinear { planes: vec![[k[0], k[1], k[2], k[3], 0.0, 0.0]], center: [cx / ax, cy / ay] })
|
||||
}
|
||||
|
||||
/// The validated conversion of a raw-IFD distortion table into a `WarpRectilinear`. Only independently validated
|
||||
/// camera models and the 16-entry layout; unknown models and implausible or non-smooth maps are left uncorrected.
|
||||
/// See docs/sony-lens-corrections.md. A camera setting of Off does not remove the lens data: the user can enable it
|
||||
@@ -454,6 +554,8 @@ mod tests {
|
||||
/// The shading samples of the public ILCE-7CR samples (raw.pixls.us DSC00795/6/8).
|
||||
const SHADING_T: [i16; 16] = [0, 64, 128, 224, 320, 480, 672, 896, 1184, 1472, 1824, 2240, 2816, 3552, 4512, 7328];
|
||||
const KEY: [u8; 4] = [0x11, 0x22, 0x33, 0x44];
|
||||
/// Public issue #1300, FE 16-25mm F2.8 G at 16 mm, raw-IFD samples.
|
||||
const A7CR_WIDE: [i16; 16] = [0, -14, -59, -130, -226, -341, -472, -615, -767, -924, -1084, -1242, -1397, -1549, -1696, -1839];
|
||||
|
||||
/// A CA table with negative and positive samples and two different curves.
|
||||
fn ca() -> [i16; 32] {
|
||||
@@ -630,7 +732,7 @@ mod tests {
|
||||
for order in [Little, Big] {
|
||||
for bigtiff in [false, true] {
|
||||
for (model, warps, distortion_status) in
|
||||
[("ILCE-7CR", 0, SonyLensStatus::NotValidatedModel), ("ILCE-7RM4A", 1, SonyLensStatus::Applied)]
|
||||
[("ILCE-7CR", 0, SonyLensStatus::NotValidatedMode), ("ILCE-7RM4A", 1, SonyLensStatus::Applied)]
|
||||
{
|
||||
let case = format!("{model} {order:?} bigtiff={bigtiff}");
|
||||
let mut arw = Arw::new(model, order);
|
||||
@@ -1030,7 +1132,7 @@ mod tests {
|
||||
("ILCE-7RM4A", [i16::MIN; 16], Rejected),
|
||||
("ILCE-7RM4A", [0, 4096, -4096, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], Rejected),
|
||||
("ILCE-7RM4", TELE, NotValidatedModel),
|
||||
("ILCE-7CR", [0; 16], NotValidatedModel),
|
||||
("ILCE-7CR", [0; 16], NotValidatedMode),
|
||||
("", TELE, NotValidatedModel),
|
||||
] {
|
||||
let mut arw = Arw::new(model, order);
|
||||
@@ -1073,6 +1175,136 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn a7cr_geometry(storage: (usize, usize)) -> SonyLensGeometry {
|
||||
let crop = Rect::new(32, 20, 9504, 6336);
|
||||
SonyLensGeometry {
|
||||
storage_width: storage.0,
|
||||
storage_height: storage.1,
|
||||
sony_crop: Some(crop),
|
||||
default_crop: Some(crop),
|
||||
image_crop: crop,
|
||||
reference: crop,
|
||||
compression: 7,
|
||||
linear_ycbcr: false,
|
||||
native_3_2: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a7cr_requires_an_enabled_setting_and_validated_full_frame_mode() {
|
||||
use SonyLensStatus::*;
|
||||
for order in [Little, Big] {
|
||||
for (setting, status) in
|
||||
[(0, CameraCorrectionDisabled), (1, Applied), (17, Applied), (255, CameraCorrectionDisabled), (2, CameraCorrectionDisabled)]
|
||||
{
|
||||
let mut arw = Arw::new("ILCE-7CR", order);
|
||||
arw.plain = plain(&A7CR_WIDE, setting);
|
||||
arw.sr2 = None;
|
||||
let bytes = arw.build();
|
||||
let tiff = Tiff::parse(&bytes).unwrap();
|
||||
let run = |model: &str, geometry: SonyLensGeometry| {
|
||||
let (record, warp) = corrections(&bytes, order, (&tiff.ifds[0], false), None, model, geometry);
|
||||
(record.unwrap().distortion.status, warp.is_some())
|
||||
};
|
||||
let geometry = a7cr_geometry((9728, 6656));
|
||||
assert_eq!(run("ILCE-7CR", geometry.clone()), (status, status == Applied));
|
||||
for model in ["ILCE-7CR ", "ILCE-7CR unknown", "ILCE-7C", "ILCE-7CM2", "ILCE-7RM4", "ILCE-7RM6"] {
|
||||
assert_eq!(run(model, geometry.clone()), (NotValidatedModel, false), "{model}");
|
||||
}
|
||||
let mut linear = geometry.clone();
|
||||
linear.linear_ycbcr = true;
|
||||
let mut apsc = geometry.clone();
|
||||
apsc.reference = Rect::new(32, 20, 6240, 4160);
|
||||
apsc.image_crop = apsc.reference;
|
||||
let mut aspect = geometry.clone();
|
||||
aspect.image_crop.height = 5346;
|
||||
aspect.native_3_2 = false;
|
||||
let mut offset = geometry.clone();
|
||||
offset.image_crop.x = 0;
|
||||
for mode in [linear, apsc, aspect, offset] {
|
||||
assert_eq!(run("ILCE-7CR", mode), (NotValidatedMode, false));
|
||||
}
|
||||
// Missing or malformed settings cannot enable the correction.
|
||||
for value in [None, Some(Value::SShort(vec![-1])), Some(Value::Long(vec![17]))] {
|
||||
arw.plain.retain(|(tag, _)| *tag != DISTORTION.setting.0);
|
||||
if let Some(value) = value {
|
||||
arw.plain.push((DISTORTION.setting.0, value));
|
||||
}
|
||||
let bytes = arw.build();
|
||||
let tiff = Tiff::parse(&bytes).unwrap();
|
||||
let (record, warp) = corrections(&bytes, order, (&tiff.ifds[0], false), None, "ILCE-7CR", geometry.clone());
|
||||
assert_eq!(record.unwrap().distortion.status, CameraCorrectionDisabled);
|
||||
assert!(warp.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a7cr_storage_padding_does_not_change_the_optical_mapping() {
|
||||
let crop = Rect::new(32, 20, 9504, 6336);
|
||||
let mut maps = Vec::new();
|
||||
for (w, h) in [(9600, 6376), (9728, 6656)] {
|
||||
let Opcode::WarpRectilinear { planes, center } = a7cr_distortion(&A7CR_WIDE, Rect::new(0, 0, w, h), crop).unwrap() else {
|
||||
panic!("warp")
|
||||
};
|
||||
let (cx, cy) = (center[0] * (w - 1) as f64, center[1] * (h - 1) as f64);
|
||||
assert!((cx - 4783.5).abs() < 1e-10 && (cy - 3187.5).abs() < 1e-10);
|
||||
let radius = cx.max((w - 1) as f64 - cx).hypot(cy.max((h - 1) as f64 - cy));
|
||||
let k = planes[0];
|
||||
maps.push(
|
||||
(0..=100)
|
||||
.map(|j| {
|
||||
let r = j as f64 / 100.0 * 5700.0;
|
||||
let r2 = (r / radius).powi(2);
|
||||
r * (k[0] + r2 * (k[1] + r2 * (k[2] + r2 * k[3])))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
for (a, b) in maps[0].iter().zip(&maps[1]) {
|
||||
assert!((a - b).abs() < 1e-9);
|
||||
}
|
||||
for bad in [[0; 16], [i16::MIN; 16], [0, 4096, -4096, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] {
|
||||
assert!(a7cr_distortion(&bad, Rect::new(0, 0, 9728, 6656), crop).is_none());
|
||||
}
|
||||
assert!(a7cr_distortion(&A7CR_WIDE, AREA, crop).is_none(), "crop outside storage");
|
||||
assert!(a7cr_distortion(&A7CR_WIDE, AREA, Rect::new(0, 0, 6240, 4160)).is_none(), "APS-C");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a7cr_matches_independently_measured_public_jpeg_geometry() {
|
||||
// Green-channel feature fits, independent of the table, on the public FE 16-25mm (issue #1300)
|
||||
// and FE 50mm files. Check only the supported radii, not extrapolated corners.
|
||||
let prime = [-4, -1, 3, 6, 12, 17, 23, 30, 37, 43, 50, 58, 65, 71, 76, 84];
|
||||
let cases = [
|
||||
(A7CR_WIDE, [1.0606515126386147, -0.24282593993968085, 0.1667903749262344, -0.059882313562606454], 0.93),
|
||||
(prime, [0.9951622479344596, 0.00741813858793482, -0.002474290073462703, -0.000235147000260114], 0.96),
|
||||
];
|
||||
let crop = Rect::new(32, 20, 9504, 6336);
|
||||
let radius = 9503f64.hypot(6335.0) / 2.0;
|
||||
for (table, reference, supported) in cases {
|
||||
let Opcode::WarpRectilinear { planes, center } = a7cr_distortion(&table, Rect::new(0, 0, 9728, 6656), crop).unwrap() else {
|
||||
panic!("warp")
|
||||
};
|
||||
let (cx, cy) = (center[0] * 9727.0, center[1] * 6655.0);
|
||||
let normalization = cx.max(9727.0 - cx).hypot(cy.max(6655.0 - cy));
|
||||
let k = planes[0];
|
||||
let mut worst = 0.0f64;
|
||||
for j in 0..=1000 {
|
||||
let r = supported * j as f64 / 1000.0;
|
||||
let rho = r * radius;
|
||||
let r2 = (rho / normalization).powi(2);
|
||||
let mapped = rho * (k[0] + r2 * (k[1] + r2 * (k[2] + r2 * k[3])));
|
||||
let expected = rho * (reference[0] + r * r * (reference[1] + r * r * (reference[2] + r * r * reference[3])));
|
||||
worst = worst.max((mapped - expected).abs() * 1440.0 / 9504.0);
|
||||
}
|
||||
// This smooth feature fit has localization noise. The combined budget allows the
|
||||
// 0.75 px camera-table residual plus the separately bounded 0.25 px polynomial error.
|
||||
assert!(worst < 1.0, "camera-JPEG shape residual {worst} px at 1440");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_ycbcr_files_are_decoded_but_not_corrected() {
|
||||
// a downsized lossless (M/S) coding, as in arw.rs's test, from the validated model with 3:2 crop tags
|
||||
|
||||
@@ -1253,7 +1253,7 @@ fn sony_lens_records_of_the_public_samples() {
|
||||
"arw-sony-a7cr-compressed.arw",
|
||||
[272, 1, 0],
|
||||
(a7cr_shading, a7cr_ca, a7cr_distortion),
|
||||
SonyLensStatus::NotValidatedModel,
|
||||
SonyLensStatus::CameraCorrectionDisabled,
|
||||
(9600, 6376),
|
||||
32767,
|
||||
false,
|
||||
@@ -1264,7 +1264,7 @@ fn sony_lens_records_of_the_public_samples() {
|
||||
"arw-sony-a7cr-lossless-l.arw",
|
||||
[272, 1, 0],
|
||||
(a7cr_shading, a7cr_ca, a7cr_distortion),
|
||||
SonyLensStatus::NotValidatedModel,
|
||||
SonyLensStatus::CameraCorrectionDisabled,
|
||||
(9728, 6656),
|
||||
7,
|
||||
false,
|
||||
@@ -1275,7 +1275,7 @@ fn sony_lens_records_of_the_public_samples() {
|
||||
"arw-sony-a7cr-lossless-m.arw",
|
||||
[272, 1, 0],
|
||||
(a7cr_shading, a7cr_ca, a7cr_distortion),
|
||||
SonyLensStatus::NotValidatedModel,
|
||||
SonyLensStatus::NotValidatedMode,
|
||||
(6656, 4608),
|
||||
7,
|
||||
true,
|
||||
@@ -1362,6 +1362,38 @@ fn sony_lens_records_of_the_public_samples() {
|
||||
eprintln!("Sony lens records: {checked} checks on corpus files");
|
||||
}
|
||||
|
||||
/// These pinned JPEGs have correction Off. Changing only the raw-IFD setting tests decoder wiring,
|
||||
/// not agreement with those JPEGs. The empirical corrected references are documented separately.
|
||||
#[test]
|
||||
fn a7cr_enabled_distortion_agrees_in_header_full_and_dng() {
|
||||
use lightcraft_raw::lens_metadata::SonyLensStatus;
|
||||
let root = corpus_root().join("raw");
|
||||
let mut checked = 0;
|
||||
for name in ["arw-sony-a7cr-compressed.arw", "arw-sony-a7cr-lossless-l.arw", "arw-sony-a7cr-lossless-m.arw"] {
|
||||
let Some(mut bytes) = pinned_sony(&root, name) else { continue };
|
||||
let original = probe_info(&bytes).unwrap();
|
||||
assert!(original.opcodes.list3.is_empty(), "Off files must remain uncorrected");
|
||||
let tiff = lightcraft_tiff::Tiff::parse(&bytes).unwrap();
|
||||
let at = tiff.find(0x7036).unwrap().offset as usize;
|
||||
let mut enabled = Vec::new();
|
||||
tiff.order.put_u16(&mut enabled, 17);
|
||||
bytes[at..at + 2].copy_from_slice(&enabled);
|
||||
let header = probe_info(&bytes).unwrap();
|
||||
let full = decode(&bytes).unwrap();
|
||||
assert_eq!(header, full.info(), "{name}");
|
||||
let s = full.lens_metadata.sony.as_ref().unwrap();
|
||||
let linear = s.geometry.linear_ycbcr;
|
||||
assert_eq!(s.distortion.status, if linear { SonyLensStatus::NotValidatedMode } else { SonyLensStatus::Applied });
|
||||
assert_eq!(full.opcodes.list3.len(), usize::from(!linear));
|
||||
// DNG export keeps the normalized map, including the different lossless storage padding.
|
||||
let dng = lightcraft_raw::write_dng(&full, &Default::default()).unwrap();
|
||||
let back = probe_info(&dng).unwrap();
|
||||
assert_eq!(back.opcodes.list3, full.opcodes.list3, "{name}");
|
||||
checked += 1;
|
||||
}
|
||||
assert!(checked == 0 || checked == 3, "run with all three pinned A7CR codings");
|
||||
}
|
||||
|
||||
/// Kodak DCS520C (`crates/raw/src/vendor/kodak.rs`): the two-component lossless JPEG strip decodes through the
|
||||
/// file's response curve to a 1736 x 1160 GRBG mosaic with 12-bit white, per-position black levels and the
|
||||
/// white balance of the vendor IFD. Looks for the corpus copy, then the raw.pixls.us folder; skips without either.
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
|
||||
| 5 B | **AI masks: Subject, Sky, People, Background, Landscape** are heuristics; Object / Describe run SAM 3 but its download has no mirror configured | `crates/segment`; ai-masks.md; MASK rows 67% | masking is how most Lightroom users edit today | 40–70 + model decision | [target-app-parity](target-app-parity.md#ai) |
|
||||
| 6 B | **Compressed ORF** (every Olympus / OM body since ~2008) | measured: 66 of 78 Olympus / OM models preview-only; `orf.rs` refuses; OM bucket 3% of photographers, 3% of them fully work | +2.6 points; best gain per hour of any raw gap (~0.2) | 8–15 | [raw-parity](raw-parity.md#compressed-orf-a-clean-room-starting-point) (the two Olympus patents that describe the method) |
|
||||
| 7 | **Nikon High Efficiency NEF** (HE / HE★ on Z 8, Z 9, Z 6III, Z f, Z50II, Z5II when set to HE) | named and preview only (#193); needs a pure-Rust JPEG XS decoder | +1.1 points (only shots taken in HE; lossless works on the same bodies); low gain per hour (~0.04), so not a beta blocker | 20–40 | [raw-parity](raw-parity.md) |
|
||||
| 8 B | **No lens profile database** (LR-EDIT-OPTICS-PROFILE): only DNG opcodes, RW2 in-file distortion, Sony ILCE-7RM4A and ILCE-7RM6 distortion (`docs/sony-ilce7rm6-lens-corrections.md`) and Nikon Z 6II / Z 8 / Z f distortion (`docs/nikon-lens-corrections.md`) | Adobe ships 3,635 lens profiles; we never use them | distortion / vignetting correction is a default step for many | 40–80 + calibration shots | [raw-parity](raw-parity.md#lens-profiles) |
|
||||
| 8 B | **No lens profile database** (LR-EDIT-OPTICS-PROFILE): only DNG opcodes, RW2 in-file distortion, Sony ILCE-7RM4A, full-frame ILCE-7CR distortion (Auto only) and ILCE-7RM6 distortion (`docs/sony-ilce7rm6-lens-corrections.md`) and Nikon Z 6II / Z 8 / Z f distortion (`docs/nikon-lens-corrections.md`) | Adobe ships 3,635 lens profiles; we never use them | distortion / vignetting correction is a default step for many | 40–80 + calibration shots | [raw-parity](raw-parity.md#lens-profiles) |
|
||||
| 9 B | **Tests don't run in CI on macOS, Windows or Linux x64**: only FreeBSD runs `cargo test`; the others build packages | `.github/workflows/` (freebsd, packaging-lint, release, windows-arm64); the linux-riscv64 release job added in 0.6.0 (#844) runs only `lightcraft-cli --version` under QEMU | regressions reach users on the main platforms; `cargo xtask ci` runs only locally | 6–12 | [target-app-parity](target-app-parity.md#by-dimension) |
|
||||
| 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 545 of 1,446 Adobe models verified (38%), ~85% of photographers on a verified body; Nikon 5 bodies verified, Canon 34, Leica 14 (the Q3 by a recorded check of 38 private DNGs). Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 / F828 regression (#702), DSLR-A200…A390 / A850 12-bit ARW (#535) | measured: [raw/coverage.tsv](raw/coverage.tsv) | per-body bugs (levels, crops, WB) surface only from users | 25–45 | [raw-parity](raw-parity.md#by-maker-measured) |
|
||||
| 11 | **Enhance: Raw Details, Super Resolution; AI denoise without shippable weights or X-Trans** | ENH rows 0%; denoise Bayer only, RawNIND weights GPL and opt-in | Fujifilm users get no AI denoise; nobody gets it out of the box | 30–60 + model decision | [raw-parity](raw-parity.md#demosaic-levels-crops-and-lens-metadata) |
|
||||
|
||||
@@ -214,7 +214,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
|
||||
| LR-EDIT-DETAIL-SUPERRES | Super resolution | P2 | ⬜ | | |
|
||||
| LR-EDIT-DETAIL-AISHARPEN | AI sharpen | OOS | 🚫 | | |
|
||||
| LR-EDIT-OPTICS-CA | Remove chromatic aberration | P1 | ✅ | `crates/ui-egui/src/panels/edit.rs` (checkbox), `ctl:optics.caRed`, `ctl:optics.caBlue` | |
|
||||
| LR-EDIT-OPTICS-PROFILE | Lens profile corrections | P1 | 🟡 | `ctl:optics.profileDistortion`, `ctl:optics.profileVignetting`, `crates/pipeline/src/optics.rs`, `crates/raw/src/vendor/rw2.rs`, `crates/raw/src/vendor/raf_lens.rs`, `crates/engine/src/crs.rs` | uses corrections embedded in the file: DNG `WarpRectilinear` / `WarpRectilinear2` (opcode 14, DNG 1.6: degree-14 radial polynomial, valid-radius clamp, reciprocal option, applied in list order with the spec's skip rule for a `WarpRectilinear` after an optional one, so Pixel DNGs' two-stage warp is corrected; up to 3 chained stages, sampled on the CPU because the polynomials need `f64`; issue #635) / `FixVignetteRadial`, and the camera's own distortion correction in Panasonic / Leica RW2 / RWL files (tag `0x0119`, read as the equivalent `WarpRectilinear`; on 79 photos from 13 bodies and 11 lenses the corrected render lines up with the camera JPEG within 0.75 px at 1440 px at the corners, from up to 13 % of the half diagonal uncorrected; issue #256); an imported preset's `LensProfileEnable=0` leaves them on, as Lightroom applies built-in corrections regardless (inferred); Sony ILCE-7RM4A Bayer ARW raw-IFD distortion tables (`0x7037`, 16-sample layout, native 3:2 framing) use the same warp path, enabled only for the independently validated ILCE-7RM4A with FE 24–105 and FE 200–600 lenses; ILCE-7RM6 verified against the camera JPEG on 54 photos with three lenses (`docs/sony-ilce7rm6-lens-corrections.md`); other Sony models remain uncorrected (see `docs/sony-lens-corrections.md`); Nikon NEF distortion from NikonNEFInfo (`0xc7d5`: polynomial, frame-filling zoom and the optical-centre offset left by in-body stabilisation; `crates/raw/src/vendor/nef_lens.rs`, `docs/nikon-lens-corrections.md`) for the Z 6II, Z 8 and Z f, full-frame image area only: F-mount lenses on the FTZ match the camera JPEG within 0.2 px median, Z lenses within 0.5–1.5 px except the 24-70 f/4's wide end, where the camera applies a further correction that is not in the readable data (7 px median at 24 mm, from 16 px / 106 px p95 uncorrected); Fujifilm RAF raw-IFD tables (`0xf00b` distortion, `0xf010` vignetting; `crates/raw/src/vendor/raf_lens.rs`, `docs/fuji-lens-corrections.md`) become a `WarpRectilinear` with the camera's frame-filling zoom and a `FixVignetteRadial` gain for 59 body models with sample files checked against the camera JPEG (the table indexed by the source radius with last knot at the corner; on 67 body/lens combinations the corrected render's radial shape error against the camera JPEG falls from a median of 0.72 % to 0.05 % of the half diagonal, largest 0.42 %, and the vignetting gain of the JPEG tracks the table at a median correlation of 0.987); files whose table scale is not the recorded image's half diagonal (X-S20, X-T30 III, X-M5, X-T5 crops) and six unmatched bodies stay uncorrected; Fujifilm's lateral CA table (`0xf00f`) is real but is not applied, because our demosaic already registers the planes and the stored shift over-corrects; vignetting and lateral CA data of Olympus raws not read yet; Sony ARW shading, lateral CA and distortion tables (raw IFD `0x7031`–`0x7037`, with the encrypted `SR2SubIFD` copies as a checked fallback) are decoded with their camera settings, provenance and framing evidence for every body that records them (`crates/raw/src/lens_metadata.rs`), but only the ILCE-7RM4A and ILCE-7RM6 distortion above is applied: on the ILCE-7CR the unchanged distortion interpretation misses the camera JPEG at 24 mm (median 1.6 px, p95 6.2 px at 2376 px), and Sony shading and CA semantics are not validated beyond the ILCE-7RM6's (lateral CA off) (`docs/sony-lens-corrections.md`); Reload from Disk reads the file's lens data for photos catalogued without it and turns the correction on for those still at import settings (issue #314); lens-profile database: a lensfun-format XML database in `LIGHTCRAFT_LENSFUN_DIR` (none is bundled) supplies distortion (poly3 / poly5 / PTLens), TCA and vignetting for lenses it lists (`crates/lensdb`, `crates/engine/src/lens_profiles.rs`, `docs/lens-profiles.md`); no settings switch or bundled database yet |
|
||||
| LR-EDIT-OPTICS-PROFILE | Lens profile corrections | P1 | 🟡 | `ctl:optics.profileDistortion`, `ctl:optics.profileVignetting`, `crates/pipeline/src/optics.rs`, `crates/raw/src/vendor/rw2.rs`, `crates/raw/src/vendor/raf_lens.rs`, `crates/engine/src/crs.rs` | uses corrections embedded in the file: DNG `WarpRectilinear` / `WarpRectilinear2` (opcode 14, DNG 1.6: degree-14 radial polynomial, valid-radius clamp, reciprocal option, applied in list order with the spec's skip rule for a `WarpRectilinear` after an optional one, so Pixel DNGs' two-stage warp is corrected; up to 3 chained stages, sampled on the CPU because the polynomials need `f64`; issue #635) / `FixVignetteRadial`, and the camera's own distortion correction in Panasonic / Leica RW2 / RWL files (tag `0x0119`, read as the equivalent `WarpRectilinear`; on 79 photos from 13 bodies and 11 lenses the corrected render lines up with the camera JPEG within 0.75 px at 1440 px at the corners, from up to 13 % of the half diagonal uncorrected; issue #256); an imported preset's `LensProfileEnable=0` leaves them on, as Lightroom applies built-in corrections regardless (inferred); Sony ILCE-7RM4A Bayer ARW raw-IFD distortion tables (`0x7037`, 16-sample layout, native 3:2 framing) use the same warp path, enabled for the independently validated ILCE-7RM4A with FE 24–105 and FE 200–600 lenses, and ILCE-7CR full-frame Bayer 3:2 files with correction Auto or forced Auto (a separate corrected-radius conversion validated against camera JPEGs with FE 24-50mm, FE 16-25mm and FE 50mm lenses); A7CR Off, APS-C, aspect crops and lossless M/S remain uncorrected; ILCE-7RM6 verified against the camera JPEG on 54 photos with three lenses (`docs/sony-ilce7rm6-lens-corrections.md`); other Sony models remain uncorrected (see `docs/sony-lens-corrections.md`); Nikon NEF distortion from NikonNEFInfo (`0xc7d5`: polynomial, frame-filling zoom and the optical-centre offset left by in-body stabilisation; `crates/raw/src/vendor/nef_lens.rs`, `docs/nikon-lens-corrections.md`) for the Z 6II, Z 8 and Z f, full-frame image area only: F-mount lenses on the FTZ match the camera JPEG within 0.2 px median, Z lenses within 0.5–1.5 px except the 24-70 f/4's wide end, where the camera applies a further correction that is not in the readable data (7 px median at 24 mm, from 16 px / 106 px p95 uncorrected); Fujifilm RAF raw-IFD tables (`0xf00b` distortion, `0xf010` vignetting; `crates/raw/src/vendor/raf_lens.rs`, `docs/fuji-lens-corrections.md`) become a `WarpRectilinear` with the camera's frame-filling zoom and a `FixVignetteRadial` gain for 59 body models with sample files checked against the camera JPEG (the table indexed by the source radius with last knot at the corner; on 67 body/lens combinations the corrected render's radial shape error against the camera JPEG falls from a median of 0.72 % to 0.05 % of the half diagonal, largest 0.42 %, and the vignetting gain of the JPEG tracks the table at a median correlation of 0.987); files whose table scale is not the recorded image's half diagonal (X-S20, X-T30 III, X-M5, X-T5 crops) and six unmatched bodies stay uncorrected; Fujifilm's lateral CA table (`0xf00f`) is real but is not applied, because our demosaic already registers the planes and the stored shift over-corrects; vignetting and lateral CA data of Olympus raws not read yet; Sony ARW shading, lateral CA and distortion tables (raw IFD `0x7031`–`0x7037`, with the encrypted `SR2SubIFD` copies as a checked fallback) are decoded with their camera settings, provenance and framing evidence for every body that records them (`crates/raw/src/lens_metadata.rs`), but only the ILCE-7RM4A, ILCE-7RM6 and ILCE-7CR use the separate distortion conversions above; Sony shading and CA semantics are not validated beyond the ILCE-7RM6's (lateral CA off) (`docs/sony-lens-corrections.md`); Reload from Disk reads the file's lens data for photos catalogued without it and turns the correction on for those still at import settings (issue #314); lens-profile database: a lensfun-format XML database in `LIGHTCRAFT_LENSFUN_DIR` (none is bundled) supplies distortion (poly3 / poly5 / PTLens), TCA and vignetting for lenses it lists (`crates/lensdb`, `crates/engine/src/lens_profiles.rs`, `docs/lens-profiles.md`); no settings switch or bundled database yet |
|
||||
)
|
||||
| LR-EDIT-OPTICS-DEFRINGE | Defringe | P1 | ✅ | `ctl:optics.defringe*` | no fringe eyedropper |
|
||||
| LR-EDIT-OPTICS-MANUAL | Manual distortion / vignetting | P1 | ✅ | `ctl:optics.distortion`, `ctl:optics.vignetting`, `ctl:optics.vignettingMidpoint` | |
|
||||
|
||||
+1
-1
@@ -234,7 +234,7 @@ essentials user (the default look is what they keep).
|
||||
| Item | Adobe | LightCraft |
|
||||
|---|---|---|
|
||||
| Profile database | 3,635 profile files, 56 makers, auto-selected | **0** (Adobe LCPs never used; no database of our own) |
|
||||
| In-file corrections | ✅ | DNG opcodes (WarpRectilinear, FixVignetteRadial, GainMap…) applied; Panasonic / Leica RW2 distortion (79 photos, 13 bodies, 11 lenses); Sony tag 0x7037 distortion for the ILCE-7RM4A (2 lenses) and the ILCE-7RM6 (3 lenses, 54 photos; `sony-ilce7rm6-lens-corrections.md`); Nikon NikonNEFInfo distortion (tag `0xc7d5`, about the sensor-shifted optical centre) for the Z 6II and Z f: F-mount lenses on the FTZ within 0.2 px of the camera JPEG, Z lenses within 0.5–1.5 px except the 24-70's wide end (7 px median; `docs/nikon-lens-corrections.md`) |
|
||||
| In-file corrections | ✅ | DNG opcodes (WarpRectilinear, FixVignetteRadial, GainMap…) applied; Panasonic / Leica RW2 distortion (79 photos, 13 bodies, 11 lenses); Sony tag 0x7037 distortion for the ILCE-7RM4A (2 lenses), full-frame Bayer 3:2 ILCE-7CR files with correction Auto or forced Auto (3 lenses; see sony-lens-corrections.md for residuals and limits) and the ILCE-7RM6 (3 lenses, 54 photos; `sony-ilce7rm6-lens-corrections.md`); Nikon NikonNEFInfo distortion (tag `0xc7d5`, about the sensor-shifted optical centre) for the Z 6II and Z f: F-mount lenses on the FTZ within 0.2 px of the camera JPEG, Z lenses within 0.5–1.5 px except the 24-70's wide end (7 px median; `docs/nikon-lens-corrections.md`) |
|
||||
| Vignetting / lateral CA from maker notes | ✅ | ⬜ |
|
||||
| Manual distortion, vignetting, CA, defringe | ✅ | ✅ |
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ convention; that page's *Two conventions* section has the comparison and what wo
|
||||
|
||||
Sony ARWs record three lens corrections per shot in the raw image IFD: shading (vignetting), lateral chromatic
|
||||
aberration (CA) and distortion, each as a camera setting and a radial table, with copies in the encrypted
|
||||
`SR2SubIFD`. LightCraft **decodes all three** wherever a file carries them and **applies one**: the distortion table
|
||||
of the **ILCE-7RM4A (A7R IVA)** in Bayer ARWs with native 3:2 framing, independently validated with the
|
||||
FE 24–105mm F4 G OSS and FE 200–600mm F5.6–6.3 G OSS (the ILCE-7RM6's distortion is applied too, by its own
|
||||
conversion above). Every other model,
|
||||
and shading and CA on every model, are decoded only: no warp, vignetting or CA correction is added for them. The decoded record stays with the image
|
||||
`SR2SubIFD`. LightCraft **decodes all three** wherever a file carries them and applies distortion here on two validated
|
||||
bodies: **ILCE-7RM4A (A7R IVA)** and **ILCE-7CR (A7CR)**. The A7R IVA keeps its existing Bayer 3:2 conversion,
|
||||
validated with the FE 24-105mm F4 G OSS and FE 200-600mm F5.6-6.3 G OSS. The A7CR uses a separate conversion,
|
||||
limited to full-frame Bayer 3:2 files with distortion Auto or forced Auto. Apart from the separate ILCE-7RM6 converter, other models and shading and CA on every model are
|
||||
decoded only: no warp, vignetting or CA correction is added for them. The decoded record stays with the image
|
||||
(`RawInfo::lens_metadata` / `RawImage::lens_metadata`, types in `crates/raw/src/lens_metadata.rs`, decoder in
|
||||
`crates/raw/src/vendor/arw_lens.rs`) for diagnostics and for the validation work described below.
|
||||
|
||||
@@ -20,7 +20,8 @@ control, CPU/GPU optics path, EXIF orientation handling, and DNG export use the
|
||||
and non-3:2 camera crops are excluded pending validation. An aspect crop may retain the full-frame radial
|
||||
normalization; the decoder does not guess that relationship. Tables of other lengths and rejected tables remain
|
||||
uncorrected, and a table found only in the encrypted `SR2SubIFD` is never applied. The camera's distortion Off
|
||||
setting does not erase its table. Newly imported photos use LightCraft's existing embedded-profile default
|
||||
setting does not erase its table. A7CR Off, missing, unavailable and unknown settings produce no warp. The A7R IVA's
|
||||
existing behavior is unchanged. Newly imported photos use LightCraft's existing embedded-profile default
|
||||
(enabled); the profile correction control can disable or adjust it.
|
||||
|
||||
## What is decoded
|
||||
@@ -67,7 +68,10 @@ setting does not erase its table. Newly imported photos use LightCraft's existin
|
||||
conversion refused the table). A readable table never implies a usable correction; only `Applied` components
|
||||
become opcodes.
|
||||
|
||||
The applied path is unchanged: the validated conversion still receives the raw IFD's `0x7037` table exactly as the
|
||||
For the A7CR, `CameraCorrectionDisabled` records an Off, missing, unavailable or unknown setting. APS-C,
|
||||
aspect crops and lossless M/S remain `NotValidatedMode`. The raw IFD's correction table is required.
|
||||
|
||||
The A7R IVA's applied path is unchanged: its conversion still receives the raw IFD's `0x7037` table exactly as the
|
||||
TIFF reader parsed it, with the same model, framing and fit checks, and produces the same warp. A repeated tag keeps
|
||||
the reader's choice and is only flagged, and a table the reader never reads (past its entry limit) is reported but
|
||||
never applied, so already-catalogued photos, their previews and their stored lens data stay consistent with a fresh
|
||||
@@ -85,17 +89,8 @@ part of this repository.
|
||||
(Auto) everywhere; distortion is 17 (Auto, fixed by the lens) on the nine FE 24–50 files and 0 (Off) on the rest.
|
||||
Every image crop is 9504 × 6336 at (32, 20), in uncompressed, compressed and lossless-L (padded to 9728 × 6656)
|
||||
storage. The decoder reproduces this inventory exactly (see *Local inventory check*).
|
||||
- **Distortion.** The table layout and crop tags match the A7R IVA's, but its interpretation applied unchanged does
|
||||
not reproduce the A7CR camera JPEG at 24 mm: at 2376 px wide, median / 95th-percentile feature residuals were
|
||||
1.61 / 6.17 px (12.98 / 51.19 px uncorrected, 6,539 matches), with a signed radial pattern (about +1.7 px at
|
||||
radii 0.25–0.5, −7.4 px at 0.8–1) that no translation removes. At 50 mm it agrees (0.23 / 1.05 px, from
|
||||
2.21 / 12.25 px). Scaling the raw table by about 10/9, or using the MakerNote's distortion array instead, brings
|
||||
24 mm to about 0.37 / 1.10 and 0.33 / 0.97 px. Those are exploratory comparisons on two files against the camera
|
||||
JPEG, not a model: the MakerNote values are roughly 1.11 times the raw table without one exact ratio, and the
|
||||
A7R IVA was validated against a different reference renderer (Imaging Edge exports). Needed: A7CR Imaging Edge
|
||||
On/Off exports at 24 and 50 mm and with another lens, compared with the raw table at its fixed scale; a decision on
|
||||
the reference (camera JPEG or Imaging Edge); then ILCE-7CR added to the validated models for the full-frame Bayer
|
||||
3:2 mode only. Neither a 10/9 multiplier nor the MakerNote table is used.
|
||||
- **Distortion.** The A7R IVA interpretation misses the A7CR camera JPEG at the wide end. The A7CR now uses the
|
||||
separately measured corrected-radius convention below. It uses the raw IFD table, not the MakerNote array.
|
||||
- **Shading.** The layout decodes, but no public specification gives the sample-to-gain function, its radius
|
||||
convention on the A7CR, or whether the raw samples already contain part of the correction. Brightness ratios on six
|
||||
natural scenes show no consistent missing peripheral brightening. Needed: evenly lit flat fields with shading
|
||||
@@ -112,6 +107,78 @@ part of this repository.
|
||||
- **Other modes.** APS-C, aspect crops and lossless M/S (linear YCbCr) captures were not measured and stay
|
||||
excluded.
|
||||
|
||||
## A7CR camera-JPEG validation
|
||||
|
||||
The A7CR conversion substantially reduces the corrected camera JPEG's geometry mismatch. Its empirical table unit is
|
||||
`15559.300457825264`, fixed by the earlier four-file FE 24-50mm fit before checking other files and lenses.
|
||||
This is a measured approximation for the tested scope, not a Sony specification or a universal Sony constant.
|
||||
|
||||
For output radius `rho` in native crop pixels:
|
||||
|
||||
```
|
||||
spacing = hypot(9504, 6336) / 30
|
||||
S(r) = r * (1 + interpolate(table, r / spacing) / 15559.300457825264)
|
||||
source_radius(rho) = S(rho / zoom)
|
||||
```
|
||||
|
||||
Interpolation is linear. The final segment extends beyond the last knot. Positive samples increase the source
|
||||
radius before the fill zoom; negative samples reduce it.
|
||||
|
||||
The fill zoom makes the maximum of
|
||||
`S(rho / zoom) / rho` on the rectangular crop border equal to one. The optical center is the default crop's
|
||||
pixel-center midpoint. The measured crop is 9504 by 6336 at (32, 20).
|
||||
|
||||
Storage padding affects DNG normalization
|
||||
only, so 9600 by 6376 and 9728 by 6656 storage produce the same physical warp.
|
||||
|
||||
A four-term odd polynomial represents the map as `WarpRectilinear`. A dense 32769-point check rejects a polynomial
|
||||
that folds or differs from the table by more than 0.25 px at a 1440 px long edge. This approximation check is
|
||||
separate from agreement with the camera JPEG. The converter applies no shading or lateral CA.
|
||||
|
||||
Validation uses all 131 owner files numerically, nine public raw.pixls.us codings, one independent public FE 50mm
|
||||
file and the public issue #1300 FE 16-25mm file. Private pixels stay in memory. No private images, names or source
|
||||
identities are saved in the new measurement artifacts.
|
||||
|
||||
The nine private correction-on files cover the FE 24-50mm
|
||||
at 24, 49 and 50 mm. The other 122 private files have correction Off and cover the Tamron 35-150mm and the
|
||||
Voigtlander APO-LANTHAR 28mm and 50mm. Off references validate preservation, not correction-on optics for those lenses.
|
||||
|
||||
Green-channel SIFT (Scale-Invariant Feature Transform) matches at 2376 px are filtered by an independent radial fit. Residuals remove median translation
|
||||
only. They fit no scale, center or lens coefficient to the candidate.
|
||||
|
||||
High-pass correlation uses held-out pixels
|
||||
at 1200 px. File checks hold out the second file and then every fifth file in each lens/focal/setting group, and
|
||||
both public correction-on files. The private checks were also examined in the earlier study, so they are file
|
||||
checks rather than a fresh independent capture campaign.
|
||||
|
||||
The public second-lens checks do not tune the unit.
|
||||
|
||||
The A7R VI study uses a 0.75 px camera validation budget at 1440 px. The reporter has support to 0.933 half
|
||||
diagonals. Its direct table's largest radial-bin residual is 0.590 px at 1200 px (0.708 px at 1440 px).
|
||||
|
||||
The implemented polynomial reaches 0.638 px in a radial bin and 0.692 px in a supported edge region at 1200 px
|
||||
in source coordinates. Actual corrected-raster matches give 0.173/0.605 px median/p95, a 0.763 px radial-bin
|
||||
maximum and a 0.969 px edge-region maximum at 1200 px. That edge region is 1.162 px at 1440 px, so this does not
|
||||
pass a strict 0.75 px budget everywhere measured.
|
||||
|
||||
The unit test against the independently fitted smooth feature curve uses the combined 1.0 px table and
|
||||
polynomial approximation budget; its observed maximum is 0.895 px at 1440 px. These are different metrics.
|
||||
|
||||
The held-out private 24 mm file has weak outer-radius support. No unsupported ideal corner is counted as measured.
|
||||
|
||||
APS-C, aspect crops and lossless M/S are excluded. Portrait captures use the same sensor-coordinate map and the
|
||||
existing EXIF orientation conversion. Other lenses and the 16-25mm's tele end need more correction-on references.
|
||||
|
||||
No Imaging Edge On/Off pairs were measured for this body. Camera JPEGs are the reference for this change.
|
||||
|
||||
This branch supplies the warp for camera-look pairing in PR #1586. It does not duplicate that pairing code.
|
||||
|
||||
`LOOK_VERSION` 11 refreshes smart previews that stored no A7CR lens data; `RENDER_CACHE_VERSION` 68 redraws cached
|
||||
renders.
|
||||
|
||||
Existing edits are preserved. Reload from Disk reads the new lens data for old imports. Enable the
|
||||
profile correction control when an existing edit has it disabled.
|
||||
|
||||
## Local inventory check
|
||||
|
||||
`crates/raw/tests/sony_lens_inventory.rs` is opt-in and does nothing in CI. With `LIGHTCRAFT_SONY_LENS_DIR` naming a
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
## 2026-10-14
|
||||
|
||||
### Sony A7CR distortion correction
|
||||
|
||||
- Full-frame 3:2 Bayer A7CR raws with distortion Auto or forced Auto now use their embedded distortion table. The corrected render is closer to the camera JPEG geometry. Off files keep their original geometry. APS-C, aspect crops and lossless M/S remain excluded.
|
||||
- The warp also supplies the geometry needed by the lens-aligned camera-look fit in PR #1586. The pairing code is a separate change. Details and numeric validation are in `docs/sony-lens-corrections.md`.
|
||||
|
||||
### Lightroom radial masks on the right side
|
||||
- Radial masks brought over from Lightroom Classic (catalog import, XMP sidecars, edits inside DNGs and presets) now
|
||||
apply their effect inside the circle, as Lightroom does, instead of outside it (issue #1815). A mask inverted in
|
||||
|
||||
Reference in New Issue
Block a user