A linear DNG in and out: the writer, and a three-sample RawImage
dr-export gains write_linear_dng — LinearRaw, DNG 1.4, u16 samples at the sensor's scale, the body's matrices with their illuminants, the as-shot neutral, the EXIF block an export writes — streamed strip by strip through a closure so the composite is never held (FR-MRG-11). The tiff crate's directory is a map, so PhotometricInterpretation is written over what new_image set, which is the trick the S15.1 spike thought it had to hand-roll around. The test reads the file back through rawler. dr-decode's RawImage carries samples_per_pixel (a linear DNG is 3), the body's profile with its calibrations mapped back to EXIF illuminant codes, and the cleaned make and model. The GPU uploads a three-sample image as it is, normalised by black and white like a photosite, through a full f16 conversion — subnormals kept, because a 14-bit LSB sits at f16's smallest normal and rounding it to zero would crush exactly the shadows the file was written to keep.
This commit is contained in:
@@ -134,6 +134,22 @@ pub struct RawImage {
|
||||
pub base_curve: BaseCurve,
|
||||
/// The usable region of `data`, excluding masked and border photosites.
|
||||
pub crop: CropRect,
|
||||
/// TRACES: FR-MRG-3
|
||||
/// Samples per photosite in `data`: 1 for a colour-filter-array capture,
|
||||
/// 3 for a *linear* DNG — demosaiced RGB, still camera-space, which is
|
||||
/// what a merge writes. With 3, `cfa_pattern` means nothing, `data` is
|
||||
/// `width × height × 3` interleaved, and the GPU uploads it as it is
|
||||
/// rather than demosaicing.
|
||||
pub samples_per_pixel: u8,
|
||||
/// TRACES: FR-MRG-3
|
||||
/// The body's colour profile as the file carried it, for a composite to
|
||||
/// carry on: calibrations and the as-shot neutral. `None` for a body the
|
||||
/// decoder has no matrix for.
|
||||
pub profile: Option<profile::CameraProfile>,
|
||||
/// The body, as rawler cleans the names: what `Make`/`Model` say and what
|
||||
/// the base-curve database matches on.
|
||||
pub make: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
/// TRACES: FR-RAW-3
|
||||
@@ -559,6 +575,21 @@ fn decode_unguarded(bytes: &[u8]) -> Result<RawImage, DecodeError> {
|
||||
image.camera.clean_model.as_str(),
|
||||
);
|
||||
|
||||
// TRACES: FR-MRG-3
|
||||
// A linear DNG — three samples per pixel, no colour filter array — is a
|
||||
// composite this application wrote (or any other demosaiced DNG). It
|
||||
// carries the same scale, matrices and neutral as a CFA file and goes
|
||||
// through the same profile; only the demosaic is skipped.
|
||||
let samples_per_pixel = match image.cpp {
|
||||
1 => 1u8,
|
||||
3 => 3,
|
||||
other => {
|
||||
return Err(DecodeError::Unsupported(format!(
|
||||
"{other} samples per pixel; only CFA (1) and linear RGB (3) are handled"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let data = match image.data {
|
||||
rawler::RawImageData::Integer(v) => v,
|
||||
rawler::RawImageData::Float(v) => {
|
||||
@@ -627,6 +658,10 @@ fn decode_unguarded(bytes: &[u8]) -> Result<RawImage, DecodeError> {
|
||||
wb_coeffs,
|
||||
color_matrix,
|
||||
base_curve,
|
||||
samples_per_pixel,
|
||||
profile,
|
||||
make: image.camera.clean_make.clone(),
|
||||
model: image.camera.clean_model.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -392,6 +392,21 @@ impl CameraProfile {
|
||||
}
|
||||
|
||||
/// The calibrations this profile was built from, coolest first.
|
||||
/// TRACES: FR-MRG-3
|
||||
/// The calibrations as a DNG carries them: `(CalibrationIlluminant,
|
||||
/// ColorMatrix)` with the EXIF light-source code, for a composite to
|
||||
/// write the profile of the body that took its sources.
|
||||
///
|
||||
/// The code is recovered from the temperature, which is lossy only for
|
||||
/// illuminants this profile never kept: `extract` drops calibrations
|
||||
/// whose illuminant has no temperature, so every one here maps back.
|
||||
pub fn dng_calibrations(&self) -> Vec<(u16, [[f32; 3]; 3])> {
|
||||
self.calibrations
|
||||
.iter()
|
||||
.map(|c| (illuminant_code(c.temperature), c.xyz_to_cam))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn calibrations(&self) -> &[Calibration] {
|
||||
&self.calibrations
|
||||
}
|
||||
@@ -528,6 +543,35 @@ fn illuminant_temperature(illuminant: Illuminant) -> Option<f32> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The EXIF `LightSource` code for a calibration temperature — the inverse
|
||||
/// of [`illuminant_temperature`], on the temperatures it produces.
|
||||
fn illuminant_code(temperature: f32) -> u16 {
|
||||
// Nearest of the table, so a temperature that came through a float
|
||||
// round-trip still lands on its illuminant. Where two illuminants share
|
||||
// a temperature (D55 and Daylight, D65 and Cloudy, D75 and Shade) the
|
||||
// CIE standard one is written: it is what every profile database means.
|
||||
const TABLE: &[(f32, u16)] = &[
|
||||
(2856.0, 17), // A
|
||||
(3200.0, 24), // ISO studio tungsten
|
||||
(3500.0, 15), // white fluorescent
|
||||
(4150.0, 14), // cool white fluorescent
|
||||
(4230.0, 2), // fluorescent
|
||||
(4874.0, 18), // B
|
||||
(5000.0, 13), // daylight white fluorescent
|
||||
(5003.0, 23), // D50
|
||||
(5503.0, 20), // D55
|
||||
(6430.0, 12), // daylight fluorescent
|
||||
(6504.0, 21), // D65
|
||||
(6774.0, 19), // C
|
||||
(7504.0, 22), // D75
|
||||
];
|
||||
TABLE
|
||||
.iter()
|
||||
.min_by(|a, b| (a.0 - temperature).abs().total_cmp(&(b.0 - temperature).abs()))
|
||||
.map(|(_, code)| *code)
|
||||
.unwrap_or(255)
|
||||
}
|
||||
|
||||
/// Compose a forward matrix into camera RGB → linear sRGB.
|
||||
///
|
||||
/// `forward` takes white-balanced camera RGB to XYZ under D50, which is the
|
||||
|
||||
Reference in New Issue
Block a user