mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 14:07:34 -06:00
112 lines
4.6 KiB
Rust
112 lines
4.6 KiB
Rust
//! Real files: probing (dimensions + metadata for import) and loading (decode → linear Rec.2020,
|
|
//! oriented, downscaled to the requested level). Standard formats go through `lightcraft-codecs`,
|
|
//! camera raws through `lightcraft-raw` (demosaic + DNG colour model).
|
|
|
|
use std::sync::Arc;
|
|
|
|
use lightcraft_catalog::{MediaKind, Meta};
|
|
use lightcraft_color::cct::xy_to_temp_tint;
|
|
use lightcraft_geom::Orientation;
|
|
use lightcraft_pipeline::SourceInfo;
|
|
use lightcraft_raster::Rgb32f;
|
|
use lightcraft_raster::resample::{Filter, fit};
|
|
|
|
use crate::media::{DenoiseSpec, FileLoader, FileProbe, PairLoader, PreviewLoader, ProbeInfo, ReducedLoader};
|
|
|
|
mod colour;
|
|
mod fs;
|
|
mod load;
|
|
mod preview;
|
|
mod probe;
|
|
|
|
pub use colour::bin_factor;
|
|
pub(crate) use colour::clipped_white;
|
|
#[cfg(test)]
|
|
use fs::quick_image_srgb;
|
|
pub use fs::{fs_hooks, fs_pair_loader, fs_reduced_loader};
|
|
pub(crate) use load::{DngWhite, WbScaleOf, dng_tone_curve};
|
|
pub use load::{load_bytes, load_vec, load_vec_pair, load_vec_reduced};
|
|
#[cfg(test)]
|
|
use preview::PLAN_PREFIX;
|
|
pub(crate) use preview::decode_raw_preview;
|
|
pub use preview::{PreviewRead, embedded_preview_srgb, fs_preview_loader, load_embedded_preview, preview_window};
|
|
pub use probe::{embedded_lens, probe_bytes};
|
|
#[cfg(test)]
|
|
use probe::{meta_of, preview_reason};
|
|
|
|
/// Sensor clip level (normalised) for highlight reconstruction.
|
|
const HIGHLIGHT_CLIP: f32 = 0.99;
|
|
|
|
/// How a raw's camera RGB becomes the linear Rec.2020 the pipeline takes.
|
|
pub(crate) struct CameraColourModel<'a> {
|
|
/// White-balance multipliers (as shot).
|
|
pub wb: [f32; 3],
|
|
/// White-balanced camera RGB → linear Rec.2020: the camera matrix, or a look fitted to the camera's JPEG on top.
|
|
pub matrix: lightcraft_color::Mat3,
|
|
/// Baseline exposure, as a gain.
|
|
pub gain: f32,
|
|
/// A DNG profile's hue/saturation map and look table.
|
|
pub tables: Option<&'a lightcraft_raw::profile::ProfileTables>,
|
|
/// A fitted camera look's hue/saturation table.
|
|
pub hue_sat: Option<&'a crate::camera_preview::HueSat<'a>>,
|
|
}
|
|
|
|
impl CameraColourModel<'_> {
|
|
/// Rebuild sensor-clipped channels; fully clipped highlights become [`clipped_white`] of the matrix, so they
|
|
/// come out of [`Self::apply`] neutral.
|
|
pub fn rebuild_highlights(&self, img: &mut Rgb32f) {
|
|
lightcraft_raw::highlight::reconstruct_with(img, self.wb, HIGHLIGHT_CLIP, clipped_white(&self.matrix));
|
|
}
|
|
|
|
/// [`Self::rebuild_highlights`] for a binned image whose clipped channels `mask` tells
|
|
/// ([`lightcraft_raw::RawImage::develop_binned_masked`]).
|
|
pub fn rebuild_highlights_masked(&self, img: &mut Rgb32f, mask: &[u8]) {
|
|
lightcraft_raw::highlight::reconstruct_masked(img, self.wb, HIGHLIGHT_CLIP, clipped_white(&self.matrix), mask);
|
|
}
|
|
|
|
/// White balance, matrix, tables and baseline exposure, in place.
|
|
pub fn apply(&self, img: &mut Rgb32f) {
|
|
let (wb, m, gain) = (self.wb, self.matrix.to_f32(), self.gain);
|
|
if let (None, Some(hue_sat)) = (self.tables, self.hue_sat) {
|
|
// a fitted camera look: `pixel`, `LANES` pixels at a time so the table step vectorises (same bits)
|
|
const LANES: usize = 16;
|
|
lightcraft_raster::par_rows(&mut img.data, img.width, |_, row| {
|
|
for chunk in row.chunks_mut(LANES) {
|
|
let mut rgb = [[0.0f32; LANES]; 3];
|
|
for (l, p) in chunk.iter().enumerate() {
|
|
let c = [p[0] * wb[0], p[1] * wb[1], p[2] * wb[2]];
|
|
for (i, rgb) in rgb.iter_mut().enumerate() {
|
|
rgb[l] = (m[i][0] * c[0] + m[i][1] * c[1] + m[i][2] * c[2]) * gain;
|
|
}
|
|
}
|
|
let out = hue_sat.apply_lanes(rgb);
|
|
for (l, p) in chunk.iter_mut().enumerate() {
|
|
*p = [out[0][l], out[1][l], out[2][l]].map(|v| v.max(0.0));
|
|
}
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
img.map_in_place(|p| self.pixel(&m, p));
|
|
}
|
|
|
|
/// [`Self::apply`] to one pixel; `m` is the matrix as `f32`.
|
|
fn pixel(&self, m: &[[f32; 3]; 3], p: [f32; 3]) -> [f32; 3] {
|
|
let (wb, gain) = (self.wb, self.gain);
|
|
let c = [p[0] * wb[0], p[1] * wb[1], p[2] * wb[2]];
|
|
let rgb = [
|
|
m[0][0] * c[0] + m[0][1] * c[1] + m[0][2] * c[2],
|
|
m[1][0] * c[0] + m[1][1] * c[1] + m[1][2] * c[2],
|
|
m[2][0] * c[0] + m[2][1] * c[1] + m[2][2] * c[2],
|
|
];
|
|
let rgb = match self.tables {
|
|
Some(tables) => tables.apply(rgb, gain),
|
|
None => rgb.map(|v| v * gain),
|
|
};
|
|
self.hue_sat.map_or(rgb, |h| h.apply(rgb)).map(|v| v.max(0.0))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) mod tests;
|