about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--Cargo.lock106
-rw-r--r--Cargo.toml2
-rw-r--r--bayer_jpeg.md29
-rw-r--r--lri-rs/src/block.rs106
-rw-r--r--lri-rs/src/lib.rs68
-rw-r--r--lri-study/Cargo.toml11
-rw-r--r--lri-study/src/main.rs139
-rw-r--r--prism/src/main.rs215
9 files changed, 542 insertions, 138 deletions
diff --git a/.gitignore b/.gitignore
index 0036efe..4064ec8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
 /target
 *.png
-lri-study
\ No newline at end of file
+*.jpg
+*.bjp
+**/.DS_Store
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index c91bb1d..32a9ec3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -33,6 +33,12 @@ 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"
@@ -69,11 +75,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
 
 [[package]]
+name = "camino"
+version = "1.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c"
+
+[[package]]
 name = "cc"
 version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
 dependencies = [
+ "jobserver",
  "libc",
 ]
 
@@ -136,6 +149,12 @@ 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"
@@ -247,6 +266,15 @@ 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"
@@ -286,6 +314,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "lri-study"
+version = "0.1.0"
+dependencies = [
+ "camino",
+ "lri-rs",
+ "owo-colors",
+]
+
+[[package]]
 name = "matrixmultiply"
 version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -321,6 +358,30 @@ 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"
@@ -348,6 +409,15 @@ 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"
@@ -403,6 +473,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
 
 [[package]]
+name = "owo-colors"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
+
+[[package]]
 name = "paste"
 version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -432,10 +508,12 @@ name = "prism"
 version = "0.1.0"
 dependencies = [
  "lri-rs",
+ "mozjpeg",
  "nalgebra",
  "png",
  "rawloader",
  "rawproc",
+ "zune-jpeg",
 ]
 
 [[package]]
@@ -632,6 +710,15 @@ 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"
@@ -882,3 +969,22 @@ name = "windows_x86_64_msvc"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "zune-core"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ca36c2e02af0d8d7ee977542bfe33ed1c516be73d3c1faa4420af46e96ceee"
+dependencies = [
+ "bitflags 2.4.0",
+]
+
+[[package]]
+name = "zune-jpeg"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2848e8f4f29dbdcc79910ab3abdff22bb0bacef8556f2a983b5ca950d8b4991e"
+dependencies = [
+ "log",
+ "zune-core",
+]
diff --git a/Cargo.toml b/Cargo.toml
index a33de68..49e3aa4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,3 @@
 [workspace]
-members = ["lri-proto", "lri-rs", "prism"]
+members = ["lri-proto", "lri-rs", "prism", "lri-study"]
 resolver = "2"
diff --git a/bayer_jpeg.md b/bayer_jpeg.md
new file mode 100644
index 0000000..94f7a6b
--- /dev/null
+++ b/bayer_jpeg.md
@@ -0,0 +1,29 @@
+# BayerJPEG
+Th BayerJPEG is a strange format used by the Light L16... *sometimes*. We don't yet know when it switches from it's normal packed 10-bit raw format.
+
+| size    | type   | meaning |
+| ------- | ------ | ------- |
+| 4 bytes | String | Magic Number "BJPG" |
+| 4 bytes | u32    | *Format type* <br/> 0: colour <br/> 1: for monochrome |
+| 4 bytes | u32    | Length of Jpeg 0 |
+| 4 bytes | u32    | Length of Jpeg 1 |
+| 4 bytes | u32    | Length of Jpeg 2 |
+| 4 bytes | u32    | Length of Jpeg 3 |
+| 1552 bytes | | unknown |
+
+***Monochrome***  
+Jpeg0 contains a full resolution grayscale image
+
+***Colour***  
+The bayered image is split across the four Jpeg, one
+for each colour location.
+
+I.E. an image from the ar1335 sensor, color filter bggr, you'd get
+- 1 jpeg for the blue channel
+- 2 jpeg for each green location
+- 1 jpeg for the red channel
+
+It's not currently known if these are in the order you'd expect.
+
+***Considerations***
+When the L16 decides to use BayerJPEG, it has to save four copies of each frame. A JPEG is limited to a bit depth of eight, but the sensors output 10-bit data. In order to not loose 75% of the precision, they seemingly divide the image into fours and expect you to sum them later.
\ No newline at end of file
diff --git a/lri-rs/src/block.rs b/lri-rs/src/block.rs
index 2dc921b..4ee0ea1 100644
--- a/lri-rs/src/block.rs
+++ b/lri-rs/src/block.rs
@@ -1,9 +1,11 @@
+use std::time::Duration;
+
 use lri_proto::{
 	gps_data::GPSData, lightheader::LightHeader, matrix3x3f::Matrix3x3F,
 	view_preferences::ViewPreferences, Message as PbMessage,
 };
 
