mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-31 21:48:53 -06:00
RAW.colour: fit the per-file look on the vignetting-corrected sensor
The RAF look fit compared the uncorrected sensor proxy with the camera JPEG, which is already corrected for vignetting, so the tone curve learned the brightening and the render then applied the embedded vignetting correction a second time. Shadows and midtones came out up to 0.6 / 0.3 EV too light on X-S10 files. `look_proxies` applies the embedded correction to the sensor proxy of every per-file fit (colour, monochrome, near-black fallback), as `fit_dng_tone` already did. LOOK_VERSION 13, RENDER_CACHE_VERSION 71. 60 held-out X-S10 RAFs: mean ΔE00 2.71 -> 2.55, 32 better, none worse by more than 0.05. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
77c0a6adfa
commit
001a58e992
@@ -79,7 +79,9 @@ use tone::{HighlightCheck, ToneFit, fit_tone, fit_tone_conditional, highlights};
|
||||
/// a tone-only fallback for near-black scenes with too few colour pairs (#1587); DNG tone fitting uses a
|
||||
/// lens-corrected proxy when the file carries lens opcodes; validated A7CR distortion, including the lens data
|
||||
/// stored with each smart preview.
|
||||
pub const LOOK_VERSION: u32 = 12;
|
||||
/// 13: the sensor proxy of every per-file fit gets the embedded vignetting correction the render applies (Fujifilm
|
||||
/// RAF; other makes with a lens database), so the render no longer applies it twice.
|
||||
pub const LOOK_VERSION: u32 = 13;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct CameraLook {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::look_fit::{fit_dng_tone, fit_preview};
|
||||
use super::look_fit::{fit_dng_tone, fit_preview, look_proxies};
|
||||
use super::mono::{fit_dark_tone, monochrome_look};
|
||||
use super::proxy::{LensGeometry, proxies};
|
||||
use super::proxy::LensGeometry;
|
||||
use super::{CameraLook, CameraTone, CameraTransform, ColorData, D50, D65, Mat3, ProfileLook, REC2020, RawFormat, RawImage, ToneCurve, Xy, bradford};
|
||||
|
||||
/// Long edge of the sensor/JPEG proxy a single photo's look is fitted on (the acceptance gates
|
||||
@@ -42,7 +42,8 @@ pub(crate) fn file_local_look(format: RawFormat) -> bool {
|
||||
/// as-shot white, the camera transform at it and the fitted look.
|
||||
/// `lens` is the file's own lens-correction warp ([`lens_geometry`], read before the caller removes it from
|
||||
/// `OpcodeList3`): the camera applied it to its JPEG, so the sensor proxy is warped the same way before pairing.
|
||||
/// `embedded` is the file's embedded lens vignetting, for the DNG tone fit.
|
||||
/// `embedded` is the file's embedded lens correction: its vignetting is applied to the sensor proxy of every fit, as
|
||||
/// the render applies it.
|
||||
pub(crate) fn starting_colour(
|
||||
raw: &mut RawImage,
|
||||
bytes: &[u8],
|
||||
@@ -51,13 +52,13 @@ pub(crate) fn starting_colour(
|
||||
) -> (Xy, CameraTransform, Option<CameraLook>) {
|
||||
let (xy, t, look) = starting_colour_with(
|
||||
raw,
|
||||
|raw, t| fit_preview(raw, bytes, t, lens),
|
||||
|raw, t| monochrome_look(raw, bytes, t, lens),
|
||||
|raw, t| fit_preview(raw, bytes, t, lens, embedded),
|
||||
|raw, t| monochrome_look(raw, bytes, t, lens, embedded),
|
||||
|raw, t| {
|
||||
if !file_local_look(raw.format) {
|
||||
return None;
|
||||
}
|
||||
let (sensor, reference, clipped) = proxies(raw, bytes, t, PROXY, 0.0, lens)?;
|
||||
let (sensor, reference, clipped) = look_proxies(raw, bytes, t, lens, embedded)?;
|
||||
fit_dark_tone(&sensor, &reference, &clipped)
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,11 +6,18 @@ use super::tone::{HighlightCheck, highlights};
|
||||
use super::{CameraLook, CameraTone, CameraTransform, HsvTable, Mat3, RawImage, Rgb32f, ToneMap, luminance_2020};
|
||||
|
||||
/// The look fitted to the file's camera JPEG, and whether that JPEG is black and white ([`monochrome`]).
|
||||
pub(crate) fn fit_preview(raw: &RawImage, bytes: &[u8], transform: &CameraTransform, lens: Option<&LensGeometry>) -> (Option<CameraLook>, bool) {
|
||||
/// `embedded`: the file's embedded lens correction, whose vignetting the render applies ([`look_proxies`]).
|
||||
pub(crate) fn fit_preview(
|
||||
raw: &RawImage,
|
||||
bytes: &[u8],
|
||||
transform: &CameraTransform,
|
||||
lens: Option<&LensGeometry>,
|
||||
embedded: Option<&lightcraft_develop::EmbeddedLens>,
|
||||
) -> (Option<CameraLook>, bool) {
|
||||
if !transform.matrix_is_fallback || !file_local_look(raw.format) {
|
||||
return (None, false);
|
||||
}
|
||||
let Some((sensor, reference, clipped)) = proxies(raw, bytes, transform, PROXY, 0.0, lens) else { return (None, false) };
|
||||
let Some((sensor, reference, clipped)) = look_proxies(raw, bytes, transform, lens, embedded) else { return (None, false) };
|
||||
let black_and_white = monochrome(&reference);
|
||||
// A camera profile pooled from many photos knows colours this photo shows too little of;
|
||||
// try its colour first and fit tone/chroma per photo (picture styles vary).
|
||||
@@ -257,6 +264,22 @@ pub(super) fn clips_more(sensor: &Rgb32f, reference: &Rgb32f, fitted: &ToneMap,
|
||||
near_white(fitted) > near_white(plain).max(preview) + CLIP_TOLERANCE
|
||||
}
|
||||
|
||||
/// The proxies a photo's look is fitted on ([`proxies`] at [`PROXY`]), the sensor's with the embedded vignetting
|
||||
/// correction the render applies ([`correct_vignetting`]). Without it the tone curve learns the camera JPEG's corrected
|
||||
/// brightness from the uncorrected sensor, and the render then corrects it again: 21 X-S10 RAFs rendered up to 0.3 EV
|
||||
/// brighter in their midtones and 0.6 EV in their shadows than their camera JPEGs, about 0.05 EV once corrected.
|
||||
pub(super) fn look_proxies(
|
||||
raw: &RawImage,
|
||||
bytes: &[u8],
|
||||
transform: &CameraTransform,
|
||||
lens: Option<&LensGeometry>,
|
||||
embedded: Option<&lightcraft_develop::EmbeddedLens>,
|
||||
) -> Option<(Rgb32f, Rgb32f, Vec<bool>)> {
|
||||
let (mut sensor, reference, clipped) = proxies(raw, bytes, transform, PROXY, 0.0, lens)?;
|
||||
correct_vignetting(&mut sensor, embedded, raw.orientation);
|
||||
Some((sensor, reference, clipped))
|
||||
}
|
||||
|
||||
/// Multiply the sensor proxy (sensor orientation, default crop) by the embedded vignetting correction, the gain the
|
||||
/// pipeline applies to the render (`optics::embedded_vignette_gain`; `lens` is in the oriented, cropped frame): the
|
||||
/// camera JPEG has it applied, so a tone fitted without it brightens the render's corners twice.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use super::colour::{PROXY, file_local_look};
|
||||
use super::colour::file_local_look;
|
||||
use super::hue_sat::HueSat;
|
||||
use super::look_fit::{with_dro_off_tone, without_clipped};
|
||||
use super::look_fit::{look_proxies, with_dro_off_tone, without_clipped};
|
||||
use super::pairs::{
|
||||
EDGE_CONTRAST, MIN_IMPROVEMENT, PARTIAL_MIN_RANK_CORRELATION, Pairs, collect_pairs, displayed, fit_tone_with_shadows, local_contrast, luma,
|
||||
rank_correlation_ties,
|
||||
};
|
||||
use super::proxy::{LensGeometry, proxies};
|
||||
use super::proxy::LensGeometry;
|
||||
use super::tone::{HighlightCheck, ToneFit, fit_tone, highlights, highlights_hold};
|
||||
use super::{CameraLook, CameraTransform, D50, D65, HsvTable, Mat3, PROPHOTO, REC2020, RawImage, Rgb32f, ToneMap, bradford, luminance_2020};
|
||||
|
||||
@@ -33,12 +33,18 @@ pub(super) fn monochrome(reference: &Rgb32f) -> bool {
|
||||
/// and flatter than the same body's photos shot in colour. But the JPEG still shows the camera's tone curve.
|
||||
/// `transform` is the colour the photo gets otherwise (the spectral matrices or the fallback's); the look keeps it and
|
||||
/// adds the tone curve alone ([`fit_tone_only_with`]), as [`fit_look_with`] tries the photo's own colour after a profile's. The
|
||||
/// chroma curve stays neutral: the JPEG has no colourfulness to learn.
|
||||
pub(super) fn monochrome_look(raw: &RawImage, bytes: &[u8], transform: &CameraTransform, lens: Option<&LensGeometry>) -> Option<CameraLook> {
|
||||
/// chroma curve stays neutral: the JPEG has no colourfulness to learn. `embedded` as for [`fit_preview`].
|
||||
pub(super) fn monochrome_look(
|
||||
raw: &RawImage,
|
||||
bytes: &[u8],
|
||||
transform: &CameraTransform,
|
||||
lens: Option<&LensGeometry>,
|
||||
embedded: Option<&lightcraft_develop::EmbeddedLens>,
|
||||
) -> Option<CameraLook> {
|
||||
if !file_local_look(raw.format) {
|
||||
return None;
|
||||
}
|
||||
let (sensor, reference, clipped) = proxies(raw, bytes, transform, PROXY, 0.0, lens)?;
|
||||
let (sensor, reference, clipped) = look_proxies(raw, bytes, transform, lens, embedded)?;
|
||||
if !monochrome(&reference) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -928,7 +928,7 @@ fn corpus_cr3_gets_a_framing_aligned_camera_look() {
|
||||
assert_eq!(raw.format, RawFormat::Cr3);
|
||||
assert_eq!(raw.info().developed_size(), (6000, 4000));
|
||||
let transform = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw));
|
||||
let look = fit_preview(&raw, &bytes, &transform, None).0;
|
||||
let look = fit_preview(&raw, &bytes, &transform, None, None).0;
|
||||
eprintln!("R100 guarded colour fit accepted: {}", look.is_some());
|
||||
assert!(look.is_some(), "this R100 corpus photo has enough matching colour after camera-framing alignment");
|
||||
let (_, info) = crate::files::load_bytes(&bytes, 400).unwrap();
|
||||
@@ -965,7 +965,7 @@ fn corpus_uses_the_bundled_profile(file: &str, model: &str) {
|
||||
let lens = lens_geometry(&raw.opcodes.list3);
|
||||
raw.opcodes.list3.retain(|op| !op.is_lens_correction());
|
||||
let transform = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw));
|
||||
let look = fit_preview(&raw, &bytes, &transform, lens.as_ref()).0.expect("fit accepted");
|
||||
let look = fit_preview(&raw, &bytes, &transform, lens.as_ref(), None).0.expect("fit accepted");
|
||||
let expected = bundled.matrix().mul(&transform.matrix.inverse().unwrap());
|
||||
for (row, want) in look.matrix.0.iter().zip(expected.0) {
|
||||
for (got, want) in row.iter().zip(want) {
|
||||
@@ -1346,7 +1346,7 @@ fn corpus_arw_with_dro_is_lowered_to_the_dro_off_tone() {
|
||||
let mut raw = lightcraft_raw::decode(&bytes).unwrap();
|
||||
raw.opcodes.list3.retain(|op| !op.is_lens_correction());
|
||||
let transform = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw));
|
||||
let look = fit_preview(&raw, &bytes, &transform, None).0.expect("a camera look");
|
||||
let look = fit_preview(&raw, &bytes, &transform, None, None).0.expect("a camera look");
|
||||
// the look fitted to the JPEG, as before this change
|
||||
let (sensor, reference, clipped) = proxies(&raw, &bytes, &transform, PROXY, 0.0, None).unwrap();
|
||||
let profile = raw.metadata.model.as_deref().and_then(crate::camera_profiles::get);
|
||||
@@ -1978,20 +1978,22 @@ fn a_rejected_profile_leaves_the_tone_curve_on_the_photos_own_colour() {
|
||||
assert!(look.is_some() && !t.matrix_is_fallback);
|
||||
}
|
||||
|
||||
/// [`monochrome_look`] on a synthetic raw file: an ILCE-7M4 (it has a bundled camera profile) whose camera JPEG,
|
||||
/// in a minimal TIFF, is black and white gets the tone curve, on the colour of the transform it is given; the
|
||||
/// same file with a tinted JPEG gets nothing from this step.
|
||||
#[test]
|
||||
fn monochrome_look_reads_the_files_black_and_white_jpeg() {
|
||||
/// A synthetic ILCE-7M4 raw (it has a bundled camera profile; demosaiced grey `sensor` levels) and its file bytes:
|
||||
/// a minimal TIFF holding the camera JPEG, a bright camera curve on the `scene` luminance as an 8-bit sRGB JPEG in
|
||||
/// `tint` (grey for `[1.0; 3]`).
|
||||
fn grey_raw_with_jpeg(
|
||||
(w, h): (usize, usize),
|
||||
sensor: impl Fn(usize, usize) -> f32,
|
||||
scene: impl Fn(usize, usize) -> f32,
|
||||
tint: [f32; 3],
|
||||
) -> (RawImage, Vec<u8>) {
|
||||
use lightcraft_raw::{BlackLevel, ColorData, OpcodeLists, Orientation, RawData, Rect};
|
||||
let (w, h) = (192usize, 128usize);
|
||||
let level = |x: usize, y: usize| 0.01 + 0.5 * (x as f32 / w as f32) * (0.3 + 0.7 * y as f32 / h as f32);
|
||||
let raw = RawImage {
|
||||
format: RawFormat::Arw,
|
||||
width: w,
|
||||
height: h,
|
||||
cpp: 3,
|
||||
data: RawData::F32((0..w * h).flat_map(|i| [level(i % w, i / w); 3]).collect()),
|
||||
data: RawData::F32((0..w * h).flat_map(|i| [sensor(i % w, i / w); 3]).collect()),
|
||||
cfa: None,
|
||||
bits: 16,
|
||||
black: BlackLevel::uniform(0.0),
|
||||
@@ -2007,38 +2009,71 @@ fn monochrome_look_reads_the_files_black_and_white_jpeg() {
|
||||
metadata: lightcraft_meta::Metadata { make: Some("SONY".into()), model: Some("ILCE-7M4".into()), ..Default::default() },
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
let srgb = |v: f32| if v <= 0.003_130_8 { 12.92 * v } else { 1.055 * v.powf(1.0 / 2.4) - 0.055 };
|
||||
let pixels: Vec<u8> = (0..w * h)
|
||||
.flat_map(|i| {
|
||||
let v = 1.0 - (-3.0 * scene(i % w, i / w)).exp();
|
||||
tint.map(|t| (srgb(v * t).clamp(0.0, 1.0) * 255.0).round() as u8)
|
||||
})
|
||||
.collect();
|
||||
let image = lightcraft_codecs::EncodeImage::new(w as u32, h as u32, 3, lightcraft_codecs::Samples::U8(&pixels));
|
||||
let jpeg = lightcraft_codecs::encode_jpeg(&image, 95, lightcraft_codecs::ChromaSubsampling::S444, &Default::default()).unwrap();
|
||||
// II*, IFD0 at 8 with JPEGInterchangeFormat (0x201) and its length (0x202), the JPEG after it
|
||||
let mut tiff = b"II*\0\x08\0\0\0\x02\0".to_vec();
|
||||
for (tag, value) in [(0x0201u16, 38u32), (0x0202, jpeg.len() as u32)] {
|
||||
tiff.extend(tag.to_le_bytes());
|
||||
tiff.extend(4u16.to_le_bytes());
|
||||
tiff.extend(1u32.to_le_bytes());
|
||||
tiff.extend(value.to_le_bytes());
|
||||
}
|
||||
tiff.extend(0u32.to_le_bytes());
|
||||
assert_eq!(tiff.len(), 38);
|
||||
tiff.extend(jpeg);
|
||||
(raw, tiff)
|
||||
}
|
||||
|
||||
/// [`monochrome_look`] on a synthetic raw file: an ILCE-7M4 whose camera JPEG is black and white gets the tone curve,
|
||||
/// on the colour of the transform it is given; the same file with a tinted JPEG gets nothing from this step.
|
||||
#[test]
|
||||
fn monochrome_look_reads_the_files_black_and_white_jpeg() {
|
||||
let (w, h) = (192usize, 128usize);
|
||||
let level = |x: usize, y: usize| 0.01 + 0.5 * (x as f32 / w as f32) * (0.3 + 0.7 * y as f32 / h as f32);
|
||||
let (raw, grey) = grey_raw_with_jpeg((w, h), level, level, [1.0; 3]);
|
||||
assert!(crate::camera_profiles::get("ILCE-7M4").is_some());
|
||||
// the camera's rendering: a bright curve on the scene's luminance, as a grey (or tinted) 8-bit sRGB JPEG
|
||||
let file = |tint: [f32; 3]| {
|
||||
let srgb = |v: f32| if v <= 0.003_130_8 { 12.92 * v } else { 1.055 * v.powf(1.0 / 2.4) - 0.055 };
|
||||
let pixels: Vec<u8> = (0..w * h)
|
||||
.flat_map(|i| {
|
||||
let v = 1.0 - (-3.0 * level(i % w, i / w)).exp();
|
||||
tint.map(|t| (srgb(v * t).clamp(0.0, 1.0) * 255.0).round() as u8)
|
||||
})
|
||||
.collect();
|
||||
let image = lightcraft_codecs::EncodeImage::new(w as u32, h as u32, 3, lightcraft_codecs::Samples::U8(&pixels));
|
||||
let jpeg = lightcraft_codecs::encode_jpeg(&image, 95, lightcraft_codecs::ChromaSubsampling::S444, &Default::default()).unwrap();
|
||||
// II*, IFD0 at 8 with JPEGInterchangeFormat (0x201) and its length (0x202), the JPEG after it
|
||||
let mut tiff = b"II*\0\x08\0\0\0\x02\0".to_vec();
|
||||
for (tag, value) in [(0x0201u16, 38u32), (0x0202, jpeg.len() as u32)] {
|
||||
tiff.extend(tag.to_le_bytes());
|
||||
tiff.extend(4u16.to_le_bytes());
|
||||
tiff.extend(1u32.to_le_bytes());
|
||||
tiff.extend(value.to_le_bytes());
|
||||
}
|
||||
tiff.extend(0u32.to_le_bytes());
|
||||
assert_eq!(tiff.len(), 38);
|
||||
tiff.extend(jpeg);
|
||||
tiff
|
||||
};
|
||||
let t = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw));
|
||||
let look = monochrome_look(&raw, &file([1.0; 3]), &t, None).expect("a tone curve from the black-and-white JPEG");
|
||||
let look = monochrome_look(&raw, &grey, &t, None, None).expect("a tone curve from the black-and-white JPEG");
|
||||
assert!(look.matrix == Mat3::IDENTITY && look.hue_sat.is_none(), "on the transform's colour");
|
||||
let curve = ToneMap::camera(&look.tone, 0.0, 0.0, 0.0);
|
||||
let mid = curve.apply(0.25);
|
||||
assert!((mid - (1.0 - (-0.75f32).exp())).abs() < 0.05, "the camera's curve at 0.25: {mid}");
|
||||
assert!(monochrome_look(&raw, &file([1.0, 0.9, 0.8]), &t, None).is_none(), "a tinted JPEG is not black and white");
|
||||
let (_, tinted) = grey_raw_with_jpeg((w, h), level, level, [1.0, 0.9, 0.8]);
|
||||
assert!(monochrome_look(&raw, &tinted, &t, None, None).is_none(), "a tinted JPEG is not black and white");
|
||||
}
|
||||
|
||||
/// The look is fitted on the sensor as the render shows it, with the file's embedded vignetting correction: the
|
||||
/// camera corrected its JPEG, so a curve fitted to the uncorrected sensor learns that brightening, and the render
|
||||
/// then applies it twice (X-S10 RAFs up to 0.3 EV brighter than their camera JPEGs).
|
||||
#[test]
|
||||
fn the_look_is_fitted_on_the_vignetting_corrected_sensor() {
|
||||
use lightcraft_develop::{EmbeddedLens, EmbeddedVignette};
|
||||
let (w, h) = (192usize, 128usize);
|
||||
let vignette = EmbeddedVignette { k: [1.0, 0.0, 0.0, 0.0, 0.0], center: lightcraft_geom::Point::new(0.5, 0.5), radius: 0.6 };
|
||||
let gain = |x: usize, y: usize| {
|
||||
let point = lightcraft_geom::Point::new(x as f64 + 0.5, y as f64 + 0.5);
|
||||
lightcraft_pipeline::optics::embedded_vignette_gain(&vignette, point, w as f64, h as f64) as f32
|
||||
};
|
||||
let scene = |x: usize, y: usize| 0.01 + 0.5 * (x as f32 / w as f32) * (0.3 + 0.7 * y as f32 / h as f32);
|
||||
let (raw, bytes) = grey_raw_with_jpeg((w, h), |x, y| scene(x, y) / gain(x, y), scene, [1.0; 3]);
|
||||
let t = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw));
|
||||
let lens = EmbeddedLens { vignette: Some(vignette), ..Default::default() };
|
||||
let camera = 1.0 - (-0.75f32).exp();
|
||||
let at_quarter = |embedded: Option<&EmbeddedLens>| {
|
||||
let look = monochrome_look(&raw, &bytes, &t, None, embedded).expect("a tone curve");
|
||||
ToneMap::camera(&look.tone, 0.0, 0.0, 0.0).apply(0.25)
|
||||
};
|
||||
let (corrected, uncorrected) = (at_quarter(Some(&lens)), at_quarter(None));
|
||||
assert!((corrected - camera).abs() < 0.03, "the camera's curve at 0.25: {corrected} vs {camera}");
|
||||
assert!(uncorrected > camera + 0.03, "without the correction the curve brightens: {uncorrected} vs {camera}");
|
||||
}
|
||||
|
||||
/// The black-and-white fit has the partial look's checks: a JPEG of another picture, or one the default curve
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{Arc, Deserialize, DevelopSettings, MediaKind, Photo, Rgb32f, Rgba8, Serialize, SourceInfo};
|
||||
|
||||
/// Bump when the pipeline's output changes, to invalidate cached thumbnails.
|
||||
pub const RENDER_CACHE_VERSION: u64 = 70;
|
||||
pub const RENDER_CACHE_VERSION: u64 = 71;
|
||||
|
||||
/// 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];
|
||||
|
||||
@@ -127,6 +127,23 @@ still has lens corrections removed, so the pipeline applies them once through En
|
||||
Corrections. Developing this proxy requires a full-resolution demosaic; files without such
|
||||
opcodes keep their binned proxy.
|
||||
|
||||
## Embedded vignetting correction (Fujifilm RAF)
|
||||
|
||||
The RAF decoder turns the maker note's vignetting data into a `FixVignetteRadial` correction, and the render applies
|
||||
it (Enable Profile Corrections). The camera applied the same correction to its JPEG. Every per-file fit
|
||||
(`fit_preview`, `monochrome_look`, the near-black tone fallback) therefore takes its sensor proxy through
|
||||
`look_proxies`, which multiplies it by that gain as `fit_dng_tone` already did (`correct_vignetting`). Before
|
||||
`LOOK_VERSION` 13 the RAF fit saw the uncorrected sensor: the tone curve learned the JPEG's corrected brightness, and
|
||||
the render added the correction again. Lens-database corrections (`LIGHTCRAFT_LENSFUN_DIR`) reach the fit the same way.
|
||||
|
||||
On 21 X-S10 photos the render was up to 0.3 EV brighter than the camera JPEG in the midtones (a wide-open blue-hour
|
||||
shot) and up to 0.6 EV in the darkest 3 % of the pixels (ISO 12800 indoors). On 60 other X-S10 photos, picked at
|
||||
random from one photographer's library and rendered without their sidecars, mean ΔE00 against the camera JPEG went
|
||||
from 2.71 to 2.55: 32 improved by more than 0.1, none got worse by more than 0.05. The median luminance error per
|
||||
quantile is now within ±0.04 EV, except +0.09 EV in the darkest 3 %. That rest comes from fitting on a 96 px proxy:
|
||||
each proxy pixel averages a shadow's texture before the curve, while the render applies the curve per pixel. A fit
|
||||
on a 4× finer proxy roughly halves it in a simulation, but the acceptance gates were set at 96 px.
|
||||
|
||||
## Camera profiles (pooled per model)
|
||||
|
||||
One photo can show too little of a colour for its own fit to learn it: in a second shot of the lime shirt only a few dozen proxy pixels show it, next to a hillside of foliage at the same hue that the camera renders differently, and the shirt stayed yellow (hue 68° against the JPEG's 82°; a 192-pixel proxy only reached 74°). `lightcraft-cli calibrate [--max N] [--out DIR] FOLDERS…` therefore pools the colour pairs of many ARWs, NEFs or RAFs per camera model — each against its own embedded JPEG, at a 192-pixel proxy, at most 4000 pairs per photo, files spread evenly over the folders — and fits one matrix (from white-balanced camera RGB, so no single photo's white point is baked in) and one hue/saturation/value table. Models need at least 5 usable photos.
|
||||
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
# RAW parity: cameras, formats, demosaic, colour and lens profiles
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (Minolta MRW decoded, 6 bodies verified; lossy JPEG XL DNG tiles with a non-linear declared transfer curve decode; Canon ColorData Kelvin; X-Trans CMOS 4 bodies take the X-T3's spectral matrices) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 (Camera Raw 18.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** patch (RAF camera look no longer applies the embedded vignetting correction twice) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 (Camera Raw 18.7)
|
||||
|
||||
Raw processing is the deepest single dimension of Lightroom parity: a photographer whose camera
|
||||
does not decode, or decodes with the wrong colour, cannot switch. This document measures it model by
|
||||
@@ -225,7 +225,8 @@ sample files. A pointer for contributors, not legal advice.
|
||||
- Against the camera JPEG (what the fit targets): the Sony bulk check (#535) accepted fits on 81 of 87
|
||||
bodies, median ΔE 5.1, ΔL\* +0.2; rejected fits fall back to a neutral matrix at median ΔE 18.5,
|
||||
ΔL\* −11, chroma ×0.39. RW2 140 of 174 files accepted, median 5.0. NEF 13 of 13, 2.9–7.8. RAF 23 of
|
||||
24, pooled X-H2S 1.62 / X-T4 2.34. SRW 2.2–7.6.
|
||||
24, pooled X-H2S 1.62 / X-T4 2.34 (measured before `LOOK_VERSION` 13 stopped applying the embedded vignetting
|
||||
correction twice); X-S10, 60 photos not used to find that bug, mean ΔE00 2.71 → 2.55 with it fixed. SRW 2.2–7.6.
|
||||
- Against Lightroom itself (few): Sony DRO median ΔE76 7.5 → 3.3; Olympus E-1 vs Lightroom Classic
|
||||
ΔE00 11.25 → 8.30; ProRAW with its gain map ΔE00 19 (why the map is off). No measurement against
|
||||
Adobe Standard across models exists.
|
||||
@@ -328,3 +329,4 @@ Beyond decoding:
|
||||
| 2026-10-14 | minor | Integration batch 36: CC0 per-model verification for 117 Nikon/Fujifilm, 97 Canon CR2/Pentax/Ricoh, 21 Sony/Leica and 23 Olympus/Samsung bodies, 22 more CR3 bodies and 30 more models; Epson ERF and Mamiya MEF decode; measured Nikon image areas (#1053, #1770): 515 verified, 675 unverified, 218 preview, 38 unsupported (re-measured with `coverage.py`) |
|
||||
| 2026-10-15 | minor | By-maker and by-use tables re-measured with `coverage.py` after batches 36–39 (545 verified, 651 unverified, 212 preview, 38 unsupported; ~94% of photographers' cameras fully work, ~85% on a verified body); rows the per-model PRs had updated are current again |
|
||||
| 2026-10-18 | minor | Minolta MRW decoded (`vendor/mrw.rs`): DiMAGE 7, 7Hi, A1, A2, A200 and Dynax / Maxxum 7D verified; 551 verified, 656 unverified, 201 preview (re-measured with `coverage.py`) |
|
||||
| 2026-10-30 | patch | RAF camera look fitted on the vignetting-corrected sensor (`LOOK_VERSION` 13): renders were up to 0.3 EV too bright in midtones and 0.6 EV in shadows; X-S10 mean ΔE00 vs the camera JPEG 2.71 → 2.55 on 60 photos |
|
||||
|
||||
Reference in New Issue
Block a user