use crate::header::BayerPattern;
use crate::image::Normalizer;
use image::{ImageBuffer, Luma, Primitive, Rgb};
use std::error::Error;
use std::ops::Deref;
#[derive(Debug, Clone)]
pub struct ImageData<T: Primitive> {
buffer: ImageBuffer<Luma<T>, Vec<T>>,
normalizer: Normalizer,
bayer_pattern: Option<BayerPattern>,
width: u32,
height: u32,
}
impl<T: Primitive> ImageData<T> {
pub fn from_data(
width: usize,
height: usize,
normalizer: Normalizer,
bayer_pattern: Option<BayerPattern>,
data: Vec<T>,
) -> Result<Self, Box<dyn Error + Send + Sync>> {
let buffer = ImageBuffer::<Luma<T>, Vec<T>>::from_raw(width as u32, height as u32, data)
.ok_or("Failed to construct image buffer")?;
Ok(Self {
buffer,
normalizer,
bayer_pattern,
width: width as u32,
height: height as u32,
})
}
pub fn bayer_pattern(&self) -> &Option<BayerPattern> {
&self.bayer_pattern
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn raw(&self) -> &[T] {
self.buffer.as_raw()
}
pub fn normalizer(&self) -> Normalizer {
self.normalizer
}
fn raw_at(&self, x: u32, y: u32) -> f64 {
if x >= self.width || y >= self.height {
return 0.0;
}
self.buffer
.get_pixel(x, y)
.0
.first()
.and_then(|sample| sample.to_f64())
.unwrap_or(0.0)
}
fn normalized_at(&self, x: u32, y: u32) -> f64 {
self.normalizer.normalize(self.raw_at(x, y))
}
pub fn normalized(&self) -> ImageBuffer<Luma<f64>, Vec<f64>> {
ImageBuffer::from_fn(self.width, self.height, |x, y| {
Luma([self.normalized_at(x, y)])
})
}
pub fn normalized_superpixel(
&self,
) -> Result<ImageBuffer<Rgb<f64>, Vec<f64>>, Box<dyn Error + Send + Sync>> {
let bayer_pattern = self
.bayer_pattern
.ok_or("Can not perform superpixel demosaic on a non rgb image")?;
let offsets = bayer_pattern.superpixel_offsets();
Ok(ImageBuffer::from_fn(
self.width / 2,
self.height / 2,
|x, y| {
let (x, y) = (x * 2, y * 2);
let sample = |(dx, dy): (u32, u32)| self.normalized_at(x + dx, y + dy);
let green = (sample(offsets.green[0]) + sample(offsets.green[1])) / 2.0;
Rgb([sample(offsets.red), green, sample(offsets.blue)])
},
))
}
}
impl ImageData<f64> {
pub fn from_buffer(buffer: ImageBuffer<Luma<f64>, Vec<f64>>) -> Self {
let width = buffer.width();
let height = buffer.height();
let normalizer = Normalizer::from_samples(0.0, 1.0, buffer.as_raw().iter().copied());
Self {
buffer,
normalizer,
bayer_pattern: None,
width,
height,
}
}
}
impl<T: Primitive> Deref for ImageData<T> {
type Target = ImageBuffer<Luma<T>, Vec<T>>;
fn deref(&self) -> &Self::Target {
&self.buffer
}
}
#[cfg(test)]
mod tests {
use super::ImageData;
use crate::header::BayerPattern;
use crate::image::Normalizer;
fn unsigned_16_bit(width: usize, height: usize, data: Vec<i16>) -> ImageData<i16> {
ImageData::from_data(
width,
height,
Normalizer::new(32768.0, 1.0, 0.0, 65535.0),
None,
data,
)
.expect("buffer dimensions match the data")
}
#[test]
fn normalizing_spreads_samples_over_the_full_range() {
let image = unsigned_16_bit(2, 2, vec![i16::MIN, -1, 0, i16::MAX]);
let normalized = image.normalized();
assert_eq!(normalized.get_pixel(0, 0)[0], 0.0);
assert_eq!(normalized.get_pixel(1, 1)[0], 1.0);
assert!((normalized.get_pixel(1, 0)[0] - 0.5).abs() < 1e-4);
}
#[test]
fn normalizing_is_monotonic() {
let image = unsigned_16_bit(4, 1, vec![i16::MIN, -16384, 16384, i16::MAX]);
let normalized = image.normalized();
let values: Vec<f64> = (0..4).map(|x| normalized.get_pixel(x, 0)[0]).collect();
for pair in values.windows(2) {
assert!(
pair[0] < pair[1],
"a brighter sample must not normalise darker: {values:?}"
);
}
}
#[test]
fn normalized_values_stay_inside_the_unit_range() {
let image = ImageData::from_data(
3,
1,
Normalizer::new(0.0, 1.0, 0.0, 255.0),
None,
vec![0_u8, 128, 255],
)
.expect("buffer dimensions match the data");
for (_, _, pixel) in image.normalized().enumerate_pixels() {
assert!(
(0.0..=1.0).contains(&pixel[0]),
"value out of range: {}",
pixel[0]
);
}
}
const RED: u8 = 200;
const GREEN: u8 = 100;
const BLUE: u8 = 50;
fn mosaic(pattern: BayerPattern, tile: [u8; 4]) -> ImageData<u8> {
let mut data = Vec::with_capacity(16);
for y in 0..4 {
for x in 0..4 {
data.push(tile[(y % 2) * 2 + (x % 2)]);
}
}
ImageData::from_data(
4,
4,
Normalizer::new(0.0, 1.0, 0.0, 255.0),
Some(pattern),
data,
)
.expect("buffer dimensions match the data")
}
#[test]
fn every_bayer_pattern_demosaics_to_the_right_channels() {
let cases = [
(BayerPattern::RGGB, [RED, GREEN, GREEN, BLUE]),
(BayerPattern::BGGR, [BLUE, GREEN, GREEN, RED]),
(BayerPattern::GRBG, [GREEN, RED, BLUE, GREEN]),
(BayerPattern::GBRG, [GREEN, BLUE, RED, GREEN]),
];
for (pattern, tile) in cases {
let demosaiced = mosaic(pattern, tile)
.normalized_superpixel()
.expect("a mosaic image can be demosaiced");
assert_eq!(demosaiced.dimensions(), (2, 2), "{pattern:?}");
for (x, y, pixel) in demosaiced.enumerate_pixels() {
let [red, green, blue] = pixel.0;
assert!(
(red - RED as f64 / 255.0).abs() < 1e-9,
"{pattern:?} red at ({x}, {y}): {red}"
);
assert!(
(green - GREEN as f64 / 255.0).abs() < 1e-9,
"{pattern:?} green at ({x}, {y}): {green}"
);
assert!(
(blue - BLUE as f64 / 255.0).abs() < 1e-9,
"{pattern:?} blue at ({x}, {y}): {blue}"
);
}
}
}
#[test]
fn demosaicing_a_monochrome_image_is_an_error() {
let image = unsigned_16_bit(2, 2, vec![0, 1, 2, 3]);
assert!(image.normalized_superpixel().is_err());
}
#[test]
fn a_large_zero_offset_does_not_overflow_a_narrow_sample_type() {
let image = ImageData::from_data(
2,
2,
Normalizer::new(32768.0, 1.0, 32768.0, 33023.0),
Some(BayerPattern::RGGB),
vec![0_u8, 128, 200, 255],
)
.expect("buffer dimensions match the data");
let demosaiced = image
.normalized_superpixel()
.expect("a mosaic image can be demosaiced");
assert_eq!(demosaiced.dimensions(), (1, 1));
for value in demosaiced.get_pixel(0, 0).0 {
assert!((0.0..=1.0).contains(&value), "value out of range: {value}");
}
}
#[test]
fn odd_dimensions_do_not_read_outside_the_image() {
let image = ImageData::from_data(
3,
3,
Normalizer::new(0.0, 1.0, 0.0, 255.0),
Some(BayerPattern::RGGB),
vec![0_u8; 9],
)
.expect("buffer dimensions match the data");
let demosaiced = image
.normalized_superpixel()
.expect("a mosaic image can be demosaiced");
assert_eq!(demosaiced.dimensions(), (1, 1));
}
#[test]
fn from_buffer_derives_its_range_from_the_data() {
let buffer = image::ImageBuffer::from_raw(2, 2, vec![10.0_f64, 20.0, 30.0, 40.0])
.expect("buffer dimensions match the data");
let image = ImageData::from_buffer(buffer);
let normalized = image.normalized();
assert_eq!(normalized.get_pixel(0, 0)[0], 0.0);
assert_eq!(normalized.get_pixel(1, 1)[0], 1.0);
assert!((normalized.get_pixel(1, 0)[0] - 1.0 / 3.0).abs() < 1e-9);
}
}