-use crate::{CameraId, CameraInfo, ColorInfo, RawImage, SensorModel};
+use crate::{CameraId, CameraInfo, ColorInfo, DataFormat, RawData, RawImage, SensorModel};
 
 pub(crate) struct Block<'lri> {
 	pub header: Header,
@@ -37,24 +39,27 @@ impl<'lri> Block<'lri> {
 
 	pub fn extract_meaningful_data(
 		&self,
+		ext: &mut ExtractedData,
 		images: &mut Vec<RawImage<'lri>>,
 		colors: &mut Vec<ColorInfo>,
 		infos: &mut Vec<CameraInfo>,
-	) -> ExtractedData {
-		let mut ext = ExtractedData {
-			reference_camera: None,
-		};
-
+	) {
 		let LightHeader {
 			mut hw_info,
-			mut module_calibration,
-			mut modules,
-			mut image_reference_camera,
+			module_calibration,
+			modules,
+			image_reference_camera,
+			device_fw_version,
+			image_focal_length,
+			af_info,
 			..
 		} = if let Message::LightHeader(lh) = self.message() {
 			lh
+		} else if let Message::ViewPreferences(vp) = self.message() {
+			self.extract_view(vp, ext);
+			return;
 		} else {
-			return ext;
+			return;
 		};
 
 		// Form the CameraInfo struct for mapping CameraId to SensorType
@@ -117,7 +122,51 @@ impl<'lri> Block<'lri> {
 			let data_length = surface.row_stride() as usize * height;
 
 			let format = surface.format().into();
-			let image_data = &self.data[offset..offset + data_length];
+			let image_data = match format {
+				DataFormat::BayerJpeg => {
+					let bjpg_header_len = 1576;
+					let mut wrk = &self.data[offset..];
+
+					let format = u32::from_le_bytes(wrk[4..8].try_into().unwrap());
+
+					let jpeg0_len = u32::from_le_bytes(wrk[8..12].try_into().unwrap()) as usize;
+					let jpeg1_len = u32::from_le_bytes(wrk[12..16].try_into().unwrap()) as usize;
+					let jpeg2_len = u32::from_le_bytes(wrk[16..20].try_into().unwrap()) as usize;
+					let jpeg3_len = u32::from_le_bytes(wrk[20..24].try_into().unwrap()) as usize;
+
+					let mut get = |len: usize| -> &[u8] {
+						let data = &wrk[..len];
+						wrk = &wrk[len..];
+						data
+					};
+
+					let header = get(bjpg_header_len);
+					let jpeg0 = get(jpeg0_len);
+
+					match format {
+						1 => RawData::BayerJpeg {
+							header,
+							format,
+							jpeg0,
+							jpeg1: &wrk[0..0],
+							jpeg2: &wrk[0..0],
+							jpeg3: &wrk[0..0],
+						},
+						0 => RawData::BayerJpeg {
+							header,
+							format,
+							jpeg0,
+							jpeg1: get(jpeg1_len),
+							jpeg2: get(jpeg2_len),
+							jpeg3: get(jpeg3_len),
+						},
+						_ => unreachable!(),
+					}
+				}
+				DataFormat::Packed10bpp => RawData::Packed10bpp {
+					data: &self.data[offset..offset + data_length],
+				},
+			};
 
 			let sbro = module.sensor_bayer_red_override.clone().unwrap();
 
@@ -139,7 +188,17 @@ impl<'lri> Block<'lri> {
 			ext.reference_camera = Some(irc.into());
 		}
 
-		ext
+		if let Some(afd) = af_info.clone().take() {
+			ext.af_achieved.get_or_insert(afd.focus_achieved());
+		}
+
+		if let Some(fwv) = device_fw_version {
+			ext.fw_version.get_or_insert(fwv);
+		}
+
+		if let Some(x) = image_focal_length {
+			ext.focal_length.get_or_insert(x);
+		}
 	}
 
 	// It kept making my neat little array very, very tall
@@ -151,10 +210,33 @@ impl<'lri> Block<'lri> {
 			mat.x20(), mat.x21(), mat.x22(),
 		]
 	}
+
+	fn extract_view(&self, vp: ViewPreferences, ext: &mut ExtractedData) {
+		let ViewPreferences {
+			image_integration_time_ns,
+			image_gain,
+			..
+		} = vp;
+
+		if let Some(ns) = image_integration_time_ns {
+			ext.image_integration_time = Some(Duration::from_nanos(ns));
+		}
+
+		if let Some(g) = image_gain {
+			ext.image_gain.get_or_insert(g);
+		}
+	}
 }
 
