mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
Merge PR #2006: LR-EDIT-COLOR-TEMP: Kelvin white balance for the Nikon Z5II from built-in presets
crates/raw/src/kelvin_presets.rs: covered_models keeps the Z5II asserts (#2006) and the Canon cases (#2009). docs/parity-checklist.md: LR-EDIT-COLOR-TEMP row keeps both sides' preset clauses.
This commit is contained in:
@@ -653,6 +653,7 @@ mod edited_tests {
|
||||
("SONY ILCE-1", "ARW", true),
|
||||
("", "ARW", true),
|
||||
("SONY ILCE-7M3", "DNG", false),
|
||||
("NIKON CORPORATION NIKON Z5_2", "NEF", false),
|
||||
] {
|
||||
let mut p = Photo::new(PhotoId(1), Source::Demo { scene: 0 }, "a.raw", format, 10, 10, "2026-10-01T00:00:00");
|
||||
p.kind = MediaKind::Raw;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
//! Built-in white-balance presets for cameras whose raws carry neither colour matrices nor a preset table, feeding
|
||||
//! the Kelvin scale of [`crate::kelvin_presets`].
|
||||
//!
|
||||
//! Nikon NEFs store only the as-shot multipliers (`WB_RBLevels`) and, in the maker note, the colour temperature the
|
||||
//! camera states for the white-balance setting it shot with (`ColorTemperatureAuto`, `0x004f`: tag name from the
|
||||
//! ExifTool Nikon tag documentation, read with exiftool run as a black-box tool on the files). A body's points are
|
||||
//! the as-shot multipliers of its own photos shot at one preset each, placed at the temperature the camera itself
|
||||
//! wrote for that preset. Presets off the daylight locus (fluorescent, flash) and shots with a fine-tune shift are
|
||||
//! left out.
|
||||
//!
|
||||
//! Sources: own photos of each body listed (a reporter's samples, never committed), the camera's own stated
|
||||
//! temperatures, and the ExifTool Nikon tag-name documentation (prose). No matrices or third-party data.
|
||||
use crate::kelvin_presets::KelvinPresets;
|
||||
|
||||
/// One body: its make's first word, its Exif model without the make, and five (Kelvin, R/G, B/G) points.
|
||||
struct Builtin {
|
||||
make: &'static str,
|
||||
model: &'static str,
|
||||
points: [(f32, f32, f32); 5],
|
||||
}
|
||||
|
||||
const BUILTINS: &[Builtin] = &[
|
||||
// Nikon Z5II (issue emmertex/lightcraft-classic#157): Incandescent 3000 K, Direct sunlight 5210 K, the Kelvin
|
||||
// setting at 5500 K, Cloudy 5990 K, Shade 8000 K (the temperatures the camera writes in `ColorTemperatureAuto`).
|
||||
// Natural Auto (the camera wrote 5030 K) reads back at 5036 K; Flash (5410 K nominal) at about 6500 K.
|
||||
Builtin {
|
||||
make: "NIKON",
|
||||
model: "Z5_2",
|
||||
points: [
|
||||
(3000.0, 1.138_671_9, 2.083_984_4),
|
||||
(5210.0, 1.726_562_5, 1.335_937_5),
|
||||
(5500.0, 1.771_484_4, 1.281_25),
|
||||
(5990.0, 1.839_843_8, 1.212_890_6),
|
||||
(8000.0, 2.097_656_3, 1.089_843_8),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
fn find_builtin(make: Option<&str>, model: &str) -> Option<&'static Builtin> {
|
||||
let model = model.trim();
|
||||
BUILTINS.iter().find(|b| {
|
||||
let n = b.make.len();
|
||||
let bare = match (model.get(..n), model.get(n..)) {
|
||||
(Some(prefix), Some(rest)) if prefix.eq_ignore_ascii_case(b.make) => rest.trim_start(),
|
||||
_ => model,
|
||||
};
|
||||
make.is_none_or(|m| m.split_whitespace().next().is_some_and(|w| w.eq_ignore_ascii_case(b.make))) && bare.eq_ignore_ascii_case(b.model)
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether this Exif make and model has built-in presets.
|
||||
pub fn covers(make: Option<&str>, model: &str) -> bool {
|
||||
find_builtin(make, model).is_some()
|
||||
}
|
||||
|
||||
/// The built-in presets of this Exif make and model, if any.
|
||||
pub fn find(make: Option<&str>, model: &str) -> Option<KelvinPresets> {
|
||||
let b = find_builtin(make, model)?;
|
||||
KelvinPresets::new(b.points.map(|(k, r, b)| (k, [r, 1.0, b])))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_builtin_makes_a_curve() {
|
||||
for b in BUILTINS {
|
||||
assert!(find(Some(b.make), b.model).is_some(), "{} {}", b.make, b.model);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn z5ii_reads_its_own_shots_near_the_camera_temperatures() {
|
||||
let p = find(Some("NIKON CORPORATION"), "NIKON Z5_2").unwrap();
|
||||
// Natural Auto, not on the curve: the camera wrote 5030 K for it
|
||||
let (t, tint) = p.temp_tint([1.695_312_5, 1.0, 1.373_046_9]).unwrap();
|
||||
assert!((1e6 / t - 1e6 / 5030.0).abs() < 10.0 && tint.abs() < 5.0, "{t} {tint}");
|
||||
// Flash (nominally 5410 K) sits near the curve on the daylight side
|
||||
let (t, tint) = p.temp_tint([1.916_015_6, 1.0, 1.164_062_5]).unwrap();
|
||||
assert!((5000.0..7500.0).contains(&t) && tint.abs() < 10.0, "{t} {tint}");
|
||||
// the 5990 K Kelvin shot with a green-magenta fine-tune shift: daylight, clearly tinted
|
||||
let (t, tint) = p.temp_tint([1.531_25, 1.0, 1.074_218_8]).unwrap();
|
||||
assert!((4000.0..7500.0).contains(&t) && tint.abs() > 20.0, "{t} {tint}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matching() {
|
||||
assert!(covers(Some("NIKON CORPORATION"), "NIKON Z5_2") && covers(None, "Z5_2") && covers(Some("Nikon"), "nikon z5_2"));
|
||||
assert!(!covers(Some("NIKON CORPORATION"), "NIKON Z5") && !covers(Some("SONY"), "Z5_2") && !covers(None, "") && !covers(None, "NIK"));
|
||||
}
|
||||
}
|
||||
@@ -56,8 +56,12 @@ const MODELS: &[(&str, &str)] = &[
|
||||
];
|
||||
|
||||
/// Whether a raw of this Exif make and model is edited in Kelvin through its presets (when it has no colour
|
||||
/// matrices of its own or measured: see [`crate::spectral::find`], which comes first).
|
||||
/// matrices of its own or measured: see [`crate::spectral::find`], which comes first), from the file's presets or
|
||||
/// built-in ones ([`crate::kelvin_builtin`]).
|
||||
pub fn covers(make: Option<&str>, model: &str) -> bool {
|
||||
if crate::kelvin_builtin::covers(make, model) {
|
||||
return true;
|
||||
}
|
||||
let model = model.trim();
|
||||
let brand = make.and_then(|m| m.split_whitespace().next());
|
||||
MODELS.iter().any(|(b, m)| {
|
||||
@@ -72,10 +76,9 @@ pub fn covers(make: Option<&str>, model: &str) -> bool {
|
||||
|
||||
/// [`covers`] for make and model joined by a space, as the catalog keeps them (`"SONY ILCA-99M2"`).
|
||||
pub fn covers_camera(camera: &str) -> bool {
|
||||
match camera.split_once(char::is_whitespace) {
|
||||
Some((make, model)) => covers(Some(make), model),
|
||||
None => covers(None, camera),
|
||||
}
|
||||
let words: Vec<&str> = camera.split_whitespace().collect();
|
||||
// the make can be several words ("NIKON CORPORATION"), so try every split, as `spectral::find_camera` does
|
||||
(1..words.len()).any(|i| covers(Some(&words[..i].join(" ")), &words[i..].join(" "))) || covers(None, camera)
|
||||
}
|
||||
|
||||
/// Most points a curve keeps (Canon writes about seven at a stated temperature, Sony five).
|
||||
@@ -294,6 +297,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn covered_models() {
|
||||
assert!(covers_camera("NIKON CORPORATION NIKON Z5_2") && covers_camera("NIKON Z5_2") && !covers_camera("NIKON CORPORATION NIKON Z6"));
|
||||
for c in [
|
||||
"SONY ILCA-99M2",
|
||||
"SONY DSC-RX10",
|
||||
|
||||
@@ -34,6 +34,7 @@ pub mod gaintable;
|
||||
pub mod highlight;
|
||||
#[cfg(feature = "jxl")]
|
||||
mod jxl;
|
||||
pub mod kelvin_builtin;
|
||||
pub mod kelvin_presets;
|
||||
pub mod lens_metadata;
|
||||
pub mod ljpeg;
|
||||
|
||||
Vendored
+1
-1
@@ -301,7 +301,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
active_area: Rect::new(0, 0, active_w, h),
|
||||
crop,
|
||||
orientation: Orientation::from_exif(ifd0.u16(t::ORIENTATION).unwrap_or(1)),
|
||||
color: ColorData::default(),
|
||||
color: ColorData { kelvin_presets: crate::kelvin_builtin::find(Some(&make), &model), ..Default::default() },
|
||||
wb_multipliers: wb,
|
||||
linearized: false,
|
||||
pixel_aspect,
|
||||
|
||||
@@ -193,7 +193,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
|
||||
| LR-EDIT-LIGHT-CURVE-TAT | Drag-on-image curve adjust | P1 | ✅ | `cmd:develop.targeted` (`target: curve`) | |
|
||||
| LR-EDIT-COLOR-WB-PRESET | White-balance presets | P0 | ✅ | `cmd:develop.wb` | |
|
||||
| LR-EDIT-COLOR-WB-PICKER | White-balance eyedropper | P0 | ✅ | `cmd:tool.wbPicker`, `cmd:develop.wbPick`, `crates/ui-egui/src/tests_wb_selector.rs` | the Edit panel's selector button does what W does (from Compare, Survey or the grid it opens the photo in Detail); no magnified loupe while picking |
|
||||
| LR-EDIT-COLOR-TEMP | Temperature | P0 | ✅ | `ctl:wb.temp`, `crates/pipeline/src/local.rs` (`wb_matrix_for`, `legacy_white`), `crates/raw/src/color.rs` (`rebalance`), `crates/raw/src/spectral.rs` (`find_camera`), `crates/catalog/src/model.rs` (`relative_wb_camera`), `crates/develop/src/controls.rs` (`ControlSpec::position`), `crates/raw/src/kelvin_presets.rs` | Kelvin (2000–50000 K, As Shot read from the file) on raws with colour matrices: in the file (DNG: re-developed for the chosen white through the camera's own matrices, camera-space white balance as Lightroom; the eyedropper / Auto invert the same model) or measured for the camera model (`lightcraft_raw::spectral`, 52 bodies, issue #730: the matrices turn the as-shot multipliers into Kelvin and the slider back, the look stays the file's fit, and the picture is adapted from the as-shot white, Bradford), or read from the camera's own white-balance presets (Sony ARW `SR2SubIFD`, Sony ILCA-99M2, ILCA-77M2, SLT-A33, DSC-RX10, ILCE-3500, ILCE-7S: Sony's Kelvin dial reads back within 0.3 % on 5 settings of 4 bodies; Canon CR2 `ColorData` presets, PowerShot G1 X Mark III: the camera's own As Shot temperature reads back within 5 % on its 9 white-balance settings). The Kelvin slider's track is Lightroom's: 7000 K in the middle, 2000–7000 K evenly on the left half, logarithmic up to 50000 K on the right (within 2 % of positions measured in Lightroom Classic), for drawing, clicks and drags; wheel / arrow-key nudges and Quick Develop still step a fixed 50 / 100 / 500 K; the relative slider stays linear. Settings written on the relative scale before a camera's matrices reached it keep their look (`WbScale`). Not verified against Lightroom's Kelvin: on the ILCE-7M3 the matrices read 7–10 % below Sony's dial (issue #730), and whether the matrices or the dial are off is open; raws without matrices or presets anywhere and rendered files use the relative −100..+100 scale (6500 K / 0 = as shot). |
|
||||
| LR-EDIT-COLOR-TEMP | Temperature | P0 | ✅ | `ctl:wb.temp`, `crates/pipeline/src/local.rs` (`wb_matrix_for`, `legacy_white`), `crates/raw/src/color.rs` (`rebalance`), `crates/raw/src/spectral.rs` (`find_camera`), `crates/catalog/src/model.rs` (`relative_wb_camera`), `crates/develop/src/controls.rs` (`ControlSpec::position`), `crates/raw/src/kelvin_presets.rs` | Kelvin (2000–50000 K, As Shot read from the file) on raws with colour matrices: in the file (DNG: re-developed for the chosen white through the camera's own matrices, camera-space white balance as Lightroom; the eyedropper / Auto invert the same model) or measured for the camera model (`lightcraft_raw::spectral`, 52 bodies, issue #730: the matrices turn the as-shot multipliers into Kelvin and the slider back, the look stays the file's fit, and the picture is adapted from the as-shot white, Bradford), or read from the camera's own white-balance presets (Sony ARW `SR2SubIFD`, Sony ILCA-99M2, ILCA-77M2, SLT-A33, DSC-RX10, ILCE-3500, ILCE-7S: Sony's Kelvin dial reads back within 0.3 % on 5 settings of 4 bodies; Canon CR2 `ColorData` presets, PowerShot G1 X Mark III: the camera's own As Shot temperature reads back within 5 % on its 9 white-balance settings; or built-in presets from the camera's own stated temperatures, `crates/raw/src/kelvin_builtin.rs`: Nikon Z5II, whose Natural Auto shot reads back at 5036 K against the 5030 K the camera wrote). The Kelvin slider's track is Lightroom's: 7000 K in the middle, 2000–7000 K evenly on the left half, logarithmic up to 50000 K on the right (within 2 % of positions measured in Lightroom Classic), for drawing, clicks and drags; wheel / arrow-key nudges and Quick Develop still step a fixed 50 / 100 / 500 K; the relative slider stays linear. Settings written on the relative scale before a camera's matrices reached it keep their look (`WbScale`). Not verified against Lightroom's Kelvin: on the ILCE-7M3 the matrices read 7–10 % below Sony's dial (issue #730), and whether the matrices or the dial are off is open; raws without matrices or presets anywhere and rendered files use the relative −100..+100 scale (6500 K / 0 = as shot). |
|
||||
| LR-EDIT-COLOR-TINT | Tint | P0 | ✅ | `ctl:wb.tint`, `docs/tint-direction.md` | DNG / Lightroom sign: negative (left) adds green, positive (right) magenta; XMP `crs:Tint` values render as in Lightroom (issue #188); CPU/GPU regression tests |
|
||||
| LR-EDIT-COLOR-VIBRANCE | Vibrance | P0 | ✅ | `ctl:color.vibrance` | |
|
||||
| LR-EDIT-COLOR-SATURATION | Saturation | P0 | ✅ | `ctl:color.saturation` | |
|
||||
|
||||
Reference in New Issue
Block a user