Read baseline exposure and profile tone curves; carry the ACR3 curve
The decoding half of camera-profiles.md §11-§12. RawImage gains baseline_exposure: the file's BaselineExposure plus the chosen profile's BaselineExposureOffset, as the DNG SDK sums them (+0.25 for the library's 6D DNGs). A profile copied out of a DNG carries that DNG's baseline as its offset, so the body's CR2s, which have none, land at the same total. ProfileTables gains the profile's ProfileToneCurve, resampled at decode onto 1025 points with a natural cubic spline; an identity curve counts as none. dr-types now holds Camera Raw's ACR3 default curve, RawTherapee's adobe_camera_raw_default_curve copied value for value, for every raw whose profile has no curve. Nothing renders through either yet.
This commit is contained in:
@@ -68,6 +68,11 @@ pub struct Dcp {
|
||||
/// `ProfileToneCurve`, as stored: input/output pairs. Carried so a copy
|
||||
/// keeps it, never applied — tone is the view transform's (D19, D20).
|
||||
pub tone_curve: Option<Vec<f32>>,
|
||||
/// `BaselineExposureOffset`, in stops: the profile's correction to the
|
||||
/// file's `BaselineExposure` (camera-profiles.md §11). A profile copied
|
||||
/// out of a DNG carries that DNG's baseline here, so a raw with no
|
||||
/// baseline of its own lands at the same brightness.
|
||||
pub baseline_exposure_offset: f32,
|
||||
}
|
||||
|
||||
impl Dcp {
|
||||
@@ -120,6 +125,7 @@ impl Dcp {
|
||||
.get_entry(DngTag::ProfileToneCurve)
|
||||
.and_then(|e| floats(&e.value))
|
||||
.filter(|v| v.len() >= 4 && v.len() % 2 == 0),
|
||||
baseline_exposure_offset: stops(ifd, DngTag::BaselineExposureOffset),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -218,6 +224,10 @@ impl Dcp {
|
||||
origin,
|
||||
hue_sat: hue_sat.filter(|t| !t.is_identity()),
|
||||
look: self.look.clone().filter(|t| !t.is_identity()),
|
||||
tone_curve: self
|
||||
.tone_curve
|
||||
.as_deref()
|
||||
.and_then(dr_types::tone::resample_tone_curve),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +315,15 @@ impl Dcp {
|
||||
if let Some(curve) = &self.tone_curve {
|
||||
dir.add_value(DngTag::ProfileToneCurve, Value::Float(curve.clone()));
|
||||
}
|
||||
if self.baseline_exposure_offset != 0.0 {
|
||||
dir.add_value(
|
||||
DngTag::BaselineExposureOffset,
|
||||
Value::SRational(vec![SRational::new(
|
||||
(self.baseline_exposure_offset * 100.0).round() as i32,
|
||||
100,
|
||||
)]),
|
||||
);
|
||||
}
|
||||
writer.build(dir).map_err(|e| e.to_string())?;
|
||||
let mut bytes = cursor.into_inner();
|
||||
// The writer stamps TIFF's 42 in its own byte order; a profile is the
|
||||
@@ -340,6 +359,15 @@ fn string(ifd: &IFD, tag: DngTag) -> Option<String> {
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// A single rational tag in stops, zero where absent or unreadable — the
|
||||
/// DNG specification's default for both exposure tags.
|
||||
fn stops(ifd: &IFD, tag: DngTag) -> f32 {
|
||||
ifd.get_entry(tag)
|
||||
.and_then(|e| e.value.get_f32(0).ok().flatten())
|
||||
.filter(|v| v.is_finite())
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
fn floats(value: &Value) -> Option<Vec<f32>> {
|
||||
(0..value.count())
|
||||
.map(|i| value.get_f32(i).ok().flatten())
|
||||
@@ -507,7 +535,12 @@ pub fn embedded_in(bytes: &[u8]) -> Option<Dcp> {
|
||||
.ifd(rawler::decoders::WellKnownIFD::Root)
|
||||
.ok()
|
||||
.flatten()?;
|
||||
Dcp::from_ifd(&root)
|
||||
// The copy carries the file's baseline as its offset, so a raw from the
|
||||
// same body that has no baseline of its own — a CR2 — gets the total the
|
||||
// DNG renders at (camera-profiles.md §11).
|
||||
let mut profile = Dcp::from_ifd(&root)?;
|
||||
profile.baseline_exposure_offset += stops(&root, DngTag::BaselineExposure);
|
||||
Some(profile)
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3e
|
||||
@@ -518,6 +551,9 @@ pub struct Resolved {
|
||||
pub profile: Option<CameraProfile>,
|
||||
pub tables: Option<Arc<ProfileTables>>,
|
||||
pub embedded: Option<Arc<Dcp>>,
|
||||
/// Stops to add at render: the file's `BaselineExposure` plus the
|
||||
/// chosen profile's `BaselineExposureOffset` (camera-profiles.md §11).
|
||||
pub baseline_exposure: f32,
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3e
|
||||
@@ -532,6 +568,7 @@ pub fn resolve(
|
||||
model: &str,
|
||||
) -> Resolved {
|
||||
let embedded = root.and_then(Dcp::from_ifd).map(Arc::new);
|
||||
let file_baseline = root.map_or(0.0, |r| stops(r, DngTag::BaselineExposure));
|
||||
if let Some(dcp) = &embedded {
|
||||
let tables = matrices
|
||||
.as_ref()
|
||||
@@ -541,6 +578,7 @@ pub fn resolve(
|
||||
return Resolved {
|
||||
profile: matrices,
|
||||
tables,
|
||||
baseline_exposure: file_baseline + dcp.baseline_exposure_offset,
|
||||
embedded,
|
||||
};
|
||||
}
|
||||
@@ -553,6 +591,7 @@ pub fn resolve(
|
||||
tables: (!tables.is_empty()).then(|| Arc::new(tables)),
|
||||
profile: Some(own),
|
||||
embedded: None,
|
||||
baseline_exposure: file_baseline + dcp.baseline_exposure_offset,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -560,6 +599,7 @@ pub fn resolve(
|
||||
profile: matrices,
|
||||
tables: None,
|
||||
embedded: None,
|
||||
baseline_exposure: file_baseline,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,6 +649,7 @@ mod tests {
|
||||
],
|
||||
look: Some(table(4, 2, 3, [0.0, 1.2, 0.95])),
|
||||
tone_curve: Some(vec![0.0, 0.0, 0.5, 0.6, 1.0, 1.0]),
|
||||
baseline_exposure_offset: 0.25,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,6 +668,7 @@ mod tests {
|
||||
assert_eq!(back.unique_camera_model, original.unique_camera_model);
|
||||
assert_eq!(back.illuminants, original.illuminants);
|
||||
assert_eq!(back.tone_curve, original.tone_curve);
|
||||
assert_eq!(back.baseline_exposure_offset, 0.25);
|
||||
assert_eq!(
|
||||
back.forward_matrix, original.forward_matrix,
|
||||
"four decimals, as the file has"
|
||||
@@ -746,6 +788,11 @@ mod tests {
|
||||
assert_eq!(tables.origin, ProfileOrigin::Embedded);
|
||||
assert_eq!(tables.name, "Adobe Standard");
|
||||
assert!(tables.hue_sat.is_some() && tables.look.is_some());
|
||||
assert!(
|
||||
tables.tone_curve.is_none(),
|
||||
"Adobe Standard has no curve of its own"
|
||||
);
|
||||
assert_eq!(raw.baseline_exposure, 0.25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -147,6 +147,12 @@ pub struct RawImage {
|
||||
/// renders a decoded file renders it through the same profile without
|
||||
/// having to be told — see camera-profiles.md §3.
|
||||
pub profile_tables: Option<std::sync::Arc<dr_types::ProfileTables>>,
|
||||
/// TRACES: FR-DEV-3e
|
||||
/// Stops the render adds before anything else: the file's
|
||||
/// `BaselineExposure` plus the profile's `BaselineExposureOffset`
|
||||
/// (camera-profiles.md §11). Applied by the GPU side as a gain on the
|
||||
/// camera matrix; `color_matrix` itself stays the file's.
|
||||
pub baseline_exposure: f32,
|
||||
/// The body, as rawler cleans the names: what `Make`/`Model` say, and
|
||||
/// what a `.dcp`'s `UniqueCameraModel` is matched against.
|
||||
pub make: String,
|
||||
@@ -602,6 +608,7 @@ fn decode_unguarded(bytes: &[u8]) -> Result<RawImage, DecodeError> {
|
||||
let dcp::Resolved {
|
||||
profile,
|
||||
tables: profile_tables,
|
||||
baseline_exposure,
|
||||
..
|
||||
} = dcp::resolve(
|
||||
profile,
|
||||
@@ -700,6 +707,7 @@ fn decode_unguarded(bytes: &[u8]) -> Result<RawImage, DecodeError> {
|
||||
samples_per_pixel,
|
||||
profile,
|
||||
profile_tables,
|
||||
baseline_exposure,
|
||||
make: image.camera.clean_make.clone(),
|
||||
model: image.camera.clean_model.clone(),
|
||||
})
|
||||
|
||||
@@ -1889,6 +1889,7 @@ mod tests {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: CropRect {
|
||||
@@ -2093,6 +2094,7 @@ mod tests {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: CropRect {
|
||||
@@ -2702,6 +2704,7 @@ mod tests {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: CropRect {
|
||||
@@ -2806,6 +2809,7 @@ mod tests {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: CropRect {
|
||||
|
||||
@@ -1610,6 +1610,7 @@ mod tests {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: CropRect {
|
||||
@@ -1726,6 +1727,7 @@ mod tests {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: CropRect {
|
||||
@@ -2023,6 +2025,7 @@ mod tests {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: CropRect {
|
||||
@@ -2108,6 +2111,7 @@ mod tests {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: CropRect {
|
||||
|
||||
@@ -91,6 +91,7 @@ fn frame(tables: Option<ProfileTables>) -> RawImage {
|
||||
samples_per_pixel: 3,
|
||||
profile: None,
|
||||
profile_tables: tables.map(Arc::new),
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: CropRect {
|
||||
@@ -133,6 +134,7 @@ fn strong_tables() -> ProfileTables {
|
||||
origin: ProfileOrigin::Embedded,
|
||||
hue_sat: Some(HueSatTable::new(hd, sd, 1, false, hue_sat).unwrap()),
|
||||
look: Some(look),
|
||||
tone_curve: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ fn flat_raw(level: u16) -> RawImage {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: CropRect {
|
||||
|
||||
@@ -35,6 +35,7 @@ fn frame(pattern: CfaPattern, level: u16, set: &[(u32, u32, u16)]) -> RawImage {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: CropRect {
|
||||
|
||||
@@ -95,6 +95,7 @@ fn flat(ctx: &GpuContext, level: f32) -> dr_gpu::DemosaicedImage {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: CropRect {
|
||||
|
||||
@@ -59,6 +59,7 @@ fn linear_frame(w: u32, h: u32, noise: bool) -> RawImage {
|
||||
samples_per_pixel: 3,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: CropRect {
|
||||
|
||||
@@ -34,6 +34,7 @@ fn flat_raw(level: u16) -> RawImage {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: CropRect {
|
||||
|
||||
@@ -536,6 +536,7 @@ mod tests {
|
||||
origin: ProfileOrigin::Embedded,
|
||||
hue_sat,
|
||||
look,
|
||||
tone_curve: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,6 +115,11 @@ pub struct ProfileTables {
|
||||
pub origin: ProfileOrigin,
|
||||
pub hue_sat: Option<HueSatTable>,
|
||||
pub look: Option<HueSatTable>,
|
||||
/// The profile's `ProfileToneCurve`, resampled onto
|
||||
/// [`crate::tone::TONE_SAMPLES`] points; `None` where it has none, and
|
||||
/// the view transform's Camera Raw curve then uses
|
||||
/// [`crate::tone::ACR3_DEFAULT`] (D21).
|
||||
pub tone_curve: Option<Vec<f32>>,
|
||||
}
|
||||
|
||||
/// Where a profile was found (camera-profiles.md §4).
|
||||
@@ -129,7 +134,7 @@ pub enum ProfileOrigin {
|
||||
impl ProfileTables {
|
||||
/// Whether there is anything to apply.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.hue_sat.is_none() && self.look.is_none()
|
||||
self.hue_sat.is_none() && self.look.is_none() && self.tone_curve.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ pub mod place;
|
||||
pub mod selector;
|
||||
pub mod settings;
|
||||
pub mod time;
|
||||
pub mod tone;
|
||||
|
||||
pub use colour::{Chromaticities, Transfer};
|
||||
pub use hue_sat::{HueSatTable, ProfileOrigin, ProfileTables};
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
//! TRACES: FR-DEV-3j | FR-DEV-3e
|
||||
//! Camera Raw's tone curves, as data (D21).
|
||||
//!
|
||||
//! # Where the numbers come from
|
||||
//!
|
||||
//! [`ACR3_DEFAULT`] is the "ACR3 default" tone curve of Adobe's DNG SDK
|
||||
//! (`dng_tone_curve_acr3_default`): what Camera Raw renders a raw through
|
||||
//! when its camera profile carries no `ProfileToneCurve` of its own, which is
|
||||
//! true of Adobe Standard. The values are RawTherapee's
|
||||
//! `adobe_camera_raw_default_curve` (`rtengine/dcp.cc`, GPLv3), copied
|
||||
//! digit for digit; `the_default_curve_is_rawtherapees` checks a sample of
|
||||
//! them.
|
||||
//!
|
||||
//! It maps linear values to linear values, 1025 samples evenly over
|
||||
//! `[0, 1]`, interpolated linearly between them. See
|
||||
//! `docs/dev/camera-profiles.md` §12 for how it is applied — on the largest
|
||||
//! and smallest channel, not on each — which is half of what it does.
|
||||
|
||||
/// Samples in a resolved tone curve: the ACR3 table's own resolution, and
|
||||
/// what a profile's curve is resampled onto.
|
||||
pub const TONE_SAMPLES: usize = 1025;
|
||||
|
||||
/// The ACR3 default tone curve, linear in, linear out.
|
||||
#[rustfmt::skip]
|
||||
pub const ACR3_DEFAULT: [f32; TONE_SAMPLES] = [
|
||||
0.00000, 0.00078, 0.00160, 0.00242, 0.00314, 0.00385, 0.00460, 0.00539,
|
||||
0.00623, 0.00712, 0.00806, 0.00906, 0.01012, 0.01122, 0.01238, 0.01359,
|
||||
0.01485, 0.01616, 0.01751, 0.01890, 0.02033, 0.02180, 0.02331, 0.02485,
|
||||
0.02643, 0.02804, 0.02967, 0.03134, 0.03303, 0.03475, 0.03648, 0.03824,
|
||||
0.04002, 0.04181, 0.04362, 0.04545, 0.04730, 0.04916, 0.05103, 0.05292,
|
||||
0.05483, 0.05675, 0.05868, 0.06063, 0.06259, 0.06457, 0.06655, 0.06856,
|
||||
0.07057, 0.07259, 0.07463, 0.07668, 0.07874, 0.08081, 0.08290, 0.08499,
|
||||
0.08710, 0.08921, 0.09134, 0.09348, 0.09563, 0.09779, 0.09996, 0.10214,
|
||||
0.10433, 0.10652, 0.10873, 0.11095, 0.11318, 0.11541, 0.11766, 0.11991,
|
||||
0.12218, 0.12445, 0.12673, 0.12902, 0.13132, 0.13363, 0.13595, 0.13827,
|
||||
0.14061, 0.14295, 0.14530, 0.14765, 0.15002, 0.15239, 0.15477, 0.15716,
|
||||
0.15956, 0.16197, 0.16438, 0.16680, 0.16923, 0.17166, 0.17410, 0.17655,
|
||||
0.17901, 0.18148, 0.18395, 0.18643, 0.18891, 0.19141, 0.19391, 0.19641,
|
||||
0.19893, 0.20145, 0.20398, 0.20651, 0.20905, 0.21160, 0.21416, 0.21672,
|
||||
0.21929, 0.22185, 0.22440, 0.22696, 0.22950, 0.23204, 0.23458, 0.23711,
|
||||
0.23963, 0.24215, 0.24466, 0.24717, 0.24967, 0.25216, 0.25465, 0.25713,
|
||||
0.25961, 0.26208, 0.26454, 0.26700, 0.26945, 0.27189, 0.27433, 0.27676,
|
||||
0.27918, 0.28160, 0.28401, 0.28641, 0.28881, 0.29120, 0.29358, 0.29596,
|
||||
0.29833, 0.30069, 0.30305, 0.30540, 0.30774, 0.31008, 0.31241, 0.31473,
|
||||
0.31704, 0.31935, 0.32165, 0.32395, 0.32623, 0.32851, 0.33079, 0.33305,
|
||||
0.33531, 0.33756, 0.33981, 0.34205, 0.34428, 0.34650, 0.34872, 0.35093,
|
||||
0.35313, 0.35532, 0.35751, 0.35969, 0.36187, 0.36404, 0.36620, 0.36835,
|
||||
0.37050, 0.37264, 0.37477, 0.37689, 0.37901, 0.38112, 0.38323, 0.38533,
|
||||
0.38742, 0.38950, 0.39158, 0.39365, 0.39571, 0.39777, 0.39982, 0.40186,
|
||||
0.40389, 0.40592, 0.40794, 0.40996, 0.41197, 0.41397, 0.41596, 0.41795,
|
||||
0.41993, 0.42191, 0.42388, 0.42584, 0.42779, 0.42974, 0.43168, 0.43362,
|
||||
0.43554, 0.43747, 0.43938, 0.44129, 0.44319, 0.44509, 0.44698, 0.44886,
|
||||
0.45073, 0.45260, 0.45447, 0.45632, 0.45817, 0.46002, 0.46186, 0.46369,
|
||||
0.46551, 0.46733, 0.46914, 0.47095, 0.47275, 0.47454, 0.47633, 0.47811,
|
||||
0.47989, 0.48166, 0.48342, 0.48518, 0.48693, 0.48867, 0.49041, 0.49214,
|
||||
0.49387, 0.49559, 0.49730, 0.49901, 0.50072, 0.50241, 0.50410, 0.50579,
|
||||
0.50747, 0.50914, 0.51081, 0.51247, 0.51413, 0.51578, 0.51742, 0.51906,
|
||||
0.52069, 0.52232, 0.52394, 0.52556, 0.52717, 0.52878, 0.53038, 0.53197,
|
||||
0.53356, 0.53514, 0.53672, 0.53829, 0.53986, 0.54142, 0.54297, 0.54452,
|
||||
0.54607, 0.54761, 0.54914, 0.55067, 0.55220, 0.55371, 0.55523, 0.55673,
|
||||
0.55824, 0.55973, 0.56123, 0.56271, 0.56420, 0.56567, 0.56715, 0.56861,
|
||||
0.57007, 0.57153, 0.57298, 0.57443, 0.57587, 0.57731, 0.57874, 0.58017,
|
||||
0.58159, 0.58301, 0.58443, 0.58583, 0.58724, 0.58864, 0.59003, 0.59142,
|
||||
0.59281, 0.59419, 0.59556, 0.59694, 0.59830, 0.59966, 0.60102, 0.60238,
|
||||
0.60373, 0.60507, 0.60641, 0.60775, 0.60908, 0.61040, 0.61173, 0.61305,
|
||||
0.61436, 0.61567, 0.61698, 0.61828, 0.61957, 0.62087, 0.62216, 0.62344,
|
||||
0.62472, 0.62600, 0.62727, 0.62854, 0.62980, 0.63106, 0.63232, 0.63357,
|
||||
0.63482, 0.63606, 0.63730, 0.63854, 0.63977, 0.64100, 0.64222, 0.64344,
|
||||
0.64466, 0.64587, 0.64708, 0.64829, 0.64949, 0.65069, 0.65188, 0.65307,
|
||||
0.65426, 0.65544, 0.65662, 0.65779, 0.65897, 0.66013, 0.66130, 0.66246,
|
||||
0.66362, 0.66477, 0.66592, 0.66707, 0.66821, 0.66935, 0.67048, 0.67162,
|
||||
0.67275, 0.67387, 0.67499, 0.67611, 0.67723, 0.67834, 0.67945, 0.68055,
|
||||
0.68165, 0.68275, 0.68385, 0.68494, 0.68603, 0.68711, 0.68819, 0.68927,
|
||||
0.69035, 0.69142, 0.69249, 0.69355, 0.69461, 0.69567, 0.69673, 0.69778,
|
||||
0.69883, 0.69988, 0.70092, 0.70196, 0.70300, 0.70403, 0.70506, 0.70609,
|
||||
0.70711, 0.70813, 0.70915, 0.71017, 0.71118, 0.71219, 0.71319, 0.71420,
|
||||
0.71520, 0.71620, 0.71719, 0.71818, 0.71917, 0.72016, 0.72114, 0.72212,
|
||||
0.72309, 0.72407, 0.72504, 0.72601, 0.72697, 0.72794, 0.72890, 0.72985,
|
||||
0.73081, 0.73176, 0.73271, 0.73365, 0.73460, 0.73554, 0.73647, 0.73741,
|
||||
0.73834, 0.73927, 0.74020, 0.74112, 0.74204, 0.74296, 0.74388, 0.74479,
|
||||
0.74570, 0.74661, 0.74751, 0.74842, 0.74932, 0.75021, 0.75111, 0.75200,
|
||||
0.75289, 0.75378, 0.75466, 0.75555, 0.75643, 0.75730, 0.75818, 0.75905,
|
||||
0.75992, 0.76079, 0.76165, 0.76251, 0.76337, 0.76423, 0.76508, 0.76594,
|
||||
0.76679, 0.76763, 0.76848, 0.76932, 0.77016, 0.77100, 0.77183, 0.77267,
|
||||
0.77350, 0.77432, 0.77515, 0.77597, 0.77680, 0.77761, 0.77843, 0.77924,
|
||||
0.78006, 0.78087, 0.78167, 0.78248, 0.78328, 0.78408, 0.78488, 0.78568,
|
||||
0.78647, 0.78726, 0.78805, 0.78884, 0.78962, 0.79040, 0.79118, 0.79196,
|
||||
0.79274, 0.79351, 0.79428, 0.79505, 0.79582, 0.79658, 0.79735, 0.79811,
|
||||
0.79887, 0.79962, 0.80038, 0.80113, 0.80188, 0.80263, 0.80337, 0.80412,
|
||||
0.80486, 0.80560, 0.80634, 0.80707, 0.80780, 0.80854, 0.80926, 0.80999,
|
||||
0.81072, 0.81144, 0.81216, 0.81288, 0.81360, 0.81431, 0.81503, 0.81574,
|
||||
0.81645, 0.81715, 0.81786, 0.81856, 0.81926, 0.81996, 0.82066, 0.82135,
|
||||
0.82205, 0.82274, 0.82343, 0.82412, 0.82480, 0.82549, 0.82617, 0.82685,
|
||||
0.82753, 0.82820, 0.82888, 0.82955, 0.83022, 0.83089, 0.83155, 0.83222,
|
||||
0.83288, 0.83354, 0.83420, 0.83486, 0.83552, 0.83617, 0.83682, 0.83747,
|
||||
0.83812, 0.83877, 0.83941, 0.84005, 0.84069, 0.84133, 0.84197, 0.84261,
|
||||
0.84324, 0.84387, 0.84450, 0.84513, 0.84576, 0.84639, 0.84701, 0.84763,
|
||||
0.84825, 0.84887, 0.84949, 0.85010, 0.85071, 0.85132, 0.85193, 0.85254,
|
||||
0.85315, 0.85375, 0.85436, 0.85496, 0.85556, 0.85615, 0.85675, 0.85735,
|
||||
0.85794, 0.85853, 0.85912, 0.85971, 0.86029, 0.86088, 0.86146, 0.86204,
|
||||
0.86262, 0.86320, 0.86378, 0.86435, 0.86493, 0.86550, 0.86607, 0.86664,
|
||||
0.86720, 0.86777, 0.86833, 0.86889, 0.86945, 0.87001, 0.87057, 0.87113,
|
||||
0.87168, 0.87223, 0.87278, 0.87333, 0.87388, 0.87443, 0.87497, 0.87552,
|
||||
0.87606, 0.87660, 0.87714, 0.87768, 0.87821, 0.87875, 0.87928, 0.87981,
|
||||
0.88034, 0.88087, 0.88140, 0.88192, 0.88244, 0.88297, 0.88349, 0.88401,
|
||||
0.88453, 0.88504, 0.88556, 0.88607, 0.88658, 0.88709, 0.88760, 0.88811,
|
||||
0.88862, 0.88912, 0.88963, 0.89013, 0.89063, 0.89113, 0.89163, 0.89212,
|
||||
0.89262, 0.89311, 0.89360, 0.89409, 0.89458, 0.89507, 0.89556, 0.89604,
|
||||
0.89653, 0.89701, 0.89749, 0.89797, 0.89845, 0.89892, 0.89940, 0.89987,
|
||||
0.90035, 0.90082, 0.90129, 0.90176, 0.90222, 0.90269, 0.90316, 0.90362,
|
||||
0.90408, 0.90454, 0.90500, 0.90546, 0.90592, 0.90637, 0.90683, 0.90728,
|
||||
0.90773, 0.90818, 0.90863, 0.90908, 0.90952, 0.90997, 0.91041, 0.91085,
|
||||
0.91130, 0.91173, 0.91217, 0.91261, 0.91305, 0.91348, 0.91392, 0.91435,
|
||||
0.91478, 0.91521, 0.91564, 0.91606, 0.91649, 0.91691, 0.91734, 0.91776,
|
||||
0.91818, 0.91860, 0.91902, 0.91944, 0.91985, 0.92027, 0.92068, 0.92109,
|
||||
0.92150, 0.92191, 0.92232, 0.92273, 0.92314, 0.92354, 0.92395, 0.92435,
|
||||
0.92475, 0.92515, 0.92555, 0.92595, 0.92634, 0.92674, 0.92713, 0.92753,
|
||||
0.92792, 0.92831, 0.92870, 0.92909, 0.92947, 0.92986, 0.93025, 0.93063,
|
||||
0.93101, 0.93139, 0.93177, 0.93215, 0.93253, 0.93291, 0.93328, 0.93366,
|
||||
0.93403, 0.93440, 0.93478, 0.93515, 0.93551, 0.93588, 0.93625, 0.93661,
|
||||
0.93698, 0.93734, 0.93770, 0.93807, 0.93843, 0.93878, 0.93914, 0.93950,
|
||||
0.93986, 0.94021, 0.94056, 0.94092, 0.94127, 0.94162, 0.94197, 0.94231,
|
||||
0.94266, 0.94301, 0.94335, 0.94369, 0.94404, 0.94438, 0.94472, 0.94506,
|
||||
0.94540, 0.94573, 0.94607, 0.94641, 0.94674, 0.94707, 0.94740, 0.94774,
|
||||
0.94807, 0.94839, 0.94872, 0.94905, 0.94937, 0.94970, 0.95002, 0.95035,
|
||||
0.95067, 0.95099, 0.95131, 0.95163, 0.95194, 0.95226, 0.95257, 0.95289,
|
||||
0.95320, 0.95351, 0.95383, 0.95414, 0.95445, 0.95475, 0.95506, 0.95537,
|
||||
0.95567, 0.95598, 0.95628, 0.95658, 0.95688, 0.95718, 0.95748, 0.95778,
|
||||
0.95808, 0.95838, 0.95867, 0.95897, 0.95926, 0.95955, 0.95984, 0.96013,
|
||||
0.96042, 0.96071, 0.96100, 0.96129, 0.96157, 0.96186, 0.96214, 0.96242,
|
||||
0.96271, 0.96299, 0.96327, 0.96355, 0.96382, 0.96410, 0.96438, 0.96465,
|
||||
0.96493, 0.96520, 0.96547, 0.96574, 0.96602, 0.96629, 0.96655, 0.96682,
|
||||
0.96709, 0.96735, 0.96762, 0.96788, 0.96815, 0.96841, 0.96867, 0.96893,
|
||||
0.96919, 0.96945, 0.96971, 0.96996, 0.97022, 0.97047, 0.97073, 0.97098,
|
||||
0.97123, 0.97149, 0.97174, 0.97199, 0.97223, 0.97248, 0.97273, 0.97297,
|
||||
0.97322, 0.97346, 0.97371, 0.97395, 0.97419, 0.97443, 0.97467, 0.97491,
|
||||
0.97515, 0.97539, 0.97562, 0.97586, 0.97609, 0.97633, 0.97656, 0.97679,
|
||||
0.97702, 0.97725, 0.97748, 0.97771, 0.97794, 0.97817, 0.97839, 0.97862,
|
||||
0.97884, 0.97907, 0.97929, 0.97951, 0.97973, 0.97995, 0.98017, 0.98039,
|
||||
0.98061, 0.98082, 0.98104, 0.98125, 0.98147, 0.98168, 0.98189, 0.98211,
|
||||
0.98232, 0.98253, 0.98274, 0.98295, 0.98315, 0.98336, 0.98357, 0.98377,
|
||||
0.98398, 0.98418, 0.98438, 0.98458, 0.98478, 0.98498, 0.98518, 0.98538,
|
||||
0.98558, 0.98578, 0.98597, 0.98617, 0.98636, 0.98656, 0.98675, 0.98694,
|
||||
0.98714, 0.98733, 0.98752, 0.98771, 0.98789, 0.98808, 0.98827, 0.98845,
|
||||
0.98864, 0.98882, 0.98901, 0.98919, 0.98937, 0.98955, 0.98973, 0.98991,
|
||||
0.99009, 0.99027, 0.99045, 0.99063, 0.99080, 0.99098, 0.99115, 0.99133,
|
||||
0.99150, 0.99167, 0.99184, 0.99201, 0.99218, 0.99235, 0.99252, 0.99269,
|
||||
0.99285, 0.99302, 0.99319, 0.99335, 0.99351, 0.99368, 0.99384, 0.99400,
|
||||
0.99416, 0.99432, 0.99448, 0.99464, 0.99480, 0.99495, 0.99511, 0.99527,
|
||||
0.99542, 0.99558, 0.99573, 0.99588, 0.99603, 0.99619, 0.99634, 0.99649,
|
||||
0.99664, 0.99678, 0.99693, 0.99708, 0.99722, 0.99737, 0.99751, 0.99766,
|
||||
0.99780, 0.99794, 0.99809, 0.99823, 0.99837, 0.99851, 0.99865, 0.99879,
|
||||
0.99892, 0.99906, 0.99920, 0.99933, 0.99947, 0.99960, 0.99974, 0.99987,
|
||||
1.00000,
|
||||
];
|
||||
|
||||
/// TRACES: FR-DEV-3e
|
||||
/// A profile's `ProfileToneCurve` — `(x, y)` pairs in `[0, 1]` — resampled
|
||||
/// onto [`TONE_SAMPLES`] even points with a natural cubic spline, the DNG
|
||||
/// SDK's `dng_spline_solver`.
|
||||
///
|
||||
/// `None` where the pairs do not describe a curve (fewer than two points,
|
||||
/// x not increasing, values outside `[0, 1]`) or describe the identity,
|
||||
/// which RawTherapee also treats as no curve.
|
||||
pub fn resample_tone_curve(pairs: &[f32]) -> Option<Vec<f32>> {
|
||||
if pairs.len() < 4 || pairs.len() % 2 != 0 {
|
||||
return None;
|
||||
}
|
||||
let xs: Vec<f64> = pairs.iter().step_by(2).map(|&v| f64::from(v)).collect();
|
||||
let ys: Vec<f64> = pairs
|
||||
.iter()
|
||||
.skip(1)
|
||||
.step_by(2)
|
||||
.map(|&v| f64::from(v))
|
||||
.collect();
|
||||
let sane = xs.windows(2).all(|w| w[1] > w[0])
|
||||
&& xs
|
||||
.iter()
|
||||
.chain(&ys)
|
||||
.all(|v| v.is_finite() && (-1e-6..=1.0 + 1e-6).contains(v));
|
||||
if !sane {
|
||||
return None;
|
||||
}
|
||||
if xs.iter().zip(&ys).all(|(x, y)| (x - y).abs() < 1e-6) {
|
||||
return None;
|
||||
}
|
||||
let n = xs.len();
|
||||
// Natural cubic spline: second derivatives zero at both ends, solved by
|
||||
// the tridiagonal (Thomas) algorithm.
|
||||
let mut m = vec![0.0f64; n];
|
||||
if n > 2 {
|
||||
let h: Vec<f64> = xs.windows(2).map(|w| w[1] - w[0]).collect();
|
||||
let mut a = vec![0.0; n];
|
||||
let mut b = vec![1.0; n];
|
||||
let mut c = vec![0.0; n];
|
||||
let mut d = vec![0.0; n];
|
||||
for i in 1..n - 1 {
|
||||
a[i] = h[i - 1];
|
||||
b[i] = 2.0 * (h[i - 1] + h[i]);
|
||||
c[i] = h[i];
|
||||
d[i] = 6.0 * ((ys[i + 1] - ys[i]) / h[i] - (ys[i] - ys[i - 1]) / h[i - 1]);
|
||||
}
|
||||
for i in 1..n {
|
||||
let w = a[i] / b[i - 1];
|
||||
b[i] -= w * c[i - 1];
|
||||
d[i] -= w * d[i - 1];
|
||||
}
|
||||
m[n - 1] = d[n - 1] / b[n - 1];
|
||||
for i in (0..n - 1).rev() {
|
||||
m[i] = (d[i] - c[i] * m[i + 1]) / b[i];
|
||||
}
|
||||
}
|
||||
let eval = |x: f64| -> f64 {
|
||||
if x <= xs[0] {
|
||||
return ys[0];
|
||||
}
|
||||
if x >= xs[n - 1] {
|
||||
return ys[n - 1];
|
||||
}
|
||||
let i = xs.windows(2).position(|w| x <= w[1]).unwrap_or(n - 2);
|
||||
let h = xs[i + 1] - xs[i];
|
||||
let t = (x - xs[i]) / h;
|
||||
let u = 1.0 - t;
|
||||
u * ys[i]
|
||||
+ t * ys[i + 1]
|
||||
+ ((u * u * u - u) * m[i] + (t * t * t - t) * m[i + 1]) * h * h / 6.0
|
||||
};
|
||||
Some(
|
||||
(0..TONE_SAMPLES)
|
||||
.map(|k| eval(k as f64 / (TONE_SAMPLES - 1) as f64).clamp(0.0, 1.0) as f32)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// A resolved curve at `x`, linear interpolation between samples, `x`
|
||||
/// clamped to `[0, 1]` — the shader's lookup, for the CPU reference.
|
||||
pub fn evaluate(curve: &[f32], x: f32) -> f32 {
|
||||
let last = curve.len() - 1;
|
||||
let s = x.clamp(0.0, 1.0) * last as f32;
|
||||
let i = (s as usize).min(last - 1);
|
||||
let f = s - i as f32;
|
||||
curve[i] + (curve[i + 1] - curve[i]) * f
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_default_curve_is_rawtherapees() {
|
||||
assert_eq!(ACR3_DEFAULT.len(), 1025);
|
||||
assert_eq!(ACR3_DEFAULT[0], 0.0);
|
||||
assert_eq!(ACR3_DEFAULT[1024], 1.0);
|
||||
assert_eq!(ACR3_DEFAULT[1], 0.00078);
|
||||
assert_eq!(ACR3_DEFAULT[256], 0.52069);
|
||||
assert_eq!(ACR3_DEFAULT[512], 0.80486);
|
||||
assert_eq!(ACR3_DEFAULT[768], 0.93986);
|
||||
assert!(ACR3_DEFAULT.windows(2).all(|w| w[1] >= w[0]), "monotone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resampled_curve_passes_through_its_points() {
|
||||
let c = resample_tone_curve(&[0.0, 0.0, 0.5, 0.6, 1.0, 1.0]).unwrap();
|
||||
assert_eq!(c.len(), TONE_SAMPLES);
|
||||
assert!((evaluate(&c, 0.5) - 0.6).abs() < 1e-4);
|
||||
assert!(evaluate(&c, 0.0).abs() < 1e-6 && (evaluate(&c, 1.0) - 1.0).abs() < 1e-6);
|
||||
assert!(
|
||||
evaluate(&c, 0.25) > 0.25,
|
||||
"a lifted curve lifts between its points"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_identity_or_broken_curve_is_no_curve() {
|
||||
assert!(resample_tone_curve(&[0.0, 0.0, 1.0, 1.0]).is_none());
|
||||
assert!(resample_tone_curve(&[0.0, 0.0, 0.5]).is_none());
|
||||
assert!(resample_tone_curve(&[0.0, 0.0, 0.6, 0.5, 0.4, 0.9, 1.0, 1.0]).is_none());
|
||||
}
|
||||
}
|
||||
+33
-33
File diff suppressed because one or more lines are too long
@@ -130,6 +130,7 @@ impl Decoder for Stub {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: "Stubco".into(),
|
||||
model: "Stubco One".into(),
|
||||
})
|
||||
|
||||
@@ -1624,6 +1624,7 @@ mod tests {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: dr_decode::CropRect {
|
||||
@@ -1878,6 +1879,7 @@ mod tests {
|
||||
samples_per_pixel: 3,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: dr_decode::CropRect {
|
||||
|
||||
@@ -832,6 +832,7 @@ mod tests {
|
||||
samples_per_pixel: 1,
|
||||
profile: None,
|
||||
profile_tables: tables.map(Arc::new),
|
||||
baseline_exposure: 0.0,
|
||||
make: "Canon".into(),
|
||||
model: "EOS 6D".into(),
|
||||
crop: dr_decode::CropRect {
|
||||
@@ -852,6 +853,7 @@ mod tests {
|
||||
origin,
|
||||
hue_sat: None,
|
||||
look: dr_types::HueSatTable::new(2, 2, 1, false, vec![[5.0, 1.1, 1.0]; 4]),
|
||||
tone_curve: None,
|
||||
};
|
||||
let open = |t| {
|
||||
DevelopSession::open(&ctx, &raw_with(t), dr_types::Orientation::NORMAL)
|
||||
@@ -907,6 +909,7 @@ mod tests {
|
||||
hue_sat: [None, None],
|
||||
look: dr_types::HueSatTable::new(2, 2, 1, false, vec![[5.0, 1.1, 1.0]; 4]),
|
||||
tone_curve: None,
|
||||
baseline_exposure_offset: 0.0,
|
||||
};
|
||||
s.set_embedded_profile(Some(profile(0)));
|
||||
assert_eq!(
|
||||
|
||||
@@ -233,6 +233,7 @@ mod tests {
|
||||
samples_per_pixel: 3,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: dr_decode::CropRect {
|
||||
@@ -307,6 +308,7 @@ mod tests {
|
||||
samples_per_pixel: 3,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: dr_decode::CropRect {
|
||||
@@ -360,6 +362,7 @@ mod tests {
|
||||
samples_per_pixel: 3,
|
||||
profile: None,
|
||||
profile_tables: None,
|
||||
baseline_exposure: 0.0,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
crop: dr_decode::CropRect {
|
||||
|
||||
Reference in New Issue
Block a user