+#[derive(Debug, Default)]
 pub(crate) struct ExtractedData {
 	pub reference_camera: Option<CameraId>,
+	pub fw_version: Option<String>,
+	pub focal_length: Option<i32>,
+
+	pub image_gain: Option<f32>,
+	pub image_integration_time: Option<Duration>,
+	pub af_achieved: Option<bool>,
 }
 
 pub enum Message {
diff --git a/lri-rs/src/lib.rs b/lri-rs/src/lib.rs
index 44b2466..0bc5a11 100644
--- a/lri-rs/src/lib.rs
+++ b/lri-rs/src/lib.rs
@@ -1,4 +1,4 @@
-use std::fmt;
+use std::{fmt, time::Duration};
 
 use block::{Block, ExtractedData, Header};
 use lri_proto::{
@@ -13,16 +13,23 @@ pub struct LriFile<'lri> {
 	pub images: Vec<RawImage<'lri>>,
 	pub colors: Vec<ColorInfo>,
 	pub camera_infos: Vec<CameraInfo>,
+
+	pub focal_length: Option<i32>,
+	pub firmware_version: Option<String>,
+	pub image_integration_time: Option<Duration>,
+	pub af_achieved: Option<bool>,
+	pub image_gain: Option<f32>,
 }
 
 impl<'lri> LriFile<'lri> {
 	/// Read
 	pub fn decode(mut data: &'lri [u8]) -> Self {
-		let mut reference = None;
 		let mut images = vec![];
 		let mut colors = vec![];
 		let mut camera_infos = vec![];
 
+		let mut ext = ExtractedData::default();
+
 		// Read data blocks and extract informtion we care about
 		loop {
 			if data.len() == 0 {
@@ -40,14 +47,7 @@ impl<'lri> LriFile<'lri> {
 				data: block_data,
 			};
 
-			match block.extract_meaningful_data(&mut images, &mut colors, &mut camera_infos) {
-				ExtractedData {
-					reference_camera: Some(irc),
-				} => {
-					reference = Some(irc);
-				}
-				_ => (),
-			}
+			block.extract_meaningful_data(&mut ext, &mut images, &mut colors, &mut camera_infos);
 		}
 
 		// Further fill in the RawImage's we extracted
@@ -66,10 +66,16 @@ impl<'lri> LriFile<'lri> {
 		}
 
 		LriFile {
-			image_reference_camera: reference,
+			image_reference_camera: ext.reference_camera,
 			images,
 			colors,
 			camera_infos,
+
+			firmware_version: ext.fw_version,
+			focal_length: ext.focal_length,
+			image_integration_time: ext.image_integration_time,
+			af_achieved: ext.af_achieved,
+			image_gain: ext.image_gain,
 		}
 	}
 
@@ -92,6 +98,20 @@ impl<'lri> LriFile<'lri> {
 	}
 }
 
+pub enum RawData<'img> {
+	BayerJpeg {
+		header: &'img [u8],
+		format: u32,
+		jpeg0: &'img [u8],
+		jpeg1: &'img [u8],
+		jpeg2: &'img [u8],
+		jpeg3: &'img [u8],
+	},
+	Packed10bpp {
+		data: &'img [u8],
+	},
+}
+
 pub struct RawImage<'img> {
 	/// Camera that captured this image
 	pub camera: CameraId,
@@ -101,10 +121,9 @@ pub struct RawImage<'img> {
 	pub width: usize,
 	pub height: usize,
 
-	/// How the image data is encoded in the file
+	/// What format the data is in
 	pub format: DataFormat,
-	/// Image data
-	pub data: &'img [u8],
+	pub data: RawData<'img>,
 	/// "sensor bayer red offset"
 	pub sbro: (i32, i32),
 	/// All color information associated with this [CameraId] for different [Whitepoint]s
@@ -135,6 +154,9 @@ 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"),
@@ -143,6 +165,7 @@ impl<'img> RawImage<'img> {
 			(1, 1) => Some("RGGB"),
 			_ => unreachable!(),
 		}
+		//}
 	}
 
 	/// Uses the [SensorModel] to determine if the image's [ColorType].
