From 4b37607028967fd178745d0986f88b83f0d65e2f Mon Sep 17 00:00:00 2001 From: gennyble Date: Sat, 9 Mar 2024 21:50:32 -0600 Subject: dirty repo; this was here; wip --- .gitignore | 1 + Cargo.lock | 65 -------------- README.md | 4 +- lri-rs/src/block.rs | 19 +++- lri-rs/src/fine.rs | 144 ------------------------------ lri-rs/src/lib.rs | 31 +++++-- lri-rs/src/types.rs | 51 ++++++++--- lri-rs/src/unpack.rs | 57 ++++++++++++ lri-study/src/main.rs | 3 - prism/Cargo.toml | 2 +- prism/src/main.rs | 220 +-------------------------------------------- prism/src/output.rs | 240 ++++++++++++++++++++++++++++++++++++++++++++++++++ prism/src/unpack.rs | 61 ------------- 13 files changed, 386 insertions(+), 512 deletions(-) delete mode 100644 lri-rs/src/fine.rs create mode 100644 lri-rs/src/unpack.rs create mode 100644 prism/src/output.rs delete mode 100644 prism/src/unpack.rs diff --git a/.gitignore b/.gitignore index 4064ec8..c3bd517 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ *.png *.jpg *.bjp +*.gif **/.DS_Store \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d44e79c..7bd524e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,12 +32,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - [[package]] name = "autocfg" version = "1.1.0" @@ -86,7 +80,6 @@ version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ - "jobserver", "libc", ] @@ -148,12 +141,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "dunce" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" - [[package]] name = "either" version = "1.9.0" @@ -265,15 +252,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -357,30 +335,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "mozjpeg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84edaffd54775e831923ce65f73288793c80fe98771b0be9eadaf753b29792fc" -dependencies = [ - "arrayvec", - "libc", - "mozjpeg-sys", - "rgb", -] - -[[package]] -name = "mozjpeg-sys" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab8f5496b7f0e8c593d33dbbbc16c6eefd3a6991d794f56e96bebc5228cfd29" -dependencies = [ - "cc", - "dunce", - "libc", - "nasm-rs", -] - [[package]] name = "nalgebra" version = "0.31.4" @@ -408,15 +362,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "nasm-rs" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4d98d0065f4b1daf164b3eafb11974c94662e5e2396cf03f32d0bb5c17da51" -dependencies = [ - "rayon", -] - [[package]] name = "num-complex" version = "0.4.4" @@ -509,7 +454,6 @@ version = "0.1.0" dependencies = [ "camino", "lri-rs", - "mozjpeg", "nalgebra", "png", "rawloader", @@ -710,15 +654,6 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" -[[package]] -name = "rgb" -version = "0.8.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" -dependencies = [ - "bytemuck", -] - [[package]] name = "rustc_version" version = "0.4.0" diff --git a/README.md b/README.md index 79b371f..157c8bc 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ A friend archived a lot of Light L16 stuff at [helloavo/Light-L16-Archive](https ### lri-rs A Rust crate for parsing LRI files. This library isn't perfect, but it works enough to be able to grab image data from the files. +The code here is a mess, but I'm working to improve it. Reading the metadata from the LRI files is weird! It's all in protobuf messages and those protobuf messages make *a lot* or stuff "optional". We end up with a lot of duplicated data + ### prism Breaks an LRI into the individual images it contains `prism ` @@ -14,7 +16,7 @@ Breaks an LRI into the individual images it contains TODO: I'd like to, one day, be able to write DNG files from prism, but currently it just spits out PNG. ### lri-proto -This is a gently modified version of the [dllu/lri.rs](https://github.com/dllu/lri-rs) repository. Without the work from Daniel pulling the protobuf definitions from the Lumen software I truly don't know if I could've got as far as I did. +This is a gently modified version of the [dllu/lri-rs](https://github.com/dllu/lri-rs) repository. Without the work from Daniel pulling the protobuf definitions from the Lumen software I truly don't know if I could've got as far as I did. MIT Copyright Daniel Lawrence Lu diff --git a/lri-rs/src/block.rs b/lri-rs/src/block.rs index e3d94a5..8608dd2 100644 --- a/lri-rs/src/block.rs +++ b/lri-rs/src/block.rs @@ -7,7 +7,7 @@ use lri_proto::{ use crate::{ AwbGain, AwbMode, CameraId, CameraInfo, ColorInfo, DataFormat, HdrMode, RawData, RawImage, - SceneMode, SensorModel, + SceneMode, SensorData, SensorModel, }; pub(crate) struct Block<'lri> { @@ -59,6 +59,7 @@ impl<'lri> Block<'lri> { image_focal_length, af_info, mut view_preferences, + sensor_data, .. } = if let Message::LightHeader(lh) = self.message() { lh @@ -210,6 +211,20 @@ impl<'lri> Block<'lri> { if let Some(x) = image_focal_length { ext.focal_length.get_or_insert(x); } + + for sd in sensor_data { + let sd: crate::SensorData = sd.into(); + println!( + "black={} white={} cliff={}", + sd.characterization.black_level, + sd.characterization.white_level, + sd.characterization + .cliff_slope + .map(|f| f.to_string()) + .unwrap_or_default() + ); + ext.sensor_data.push(sd); + } } // It kept making my neat little array very, very tall @@ -279,6 +294,8 @@ pub(crate) struct ExtractedData { pub awb: Option, pub awb_gain: Option, + + pub sensor_data: Vec, } pub enum Message { diff --git a/lri-rs/src/fine.rs b/lri-rs/src/fine.rs deleted file mode 100644 index 52b2527..0000000 --- a/lri-rs/src/fine.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::collections::HashMap; - -use lri_proto::{lightheader::LightHeader, view_preferences::ViewPreferences}; - -#[derive(Debug)] -pub struct Signature { - pub lh: HashMap<&'static str, Vec>, - pub vp: HashMap<&'static str, Vec>, -} - -impl Signature { - pub fn new() -> Self { - Self { - lh: HashMap::new(), - vp: HashMap::new(), - } - } - - pub fn merge(&mut self, lh: &LightHeader) { - let LightHeader { - image_unique_id_low, - image_unique_id_high, - image_time_stamp, - image_focal_length, - image_reference_camera, - device_unique_id_low, - device_unique_id_high, - device_model_name, - device_fw_version, - device_asic_fw_version, - device_temperature, - modules, - module_calibration, - device_calibration, - gold_cc, - sensor_data, - tof_range, - hw_info, - view_preferences, - proximity_sensors, - flash_data, - imu_data, - af_info, - gps_data, - compatibility, - face_data, - special_fields, - } = lh; - - macro_rules! hh { - ($field:ident) => { - let i = match $field { - Some(v) => vec![v.to_string()], - None => vec![], - }; - - self.lh - .entry(stringify!($field)) - .and_modify(|v| v.extend_from_slice(&i)) - .or_insert(i); - }; - } - - macro_rules! mf { - ($field:ident) => { - let add = if $field.is_some() { 1 } else { 0 }; - - self.lh - .entry(stringify!($field)) - .and_modify(|count| *count += add) - .or_insert(add); - }; - } - - macro_rules! hv { - ($field:ident) => { - let add = $field.len(); - - self.lh - .entry(stringify!($field)) - .and_modify(|count| *count += add) - .or_insert(add); - }; - } - - hh!(image_unique_id_low); - hh!(image_unique_id_high); - mf!(image_time_stamp); - hh!(image_focal_length); - hh!(image_reference_camera); - hh!(device_unique_id_low); - hh!(device_unique_id_high); - hh!(device_model_name); - hh!(device_fw_version); - hh!(device_asic_fw_version); - mf!(device_temperature); - hv!(modules); - hv!(module_calibration); - mf!(device_calibration); - hv!(gold_cc); - hv!(sensor_data); - hh!(tof_range); - mf!(hw_info); - mf!(view_preferences); - mf!(proximity_sensors); - mf!(flash_data); - hv!(imu_data); - mf!(af_info); - mf!(gps_data); - mf!(compatibility); - hv!(face_data); - } - - pub fn vp(&mut self, vp: &ViewPreferences) {} -} - -/* -optional uint64 image_unique_id_low = 1; -optional uint64 image_unique_id_high = 2; -optional TimeStamp image_time_stamp = 3; -optional int32 image_focal_length = 4; -optional CameraID image_reference_camera = 5; -optional uint64 device_unique_id_low = 6; -optional uint64 device_unique_id_high = 7; -optional string device_model_name = 8; -optional string device_fw_version = 9; -optional string device_asic_fw_version = 10; -optional DeviceTemp device_temperature = 11; -repeated CameraModule modules = 12; -repeated FactoryModuleCalibration module_calibration = 13; -optional FactoryDeviceCalibration device_calibration = 14; -repeated ColorCalibrationGold gold_cc = 15; -repeated SensorData sensor_data = 16; -optional float tof_range = 17; -optional HwInfo hw_info = 18; -optional ViewPreferences view_preferences = 19; -optional ProximitySensors proximity_sensors = 20; -optional FlashData flash_data = 22; -repeated IMUData imu_data = 23; -optional AFDebugInfo af_info = 24; -optional GPSData gps_data = 25; -optional Compatibility compatibility = 26; -repeated FaceData face_data = 27; -*/ diff --git a/lri-rs/src/lib.rs b/lri-rs/src/lib.rs index 342248a..d59ac3b 100644 --- a/lri-rs/src/lib.rs +++ b/lri-rs/src/lib.rs @@ -4,6 +4,7 @@ use block::{Block, ExtractedData, Header}; mod block; mod types; +mod unpack; pub use types::*; @@ -153,6 +154,11 @@ impl<'img> RawImage<'img> { self.color.iter().find(|c| c.whitepoint == whitepoint) } + /// Return a string describing the colour filter array used for this image. + /// `None` is returned if the sensor is monochromatic. + /// + /// NOTE: The same sensor can return differing cfa patterns for different + /// images. This is likely due to in-camera corrected rotation. pub fn cfa_string(&self) -> Option<&'static str> { match self.sensor { SensorModel::Ar1335Mono => None, @@ -163,9 +169,6 @@ impl<'img> RawImage<'img> { // The AR1335 seems to be BGGR, which was weird. fn cfa_string_ar1335(&self) -> Option<&'static str> { - //if self.format == DataFormat::BayerJpeg { - // Some("BGGR") - //} else { match self.sbro { (-1, -1) => None, (0, 0) => Some("BGGR"), @@ -174,17 +177,27 @@ impl<'img> RawImage<'img> { (1, 1) => Some("RGGB"), _ => unreachable!(), } - //} } /// Uses the [SensorModel] to determine if the image's [ColorType]. /// If the sensor model is unknown, [SensorModel::Unknown], then [ColorType::Grayscale] is returned pub fn color_type(&self) -> ColorType { match self.sensor { - SensorModel::Ar1335 | SensorModel::Ar835 | SensorModel::Imx386 => ColorType::Rgb, - SensorModel::Ar1335Mono | SensorModel::Imx386Mono | SensorModel::Unknown => { - ColorType::Grayscale - } + SensorModel::Ar1335 => ColorType::Rgb, + SensorModel::Ar1335Mono | SensorModel::Unknown => ColorType::Grayscale, + } + } + + /// Returns the unpacked data if the DataFormat is Packed10bpp, otherwise + /// returns None + pub fn unpack(&self) -> Option> { + if let RawData::Packed10bpp { data } = self.data { + let count = self.width * self.height; + let mut upack = vec![0; count]; + unpack::tenbit(data, count, &mut upack); + Some(upack) + } else { + None } } } @@ -197,7 +210,7 @@ pub enum ColorType { #[derive(Copy, Clone, Debug)] /// Colour information about the camera. Used to correct the image pub struct ColorInfo { - /// Which specific colour this image was taken by + /// Which specific camera this image was taken by pub camera: CameraId, /// The whitepoint that the forward matrix corresponds to. diff --git a/lri-rs/src/types.rs b/lri-rs/src/types.rs index 8b759ac..5e1f581 100644 --- a/lri-rs/src/types.rs +++ b/lri-rs/src/types.rs @@ -13,7 +13,7 @@ use lri_proto::{ pub enum DataFormat { BayerJpeg, Packed10bpp, - // Never seen + // Never seen. These are likely from other devices with the Light ASIC //Packed12bpp, //Packed14bpp, } @@ -23,8 +23,6 @@ impl fmt::Display for DataFormat { let str = match self { Self::BayerJpeg => "BayerJpeg", Self::Packed10bpp => "Packed10bpp", - //Self::Packed12bpp => "Packed12bpp", - //Self::Packed14bpp => "Packed14bpp", }; write!(f, "{str}") @@ -126,14 +124,15 @@ impl From for Whitepoint { } } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum SensorModel { Unknown, - Ar835, Ar1335, Ar1335Mono, - Imx386, - Imx386Mono, + // Never Seen. Likely from other devices with the Light ASIC + //Ar835, + //Imx386, + //Imx386Mono, } impl From for SensorModel { @@ -142,11 +141,11 @@ impl From for SensorModel { match pbst { ProtoSt::SENSOR_UNKNOWN => Self::Unknown, - ProtoSt::SENSOR_AR835 => Self::Ar835, ProtoSt::SENSOR_AR1335 => Self::Ar1335, ProtoSt::SENSOR_AR1335_MONO => Self::Ar1335Mono, - ProtoSt::SENSOR_IMX386 => Self::Imx386, - ProtoSt::SENSOR_IMX386_MONO => Self::Imx386Mono, + ProtoSt::SENSOR_AR835 | ProtoSt::SENSOR_IMX386 | ProtoSt::SENSOR_IMX386_MONO => { + unimplemented!() + } } } } @@ -233,3 +232,35 @@ impl From for AwbGai } } } + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct SensorData { + pub sensor_type: SensorModel, + pub characterization: SensorCharacterization, +} + +impl From for SensorData { + fn from(sd: lri_proto::lightheader::SensorData) -> Self { + Self { + sensor_type: sd.type_().into(), + characterization: sd.data.into_option().unwrap().into(), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct SensorCharacterization { + pub black_level: f32, + pub white_level: f32, + pub cliff_slope: Option, +} + +impl From for SensorCharacterization { + fn from(sc: lri_proto::sensor_characterization::SensorCharacterization) -> Self { + Self { + black_level: sc.black_level.unwrap(), + white_level: sc.white_level.unwrap(), + cliff_slope: sc.cliff_slope, + } + } +} diff --git a/lri-rs/src/unpack.rs b/lri-rs/src/unpack.rs new file mode 100644 index 0000000..b14b79c --- /dev/null +++ b/lri-rs/src/unpack.rs @@ -0,0 +1,57 @@ +const TEN_MASK: u64 = 1023; // ten bits + +pub fn tenbit(packd: &[u8], count: usize, upack: &mut [u16]) { + let required_len_packd = (count as f32 * (10.0 / 8.0)).ceil() as usize; + + if count > upack.len() { + panic!( + "expected output buffer to be {count} bytes, got {} bytes", + upack.len() + ) + } + + if required_len_packd > packd.len() { + panic!( + "expected input to be at least {required_len_packd} bytes, it was {}", + packd.len() + ) + } + + let mut packd = packd[..required_len_packd].to_vec(); + packd.reverse(); + let chunker = packd.chunks_exact(5); + let remain = chunker.remainder(); + + for (idx, chnk) in chunker.enumerate() { + let long = u64::from_be_bytes([ + 0x00, 0x00, 0x00, chnk[0], chnk[1], chnk[2], chnk[3], chnk[4], + ]); + + let b4 = long & TEN_MASK; + let b3 = (long >> 10) & TEN_MASK; + let b2 = (long >> 20) & TEN_MASK; + let b1 = (long >> 30) & TEN_MASK; + + let idx = idx * 4; + upack[idx] = b1 as u16; + upack[idx + 1] = b2 as u16; + upack[idx + 2] = b3 as u16; + upack[idx + 3] = b4 as u16; + } + + if !remain.is_empty() { + let mut long_bytes = [0x00; 8]; + + for (idx, byte) in remain.iter().enumerate() { + long_bytes[idx] = *byte; + } + + let long = u64::from_le_bytes(long_bytes); + + let count_remain = count % 4; + let start = count - count_remain; + for idx in 0..count_remain { + upack[start + idx] = ((long >> (10 * idx)) & TEN_MASK) as u16; + } + } +} diff --git a/lri-study/src/main.rs b/lri-study/src/main.rs index 2b4c7cd..4531cb2 100644 --- a/lri-study/src/main.rs +++ b/lri-study/src/main.rs @@ -131,9 +131,6 @@ fn gather() -> ! { let sens = match img.sensor { SensorModel::Ar1335 => "a13", SensorModel::Ar1335Mono => "a1m", - SensorModel::Ar835 => "!!!ar8", - SensorModel::Imx386 => "!!!imx", - SensorModel::Imx386Mono => "!!!imm", SensorModel::Unknown => "???", }; diff --git a/prism/Cargo.toml b/prism/Cargo.toml index 6861a34..407596c 100644 --- a/prism/Cargo.toml +++ b/prism/Cargo.toml @@ -11,6 +11,6 @@ png = "0.17.10" rawproc = { git = "https://github.com/eclecticnybles/gaze" } rawloader = "0.37.1" nalgebra = "0.31.4" -mozjpeg = "0.10.1" +#mozjpeg = "0.10.1" zune-jpeg = "0.3.17" camino = "1.1.6" diff --git a/prism/src/main.rs b/prism/src/main.rs index 43278a6..8a52e0f 100644 --- a/prism/src/main.rs +++ b/prism/src/main.rs @@ -4,8 +4,8 @@ use camino::Utf8PathBuf; use lri_rs::{AwbGain, CameraId, LriFile, RawData, RawImage, SensorModel, Whitepoint}; use nalgebra::{Matrix3, Matrix3x1}; +mod output; mod rotate; -mod unpack; pub struct Entry { sensor: SensorModel, @@ -34,7 +34,7 @@ fn main() { println!("{} images", lri.image_count()); if let Some(refimg) = lri.reference_image() { - make(refimg, directory.join("reference.png"), gain); + output::make_png(refimg, directory.join("reference.png"), gain); } let mut set: HashMap = HashMap::new(); @@ -53,220 +53,6 @@ fn main() { });*/ for (idx, img) in lri.images().enumerate() { - make(img, directory.join(format!("image_{idx}.png")), gain); + output::make_png(img, directory.join(format!("image_{idx}.png")), gain); } } - -fn make(img: &RawImage, path: Utf8PathBuf, awb_gain: AwbGain) { - use rawproc::image::RawMetadata; - use rawproc::{colorspace::BayerRgb, image::Image}; - - let RawImage { - camera, - sensor, - width, - height, - format, - data, - sbro, - color, - } = img; - - println!( - "{camera} {sensor:?} [{}:{}] {width}x{height} {format}", - sbro.0, sbro.1 - ); - - let bayered = bayer(data, *width, *height); - - let (mut rgb, color_format) = match img.cfa_string() { - Some(cfa_string) => { - let rawimg: Image = Image::from_raw_parts( - 4160, - 3120, - // We only care about CFA here because all we're doing is debayering - RawMetadata { - whitebalance: [1.0; 3], - whitelevels: [1024; 3], - crop: None, - // ugh CFA isn't exposed, so we pulled in rawloader for now - cfa: rawloader::CFA::new(cfa_string), - cam_to_xyz: nalgebra::Matrix3::zeros(), - }, - bayered, - ); - - (rawimg.debayer().data, png::ColorType::Rgb) - //(bayered, png::ColorType::Grayscale) - } - None => (bayered, png::ColorType::Grayscale), - }; - - rotate::rotate_180(rgb.as_mut_slice()); - let mut floats: Vec = rgb.into_iter().map(|p| p as f32 / 255.0).collect(); - - if !color.is_empty() { - print!("\tAvailable whitepoints: "); - color.iter().for_each(|c| print!("{:?} ", c.whitepoint)); - println!(); - } - - match img.color_info(Whitepoint::D65) { - Some(c) => { - println!("\tUsing D65"); - let to_xyz = Matrix3::from_row_slice(&c.forward_matrix); - // We're using Whitepoint::D65, but there is no D50 profile. - // If we use the BRUCE_XYZ_RGB_D65 matrix the image - // comes out too warm. - let to_srgb = Matrix3::from_row_slice(&BRUCE_XYZ_RGB_D50); - - let premul = to_xyz * to_srgb; - - for chnk in floats.chunks_mut(3) { - /*let r = chnk[0] * (1.0 / c.rg); - let g = chnk[1]; - let b = chnk[2] * (1.0 / c.bg);*/ - let r = chnk[0] * awb_gain.r; - let g = chnk[1]; - let b = chnk[2] * awb_gain.b; - - let px = Matrix3x1::new(r, g, b); - let rgb = premul * px; - - chnk[0] = srgb_gamma(rgb[0]) * 255.0; - chnk[1] = srgb_gamma(rgb[1]) * 255.0; - chnk[2] = srgb_gamma(rgb[2]) * 255.0; - } - } - None => { - println!("\tColor profile for D65 not found. Doing gamma and nothing else!"); - floats.iter_mut().for_each(|f| *f = srgb_gamma(*f) * 255.0); - } - } - - let bytes: Vec = floats.into_iter().map(|f| f as u8).collect(); - - println!("\tWriting {}", &path); - make_png(path, *width, *height, &bytes, color_format) -} - -#[rustfmt::skip] -#[allow(dead_code)] -const BRUCE_XYZ_RGB_D50: [f32; 9] = [ - 3.1338561, -1.6168667, -0.4906146, - -0.9787684, 1.9161415, 0.0334540, - 0.0719453, -0.2289914, 1.4052427 -]; - -#[rustfmt::skip] -const BRUCE_XYZ_RGB_D65: [f32; 9] = [ - 3.2404542, -1.5371385, -0.4985314, - -0.9692660, 1.8760108, 0.0415560, - 0.0556434, -0.2040259, 1.0572252 -]; - -#[rustfmt::skip] -const BRADFORD_D50_D65: [f32; 9] = [ - 0.9555766, -0.0230393, 0.0631636, - -0.0282895, 1.0099416, 0.0210077, - 0.0122982, -0.0204830, 1.3299098, -]; - -#[inline] -pub fn srgb_gamma(mut float: f32) -> f32 { - if float <= 0.0031308 { - float *= 12.92; - } else { - float = float.powf(1.0 / 2.4) * 1.055 - 0.055; - } - - float.clamp(0.0, 1.0) -} - -fn bayer(data: &RawData<'_>, width: usize, height: usize) -> Vec { - match data { - RawData::Packed10bpp { data } => { - let size = width * height; - let mut ten_data = vec![0; size]; - unpack::tenbit(data, width * height, ten_data.as_mut_slice()); - - // I've only seen it on one color defintion or - // something, but there's a black level of 42, so subtract it. - // without it the image is entirely too red. - //ten_data.iter_mut().for_each(|p| *p = p.saturating_sub(42)); - - ten_data - .into_iter() - .map(|p| ((p.saturating_sub(42)) >> 2) as u8) - .collect() - } - RawData::BayerJpeg { - header: _, - format, - jpeg0, - jpeg1, - jpeg2, - jpeg3, - } => { - let mut bayered = vec![0; width * height]; - - match format { - 0 => { - let mut into = vec![0; (width * height) / 4]; - - let mut channel = |jpeg: &[u8], offset: usize| { - zune_jpeg::JpegDecoder::new(jpeg) - .decode_into(&mut into) - .unwrap(); - - for idx in 0..into.len() { - let ww = width / 2; - let in_x = idx % ww; - let in_y = idx / ww; - - let bayer_x = (in_x * 2) + (offset % 2); - let bayer_y = (in_y * 2) + (offset / 2); - - let bayer_idx = bayer_y * width + bayer_x; - bayered[bayer_idx] = into[idx]; - } - }; - - //BGGR - //RGGB - //GRBG - channel(jpeg0, 0); - channel(jpeg1, 1); - channel(jpeg2, 2); - channel(jpeg3, 3); - } - 1 => { - zune_jpeg::JpegDecoder::new(jpeg0) - .decode_into(&mut bayered) - .unwrap(); - } - _ => unreachable!(), - } - - bayered - } - } -} - -fn make_png>( - path: P, - width: usize, - height: usize, - data: &[u8], - color_format: png::ColorType, -) { - //return; - use std::fs::File; - - let file = File::create(path).unwrap(); - let mut enc = png::Encoder::new(file, width as u32, height as u32); - enc.set_color(color_format); - enc.set_depth(png::BitDepth::Eight); - let mut writer = enc.write_header().unwrap(); - writer.write_image_data(data).unwrap(); -} diff --git a/prism/src/output.rs b/prism/src/output.rs new file mode 100644 index 0000000..043f23d --- /dev/null +++ b/prism/src/output.rs @@ -0,0 +1,240 @@ +use camino::Utf8PathBuf; +use lri_rs::{AwbGain, RawData, RawImage, Whitepoint}; +use nalgebra::{Matrix3, Matrix3x1}; + +use crate::rotate; + +pub fn make_png(img: &RawImage, path: Utf8PathBuf, awb_gain: AwbGain) { + use rawproc::image::RawMetadata; + use rawproc::{colorspace::BayerRgb, image::Image}; + + let RawImage { + camera, + sensor, + width, + height, + format, + data: _data, + sbro, + color, + } = img; + + println!( + "{camera} {sensor:?} [{}:{}] {width}x{height} {format}", + sbro.0, sbro.1 + ); + + let bayered = img.unpack().unwrap(); + + let (mut rgb, color_format) = match img.cfa_string() { + Some(cfa_string) => { + let rawimg: Image = Image::from_raw_parts( + 4160, + 3120, + // We only care about CFA here because all we're doing is debayering + RawMetadata { + whitebalance: [1.0; 3], + whitelevels: [1024; 3], + crop: None, + // ugh CFA isn't exposed, so we pulled in rawloader for now + cfa: rawloader::CFA::new(cfa_string), + cam_to_xyz: nalgebra::Matrix3::zeros(), + }, + bayered, + ); + + (rawimg.debayer().data, png::ColorType::Rgb) + //(bayered, png::ColorType::Grayscale) + } + None => (bayered, png::ColorType::Grayscale), + }; + + rotate::rotate_180(rgb.as_mut_slice()); + let mut floats: Vec = rgb + .into_iter() + .map(|p| ((p.saturating_sub(42) as f32) / 1023.0)) + .collect(); + + if !color.is_empty() { + print!("\tAvailable whitepoints: "); + color.iter().for_each(|c| print!("{:?} ", c.whitepoint)); + println!(); + } + + match img.color_info(Whitepoint::D65) { + Some(c) => { + println!("\tUsing D65"); + let to_xyz = Matrix3::from_row_slice(&c.forward_matrix); + // We're using Whitepoint::D65, but there is no D50 profile. + // If we use the BRUCE_XYZ_RGB_D65 matrix the image + // comes out too warm. + let to_srgb = Matrix3::from_row_slice(&BRUCE_XYZ_RGB_D50); + + let premul = to_xyz * to_srgb; + + let wb_max = awb_gain.r.max(awb_gain.b); + + let wb_r = awb_gain.r; // / wb_max; + let wb_g = 1.0; // / wb_max; + let wb_b = awb_gain.b; // / wb_max; + + for (idx, chnk) in floats.chunks_mut(3).enumerate() { + /*let r = chnk[0] * (1.0 / c.rg); + let g = chnk[1]; + let b = chnk[2] * (1.0 / c.bg);*/ + let r = chnk[0] * wb_r; + let g = chnk[1] * wb_g; + let b = chnk[2] * wb_b; + + if idx == 4 { + println!( + "R: {:.2},{:.2},{:.2} - W: {:.2},{:.2},{:.2}", + chnk[0], chnk[1], chnk[2], r, g, b + ); + } + + let px = Matrix3x1::new(r, g, b); + let rgb = premul * px; + + chnk[0] = srgb_gamma(rgb[0]); + chnk[1] = srgb_gamma(rgb[1]); + chnk[2] = srgb_gamma(rgb[2]); + + if idx == 4 { + println!( + "S: {:.2},{:.2},{:.2} - G: {:.2},{:.2},{:.2}", + rgb[0], rgb[1], rgb[2], chnk[0], chnk[1], chnk[2] + ); + } + } + } + None => { + println!("\tColor profile for D65 not found. Doing gamma and nothing else!"); + floats.iter_mut().for_each(|f| *f = srgb_gamma(*f)); + } + } + + let bytes: Vec = floats.into_iter().map(|f| (f * 255.0) as u8).collect(); + + println!("\tWriting {}", &path); + write_png(path, *width, *height, &bytes, color_format) +} + +#[rustfmt::skip] +#[allow(dead_code)] +const BRUCE_XYZ_RGB_D50: [f32; 9] = [ + 3.1338561, -1.6168667, -0.4906146, + -0.9787684, 1.9161415, 0.0334540, + 0.0719453, -0.2289914, 1.4052427 +]; + +#[rustfmt::skip] +const BRUCE_XYZ_RGB_D65: [f32; 9] = [ + 3.2404542, -1.5371385, -0.4985314, + -0.9692660, 1.8760108, 0.0415560, + 0.0556434, -0.2040259, 1.0572252 +]; + +#[rustfmt::skip] +const BRADFORD_D50_D65: [f32; 9] = [ + 0.9555766, -0.0230393, 0.0631636, + -0.0282895, 1.0099416, 0.0210077, + 0.0122982, -0.0204830, 1.3299098, +]; + +#[inline] +pub fn srgb_gamma(mut float: f32) -> f32 { + if float <= 0.0031308 { + float *= 12.92; + } else { + float = float.powf(1.0 / 2.4) * 1.055 - 0.055; + } + + float.clamp(0.0, 1.0) +} + +fn bayer(img: &RawImage, width: usize, height: usize) -> Vec { + match img.data { + RawData::Packed10bpp { data } => { + let unpacked = img.unpack().unwrap(); + + // I've only seen it on one color defintion or + // something, but there's a black level of 42, so subtract it. + // without it the image is entirely too red. + //ten_data.iter_mut().for_each(|p| *p = p.saturating_sub(42)); + + unpacked + .into_iter() + .map(|p| ((p.saturating_sub(42)) >> 2) as u8) + .collect() + } + RawData::BayerJpeg { + header: _, + format, + jpeg0, + jpeg1, + jpeg2, + jpeg3, + } => { + let mut bayered = vec![0; width * height]; + + match format { + 0 => { + let mut into = vec![0; (width * height) / 4]; + + let mut channel = |jpeg: &[u8], offset: usize| { + zune_jpeg::JpegDecoder::new(jpeg) + .decode_into(&mut into) + .unwrap(); + + for idx in 0..into.len() { + let ww = width / 2; + let in_x = idx % ww; + let in_y = idx / ww; + + let bayer_x = (in_x * 2) + (offset % 2); + let bayer_y = (in_y * 2) + (offset / 2); + + let bayer_idx = bayer_y * width + bayer_x; + bayered[bayer_idx] = into[idx]; + } + }; + + //BGGR + //RGGB + //GRBG + channel(jpeg0, 0); + channel(jpeg1, 1); + channel(jpeg2, 2); + channel(jpeg3, 3); + } + 1 => { + zune_jpeg::JpegDecoder::new(jpeg0) + .decode_into(&mut bayered) + .unwrap(); + } + _ => unreachable!(), + } + + bayered + } + } +} + +fn write_png>( + path: P, + width: usize, + height: usize, + data: &[u8], + color_format: png::ColorType, +) { + //return; + use std::fs::File; + + let file = File::create(path).unwrap(); + let mut enc = png::Encoder::new(file, width as u32, height as u32); + enc.set_color(color_format); + enc.set_depth(png::BitDepth::Eight); + let mut writer = enc.write_header().unwrap(); + writer.write_image_data(data).unwrap(); +} diff --git a/prism/src/unpack.rs b/prism/src/unpack.rs deleted file mode 100644 index bc761f3..0000000 --- a/prism/src/unpack.rs +++ /dev/null @@ -1,61 +0,0 @@ -const TEN_MASK: u64 = 1023; // ten bits - -pub fn tenbit(packd: &[u8], count: usize, upack: &mut [u16]) { - let required_len_packd = (count as f32 * (10.0 / 8.0)).ceil() as usize; - - if count > upack.len() { - panic!( - "expected output buffer to be {count} bytes, got {} bytes", - upack.len() - ) - } - - if required_len_packd > packd.len() { - panic!( - "expected input to be at least {required_len_packd} bytes, it was {}", - packd.len() - ) - } - - let mut packd = packd[..required_len_packd].to_vec(); - packd.reverse(); - let chunker = packd.chunks_exact(5); - let remain = chunker.remainder(); - - for (idx, chnk) in chunker.enumerate() { - let long = u64::from_be_bytes([ - 0x00, 0x00, 0x00, chnk[0], chnk[1], chnk[2], chnk[3], chnk[4], - ]); - - let b4 = long & TEN_MASK; - let b3 = (long >> 10) & TEN_MASK; - let b2 = (long >> 20) & TEN_MASK; - let b1 = (long >> 30) & TEN_MASK; - - let idx = idx * 4; - upack[idx] = b1 as u16; - upack[idx + 1] = b2 as u16; - upack[idx + 2] = b3 as u16; - upack[idx + 3] = b4 as u16; - } - - if remain.len() > 0 { - let mut long_bytes = [0x00; 8]; - - for (idx, byte) in remain.iter().enumerate() { - long_bytes[idx] = *byte; - } - - let long = u64::from_le_bytes(long_bytes); - - let count_remain = count % 4; - let start = count - count_remain; - for idx in 0..count_remain { - upack[start + idx] = ((long >> (10 * idx)) & TEN_MASK) as u16; - } - } -} - -//pub fn twelvebit(packed: &[u8]) { -// 3 bytes per 2 12-bits -//} -- cgit 1.4.1-3-g733a5