about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--LRI.md4
-rw-r--r--lri-rs/src/block.rs20
-rw-r--r--lri-rs/src/lib.rs217
-rw-r--r--lri-rs/src/types.rs235
-rw-r--r--lri-study/src/main.rs35
-rw-r--r--prism/Cargo.toml1
-rw-r--r--prism/src/main.rs123
8 files changed, 337 insertions, 299 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 32a9ec3..d44e79c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -507,6 +507,7 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
 name = "prism"
 version = "0.1.0"
 dependencies = [
+ "camino",
  "lri-rs",
  "mozjpeg",
  "nalgebra",
diff --git a/LRI.md b/LRI.md
index bb5f324..f4668be 100644
--- a/LRI.md
+++ b/LRI.md
@@ -3,10 +3,8 @@ The file is made up of many blocks, usually 10 or 11 but cases of 40 have occurr
 
 Blocks start with a header and contain some data. There is always a protobuf message within that data, and sometimes stuff like the images themselves.
 
-Little-endian.
-
 ## Block Header
-The header is 32 bytes long. and goes as follows:  
+The header is 32 bytes long, uses little-endian, and goes as follows:  
 | bytes | type | meaning |
 | ----- | -----| ------- |
 | 4     | -    | Signature: "LELR" |
diff --git a/lri-rs/src/block.rs b/lri-rs/src/block.rs
index d973430..e3d94a5 100644
--- a/lri-rs/src/block.rs
+++ b/lri-rs/src/block.rs
@@ -6,7 +6,7 @@ use lri_proto::{
 };
 
 use crate::{
-	fine::Signature, CameraId, CameraInfo, ColorInfo, DataFormat, HdrMode, RawData, RawImage,
+	AwbGain, AwbMode, CameraId, CameraInfo, ColorInfo, DataFormat, HdrMode, RawData, RawImage,
 	SceneMode, SensorModel,
 };
 
@@ -17,15 +17,18 @@ pub(crate) struct Block<'lri> {
 }
 
 impl<'lri> Block<'lri> {
+	/// Get a slice to the entire body of this block
 	pub fn body(&self) -> &[u8] {
 		&self.data[32..]
 	}
 
+	/// Get a slice to this block's messge data
 	pub fn message_data(&self) -> &[u8] {
 		let end = self.header.message_offset + self.header.message_length;
 		&self.data[self.header.message_offset..end]
 	}
 
+	/// Parse the message
 	pub fn message(&self) -> Message {
 		match self.header.kind {
 			BlockType::LightHeader => {
@@ -46,7 +49,6 @@ impl<'lri> Block<'lri> {
 		images: &mut Vec<RawImage<'lri>>,
 		colors: &mut Vec<ColorInfo>,
 		infos: &mut Vec<CameraInfo>,
-		sig: &mut Signature,
 	) {
 		let LightHeader {
 			mut hw_info,
@@ -59,7 +61,6 @@ impl<'lri> Block<'lri> {
 			mut view_preferences,
 			..
 		} = if let Message::LightHeader(lh) = self.message() {
-			sig.merge(&lh);
 			lh
 		} else if let Message::ViewPreferences(vp) = self.message() {
 			self.extract_view(vp, ext);
@@ -228,6 +229,8 @@ impl<'lri> Block<'lri> {
 			hdr_mode,
 			scene_mode,
 			is_on_tripod,
+			awb_mode,
+			awb_gains,
 			..
 		} = vp;
 
@@ -250,6 +253,14 @@ impl<'lri> Block<'lri> {
 		if let Some(tri) = is_on_tripod {
 			ext.on_tripod = Some(tri);
 		}
+
+		if let Some(Ok(awbmode)) = awb_mode.map(|ev| ev.enum_value()) {
+			ext.awb = Some(awbmode.into());
+		}
+
+		if let Some(gain) = awb_gains.into_option() {
+			ext.awb_gain = Some(gain.into());
+		}
 	}
 }
 
@@ -265,6 +276,9 @@ pub(crate) struct ExtractedData {
 	pub hdr: Option<HdrMode>,
 	pub scene: Option<SceneMode>,
 	pub on_tripod: Option<bool>,
+
+	pub awb: Option<AwbMode>,
+	pub awb_gain: Option<AwbGain>,
 }
 
 pub enum Message {
diff --git a/lri-rs/src/lib.rs b/lri-rs/src/lib.rs
index c136669..342248a 100644
--- a/lri-rs/src/lib.rs
+++ b/lri-rs/src/lib.rs
@@ -1,15 +1,11 @@
-use std::{fmt, time::Duration};
+use std::time::Duration;
 
 use block::{Block, ExtractedData, Header};
-use fine::Signature;
-use lri_proto::{
-	camera_id::CameraID as PbCameraID, camera_module::camera_module::surface::FormatType,
-	color_calibration::color_calibration::IlluminantType,
-	view_preferences::view_preferences::HDRMode,
-};
 
 mod block;
-mod fine;
+mod types;
+
+pub use types::*;
 
 pub struct LriFile<'lri> {
 	pub image_reference_camera: Option<CameraId>,
@@ -25,7 +21,8 @@ pub struct LriFile<'lri> {
 	pub hdr: Option<HdrMode>,
 	pub scene: Option<SceneMode>,
 	pub on_tripod: Option<bool>,
-	pub sig: Signature,
+	pub awb: Option<AwbMode>,
+	pub awb_gain: Option<AwbGain>,
 }
 
 impl<'lri> LriFile<'lri> {
@@ -36,11 +33,10 @@ impl<'lri> LriFile<'lri> {
 		let mut camera_infos = vec![];
 
 		let mut ext = ExtractedData::default();
-		let mut sig = Signature::new();
 
 		// Read data blocks and extract informtion we care about
 		loop {
-			if data.len() == 0 {
+			if data.is_empty() {
 				break;
 			}
 
@@ -55,13 +51,7 @@ impl<'lri> LriFile<'lri> {
 				data: block_data,
 			};
 
-			block.extract_meaningful_data(
-				&mut ext,
-				&mut images,
-				&mut colors,
-				&mut camera_infos,
-				&mut sig,
-			);
+			block.extract_meaningful_data(&mut ext, &mut images, &mut colors, &mut camera_infos);
 		}
 
 		// Further fill in the RawImage's we extracted
@@ -93,7 +83,8 @@ impl<'lri> LriFile<'lri> {
 			hdr: ext.hdr,
 			scene: ext.scene,
 			on_tripod: ext.on_tripod,
-			sig,
+			awb: ext.awb,
+			awb_gain: ext.awb_gain,
 		}
 	}
 
@@ -232,191 +223,3 @@ pub struct CameraInfo {
 	camera: CameraId,
 	sensor: SensorModel,
 }
-
-#[derive(Copy, Clone, Debug, PartialEq)]
-/// The representation of the raw data in the LRI file
-pub enum DataFormat {
-	// I'm not sure what this is?? Do we ever see it???
-	BayerJpeg,
-	Packed10bpp,
-	// Never seen
-	//Packed12bpp,
-	//Packed14bpp,
-}
-
-impl fmt::Display for DataFormat {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		let str = match self {
-			Self::BayerJpeg => "BayerJpeg",
-			Self::Packed10bpp => "Packed10bpp",
-			//Self::Packed12bpp => "Packed12bpp",
-			//Self::Packed14bpp => "Packed14bpp",
-		};
-
-		write!(f, "{str}")
-	}
-}
-
-impl From<FormatType> for DataFormat {
-	fn from(proto: FormatType) -> Self {
-		match proto {
-			FormatType::RAW_BAYER_JPEG => Self::BayerJpeg,
-			FormatType::RAW_PACKED_10BPP => Self::Packed10bpp,
-			FormatType::RAW_PACKED_12BPP => unreachable!(),
-			FormatType::RAW_PACKED_14BPP => unreachable!(),
-			FormatType::RAW_RESERVED_0
-			| FormatType::RAW_RESERVED_1
-			| FormatType::RAW_RESERVED_2
-			| FormatType::RAW_RESERVED_3
-			| FormatType::RAW_RESERVED_4
-			| FormatType::RAW_RESERVED_5 => unimplemented!(),
-		}
-	}
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
-pub enum CameraId {
-	A1,
-	A2,
-	A3,
-	A4,
-	A5,
-	B1,
-	B2,
-	B3,
-	B4,
-	B5,
-	C1,
-	C2,
-	C3,
-	C4,
-	C5,
-	C6,
-}
-
-impl From<PbCameraID> for CameraId {
-	fn from(pbid: PbCameraID) -> Self {
-		match pbid {
-			PbCameraID::A1 => Self::A1,
-			PbCameraID::A2 => Self::A2,
-			PbCameraID::A3 => Self::A3,
-			PbCameraID::A4 => Self::A4,
-			PbCameraID::A5 => Self::A5,
-			PbCameraID::B1 => Self::B1,
-			PbCameraID::B2 => Self::B2,
-			PbCameraID::B3 => Self::B3,
-			PbCameraID::B4 => Self::B4,
-			PbCameraID::B5 => Self::B5,
-			PbCameraID::C1 => Self::C1,
-			PbCameraID::C2 => Self::C2,
-			PbCameraID::C3 => Self::C3,
-			PbCameraID::C4 => Self::C4,
-			PbCameraID::C5 => Self::C5,
-			PbCameraID::C6 => Self::C6,
-		}
-	}
-}
-
-impl fmt::Display for CameraId {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		// this is good; i write good code
-		write!(f, "{self:?}")
-	}
-}
-
-#[derive(Copy, Clone, Debug, PartialEq)]
-pub enum Whitepoint {
-	A,
-	D50,
-	D65,
-	D75,
-	F2,
-	F7,
-	F11,
-	TL84,
-}
-
-impl From<IlluminantType> for Whitepoint {
-	fn from(it: IlluminantType) -> Self {
-		match it {
-			IlluminantType::A => Self::A,
-			IlluminantType::D50 => Self::D50,
-			IlluminantType::D65 => Self::D65,
-			IlluminantType::D75 => Self::D75,
-			IlluminantType::F2 => Self::F2,
-			IlluminantType::F7 => Self::F7,
-			IlluminantType::F11 => Self::F11,
-			IlluminantType::TL84 => Self::TL84,
-			IlluminantType::UNKNOWN => unimplemented!(),
-		}
-	}
-}
-
-#[derive(Copy, Clone, Debug)]
-pub enum SensorModel {
-	Unknown,
-	Ar835,
-	Ar1335,
-	Ar1335Mono,
-	Imx386,
-	Imx386Mono,
-}
-
-impl From<lri_proto::sensor_type::SensorType> for SensorModel {
-	fn from(pbst: lri_proto::sensor_type::SensorType) -> Self {
-		use lri_proto::sensor_type::SensorType as ProtoSt;
-
-		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,
-		}
-	}
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub enum HdrMode {
-	None,
-	Default,
-	Natural,
-	Surreal,
-}
-
-impl From<HDRMode> for HdrMode {
-	fn from(h: HDRMode) -> Self {
-		match h {
-			HDRMode::HDR_MODE_NONE => Self::None,
-			HDRMode::HDR_MODE_DEFAULT => Self::Default,
-			HDRMode::HDR_MODE_NATURAL => Self::Natural,
-			HDRMode::HDR_MODE_SURREAL => Self::Surreal,
-		}
-	}
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub enum SceneMode {
-	Portrait,
-	Landscape,
-	Sport,
-	Macro,
-	Night,
-	None,
-}
-
-impl From<lri_proto::view_preferences::view_preferences::SceneMode> for SceneMode {
-	fn from(sm: lri_proto::view_preferences::view_preferences::SceneMode) -> Self {
-		use lri_proto::view_preferences::view_preferences::SceneMode as PbSceneMode;
-
-		match sm {
-			PbSceneMode::SCENE_MODE_PORTRAIT => Self::Portrait,
-			PbSceneMode::SCENE_MODE_LANDSCAPE => Self::Landscape,
-			PbSceneMode::SCENE_MODE_SPORT => Self::Sport,
-			PbSceneMode::SCENE_MODE_MACRO => Self::Macro,
-			PbSceneMode::SCENE_MODE_NIGHT => Self::Night,
-			PbSceneMode::SCENE_MODE_NONE => Self::None,
-		}
-	}
-}
diff --git a/lri-rs/src/types.rs b/lri-rs/src/types.rs
new file mode 100644
index 0000000..8b759ac
--- /dev/null
+++ b/lri-rs/src/types.rs
@@ -0,0 +1,235 @@
+/// Responsible for mapping generated protobuf enums to enums defined here. It
+/// seemed like a bad idea to rexport from lri-proto.
+use std::fmt;
+
+use lri_proto::{
+	camera_id::CameraID as PbCameraID, camera_module::camera_module::surface::FormatType,
+	color_calibration::color_calibration::IlluminantType,
+	view_preferences::view_preferences::HDRMode,
+};
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+/// The representation of the raw data in the LRI file
+pub enum DataFormat {
+	BayerJpeg,
+	Packed10bpp,
+	// Never seen
+	//Packed12bpp,
+	//Packed14bpp,
+}
+
+impl fmt::Display for DataFormat {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		let str = match self {
+			Self::BayerJpeg => "BayerJpeg",
+			Self::Packed10bpp => "Packed10bpp",
+			//Self::Packed12bpp => "Packed12bpp",
+			//Self::Packed14bpp => "Packed14bpp",
+		};
+
+		write!(f, "{str}")
+	}
+}
+
+impl From<FormatType> for DataFormat {
+	fn from(proto: FormatType) -> Self {
+		match proto {
+			FormatType::RAW_BAYER_JPEG => Self::BayerJpeg,
+			FormatType::RAW_PACKED_10BPP => Self::Packed10bpp,
+			FormatType::RAW_PACKED_12BPP => unreachable!(),
+			FormatType::RAW_PACKED_14BPP => unreachable!(),
+			FormatType::RAW_RESERVED_0
+			| FormatType::RAW_RESERVED_1
+			| FormatType::RAW_RESERVED_2
+			| FormatType::RAW_RESERVED_3
+			| FormatType::RAW_RESERVED_4
+			| FormatType::RAW_RESERVED_5 => unimplemented!(),
+		}
+	}
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub enum CameraId {
+	A1,
+	A2,
+	A3,
+	A4,
+	A5,
+	B1,
+	B2,
+	B3,
+	B4,
+	B5,
+	C1,
+	C2,
+	C3,
+	C4,
+	C5,
+	C6,
+}
+
+impl From<PbCameraID> for CameraId {
+	fn from(pbid: PbCameraID) -> Self {
+		match pbid {
+			PbCameraID::A1 => Self::A1,
+			PbCameraID::A2 => Self::A2,
+			PbCameraID::A3 => Self::A3,
+			PbCameraID::A4 => Self::A4,
+			PbCameraID::A5 => Self::A5,
+			PbCameraID::B1 => Self::B1,
+			PbCameraID::B2 => Self::B2,
+			PbCameraID::B3 => Self::B3,
+			PbCameraID::B4 => Self::B4,
+			PbCameraID::B5 => Self::B5,
+			PbCameraID::C1 => Self::C1,
+			PbCameraID::C2 => Self::C2,
+			PbCameraID::C3 => Self::C3,
+			PbCameraID::C4 => Self::C4,
+			PbCameraID::C5 => Self::C5,
+			PbCameraID::C6 => Self::C6,
+		}
+	}
+}
+
+impl fmt::Display for CameraId {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		// this is good; i write good code
+		write!(f, "{self:?}")
+	}
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub enum Whitepoint {
+	A,
+	D50,
+	D65,
+	D75,
+	F2,
+	F7,
+	F11,
+	TL84,
+}
+
+impl From<IlluminantType> for Whitepoint {
+	fn from(it: IlluminantType) -> Self {
+		match it {
+			IlluminantType::A => Self::A,
+			IlluminantType::D50 => Self::D50,
+			IlluminantType::D65 => Self::D65,
+			IlluminantType::D75 => Self::D75,
+			IlluminantType::F2 => Self::F2,
+			IlluminantType::F7 => Self::F7,
+			IlluminantType::F11 => Self::F11,
+			IlluminantType::TL84 => Self::TL84,
+			IlluminantType::UNKNOWN => unimplemented!(),
+		}
+	}
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum SensorModel {
+	Unknown,
+	Ar835,
+	Ar1335,
+	Ar1335Mono,
+	Imx386,
+	Imx386Mono,
+}
+
+impl From<lri_proto::sensor_type::SensorType> for SensorModel {
+	fn from(pbst: lri_proto::sensor_type::SensorType) -> Self {
+		use lri_proto::sensor_type::SensorType as ProtoSt;
+
+		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,
+		}
+	}
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum HdrMode {
+	None,
+	Default,
+	Natural,
+	Surreal,
+}
+
+impl From<HDRMode> for HdrMode {
+	fn from(h: HDRMode) -> Self {
+		match h {
+			HDRMode::HDR_MODE_NONE => Self::None,
+			HDRMode::HDR_MODE_DEFAULT => Self::Default,
+			HDRMode::HDR_MODE_NATURAL => Self::Natural,
+			HDRMode::HDR_MODE_SURREAL => Self::Surreal,
+		}
+	}
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum SceneMode {
+	Portrait,
+	Landscape,
+	Sport,
+	Macro,
+	Night,
+	None,
+}
+
+impl From<lri_proto::view_preferences::view_preferences::SceneMode> for SceneMode {
+	fn from(sm: lri_proto::view_preferences::view_preferences::SceneMode) -> Self {
+		use lri_proto::view_preferences::view_preferences::SceneMode as PbSceneMode;
+
+		match sm {
+			PbSceneMode::SCENE_MODE_PORTRAIT => Self::Portrait,
+			PbSceneMode::SCENE_MODE_LANDSCAPE => Self::Landscape,
+			PbSceneMode::SCENE_MODE_SPORT => Self::Sport,
+			PbSceneMode::SCENE_MODE_MACRO => Self::Macro,
+			PbSceneMode::SCENE_MODE_NIGHT => Self::Night,
+			PbSceneMode::SCENE_MODE_NONE => Self::None,
+		}
+	}
+}
+
+/// Auto White Balance Mode
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum AwbMode {
+	Auto,
+	Daylight,
+}
+
+impl From<lri_proto::view_preferences::view_preferences::AWBMode> for AwbMode {
+	fn from(awb: lri_proto::view_preferences::view_preferences::AWBMode) -> Self {
+		use lri_proto::view_preferences::view_preferences::AWBMode as PbAwbMode;
+
+		match awb {
+			PbAwbMode::AWB_MODE_AUTO => Self::Auto,
+			PbAwbMode::AWB_MODE_DAYLIGHT => Self::Daylight,
+			_ => panic!("{awb:?}"),
+		}
+	}
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub struct AwbGain {
+	pub r: f32,
+	pub gr: f32,
+	pub gb: f32,
+	pub b: f32,
+}
+
+impl From<lri_proto::view_preferences::view_preferences::ChannelGain> for AwbGain {
+	fn from(gain: lri_proto::view_preferences::view_preferences::ChannelGain) -> Self {
+		// all fields in ChannelGain are marked as required
+		Self {
+			r: gain.r.unwrap(),
+			gr: gain.g_r.unwrap(),
+			gb: gain.g_b.unwrap(),
+			b: gain.b.unwrap(),
+		}
+	}
+}
diff --git a/lri-study/src/main.rs b/lri-study/src/main.rs
index 962fc26..2b4c7cd 100644
--- a/lri-study/src/main.rs
+++ b/lri-study/src/main.rs
@@ -4,10 +4,11 @@ use std::{
 };
 
 use camino::Utf8PathBuf;
-use lri_rs::{DataFormat, HdrMode, LriFile, SceneMode, SensorModel};
+use lri_rs::{AwbMode, DataFormat, HdrMode, LriFile, SceneMode, SensorModel};
 use owo_colors::OwoColorize;
 
 fn main() {
+	#[allow(clippy::single_match)]
 	match std::env::args().nth(1).as_deref() {
 		Some("gather") => gather(),
 		_ => (),
@@ -31,15 +32,15 @@ fn gather() -> ! {
 				Some("jpg") => files
 					.entry(stub.clone())
 					.and_modify(|e| e.jpg = Some(path.to_owned()))
-					.or_insert(Photo::new_jpg(&path)),
+					.or_insert(Photo::new_jpg(path)),
 				Some("lri") => files
 					.entry(stub.clone())
 					.and_modify(|e| e.lri = Some(path.to_owned()))
-					.or_insert(Photo::new_lri(&path)),
+					.or_insert(Photo::new_lri(path)),
 				Some("lris") => files
 					.entry(stub.clone())
 					.and_modify(|e| e.lris = Some(path.to_owned()))
-					.or_insert(Photo::new_lris(&path)),
+					.or_insert(Photo::new_lris(path)),
 				None | Some(_) => continue,
 			};
 		}
@@ -50,7 +51,7 @@ fn gather() -> ! {
 	let mut photos: Vec<Photo> = files.into_values().collect();
 	photos.sort_by(|a, b| a.lri.as_deref().unwrap().cmp(b.lri.as_deref().unwrap()));
 
-	for (idx, photo) in photos.into_iter().enumerate() {
+	for (_idx, photo) in photos.into_iter().enumerate() {
 		let lri_path = match photo.lri {
 			Some(p) => p,
 			None => continue,
@@ -66,10 +67,6 @@ fn gather() -> ! {
 
 		print!("{} - ", lri_path.file_stem().unwrap());
 
-		let path = format!("{}_{idx}", lri_path.file_stem().unwrap_or_default());
-		let dbg = format!("{:#?}", lri.sig);
-		std::fs::write(path, dbg.as_bytes()).unwrap();
-
 		if let Some(fwv) = lri.firmware_version.as_ref() {
 			print!(
 				"[{}] focal:{:<3} iit:{:>2}ms gain:{:2.0} ",
@@ -110,6 +107,24 @@ fn gather() -> ! {
 				Some(false) => print!("{} - ", "af".red()),
 				Some(true) => print!("{} - ", "af".green()),
 			}
+
+			match lri.awb {
+				None => print!("{}:", "awb".dimmed()),
+				Some(AwbMode::Auto) => print!("{}:", "awb".white()),
+				Some(AwbMode::Daylight) => print!("{}:", "awb".yellow()),
+			}
+
+			match lri.awb_gain {
+				None => print!("{} - ", "gain".dimmed()),
+				Some(gain) => print!(
+					"{} - [{:.2},{:.2},{:.2},{:.2}] ",
+					"gain".white(),
+					gain.r,
+					gain.gr,
+					gain.gr,
+					gain.b
+				),
+			}
 		}
 
 		for img in lri.images() {
@@ -127,7 +142,7 @@ fn gather() -> ! {
 				DataFormat::Packed10bpp => print!("{} ", sens.yellow()),
 			}
 		}
-		println!("");
+		println!();
 	}
 
 	println!("        ---\nTook {:.2}s", start.elapsed().as_secs_f32());
diff --git a/prism/Cargo.toml b/prism/Cargo.toml
index d296b79..6861a34 100644
--- a/prism/Cargo.toml
+++ b/prism/Cargo.toml
@@ -13,3 +13,4 @@ rawloader = "0.37.1"
 nalgebra = "0.31.4"
 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 291d725..43278a6 100644
--- a/prism/src/main.rs
+++ b/prism/src/main.rs
@@ -1,6 +1,7 @@
 use std::collections::HashMap;
 
-use lri_rs::{CameraId, DataFormat, LriFile, RawData, RawImage, SensorModel, Whitepoint};
+use camino::Utf8PathBuf;
+use lri_rs::{AwbGain, CameraId, LriFile, RawData, RawImage, SensorModel, Whitepoint};
 use nalgebra::{Matrix3, Matrix3x1};
 
 mod rotate;
@@ -12,14 +13,29 @@ pub struct Entry {
 }
 
 fn main() {
+	let args = std::env::args().skip(1);
+
+	if args.len() != 2 {
+		eprintln!("Usage: prism <lri_file> <output_directory>");
+		std::process::exit(1);
+	}
+
 	let file_name = std::env::args().nth(1).unwrap();
+	let directory = Utf8PathBuf::from(std::env::args().nth(2).unwrap());
+
+	if !directory.exists() {
+		std::fs::create_dir_all(&directory).unwrap();
+	}
+
 	let bytes = std::fs::read(file_name).unwrap();
 	let lri = LriFile::decode(&bytes);
+	let gain = lri.awb_gain.unwrap();
 
 	println!("{} images", lri.image_count());
 
-	lri.reference_image()
-		.map(|raw| make(raw, String::from("reference.png")));
+	if let Some(refimg) = lri.reference_image() {
+		make(refimg, directory.join("reference.png"), gain);
+	}
 
 	let mut set: HashMap<CameraId, Entry> = HashMap::new();
 
@@ -32,36 +48,16 @@ fn main() {
 			});
 	}
 
-	set.into_iter().for_each(|kv| {
+	/*set.into_iter().for_each(|kv| {
 		println!("{} {:?} {}", kv.0, kv.1.sensor, kv.1.count);
-	});
+	});*/
 
 	for (idx, img) in lri.images().enumerate() {
-		/*for color in &img.color {
-			println!(
-				"{:?} rg = {}  bg = {}",
-				color.whitepoint, color.rg, color.bg
-			);
-
-			let white =
-				Matrix3::from_row_slice(&color.forward_matrix) * Matrix3x1::new(1.0, 1.0, 1.0);
-
-			let white_x = white[0] / (white[0] + white[1] + white[2]);
-			let white_y = white[1] / (white[0] + white[1] + white[2]);
-			let white_z = 1.0 - white_x - white_y;
-
-			println!("\twhite: x = {} y = {} z = {}", white_x, white_y, white_z);
-
-			println!("\t{:?}", color.forward_matrix);
-		}*/
-		//std::process::exit(0);
-
-		make(img, format!("image_{idx}.png"));
-		//return;
+		make(img, directory.join(format!("image_{idx}.png")), gain);
 	}
 }
 
-fn make(img: &RawImage, path: String) {
+fn make(img: &RawImage, path: Utf8PathBuf, awb_gain: AwbGain) {
 	use rawproc::image::RawMetadata;
 	use rawproc::{colorspace::BayerRgb, image::Image};
 
@@ -81,14 +77,7 @@ fn make(img: &RawImage, path: String) {
 		sbro.0, sbro.1
 	);
 
-	let mut bayered = bayer(
-		data,
-		*width,
-		*height,
-		format!("{}_bjpg", &path[..path.len() - 4]),
-	);
-
-	//bayered.iter_mut().for_each(|p| *p = p.saturating_sub(42));
+	let bayered = bayer(data, *width, *height);
 
 	let (mut rgb, color_format) = match img.cfa_string() {
 		Some(cfa_string) => {
@@ -114,56 +103,35 @@ fn make(img: &RawImage, path: String) {
 	};
 
 	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.len() > 0 {
-		print!("\t");
+	if !color.is_empty() {
+		print!("\tAvailable whitepoints: ");
 		color.iter().for_each(|c| print!("{:?} ", c.whitepoint));
 		println!();
 	}
 
-	match img.color_info(Whitepoint::F11) {
+	match img.color_info(Whitepoint::D65) {
 		Some(c) => {
-			//println!("\tApplying color profile: {:?}", c.color_matrix);
+			println!("\tUsing D65");
 			let to_xyz = Matrix3::from_row_slice(&c.forward_matrix);
-			let to_srgb = Matrix3::from_row_slice(&BRUCE_XYZ_RGB_D65);
-			let color = Matrix3::from_row_slice(&c.color_matrix);
-			let d50_d65 = Matrix3::from_row_slice(&BRADFORD_D50_D65);
-
-			let xyz_d65 = to_xyz * d50_d65;
-
-			//println!("{color}");
-
-			let white = xyz_d65 * Matrix3x1::new(1.0, 1.0, 1.0);
-
-			let white_x = white[0] / (white[0] + white[1] + white[2]);
-			let white_y = white[1] / (white[0] + white[1] + white[2]);
-			let white_z = 1.0 - white_x - white_y;
-
-			/*println!(
-				"\t{:?} ||| white: x = {} y = {} z = {}",
-				c.whitepoint, white_x, white_y, white_z
-			);*/
+			// 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 prenorm = premul.normalize();
-			//println!("{prenorm}");
-
 			for chnk in floats.chunks_mut(3) {
-				let r = chnk[0] * (1.0 / c.rg);
+				/*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] * (1.0 / c.bg);
+				let b = chnk[2] * awb_gain.b;
 
 				let px = Matrix3x1::new(r, g, b);
-
-				//let rgb = premul * px;
-				//let px = color * px;
-				let xyz = to_xyz * px;
-				//let xyz = d50_d65 * xyz;
-				//let xyz_white = color * xyz;
-				let rgb = to_srgb * xyz;
+				let rgb = premul * px;
 
 				chnk[0] = srgb_gamma(rgb[0]) * 255.0;
 				chnk[1] = srgb_gamma(rgb[1]) * 255.0;
@@ -171,14 +139,14 @@ fn make(img: &RawImage, path: String) {
 			}
 		}
 		None => {
-			println!("\tno color profile found");
+			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!("Writing {}", &path);
+	println!("\tWriting {}", &path);
 	make_png(path, *width, *height, &bytes, color_format)
 }
 
@@ -215,19 +183,22 @@ pub fn srgb_gamma(mut float: f32) -> f32 {
 	float.clamp(0.0, 1.0)
 }
 
-fn bayer(data: &RawData<'_>, width: usize, height: usize, path: String) -> Vec<u8> {
+fn bayer(data: &RawData<'_>, width: usize, height: usize) -> Vec<u8> {
 	match data {
 		RawData::Packed10bpp { data } => {
-			// Assume 10-bit
 			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
+			// 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 >> 2) as u8).collect()
+			ten_data
+				.into_iter()
+				.map(|p| ((p.saturating_sub(42)) >> 2) as u8)
+				.collect()
 		}
 		RawData::BayerJpeg {
 			header: _,