use super::{Error, MeasurementArray};
use crate::{
schema::{
array_uri_reference::ArrayUriReference,
axes::{Base, ChannelAxis, LabelledAxis},
Label,
},
strided_array_file::{ArrayProxy, Statistics},
};
use image::{DynamicImage, ImageReader};
use ndarray::{ArrayViewD, IxDyn, ShapeBuilder, SliceInfoElem};
use std::{path::PathBuf, sync::Arc};
#[derive(Debug)]
pub(crate) struct ImageData<'a> {
axes: Vec<ChannelAxis>,
label: Option<Label>,
#[expect(dead_code)]
im: DynamicImage,
data: ArrayProxy<'a>,
}
impl<'a> ImageData<'a> {
pub(crate) fn from_uri(
uri: &ArrayUriReference,
mut axes: Vec<ChannelAxis>,
label: Option<Label>,
) -> Result<Arc<dyn MeasurementArray<'a> + 'a>, Error> {
if axes.len() != 2 {
return Err("ImageData expects to get exactly 2 axes".to_string());
}
let path: PathBuf = uri
.try_into()
.map_err(|_| format!("Failed to extract file path from URI: {uri:?}"))?;
let dyn_im = ImageReader::open(path)
.map_err(|e| e.to_string())?
.decode()
.map_err(|e| e.to_string())?;
if dyn_im.width() as usize != axes[1].size()?
|| dyn_im.height() as usize != axes[0].size()?
{
return Err("Inconsistent image resolution".to_string());
}
let (data, colour_channels_names) = match &dyn_im {
DynamicImage::ImageRgb8(im) => {
let data = create_proxy::<u8>(&im.as_flat_samples())?;
Ok((data, &["R", "G", "B"]))
}
_ => Err("Unsupported image type".to_string()),
}?;
axes.push(create_colour_channels_axis(&data, colour_channels_names)?);
Ok(Arc::new(Self {
axes,
label,
im: dyn_im,
data,
}))
}
}
impl<'a> MeasurementArray<'a> for ImageData<'a> {
fn data(&self) -> ArrayProxy<'a> {
self.data.clone()
}
fn axes(&self) -> &Vec<ChannelAxis> {
&self.axes
}
fn label(&self) -> &Option<Label> {
&self.label
}
}
fn create_proxy<'a, T: 'static>(im: &image::FlatSamples<&[u8]>) -> Result<ArrayProxy<'a>, Error> {
let (data, layout) = (im.samples, im.layout);
let strided_shape = IxDyn(&[
layout.height as usize,
layout.width as usize,
layout.channels as usize,
])
.strides(IxDyn(&[
layout.height_stride,
layout.width_stride,
layout.channel_stride,
]));
let view = unsafe { ArrayViewD::from_shape_ptr(strided_shape, data.as_ptr() as *const T) };
ArrayProxy::from_view(view)
}
fn create_colour_channels_axis(
image: &ArrayProxy<'_>,
colour_channels_names: &[&str],
) -> Result<ChannelAxis, Error> {
if image.dimensions() != 3 {
return Err("Image is expected to have exactly 3 dimensions".to_string());
}
if image.shape()[2] != colour_channels_names.len() {
return Err("Inconsistent size of colour channels names".to_string());
}
let mut slice = vec![
SliceInfoElem::Slice {
start: 0,
end: None,
step: 1,
};
image.dimensions()
];
let mut labels = Vec::<Label>::with_capacity(image.dimensions());
for (channel_index, channel_name) in colour_channels_names.iter().enumerate() {
slice[2] = SliceInfoElem::Index(channel_index as isize);
let values = image.slice(slice.clone())?;
labels.push(Label {
name: channel_name.to_string(),
bounds: [
values.calc_stats(Statistics::Min)?,
values.calc_stats(Statistics::Max)?,
],
units: None,
transform: None,
});
}
Ok(LabelledAxis { labels }.into())
}