Files
dtourolle 77a1925bac Vendor rawler 0.7.2 unmodified
The crate as crates.io publishes it, minus .cargo-ok, its Cargo.lock and
data/testdata (13 MB of sample files only its own tests read). Not yet
routed through [patch.crates-io]; the next commit is the patch.
2026-09-27 17:33:19 -04:00

318 lines
12 KiB
Rust

use std::io;
use image::{DynamicImage, ImageBuffer};
use crate::{
RawImage,
decoders::RawMetadata,
formats::tiff::{DirectoryWriter, TiffWriter},
pixarray::{Color2D, PixF32},
rawimage::RawPhotometricInterpretation,
tags::{ExifTag, TiffCommonTag},
};
use super::{
Dim2, Rect, convert_from_f32_scaled_u16,
raw::{map_3ch_to_rgb, map_4ch_to_rgb},
sensor::bayer::{Demosaic, bilinear::Bilinear4Channel, ppg::PPGDemosaic},
xyz::Illuminant,
};
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub enum ProcessingStep {
Rescale,
Demosaic,
CropActiveArea,
WhiteBalance,
Calibrate,
CropDefault,
SRgb,
}
pub struct RawDevelopBuilder {}
#[derive(Clone)]
pub enum Intermediate {
Monochrome(PixF32),
ThreeColor(Color2D<f32, 3>),
FourColor(Color2D<f32, 4>),
}
impl Intermediate {
pub fn dim(&self) -> Dim2 {
match self {
Intermediate::Monochrome(pixels) => pixels.dim(),
Intermediate::ThreeColor(pixels) => pixels.dim(),
Intermediate::FourColor(pixels) => pixels.dim(),
}
}
pub fn rect(&self) -> Rect {
match self {
Intermediate::Monochrome(pixels) => pixels.rect(),
Intermediate::ThreeColor(pixels) => pixels.rect(),
Intermediate::FourColor(pixels) => pixels.rect(),
}
}
pub fn to_dynamic_image(self) -> Option<DynamicImage> {
Some(match self {
Intermediate::Monochrome(pixels) => {
let data = convert_from_f32_scaled_u16(&pixels.data, 0, u16::MAX);
DynamicImage::ImageLuma16(ImageBuffer::from_raw(pixels.dim().w as u32, pixels.dim().h as u32, data)?)
}
Intermediate::ThreeColor(pixels) => {
let data = convert_from_f32_scaled_u16(&pixels.flatten(), 0, u16::MAX);
DynamicImage::ImageRgb16(ImageBuffer::from_raw(pixels.dim().w as u32, pixels.dim().h as u32, data)?)
}
Intermediate::FourColor(pixels) => {
let data = convert_from_f32_scaled_u16(&pixels.flatten(), 0, u16::MAX);
DynamicImage::ImageRgba16(ImageBuffer::from_raw(pixels.dim().w as u32, pixels.dim().h as u32, data)?)
}
})
}
}
#[derive(Clone)]
pub struct RawDevelop {
pub steps: Vec<ProcessingStep>,
}
impl Default for RawDevelop {
fn default() -> Self {
Self {
steps: vec![
ProcessingStep::Rescale,
ProcessingStep::Demosaic,
ProcessingStep::CropActiveArea,
ProcessingStep::WhiteBalance,
ProcessingStep::Calibrate,
ProcessingStep::CropDefault,
ProcessingStep::SRgb,
],
}
}
}
impl RawDevelop {
/*
pub fn linearize(rawimage: &RawImage) -> crate::Result<RgbF32> {
todo!()
}
pub fn develop_monochrome_image(&self, rawimage: &RawImage) -> crate::Result<PixF32> {
todo!()
}
pub fn develop_rgb_image(&self, rawimage: &RawImage) -> crate::Result<RgbF32> {
todo!()
}
*/
/// Develop raw image and write result into TIFF.
/// If demosaic is disabled or camera raw is monochrome, the TIFF
/// has only one color channel.
pub fn develop_intermediate(&self, rawimage: &RawImage) -> crate::Result<Intermediate> {
let mut rawimage = rawimage.clone();
if self.steps.contains(&ProcessingStep::Rescale) {
rawimage.apply_scaling()?;
}
let mut intermediate = match rawimage.cpp {
1 => Intermediate::Monochrome(PixF32::new_with(rawimage.data.as_f32().into_owned(), rawimage.width, rawimage.height)),
3 => Intermediate::ThreeColor(Color2D::<f32, 3>::new_with(
rawimage.data.as_f32().chunks_exact(3).map(|x| [x[0], x[1], x[2]]).collect(),
rawimage.width,
rawimage.height,
)),
4 => Intermediate::FourColor(Color2D::<f32, 4>::new_with(
rawimage.data.as_f32().chunks_exact(4).map(|x| [x[0], x[1], x[2], x[3]]).collect(),
rawimage.width,
rawimage.height,
)),
_ => todo!(),
};
if self.steps.contains(&ProcessingStep::Demosaic) {
intermediate = match &rawimage.photometric {
RawPhotometricInterpretation::Cfa(config) => {
if let Intermediate::Monochrome(pixels) = intermediate {
let roi = if self.steps.contains(&ProcessingStep::CropActiveArea) {
rawimage.active_area.unwrap_or(pixels.rect())
} else {
pixels.rect()
};
if config.cfa.is_rgb() {
let ppg = PPGDemosaic::new();
Intermediate::ThreeColor(ppg.demosaic(&pixels, &config.cfa, &config.colors, roi))
} else if config.cfa.unique_colors() == 4 {
let linear = Bilinear4Channel::new();
Intermediate::FourColor(linear.demosaic(&pixels, &config.cfa, &config.colors, roi))
} else {
todo!()
}
} else {
intermediate
}
}
_ => intermediate,
};
}
if self.steps.contains(&ProcessingStep::Calibrate) {
let found_matrix = rawimage
.color_matrix
.iter()
.find(|(illuminant, _m)| **illuminant == Illuminant::D65)
.or_else(|| rawimage.color_matrix.iter().next());
if let Some((_illuminant, color_matrix)) = found_matrix {
// Safety check: ensure matrix is valid length
if color_matrix.len() % 3 == 0 {
let mut xyz2cam: [[f32; 3]; 4] = [[0.0; 3]; 4];
let components = color_matrix.len() / 3;
for i in 0..components {
for j in 0..3 {
xyz2cam[i][j] = color_matrix[i * 3 + j];
}
}
let mut wb = if rawimage.wb_coeffs[0].is_nan() {
[1.0, 1.0, 1.0, 1.0]
} else {
rawimage.wb_coeffs
};
if !self.steps.contains(&ProcessingStep::WhiteBalance) {
wb = [1.0, 1.0, 1.0, 1.0];
}
log::debug!("wb: {:?}, coeff: {:?}", wb, xyz2cam);
intermediate = match intermediate {
Intermediate::Monochrome(_) => intermediate,
Intermediate::ThreeColor(pixels) => Intermediate::ThreeColor(map_3ch_to_rgb(&pixels, &wb, xyz2cam)),
Intermediate::FourColor(pixels) => Intermediate::ThreeColor(map_4ch_to_rgb(&pixels, &wb, xyz2cam)),
};
} else {
log::warn!("Color matrix found but has invalid length. Skipping calibration.");
}
} else {
log::warn!("Illuminant matrix D65 not found and no fallback available. Skipping calibration.");
}
}
if self.steps.contains(&ProcessingStep::CropDefault) {
if let Some(mut crop) = rawimage.crop_area.or(rawimage.active_area) {
if self.steps.contains(&ProcessingStep::Demosaic) && self.steps.contains(&ProcessingStep::CropActiveArea) {
// If active area crop was already applied during demosaic, we need to
// adapt default crop to active area crop.
crop = crop.adapt(&rawimage.active_area.unwrap_or(crop));
}
if intermediate.dim().w == rawimage.active_area.map(|area| area.d).unwrap_or(rawimage.dim()).w / 2 {
// Superpixel debayer used
crop.scale(0.5);
}
// Only apply crop if dimensions differ.
if crop.d != intermediate.dim() {
log::debug!("crop: {:?}, intermediate dim: {:?}, rawimage: {:?}", crop, intermediate.dim(), rawimage.dim());
intermediate = match intermediate {
Intermediate::Monochrome(pixels) => Intermediate::Monochrome(pixels.crop(crop)),
Intermediate::ThreeColor(pixels) => Intermediate::ThreeColor(pixels.crop(crop)),
Intermediate::FourColor(pixels) => Intermediate::FourColor(pixels.crop(crop)),
};
}
}
}
if self.steps.contains(&ProcessingStep::SRgb) {
match &mut intermediate {
Intermediate::Monochrome(pixels) => pixels.for_each(super::srgb::srgb_apply_gamma),
Intermediate::ThreeColor(pixels) => pixels.for_each(super::srgb::srgb_apply_gamma_n),
Intermediate::FourColor(pixels) => pixels.for_each(super::srgb::srgb_apply_gamma_n),
};
}
Ok(intermediate)
}
/// Develop raw image and write result into TIFF.
/// If demosaic is disabled or camera raw is monochrome, the TIFF
/// has only one color channel.
pub fn develop<W>(&self, rawimage: &RawImage, md: &RawMetadata, writer: W) -> crate::Result<()>
where
W: io::Write + io::Seek,
{
let intermediate = self.develop_intermediate(rawimage)?;
let mut tiff = TiffWriter::new(writer)?;
let mut root_ifd = DirectoryWriter::new();
let mut exif_ifd = DirectoryWriter::new();
// Add EXIF version 0220
exif_ifd.add_tag_undefined(ExifTag::ExifVersion, vec![48, 50, 50, 48]);
md.write_exif_tags(&mut tiff, &mut root_ifd, &mut exif_ifd)?;
root_ifd.add_tag(TiffCommonTag::Make, rawimage.clean_make.as_str());
root_ifd.add_tag(TiffCommonTag::Model, rawimage.clean_model.as_str());
let exif_offset = exif_ifd.build(&mut tiff)?;
root_ifd.add_tag(TiffCommonTag::ExifIFDPointer, exif_offset);
match intermediate {
Intermediate::Monochrome(pixels) => {
let data = convert_from_f32_scaled_u16(&pixels.data, 0, u16::MAX);
let (strip_rows, strips) = tiff.write_strips_lzw(&data, 1, pixels.dim(), 0)?;
let strip_offsets: Vec<u32> = strips.iter().map(|(offset, _)| *offset).collect();
let strip_bytes: Vec<u32> = strips.iter().map(|(_, bytes)| *bytes).collect();
root_ifd.add_tag(TiffCommonTag::Compression, 5);
root_ifd.add_tag(TiffCommonTag::Predictor, 1);
root_ifd.add_tag(TiffCommonTag::StripOffsets, &strip_offsets);
root_ifd.add_tag(TiffCommonTag::StripByteCounts, &strip_bytes);
root_ifd.add_tag(TiffCommonTag::BitsPerSample, [16_u16]);
root_ifd.add_tag(TiffCommonTag::SamplesPerPixel, [1_u16]);
root_ifd.add_tag(TiffCommonTag::PhotometricInt, [1_u16]);
root_ifd.add_tag(TiffCommonTag::RowsPerStrip, strip_rows);
root_ifd.add_tag(TiffCommonTag::ImageWidth, pixels.width as u16);
root_ifd.add_tag(TiffCommonTag::ImageLength, pixels.height as u16);
}
Intermediate::ThreeColor(pixels) => {
let data = convert_from_f32_scaled_u16(&pixels.flatten(), 0, u16::MAX);
let (strip_rows, strips) = tiff.write_strips_lzw(&data, 3, pixels.dim(), 0)?;
let strip_offsets: Vec<u32> = strips.iter().map(|(offset, _)| *offset).collect();
let strip_bytes: Vec<u32> = strips.iter().map(|(_, bytes)| *bytes).collect();
root_ifd.add_tag(TiffCommonTag::Compression, 5);
root_ifd.add_tag(TiffCommonTag::Predictor, 1);
root_ifd.add_tag(TiffCommonTag::StripOffsets, &strip_offsets);
root_ifd.add_tag(TiffCommonTag::StripByteCounts, &strip_bytes);
root_ifd.add_tag(TiffCommonTag::BitsPerSample, [16_u16, 16, 16]);
root_ifd.add_tag(TiffCommonTag::SamplesPerPixel, [3_u16]);
root_ifd.add_tag(TiffCommonTag::PhotometricInt, [2_u16]);
root_ifd.add_tag(TiffCommonTag::RowsPerStrip, strip_rows);
root_ifd.add_tag(TiffCommonTag::ImageWidth, pixels.width as u16);
root_ifd.add_tag(TiffCommonTag::ImageLength, pixels.height as u16);
}
Intermediate::FourColor(pixels) => {
let data = convert_from_f32_scaled_u16(&pixels.flatten(), 0, u16::MAX);
let (strip_rows, strips) = tiff.write_strips_lzw(&data, 4, pixels.dim(), 0)?;
let strip_offsets: Vec<u32> = strips.iter().map(|(offset, _)| *offset).collect();
let strip_bytes: Vec<u32> = strips.iter().map(|(_, bytes)| *bytes).collect();
root_ifd.add_tag(TiffCommonTag::Compression, 5);
root_ifd.add_tag(TiffCommonTag::Predictor, 1);
root_ifd.add_tag(TiffCommonTag::StripOffsets, &strip_offsets);
root_ifd.add_tag(TiffCommonTag::StripByteCounts, &strip_bytes);
root_ifd.add_tag(TiffCommonTag::BitsPerSample, [16_u16, 16, 16, 16]); // Extra-channel, even if PhotometricInt is RGB!
root_ifd.add_tag(TiffCommonTag::SamplesPerPixel, [4_u16]);
root_ifd.add_tag(TiffCommonTag::PhotometricInt, [2_u16]);
root_ifd.add_tag(TiffCommonTag::RowsPerStrip, strip_rows);
root_ifd.add_tag(TiffCommonTag::ImageWidth, pixels.width as u16);
root_ifd.add_tag(TiffCommonTag::ImageLength, pixels.height as u16);
}
}
tiff.build(root_ifd)?;
Ok(())
}
}