mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-19 02:10:25 -05:00
LR-MERGE-HDR: merged DNGs declare the white balance their source shows (follow-up to #1123)
#1123 gives HDR and panorama merges of raws without colour matrices the colour they open with, but writes ColorMatrix1 from the forward matrix, so the as-shot white the DNG declares lands wherever the fitted look and the generic camera model take it. On real ILCE-7M3 brackets the HDRs opened at As Shot 6113–7307 K / +115 (Duv +0.038) where the ARWs show 3932–5268 K / +7, and any white-balance edit turned them green (CIEDE2000 11–13 against the ARW at the same Temp/Tint). The render at As Shot was right (0.4–0.8). The colour matrices now declare the white balance the source shows, decided by files::WbScaleOf (new dng_white): the camera's measured matrices when they give the source's scale (lightcraft_raw::spectral; the DNG then reads every white as the source does), else a matrix under which the as-shot neutral is the white the scale shows (Sony's presets' reading, 6500 K / 0 on the relative scale, the file's own white otherwise). Forward matrix and neutral are unchanged, so is the render at As Shot. Measured on the same brackets with this change on a fork build: As Shot 4813 / 5268 / 3932 K, +8 / +8 / +7 as the ARWs (declared white Duv +0.003); same Temp/Tint within CIEDE2000 0.8–1.7 of the ARW for most changes, about 5 for the largest (5268 → 3200 K, 3932 → 6500 K: the ARW adapts its look with Bradford, the DNG rebalances in camera space). Tests: a merged DNG shows its source's As Shot on all four scales (measured matrices, presets, relative, own); with a look that tints white the DNG declares the shown white, gives the measured model's multipliers at other whites and renders at As Shot as the source does. Both fail on main (ILCE-7M3 case: (7342, 115) for (5268, 8)). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gRtdWF1E9358FfzhzYqCS
This commit is contained in:
5 files changed
+167
-15
No files matched your search
@@ -119,8 +119,11 @@ pub(crate) fn starting_colour_tags(raw: &mut RawImage, bytes: &[u8]) -> Option<C
|
||||
if lightcraft_raw::color::has_matrix(&raw.color) {
|
||||
return None;
|
||||
}
|
||||
// the white the source shows, decided before `starting_colour` may fill in measured matrices
|
||||
let scale = crate::files::WbScaleOf::new(raw.format, &raw.color, raw.wb_multipliers, &raw.metadata);
|
||||
let white = scale.dng_white(&raw.color, lightcraft_raw::color::as_shot_white_xy(raw));
|
||||
let (_, t, look) = starting_colour(raw, bytes);
|
||||
dng_colour(&raw.color, &t, look.as_ref())
|
||||
dng_colour(&raw.color, &white, &t, look.as_ref())
|
||||
}
|
||||
|
||||
/// DNG tags that render white-balanced camera RGB at its as-shot white as `t` with `look` on top does: one forward
|
||||
@@ -129,8 +132,13 @@ pub(crate) fn starting_colour_tags(raw: &mut RawImage, bytes: &[u8]) -> Option<C
|
||||
/// doesn't keep each pixel's luminance as the fitted one does ([`HueSat`]), and the chroma curve has no tag; on two
|
||||
/// CC0 CR2s (EOS 550D, 40D) an HDR merge of three copies renders within mean CIE76 ΔE 1.07 and 0.47 of the photo
|
||||
/// (1.47 and 0.66 without the table; 26 and 23 with no colour tags at all).
|
||||
/// The colour matrices declare `declared`, the white balance the source shows ([`crate::files::WbScaleOf::dng_white`]),
|
||||
/// so the DNG opens at the source's As Shot temperature and tint and a white-balance edit starts from it. Matrices
|
||||
/// derived from `forward` put the declared white wherever the look and the generic camera model took it: ARW merges
|
||||
/// opened at about 7000 K / +115 where the ARWs show 4800 K / +8, and any white-balance edit turned them green.
|
||||
/// `forward` is used only with the as-shot neutral, so the render at As Shot doesn't depend on them.
|
||||
/// `None` when the white balance or the matrix can't give tags.
|
||||
fn dng_colour(tags: &ColorData, t: &CameraTransform, look: Option<&CameraLook>) -> Option<ColorData> {
|
||||
pub(crate) fn dng_colour(tags: &ColorData, declared: &crate::files::DngWhite, t: &CameraTransform, look: Option<&CameraLook>) -> Option<ColorData> {
|
||||
let m = look.map_or(t.matrix, |l| l.matrix.mul(&t.matrix));
|
||||
// the DNG model renders the camera's white at unit brightness: what a look does to white's brightness goes
|
||||
// into the baseline exposure
|
||||
@@ -143,11 +151,19 @@ fn dng_colour(tags: &ColorData, t: &CameraTransform, look: Option<&CameraLook>)
|
||||
let forward = bradford(D65, D50).mul(&REC2020.to_xyz()).mul(&m);
|
||||
let wb = t.wb.map(f64::from);
|
||||
let neutral = wb.map(|w| wb.iter().cloned().fold(f64::MAX, f64::min) / w);
|
||||
let camera_to_xyz = bradford(D50, t.white_xy).mul(&forward).mul(&Mat3::diag(1.0 / neutral[0], 1.0 / neutral[1], 1.0 / neutral[2]));
|
||||
let (illuminant, color_matrix) = match declared {
|
||||
crate::files::DngWhite::Matrices(c) if lightcraft_raw::color::has_matrix(c) => (c.illuminant, c.color_matrix),
|
||||
crate::files::DngWhite::Matrices(_) => ([21, 0], [Some(declaring(t.white_xy, neutral)?), None]),
|
||||
crate::files::DngWhite::At(xy) => ([21, 0], [Some(declaring(*xy, neutral)?), None]),
|
||||
};
|
||||
if !color_matrix.iter().flatten().all(|m| m.0.iter().flatten().all(|v| v.is_finite())) {
|
||||
return None;
|
||||
}
|
||||
Some(ColorData {
|
||||
illuminant: [21, 0],
|
||||
color_matrix: [Some(camera_to_xyz.inverse()?), None],
|
||||
forward_matrix: [Some(forward), None],
|
||||
illuminant,
|
||||
color_matrix,
|
||||
// a second colour matrix comes with a second forward matrix (DNG spec): the same one, fitted at one white
|
||||
forward_matrix: [Some(forward), color_matrix[1].map(|_| forward)],
|
||||
camera_calibration: [None, None],
|
||||
analog_balance: None,
|
||||
as_shot_neutral: Some(neutral),
|
||||
@@ -163,6 +179,17 @@ fn dng_colour(tags: &ColorData, t: &CameraTransform, look: Option<&CameraLook>)
|
||||
})
|
||||
}
|
||||
|
||||
/// An XYZ → camera colour matrix under which camera `neutral` is white `xy`: the generic camera model, scaled per
|
||||
/// channel. `None` when the model gives no positive neutral there.
|
||||
fn declaring(xy: Xy, neutral: [f64; 3]) -> Option<Mat3> {
|
||||
let g = lightcraft_raw::color::xyz_to_camera(&ColorData::default(), xy);
|
||||
let n = g.apply(xy.to_xyz());
|
||||
if !n.iter().all(|v| v.is_finite() && *v > 1e-9) {
|
||||
return None;
|
||||
}
|
||||
Some(Mat3::diag(neutral[0] / n[0], neutral[1] / n[1], neutral[2] / n[2]).mul(&g))
|
||||
}
|
||||
|
||||
/// A camera tone curve as a DNG `ProfileToneCurve` (linear in, linear out, 1 = white): sampled densely over the
|
||||
/// 12 stops below white, the range the curve is read back over (`files::dng_tone_curve`).
|
||||
fn dng_tone_curve(tone: &CameraTone) -> Option<ToneCurve> {
|
||||
@@ -2379,7 +2406,7 @@ mod tests {
|
||||
let samples = [[0.2, 0.3, 0.1], [0.05, 0.02, 0.4], [0.6, 0.6, 0.6], [0.01, 0.5, 0.03]];
|
||||
let render = |m: &Mat3, wb: [f32; 3], ev: f64, c: [f64; 3]| m.apply(std::array::from_fn(|i| c[i] * wb[i] as f64)).map(|v| v * ev.exp2());
|
||||
for look in [Some(&look), None] {
|
||||
let dng = dng_colour(&ColorData::default(), &t, look).unwrap();
|
||||
let dng = dng_colour(&ColorData::default(), &crate::files::DngWhite::At(t.white_xy), &t, look).unwrap();
|
||||
let back = camera_transform_of(&dng, as_shot_white_of(&dng, None));
|
||||
for i in 0..3 {
|
||||
assert!((back.wb[i] - t.wb[i]).abs() < 1e-4 * t.wb[i], "{:?} vs {:?}", back.wb, t.wb);
|
||||
@@ -2404,6 +2431,52 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The merged DNG declares the white balance its source shows, also when the look's matrix doesn't keep white
|
||||
/// neutral, as fitted looks often don't, and renders at As Shot as the source does. Real ARW merges declared their
|
||||
/// colour matrix from the look: As Shot about 7000 K / +115 where the ARWs show 4800 K / +8.
|
||||
#[test]
|
||||
fn dng_colour_declares_the_white_balance_the_source_shows() {
|
||||
use crate::files::DngWhite;
|
||||
use lightcraft_raw::color::{as_shot_white_of, camera_transform_of, wb_multipliers};
|
||||
let tags = ColorData::default();
|
||||
let as_shot = [2.5039f32, 1.0, 1.5078];
|
||||
let t = camera_transform_of(&tags, as_shot_white_of(&tags, Some(as_shot)));
|
||||
let mut look = dro_test_look(&std::array::from_fn(|i| 0.005 * 1.2f32.powi(i as i32)), &|x| 0.95 * x / (x + 0.1));
|
||||
look.matrix = Mat3([[1.75, -0.4, -0.2], [-0.2, 1.35, -0.2], [0.05, -0.3, 1.3]]);
|
||||
let tint = look.matrix.apply([1.0; 3]);
|
||||
assert!((tint[0] - tint[1]).abs() > 0.1, "a look that tints white: {tint:?}");
|
||||
let renders_as_shot = |dng: &ColorData| {
|
||||
let r = camera_transform_of(dng, as_shot_white_of(dng, None));
|
||||
assert!(r.wb.iter().zip(t.wb).all(|(a, b)| (a - b).abs() < 1e-4 * b), "{:?} vs {:?}", r.wb, t.wb);
|
||||
let m = look.matrix.mul(&t.matrix);
|
||||
for c in [[0.2, 0.3, 0.1], [0.05, 0.02, 0.4], [0.6, 0.6, 0.6]] {
|
||||
let want = m.apply(std::array::from_fn(|i| c[i] * t.wb[i] as f64)).map(|v| v * t.baseline_exposure.exp2());
|
||||
let got = r.matrix.apply(std::array::from_fn(|i| c[i] * r.wb[i] as f64)).map(|v| v * dng.baseline_exposure.exp2());
|
||||
assert!(want.iter().zip(got).all(|(a, b)| (a - b).abs() < 1e-4), "{c:?}: {want:?} vs {got:?}");
|
||||
}
|
||||
};
|
||||
// a white the source's scale shows (Sony's presets, the relative scale's 6500 K / 0)
|
||||
for xy in [lightcraft_color::cct::temp_tint_to_xy(5600.0, 3.0), lightcraft_color::cct::temp_tint_to_xy(6500.0, 0.0)] {
|
||||
let dng = dng_colour(&tags, &DngWhite::At(xy), &t, Some(&look)).unwrap();
|
||||
let back = as_shot_white_of(&dng, None);
|
||||
assert!((back.x - xy.x).abs() < 1e-6 && (back.y - xy.y).abs() < 1e-6, "as shot {back:?}, shown {xy:?}");
|
||||
renders_as_shot(&dng);
|
||||
}
|
||||
// a camera with measured matrices (the ILCE-7M3): every white read through them, as for the source
|
||||
let camera = lightcraft_raw::spectral::find(Some("SONY"), "ILCE-7M3").expect("a covered camera");
|
||||
let mut model = tags.clone();
|
||||
assert!(camera.fill(&mut model));
|
||||
let shown = lightcraft_raw::spectral::as_shot_white_through(camera, &tags, Some(as_shot)).unwrap();
|
||||
let dng = dng_colour(&tags, &DngWhite::Matrices(Box::new(model.clone())), &t, Some(&look)).unwrap();
|
||||
let back = as_shot_white_of(&dng, None);
|
||||
assert!((back.x - shown.x).abs() < 1e-5 && (back.y - shown.y).abs() < 1e-5, "as shot {back:?}, shown {shown:?}");
|
||||
for w in [Xy::new(0.3127, 0.329), Xy::new(0.4476, 0.4074), Xy::new(0.3457, 0.3585)] {
|
||||
let (a, b) = (wb_multipliers(&model, w), wb_multipliers(&dng, w));
|
||||
assert!(a.iter().zip(b).all(|(a, b)| (a - b).abs() < 1e-6 * a), "{w:?}: {a:?} vs {b:?}");
|
||||
}
|
||||
renders_as_shot(&dng);
|
||||
}
|
||||
|
||||
/// A look with tone curve `f` at knots `xs` and a colour model and chroma curve to keep.
|
||||
fn dro_test_look(xs: &[f32; 32], f: &dyn Fn(f32) -> f32) -> CameraLook {
|
||||
CameraLook {
|
||||
|
||||
@@ -513,17 +513,28 @@ fn load_bytes_now(
|
||||
/// (`lightcraft_raw::spectral`), else when its camera's white-balance presets give one
|
||||
/// (`lightcraft_raw::kelvin_presets`): either turns its as-shot multipliers into Kelvin and tint, nothing else — the
|
||||
/// look stays the file's fit. Any other raw is edited relative to its as-shot look (6500 K / 0 means as shot).
|
||||
struct WbScaleOf {
|
||||
pub(crate) struct WbScaleOf {
|
||||
own_matrix: bool,
|
||||
relative: bool,
|
||||
/// Kelvin through the camera's measured matrices or presets: edited on the relative scale before those were used.
|
||||
legacy_relative: bool,
|
||||
/// The as-shot white through the measured matrices or the presets, when that is where the scale comes from.
|
||||
scale_white: Option<lightcraft_color::Xy>,
|
||||
/// The camera's measured matrices, when they give the scale.
|
||||
camera: Option<&'static lightcraft_raw::spectral::Camera>,
|
||||
}
|
||||
|
||||
/// The white balance a DNG of a raw's camera RGB (Photo Merge) declares so that it shows as its source does
|
||||
/// ([`WbScaleOf::dng_white`]).
|
||||
pub(crate) enum DngWhite {
|
||||
/// Colour tags with the camera's measured matrices: the DNG reads every white through them, as the source does.
|
||||
Matrices(Box<lightcraft_raw::ColorData>),
|
||||
/// The white the source's scale shows at As Shot.
|
||||
At(lightcraft_color::Xy),
|
||||
}
|
||||
|
||||
impl WbScaleOf {
|
||||
fn new(
|
||||
pub(crate) fn new(
|
||||
format: lightcraft_raw::RawFormat,
|
||||
color: &lightcraft_raw::ColorData,
|
||||
wb_multipliers: Option<[f32; 3]>,
|
||||
@@ -540,7 +551,23 @@ impl WbScaleOf {
|
||||
None => None,
|
||||
};
|
||||
let covered = camera.is_some() || presets;
|
||||
WbScaleOf { own_matrix, relative: fitted && !covered, legacy_relative: fitted && covered, scale_white }
|
||||
WbScaleOf { own_matrix, relative: fitted && !covered, legacy_relative: fitted && covered, scale_white, camera }
|
||||
}
|
||||
|
||||
/// What a DNG of this raw's camera RGB declares so that it shows the white balance as this scale does: the
|
||||
/// camera's measured matrices when they give the scale, else the white the scale shows at As Shot (the presets'
|
||||
/// reading; 6500 K / 0 on the relative scale; else `own`, the white the file's tags give).
|
||||
pub(crate) fn dng_white(&self, color: &lightcraft_raw::ColorData, own: lightcraft_color::Xy) -> DngWhite {
|
||||
if let Some(camera) = self.camera {
|
||||
let mut c = lightcraft_raw::ColorData { profile: Default::default(), kelvin_presets: None, ..color.clone() };
|
||||
if camera.fill(&mut c) {
|
||||
return DngWhite::Matrices(Box::new(c));
|
||||
}
|
||||
}
|
||||
if self.relative {
|
||||
return DngWhite::At(lightcraft_color::cct::temp_tint_to_xy(6500.0, 0.0));
|
||||
}
|
||||
DngWhite::At(self.scale_white.unwrap_or(own))
|
||||
}
|
||||
/// The as-shot temperature and tint on the file's scale: the relative scale's reference, the white through
|
||||
/// the measured matrices or the presets, else `own` (the white the file's own tags give). Whole numbers, as the
|
||||
@@ -1096,6 +1123,40 @@ mod tests {
|
||||
assert!(!lightcraft_catalog::relative_wb_camera("DNG", "SONY ILCE-7M3"));
|
||||
}
|
||||
|
||||
/// A merged DNG (Photo Merge) of a raw without colour matrices shows the As Shot temperature and tint its source
|
||||
/// shows, on every scale: through the measured matrices (ILCE-7M3), through the presets (ILCA-99M2), the relative
|
||||
/// scale's 6500 K / 0 (ILCE-7M2) and a file's own white (an unfitted format). Merges of real ILCE-7M3 brackets
|
||||
/// opened at 6113–7307 K / +115 where the ARWs show 3932–5268 K / +7.
|
||||
#[test]
|
||||
fn a_merged_dng_shows_the_white_balance_its_source_shows() {
|
||||
use lightcraft_raw::{ColorData, RawFormat};
|
||||
let meta = |make: &str, model: &str| lightcraft_meta::Metadata { make: Some(make.into()), model: Some(model.into()), ..Default::default() };
|
||||
let g = |r: f32, b: f32| [r / 1024.0, 1.0, b / 1024.0];
|
||||
let presets = lightcraft_raw::kelvin_presets::KelvinPresets::new([
|
||||
(2500.0, g(1336.0, 3672.0)),
|
||||
(3200.0, g(1720.0, 2620.0)),
|
||||
(4500.0, g(2256.0, 1880.0)),
|
||||
(6000.0, g(2680.0, 1544.0)),
|
||||
(8500.0, g(3136.0, 1300.0)),
|
||||
]);
|
||||
let with_presets = ColorData { kelvin_presets: presets, ..Default::default() };
|
||||
let cases = [
|
||||
(RawFormat::Arw, ColorData::default(), [2.5039f32, 1.0, 1.5078], meta("SONY", "ILCE-7M3")),
|
||||
(RawFormat::Arw, with_presets, g(2500.0, 1668.0), meta("SONY", "ILCA-99M2")),
|
||||
(RawFormat::Arw, ColorData::default(), [2.2, 1.0, 1.6], meta("SONY", "ILCE-7M2")),
|
||||
(RawFormat::Rw2, ColorData::default(), [2.0, 1.0, 1.4], meta("Panasonic", "DC-G9")),
|
||||
];
|
||||
for (format, color, mul, meta) in cases {
|
||||
let own = lightcraft_raw::color::as_shot_white_of(&color, Some(mul));
|
||||
let source = WbScaleOf::new(format, &color, Some(mul), &meta);
|
||||
let t = lightcraft_raw::color::camera_transform_of(&color, own);
|
||||
let dng = crate::camera_preview::dng_colour(&color, &source.dng_white(&color, own), &t, None).unwrap();
|
||||
let merged = WbScaleOf::new(RawFormat::Dng, &dng, None, &meta);
|
||||
let shown = source.as_shot(own);
|
||||
assert_eq!(merged.as_shot(lightcraft_raw::color::as_shot_white_of(&dng, None)), shown, "{:?}", meta.model);
|
||||
}
|
||||
}
|
||||
|
||||
/// A RAF whose preview pointer is in its header, then a large raw tail the reader must not need.
|
||||
fn raf_with_embedded_jpeg(jpeg: &[u8], tail: usize) -> Vec<u8> {
|
||||
let head = 160;
|
||||
|
||||
@@ -184,10 +184,22 @@ A merge (HDR, panorama, HDR panorama) writes a linear DNG of the sources' camera
|
||||
tags. A raw without colour matrices of its own has none to give, so its merge used to render with no camera colour: an
|
||||
HDR merge of CR2s came out green and dark (issue #965). Each source now goes through `starting_colour` as when it is
|
||||
opened alone, and `starting_colour_tags` writes the result as DNG tags: one forward matrix (from the look's matrix on
|
||||
top of the camera transform) with a colour matrix and `AsShotNeutral` that agree with it at the as-shot white, the
|
||||
look's brightness of white as baseline exposure, its hue/saturation table as `ProfileHueSatMap` and its tone curve as
|
||||
`ProfileToneCurve`. Raws with their own matrices (DNG) keep their tags. Without a look the tags give the colour the raw
|
||||
falls back to (spectral matrices or the neutral fallback).
|
||||
top of the camera transform) with `AsShotNeutral`, the look's brightness of white as baseline exposure, its
|
||||
hue/saturation table as `ProfileHueSatMap` and its tone curve as `ProfileToneCurve`. Raws with their own matrices (DNG)
|
||||
keep their tags. Without a look the tags give the colour the raw falls back to (spectral matrices or the neutral
|
||||
fallback).
|
||||
|
||||
The colour matrices declare the white balance the source shows (`files::WbScaleOf::dng_white`), so the DNG opens at the
|
||||
source's As Shot temperature and tint and a white-balance edit starts from there: the camera's measured matrices
|
||||
(`lightcraft_raw::spectral`) when they give the source's scale, which reads every white as the source does; else a
|
||||
matrix under which the as-shot neutral is the white the source's scale shows (Sony's presets' reading, 6500 K / 0 on
|
||||
the relative scale, the file's own white otherwise). The forward matrix is used only with the as-shot neutral, so the
|
||||
render at As Shot doesn't depend on them. A colour matrix derived from the forward matrix, as first written, declared
|
||||
wherever the look and the generic camera model put the white: merges of real ILCE-7M3 brackets opened at 6113–7307 K /
|
||||
+115 where the ARWs show 3932–5268 K / +7, and any white-balance edit turned them green (CIEDE2000 11–13 against the
|
||||
ARW at the same Temp/Tint). With the measured matrices they open at the ARWs' As Shot, and the same Temp/Tint renders
|
||||
within CIEDE2000 0.8–1.7 of the ARW for most changes, about 5 for the largest (5268 → 3200 K, 3932 → 6500 K): the ARW
|
||||
adapts its fitted look with Bradford, the DNG rebalances in camera space.
|
||||
|
||||
The DNG renders close to, not exactly like, the source: a DNG hue/saturation table doesn't keep each pixel's luminance
|
||||
as the fitted table does, and the chroma curve has no DNG tag, so it is left out. On two CC0 CR2s (EOS 550D, 40D), an
|
||||
|
||||
@@ -346,7 +346,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
|
||||
|
||||
| Id | Feature | Tier | Status | Evidence | Missing / notes |
|
||||
|---|---|---|---|---|---|
|
||||
| LR-MERGE-HDR | HDR merge | P2 | ✅ | `cmd:merge.hdr`, `cmd:dialog.mergeHdr` | auto align, deghost None–High + overlay, auto settings, float DNG, Create Stack; JPEG brackets treated as linear; raws without colour matrices of their own (CR2, NEF, ARW…) keep the colour they open with, written as DNG tags (`starting_colour_tags`; the look's chroma curve has no tag; panoramas too), issue #965 |
|
||||
| LR-MERGE-HDR | HDR merge | P2 | ✅ | `cmd:merge.hdr`, `cmd:dialog.mergeHdr` | auto align, deghost None–High + overlay, auto settings, float DNG, Create Stack; JPEG brackets treated as linear; raws without colour matrices of their own (CR2, NEF, ARW…) keep the colour they open with, written as DNG tags (`starting_colour_tags`; the look's chroma curve has no tag; panoramas too), issue #965, and the white balance they show (`a_merged_dng_shows_the_white_balance_its_source_shows`) |
|
||||
| LR-MERGE-PANO | Panorama | P2 | ✅ | `cmd:merge.panorama`, `cmd:dialog.mergePanorama` | spherical/cylindrical/perspective + auto, boundary warp, auto crop, fill edges (diffusion), DNG; no lens model / 360° wrap |
|
||||
| LR-MERGE-HDRPANO | HDR panorama | P2 | ✅ | `cmd:merge.hdrPanorama`, `cmd:dialog.mergeHdrPanorama` | brackets grouped by EXIF |
|
||||
| LR-MERGE-HEADLESS | Merge with last settings | P2 | ✅ | `cmd:merge.hdrLast`, `cmd:merge.panoramaLast`, `cmd:merge.hdrPanoramaLast` | no dialog; the options of the last merge of that kind (defaults the first time) |
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
## 2026-10-13
|
||||
|
||||
### HDR merges keep the white balance
|
||||
- An HDR merged from raws that LightCraft colours from the camera's JPEG (CR2, NEF, ARW…) now opens at the same
|
||||
As Shot temperature and tint as its photos, and changing the white balance no longer turns it green. Sony ARW
|
||||
merges used to open at about 7000 K / +115 where the photos show 4000–5300 K / +8. HDRs merged before this keep
|
||||
their old white balance; merge them again.
|
||||
|
||||
### Slider drags on large raws
|
||||
- Dragging a slider no longer re-renders the edited photo's filmstrip thumbnail on every frame; it follows once you
|
||||
let go. On a 64 MP raw the first of those renders decoded the whole file again, which held up the loupe's
|
||||
|
||||
Reference in new issue
Block a user