use std::sync::Arc;
use super::buffer_pool::BufferPool;
use super::{BatchLayout, ImageBatch};
use crate::collator::Collator;
use crate::error::Result;
use crate::image::{Image, Layout, Sample, invalid, sample_count, with_capacity};
#[derive(Clone, Debug, Default)]
pub struct ImageCollator {
layout: BatchLayout,
pool: Option<Arc<BufferPool>>,
}
impl ImageCollator {
pub fn new(layout: BatchLayout) -> Self {
Self { layout, pool: None }
}
pub fn reuse_buffers(mut self, max: usize) -> Self {
self.pool = (max > 0).then(|| BufferPool::new(max));
self
}
pub fn layout(&self) -> BatchLayout {
self.layout
}
pub fn collate_refs<T: Sample, L: Layout>(
&self,
images: &[&Image<T, L>],
) -> Result<ImageBatch<T>> {
let Some(first) = images.first() else {
return Err(invalid("an empty batch has no shape".into()));
};
let (w, h, c) = (first.width(), first.height(), first.channels());
if let Some((i, other)) = images
.iter()
.enumerate()
.find(|(_, i)| i.shape() != first.shape())
{
return Err(invalid(format!(
"image {i} of the batch is {:?}, image 0 is {:?}: all images of a batch must \
have the same shape (pad, crop or resize them)",
other.shape(),
first.shape()
)));
}
let per_image = sample_count(w, h, c)?;
let len = per_image
.checked_mul(images.len())
.ok_or_else(|| invalid(format!("{} images of {per_image} values", images.len())))?;
let mut data = match &self.pool {
Some(pool) => pool.take(len)?,
None => with_capacity(len)?,
};
let planar = self.layout == BatchLayout::Nchw;
for image in images {
write(&mut data, image, planar);
}
let n = images.len();
let shape = match self.layout {
BatchLayout::Nchw => [n, c, h, w],
BatchLayout::Nhwc => [n, h, w, c],
};
Ok(ImageBatch::new(shape, self.layout, data, self.pool.clone()))
}
}
impl<T: Sample, L: Layout> Collator<Image<T, L>> for ImageCollator {
type Batch = ImageBatch<T>;
fn collate(&self, images: Vec<Image<T, L>>) -> Result<ImageBatch<T>> {
let refs: Vec<&Image<T, L>> = images.iter().collect();
self.collate_refs(&refs)
}
}
fn write<T: Sample, L: Layout>(out: &mut Vec<T>, image: &Image<T, L>, planar: bool) {
let src = image.data();
if L::PLANAR == planar {
out.extend_from_slice(src);
return;
}
let (c, plane) = (image.channels(), image.width() * image.height());
let start = out.len();
out.resize(start + src.len(), T::default());
let dst = &mut out[start..];
for channel in 0..c {
match planar {
true => {
let dst = &mut dst[channel * plane..(channel + 1) * plane];
for (d, pixel) in dst.iter_mut().zip(src.chunks_exact(c)) {
*d = pixel[channel];
}
}
false => {
let src = &src[channel * plane..(channel + 1) * plane];
for (pixel, &s) in dst.chunks_exact_mut(c).zip(src) {
pixel[channel] = s;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::{BatchLayout, ImageCollator};
use crate::collator::Collator;
use crate::image::{Chw, Image};
fn image(v: u8) -> Image<u8> {
Image::new(2, 1, 3, (v..v + 6).collect()).unwrap()
}
#[test]
fn hwc_items_in_both_layouts() {
let nchw = ImageCollator::default()
.collate(vec![image(0), image(10)])
.unwrap();
assert_eq!(nchw.shape(), [2, 3, 1, 2]);
assert_eq!(nchw.data(), [0, 3, 1, 4, 2, 5, 10, 13, 11, 14, 12, 15]);
let nhwc = ImageCollator::new(BatchLayout::Nhwc)
.collate(vec![image(0), image(10)])
.unwrap();
assert_eq!(nhwc.shape(), [2, 1, 2, 3]);
assert_eq!(nhwc.data(), [0, 1, 2, 3, 4, 5, 10, 11, 12, 13, 14, 15]);
}
#[test]
fn chw_items_in_both_layouts() {
let planar = Image::<f32, Chw>::new(2, 1, 3, vec![0.0, 3.0, 1.0, 4.0, 2.0, 5.0]).unwrap();
let nchw = ImageCollator::default()
.collate(vec![planar.clone()])
.unwrap();
assert_eq!(nchw.data(), [0.0, 3.0, 1.0, 4.0, 2.0, 5.0]);
let nhwc = ImageCollator::new(BatchLayout::Nhwc)
.collate(vec![planar])
.unwrap();
assert_eq!(nhwc.data(), [0.0, 1.0, 2.0, 3.0, 4.0, 5.0]);
}
#[test]
fn dropped_batches_give_their_buffer_back() {
let collator = ImageCollator::default().reuse_buffers(1);
let first = collator.collate(vec![image(0)]).unwrap();
let ptr = first.data().as_ptr();
let second = collator.collate(vec![image(1)]).unwrap();
assert_ne!(second.data().as_ptr(), ptr, "nothing free: a new buffer");
drop(first);
drop(second);
let third = collator.collate(vec![image(2)]).unwrap();
assert_eq!(third.data().as_ptr(), ptr, "kept: the first one");
assert_eq!(third.data(), [2, 5, 3, 6, 4, 7]);
let owned = third.into_vec();
let fourth = collator.collate(vec![image(3)]).unwrap();
assert_ne!(
fourth.data().as_ptr(),
owned.as_ptr(),
"taken out with into_vec"
);
}
#[test]
fn shapes_must_match() {
let other = Image::new(1, 2, 3, vec![0u8; 6]).unwrap();
let err = ImageCollator::default()
.collate(vec![image(0), image(1), other])
.unwrap_err();
assert!(
err.to_string()
.contains("image 2 of the batch is [2, 1, 3], image 0 is [1, 2, 3]"),
"{err}"
);
assert!(
ImageCollator::default()
.collate(Vec::<Image<u8>>::new())
.is_err()
);
}
}