about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2024-03-09 21:50:32 -0600
committergennyble <gen@nyble.dev>2024-03-09 21:50:32 -0600
commit4b37607028967fd178745d0986f88b83f0d65e2f (patch)
tree3a411673a11e2326d19b602b37b44d73ca314423
parente9531eefae80caeb0b32b8e0acd568c14f65b384 (diff)
downloadlri-rs-4b37607028967fd178745d0986f88b83f0d65e2f.tar.gz
lri-rs-4b37607028967fd178745d0986f88b83f0d65e2f.zip
dirty repo; this was here; wip
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock65
-rw-r--r--README.md4
-rw-r--r--lri-rs/src/block.rs19
-rw-r--r--lri-rs/src/fine.rs144
-rw-r--r--lri-rs/src/lib.rs31
-rw-r--r--lri-rs/src/types.rs51
-rw-r--r--lri-rs/src/unpack.rs (renamed from prism/src/unpack.rs)6
-rw-r--r--lri-study/src/main.rs3
-rw-r--r--prism/Cargo.toml2
-rw-r--r--prism/src/main.rs220
-rw-r--r--prism/src/output.rs240
12 files changed, 330 insertions, 456 deletions
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
@@ -33,12 +33,6 @@ dependencies = [
 ]
 
 [[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"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -86,7 +80,6 @@ version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
 dependencies = [
- "jobserver",
  "libc",
 ]
 
@@ -149,12 +142,6 @@ dependencies = [
 ]
 
 [[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"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -266,15 +253,6 @@ dependencies = [
 ]
 
 [[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"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -358,30 +336,6 @@ dependencies = [
 ]
 
 [[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"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -409,15 +363,6 @@ dependencies = [
 ]
 
 [[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"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -509,7 +454,6 @@ version = "0.1.0"
 dependencies = [
  "camino",
  "lri-rs",
- "mozjpeg",
  "nalgebra",
  "png",
  "rawloader",
@@ -711,15 +655,6 @@ 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"
 source = "registry+https://github.com/rust-lang/crates.io-index"
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 <lri> <output_directory>`
@@ -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<AwbMode>,
 	pub awb_gain: Option<AwbGain>,
+
+	pub sensor_data: Vec<SensorData>,
 }
 
 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<String>>,
-	pub vp: HashMap<&'static str, Vec<String>>,
-}
-
-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<Vec<u16>> {
+		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<IlluminantType> 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<lri_proto::sensor_type::SensorType> for SensorModel {
@@ -142,11 +141,11 @@ impl From<lri_proto::sensor_type::SensorType> 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<lri_proto::view_preferences::view_preferences::ChannelGain> for AwbGai
 		}
 	}
 }
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub struct SensorData {
+	pub sensor_type: SensorModel,
+	pub characterization: SensorCharacterization,
+}
+
+impl From<lri_proto::lightheader::SensorData> 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<f32>,
+}
+
+impl From<lri_proto::sensor_characterization::SensorCharacterization> 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/prism/src/unpack.rs b/lri-rs/src/unpack.rs
index bc761f3..b14b79c 100644
--- a/prism/src/unpack.rs
+++ b/lri-rs/src/unpack.rs
@@ -39,7 +39,7 @@ pub fn tenbit(packd: &[u8], count: usize, upack: &mut [u16]) {
 		upack[idx + 3] = b4 as u16;
 	}
 
-	if remain.len() > 0 {
+	if !remain.is_empty() {
 		let mut long_bytes = [0x00; 8];
 
 		for (idx, byte) in remain.iter().enumerate() {
@@ -55,7 +55,3 @@ pub fn tenbit(packd: &[u8], count: usize, upack: &mut [u16]) {
 		}
 	}
 }
-
-//pub fn twelvebit(packed: &[u8]) {
-// 3 bytes per 2 12-bits
-//}
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<CameraId, Entry> = 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<u8, BayerRgb> = 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<f32> = 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<u8> = 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<u8> {
-	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<P: AsRef<std::path::Path>>(
-	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<u16, BayerRgb> = 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<f32> = 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<u8> = 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<u8> {
+	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<P: AsRef<std::path::Path>>(
+	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();
+}