@@ -192,14 +215,15 @@ pub struct CameraInfo {
 	sensor: SensorModel,
 }
 
-#[derive(Copy, Clone, Debug)]
+#[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,
-	Packed12bpp,
-	Packed14bpp,
+	// Never seen
+	//Packed12bpp,
+	//Packed14bpp,
 }
 
 impl fmt::Display for DataFormat {
@@ -207,8 +231,8 @@ impl fmt::Display for DataFormat {
 		let str = match self {
 			Self::BayerJpeg => "BayerJpeg",
 			Self::Packed10bpp => "Packed10bpp",
-			Self::Packed12bpp => "Packed12bpp",
-			Self::Packed14bpp => "Packed14bpp",
+			//Self::Packed12bpp => "Packed12bpp",
+			//Self::Packed14bpp => "Packed14bpp",
 		};
 
 		write!(f, "{str}")
@@ -220,8 +244,8 @@ impl From<FormatType> for DataFormat {
 		match proto {
 			FormatType::RAW_BAYER_JPEG => Self::BayerJpeg,
 			FormatType::RAW_PACKED_10BPP => Self::Packed10bpp,
-			FormatType::RAW_PACKED_12BPP => Self::Packed12bpp,
-			FormatType::RAW_PACKED_14BPP => Self::Packed14bpp,
+			FormatType::RAW_PACKED_12BPP => unreachable!(),
+			FormatType::RAW_PACKED_14BPP => unreachable!(),
 			FormatType::RAW_RESERVED_0
 			| FormatType::RAW_RESERVED_1
 			| FormatType::RAW_RESERVED_2
@@ -232,7 +256,7 @@ impl From<FormatType> for DataFormat {
 	}
 }
 
-#[derive(Copy, Clone, Debug, PartialEq)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 pub enum CameraId {
 	A1,
 	A2,
diff --git a/lri-study/Cargo.toml b/lri-study/Cargo.toml
new file mode 100644
index 0000000..f92b7de
--- /dev/null
+++ b/lri-study/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "lri-study"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+camino = "1.1.6"
+lri-rs = { path = "../lri-rs" }
+owo-colors = "3.5.0"
diff --git a/lri-study/src/main.rs b/lri-study/src/main.rs
new file mode 100644
index 0000000..3e3f225
--- /dev/null
+++ b/lri-study/src/main.rs
@@ -0,0 +1,139 @@
+use std::{
+	collections::HashMap,
+	time::{Duration, Instant},
+};
+
+use camino::Utf8PathBuf;
+use lri_rs::{DataFormat, LriFile, SensorModel};
+use owo_colors::OwoColorize;
+
+const DATA: &'static str = "/Users/gen/thanks_lak";
+
+fn main() {
+	match std::env::args().nth(1).as_deref() {
+		Some("gather") => gather(),
+		_ => (),
+	}
+}
+
+fn gather() -> ! {
+	let data_dir = Utf8PathBuf::from(DATA);
+	let mut files: HashMap<String, Photo> = HashMap::new();
+
+	for entry in data_dir.read_dir_utf8().unwrap() {
+		let entry = entry.unwrap();
+		let meta = entry.metadata().unwrap();
+		let path = entry.path();
+
+		if meta.is_file() {
+			let stub = path.file_stem().unwrap().to_owned();
+
+			match path.extension() {
+				Some("jpg") => files
+					.entry(stub.clone())
+					.and_modify(|e| e.jpg = Some(path.to_owned()))
+					.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)),
+				Some("lris") => files
+					.entry(stub.clone())
+					.and_modify(|e| e.lris = Some(path.to_owned()))
+					.or_insert(Photo::new_lris(&path)),
+				None | Some(_) => continue,
+			};
+		}
+	}
+
+	let start = Instant::now();
+
+	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 photo in photos {
+		let lri_path = match photo.lri {
+			Some(p) => p,
+			None => continue,
+		};
+		let data = match std::fs::read(&lri_path) {
+			Ok(d) => d,
+			Err(e) => {
+				println!("{}: {}", lri_path.red(), e);
+				continue;
+			}
+		};
+		let lri = LriFile::decode(&data);
+
+		print!("{} - ", lri_path.file_stem().unwrap());
+
+		if let Some(fwv) = lri.firmware_version.as_ref() {
+			print!(
+				"[{}] focal:{:<3} iit:{:>2}ms gain:{:2.0} ",
+				fwv,
+				lri.focal_length.unwrap(),
+				lri.image_integration_time
+					.unwrap_or(Duration::ZERO)
+					.as_millis(),
+				lri.image_gain.unwrap_or_default()
+			);
+
+			match lri.af_achieved {
+				None => print!("{} - ", "af".dimmed()),
+				Some(false) => print!("{} - ", "af".red()),
+				Some(true) => print!("{} - ", "af".green()),
+			}
+		}
+
+		for img in lri.images() {
+			let sens = match img.sensor {
+				SensorModel::Ar1335 => "a13",
+				SensorModel::Ar1335Mono => "a1m",
+				SensorModel::Ar835 => "!!!ar8",
+				SensorModel::Imx386 => "!!!imx",
+				SensorModel::Imx386Mono => "!!!imm",
+				SensorModel::Unknown => "???",
+			};
+
+			match img.format {
+				DataFormat::BayerJpeg => print!("{} ", sens.cyan()),
+				DataFormat::Packed10bpp => print!("{} ", sens.yellow()),
+			}
+		}
+		println!("");
+	}
+
+	println!("        ---\nTook {:.2}s", start.elapsed().as_secs_f32());
+
+	std::process::exit(0)
+}
+
+struct Photo {
+	jpg: Option<Utf8PathBuf>,
+	lri: Option<Utf8PathBuf>,
+	lris: Option<Utf8PathBuf>,
+}
+
+impl Photo {
+	pub fn new_jpg<P: Into<Utf8PathBuf>>(jpg: P) -> Self {
+		Self {
+			jpg: Some(jpg.into()),
+			lri: None,
+			lris: None,
+		}
+	}
+	pub fn new_lri<P: Into<Utf8PathBuf>>(lri: P) -> Self {
+		Self {
+			lri: Some(lri.into()),
+			jpg: None,
+			lris: None,
+		}
+	}
+	pub fn new_lris<P: Into<Utf8PathBuf>>(lris: P) -> Self {
+		Self {
+			lris: Some(lris.into()),
+			lri: None,
+			jpg: None,
+		}
+	}
+}
diff --git a/prism/src/main.rs b/prism/src/main.rs
index 60baa18..e0da42a 100644
--- a/prism/src/main.rs
+++ b/prism/src/main.rs
@@ -1,9 +1,16 @@
-use lri_rs::{DataFormat, LriFile, RawImage, Whitepoint};
+use std::collections::HashMap;
+
+use lri_rs::{CameraId, DataFormat, LriFile, RawData, RawImage, SensorModel, Whitepoint};
 use nalgebra::{Matrix3, Matrix3x1};
 
 mod rotate;
 mod unpack;
 
+pub struct Entry {
+	sensor: SensorModel,
+	count: usize,
+}
+
 fn main() {
 	let file_name = std::env::args().nth(1).unwrap();
 	let bytes = std::fs::read(file_name).unwrap();
@@ -14,6 +21,21 @@ fn main() {
 	lri.reference_image()
 		.map(|raw| make(raw, String::from("reference.png")));
 
+	let mut set: HashMap<CameraId, Entry> = HashMap::new();
+
+	for img in lri.images() {
+		set.entry(img.camera)
+			.and_modify(|e| e.count += 1)
+			.or_insert(Entry {
+				sensor: img.sensor,
+				count: 1,
+			});
+	}
+
+	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!(
@@ -35,7 +57,7 @@ fn main() {
 		//std::process::exit(0);
 
 		make(img, format!("image_{idx}.png"));
-		return;
+		//return;
 	}
 }
 
@@ -49,7 +71,7 @@ fn make(img: &RawImage, path: String) {
 		width,
 		height,
 		format,
-		mut data,
+		data,
 		sbro,
 		color,
 	} = img;
@@ -59,99 +81,18 @@ fn make(img: &RawImage, path: String) {
 		sbro.0, sbro.1
 	);
 
-	let stem = &path[..path.len() - 4];
-
-	if *format == DataFormat::BayerJpeg {
-		//FF d9 | 42 4A 50 47
-		let bjp_end = &[0xFF, 0xd9, 0x42, 0x4A, 0x50, 0x47];
-		let jfif_start = &[0xFF, 0xD8, 0xFF, 0xE0];
-		let jfif_end = &[0xFF, 0xD9];
-
-		for idx in 0..data.len() {
-			if &data[idx..idx + 6] == bjp_end {
-				data = &data[..idx + 2];
-				break;
-			}
-		}
-
-		let mut start = None;
-		let mut idx = 0;
-		let mut jfif_count = 0;
-		loop {
-			if idx >= data.len() {
-				break;
-			}
-
-			match start {
-				None => {
-					if &data[idx..idx + 4] == jfif_start {
-						start = Some(idx);
-
-						if jfif_count == 0 {
-							let path = format!("{stem}_only.bjp");
-							let out = &data[..idx];
-							std::fs::write(path, out).unwrap();
-						}
-
-						idx += 4;
-						continue;
-					}
-				}
-				Some(start_idx) => {
-					if &data[idx..idx + 2] == jfif_end {
-						let path = format!("{stem}_{jfif_count}.jpg");
-						let out = &data[start_idx..idx + 2];
-						std::fs::write(path, out).unwrap();
-
-						start = None;
-						jfif_count += 1;
-						idx += 2;
-						continue;
-					}
-				}
-			}
-
-			idx += 1;
-		}
-
-		std::fs::write(format!("{}.bjp", &path[..path.len() - 4]), data).unwrap();
-		return;
-	}
+	let mut bayered = bayer(
+		data,
+		*width,
+		*height,
+		format!("{}_bjpg", &path[..path.len() - 4]),
+	);
 
-	// 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
-	ten_data.iter_mut().for_each(|p| *p = p.saturating_sub(42));
-
-	// B G B G B G
-	// G R G R G R
-
-	// A1 - 1:0
-	// A2 - -1:-1
-	// A3 - 1:0
-	// A4 - 1:0
-	// A5 - 0:1
-
-	// B1 - NO
-	// B2 - RO
-	// B3 - RO
-	// B4 - RO
-	// B5 - NO
-
-	// C1 - NO
-	// C2 - RO
-	// C3 - NO
-	// C4 - RO
-	// C5 - RO
-	// C6 - -1:-1
+	bayered.iter_mut().for_each(|p| *p = p.saturating_sub(42));
 
 	let (mut rgb, color_format) = match img.cfa_string() {
 		Some(cfa_string) => {
-			let rawimg: Image<u16, BayerRgb> = Image::from_raw_parts(
+			let rawimg: Image<u8, BayerRgb> = Image::from_raw_parts(
 				4160,
 				3120,
 				// We only care about CFA here because all we're doing is debayering
@@ -163,21 +104,24 @@ fn make(img: &RawImage, path: String) {
 					cfa: rawloader::CFA::new(cfa_string),
 					cam_to_xyz: nalgebra::Matrix3::zeros(),
 				},
-				ten_data,
+				bayered,
 			);
 
 			(rawimg.debayer().data, png::ColorType::Rgb)
+			//(bayered, png::ColorType::Grayscale)
 		}
-		None => (ten_data, 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 / 1023.0).collect();
+	let mut floats: Vec<f32> = rgb.into_iter().map(|p| p as f32 / 255.0).collect();
 
-	print!("\t");
-	color.iter().for_each(|c| print!("{:?} ", c.whitepoint));
-	println!();
+	if color.len() > 0 {
+		print!("\t");
+		color.iter().for_each(|c| print!("{:?} ", c.whitepoint));
+		println!();
+	}
 
 	match img.color_info(Whitepoint::F11) {
 		Some(c) => {
@@ -189,7 +133,7 @@ fn make(img: &RawImage, path: String) {
 
 			let xyz_d65 = to_xyz * d50_d65;
 
-			println!("{color}");
+			//println!("{color}");
 
 			let white = xyz_d65 * Matrix3x1::new(1.0, 1.0, 1.0);
 
@@ -197,15 +141,15 @@ fn make(img: &RawImage, path: String) {
 			let white_y = white[1] / (white[0] + white[1] + white[2]);
 			let white_z = 1.0 - white_x - white_y;
 
-			println!(
+			/*println!(
 				"\t{:?} ||| white: x = {} y = {} z = {}",
 				c.whitepoint, white_x, white_y, white_z
-			);
+			);*/
 
 			let premul = to_xyz * to_srgb;
 
 			let prenorm = premul.normalize();
-			println!("{prenorm}");
+			//println!("{prenorm}");
 
 			for chnk in floats.chunks_mut(3) {
 				let r = chnk[0] * (1.0 / c.rg);
@@ -271,6 +215,73 @@ 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> {
+	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
+			//ten_data.iter_mut().for_each(|p| *p = p.saturating_sub(42));
+
+			ten_data.into_iter().map(|p| (p >> 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,