use crate::{allocator::ImageAllocator, error::ImageError};
use kornia_tensor::{Tensor, Tensor2, Tensor3};
use rayon::prelude::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ImageSize {
pub width: usize,
pub height: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PixelFormat {
U8,
U16,
F32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterpolationMode {
Bilinear,
Nearest,
Lanczos,
Bicubic,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ImageLayout {
pub image_size: ImageSize,
pub channels: u8,
pub pixel_format: PixelFormat,
}
impl ImageLayout {
pub fn new(image_size: ImageSize, channels: u8, pixel_format: PixelFormat) -> Self {
Self {
image_size,
channels,
pixel_format,
}
}
}
impl std::fmt::Display for ImageSize {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"ImageSize {{ width: {}, height: {} }}",
self.width, self.height
)
}
}
impl From<[usize; 2]> for ImageSize {
fn from(size: [usize; 2]) -> Self {
ImageSize {
width: size[0],
height: size[1],
}
}
}
impl From<ImageSize> for [u32; 2] {
fn from(size: ImageSize) -> Self {
[size.width as u32, size.height as u32]
}
}
impl ImageSize {
#[inline]
pub fn index(&self, y: usize, x: usize) -> usize {
y * self.width + x
}
#[inline]
pub fn coords(&self, idx: usize) -> (usize, usize) {
let y = idx / self.width;
let x = idx % self.width;
(y, x)
}
}
#[derive(Clone)]
pub struct Image<T, const C: usize, A: ImageAllocator>(pub Tensor3<T, A>);
impl<T, const C: usize, A: ImageAllocator> std::ops::Deref for Image<T, C, A> {
type Target = Tensor3<T, A>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T, const C: usize, A: ImageAllocator> std::ops::DerefMut for Image<T, C, A> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T, const C: usize, A: ImageAllocator> Image<T, C, A> {
pub fn new(size: ImageSize, data: Vec<T>, alloc: A) -> Result<Self, ImageError> {
if data.len() != size.width * size.height * C {
return Err(ImageError::InvalidChannelShape(
data.len(),
size.width * size.height * C,
));
}
Ok(Self(Tensor3::from_shape_vec(
[size.height, size.width, C],
data,
alloc,
)?))
}
pub fn from_size_val(size: ImageSize, val: T, alloc: A) -> Result<Self, ImageError>
where
T: Clone,
{
let data = vec![val; size.width * size.height * C];
let image = Image::new(size, data, alloc)?;
Ok(image)
}
pub unsafe fn from_raw_parts(
size: ImageSize,
data: *const T,
len: usize,
alloc: A,
) -> Result<Self, ImageError>
where
T: Clone,
{
Tensor::from_raw_parts([size.height, size.width, C], data, len, alloc)?.try_into()
}
pub fn from_size_slice(size: ImageSize, data: &[T], alloc: A) -> Result<Self, ImageError>
where
T: Clone,
{
let tensor: Tensor3<T, A> =
Tensor::from_shape_slice([size.height, size.width, C], data, alloc)?;
Image::try_from(tensor)
}
pub fn map<U>(&self, f: impl Fn(&T) -> U) -> Result<Image<U, C, A>, ImageError> {
let data = self.as_slice().iter().map(f).collect::<Vec<U>>();
let alloc = self.storage.alloc();
Image::<U, C, A>::new(self.size(), data, alloc.clone())
}
pub fn cast<U>(&self) -> Result<Image<U, C, A>, ImageError>
where
U: num_traits::NumCast + Copy,
T: num_traits::NumCast + Copy,
{
let data = self
.as_slice()
.iter()
.map(|&x| U::from(x).ok_or(ImageError::CastError))
.collect::<Result<Vec<U>, ImageError>>()?;
let alloc = self.storage.alloc().clone();
let tensor = Tensor3::from_shape_vec(self.0.shape, data, alloc)?;
Ok(Image(tensor))
}
pub fn channel(&self, channel: usize) -> Result<Image<T, 1, A>, ImageError>
where
T: Clone,
{
if channel >= C {
return Err(ImageError::ChannelIndexOutOfBounds(channel, C));
}
let channel_data = self
.as_slice()
.iter()
.skip(channel)
.step_by(C)
.cloned()
.collect();
let alloc = self.storage.alloc();
Image::new(self.size(), channel_data, alloc.clone())
}
pub fn split_channels(&self) -> Result<Vec<Image<T, 1, A>>, ImageError>
where
T: Copy,
{
let mut channels = Vec::with_capacity(C);
for i in 0..C {
channels.push(self.channel(i)?);
}
Ok(channels)
}
pub fn size(&self) -> ImageSize {
ImageSize {
width: self.shape[1],
height: self.shape[0],
}
}
pub fn cols(&self) -> usize {
self.shape[1]
}
pub fn rows(&self) -> usize {
self.shape[0]
}
pub fn width(&self) -> usize {
self.cols()
}
pub fn height(&self) -> usize {
self.rows()
}
pub fn num_channels(&self) -> usize {
C
}
#[allow(clippy::uninit_vec)]
pub fn cast_and_scale<U>(self, scale: U) -> Result<Image<U, C, A>, ImageError>
where
U: num_traits::NumCast + std::ops::Mul<Output = U> + Clone + Copy + Send + Sync,
T: num_traits::NumCast + Clone + Copy + Send + Sync,
{
let slice = self.as_slice();
let mut casted_data = Vec::with_capacity(slice.len());
unsafe {
casted_data.set_len(slice.len());
}
slice
.par_iter()
.zip(casted_data.par_iter_mut())
.try_for_each(|(&x, out)| {
let xu = U::from(x).ok_or(ImageError::CastError)?;
*out = xu * scale;
Ok::<(), ImageError>(())
})?;
let alloc = self.storage.alloc();
Image::new(self.size(), casted_data, alloc.clone())
}
#[allow(clippy::uninit_vec)]
pub fn scale_and_cast<U>(&self, scale: T) -> Result<Image<U, C, A>, ImageError>
where
U: num_traits::NumCast + Clone + Copy + Send + Sync,
T: num_traits::NumCast + std::ops::Mul<Output = T> + Clone + Copy + Send + Sync,
{
let slice = self.as_slice();
let mut casted_data = Vec::with_capacity(slice.len());
unsafe {
casted_data.set_len(slice.len());
}
slice
.par_iter()
.zip(casted_data.par_iter_mut())
.try_for_each(|(&x, out)| {
*out = U::from(x * scale).ok_or(ImageError::CastError)?;
Ok::<(), ImageError>(())
})?;
let alloc = self.storage.alloc();
Image::new(self.size(), casted_data, alloc.clone())
}
pub fn get_pixel(&self, x: usize, y: usize, ch: usize) -> Result<&T, ImageError> {
if x >= self.width() || y >= self.height() {
return Err(ImageError::PixelIndexOutOfBounds(
x,
y,
self.width(),
self.height(),
));
}
if ch >= C {
return Err(ImageError::ChannelIndexOutOfBounds(ch, C));
}
let val = match self.get([y, x, ch]) {
Some(v) => v,
None => return Err(ImageError::ImageDataNotContiguous),
};
Ok(val)
}
pub fn set_pixel(&mut self, x: usize, y: usize, ch: usize, val: T) -> Result<(), ImageError> {
if x >= self.width() || y >= self.height() {
return Err(ImageError::PixelIndexOutOfBounds(
x,
y,
self.width(),
self.height(),
));
}
if ch >= C {
return Err(ImageError::ChannelIndexOutOfBounds(ch, C));
}
let idx = y * self.width() * C + x * C + ch;
self.as_slice_mut()[idx] = val;
Ok(())
}
pub fn into_vec(self) -> Vec<T> {
self.0.into_vec()
}
pub fn to_vec(&self) -> Vec<T>
where
T: Clone,
{
self.as_slice().to_vec()
}
}
impl<T, A: ImageAllocator> TryFrom<Tensor2<T, A>> for Image<T, 1, A>
where
T: Clone,
{
type Error = ImageError;
fn try_from(value: Tensor2<T, A>) -> Result<Self, Self::Error> {
let alloc = value.storage.alloc();
Self::from_size_slice(
ImageSize {
width: value.shape[1],
height: value.shape[0],
},
value.as_slice(),
alloc.clone(),
)
}
}
impl<T, const C: usize, A: ImageAllocator> TryFrom<Tensor3<T, A>> for Image<T, C, A> {
type Error = ImageError;
fn try_from(value: Tensor3<T, A>) -> Result<Self, Self::Error> {
if value.shape[2] != C {
return Err(ImageError::InvalidChannelShape(value.shape[2], C));
}
Ok(Self(value))
}
}
impl<T, const C: usize, A: ImageAllocator> TryInto<Tensor3<T, A>> for Image<T, C, A> {
type Error = ImageError;
fn try_into(self) -> Result<Tensor3<T, A>, Self::Error> {
Ok(self.0)
}
}
#[cfg(test)]
mod tests {
use crate::image::{Image, ImageError, ImageSize};
use kornia_tensor::{CpuAllocator, Tensor};
#[test]
fn test_image_size() {
let image_size = ImageSize {
width: 10,
height: 20,
};
assert_eq!(image_size.width, 10);
assert_eq!(image_size.height, 20);
}
#[test]
fn test_image_size_index_coords() {
let size = ImageSize {
width: 8,
height: 6,
};
assert_eq!(size.index(0, 0), 0);
assert_eq!(size.index(1, 0), 8);
assert_eq!(size.index(2, 3), 2 * 8 + 3);
assert_eq!(size.index(5, 7), 5 * 8 + 7);
assert_eq!(size.coords(0), (0, 0));
assert_eq!(size.coords(8), (1, 0));
assert_eq!(size.coords(19), (2, 3));
assert_eq!(size.coords(47), (5, 7));
}
#[test]
fn test_image_smoke() -> Result<(), ImageError> {
let image = Image::<u8, 3, CpuAllocator>::new(
ImageSize {
width: 10,
height: 20,
},
vec![0u8; 10 * 20 * 3],
CpuAllocator,
)?;
assert_eq!(image.size().width, 10);
assert_eq!(image.size().height, 20);
assert_eq!(image.num_channels(), 3);
Ok(())
}
#[test]
fn test_image_from_vec() -> Result<(), ImageError> {
let image: Image<f32, 3, CpuAllocator> = Image::new(
ImageSize {
height: 3,
width: 2,
},
vec![0.0; 3 * 2 * 3],
CpuAllocator,
)?;
assert_eq!(image.size().width, 2);
assert_eq!(image.size().height, 3);
assert_eq!(image.num_channels(), 3);
Ok(())
}
#[test]
fn test_image_from_empty_vec() -> Result<(), ImageError> {
let image: Result<Image<f32, 1, CpuAllocator>, ImageError> = Image::new(
ImageSize {
height: 0,
width: 0,
},
vec![0.0; 0],
CpuAllocator,
);
assert!(
image.is_ok(),
"Image::new should create an empty image and drop it without segfault"
);
Ok(())
}
#[test]
fn test_image_cast() -> Result<(), ImageError> {
let data = vec![0, 1, 2, 3, 4, 5];
let image_u8 = Image::<_, 3, CpuAllocator>::new(
ImageSize {
height: 2,
width: 1,
},
data,
CpuAllocator,
)?;
assert_eq!(image_u8.get([1, 0, 2]), Some(&5u8));
let image_i32: Image<i32, 3, CpuAllocator> = image_u8.cast()?;
assert_eq!(image_i32.get([1, 0, 2]), Some(&5i32));
Ok(())
}
#[test]
fn test_image_rgbd() -> Result<(), ImageError> {
let image = Image::<f32, 4, CpuAllocator>::new(
ImageSize {
height: 2,
width: 3,
},
vec![0f32; 2 * 3 * 4],
CpuAllocator,
)?;
assert_eq!(image.size().width, 3);
assert_eq!(image.size().height, 2);
assert_eq!(image.num_channels(), 4);
Ok(())
}
#[test]
fn test_image_channel() -> Result<(), ImageError> {
let image = Image::<f32, 3, CpuAllocator>::new(
ImageSize {
height: 2,
width: 1,
},
vec![0., 1., 2., 3., 4., 5.],
CpuAllocator,
)?;
let channel = image.channel(2)?;
assert_eq!(channel.get([1, 0, 0]), Some(&5.0f32));
Ok(())
}
#[test]
fn test_image_split_channels() -> Result<(), ImageError> {
let image = Image::<f32, 3, CpuAllocator>::new(
ImageSize {
height: 2,
width: 1,
},
vec![0., 1., 2., 3., 4., 5.],
CpuAllocator,
)
.unwrap();
let channels = image.split_channels()?;
assert_eq!(channels.len(), 3);
assert_eq!(channels[0].get([1, 0, 0]), Some(&3.0f32));
assert_eq!(channels[1].get([1, 0, 0]), Some(&4.0f32));
assert_eq!(channels[2].get([1, 0, 0]), Some(&5.0f32));
Ok(())
}
#[test]
fn test_scale_and_cast() -> Result<(), ImageError> {
let data = vec![0u8, 0, 255, 0, 0, 255];
let image_u8 = Image::<u8, 3, CpuAllocator>::new(
ImageSize {
height: 2,
width: 1,
},
data,
CpuAllocator,
)?;
let image_f32 = image_u8.cast_and_scale::<f32>(1. / 255.0)?;
assert_eq!(image_f32.get([1, 0, 2]), Some(&1.0f32));
Ok(())
}
#[test]
fn test_cast_and_scale() -> Result<(), ImageError> {
let data = vec![0u8, 0, 255, 0, 0, 255];
let image_u8 = Image::<u8, 3, CpuAllocator>::new(
ImageSize {
height: 2,
width: 1,
},
data,
CpuAllocator,
)?;
let image_f32 = image_u8.cast_and_scale::<f32>(1. / 255.0)?;
assert_eq!(image_f32.get([1, 0, 2]), Some(&1.0f32));
Ok(())
}
#[test]
fn test_image_from_tensor() -> Result<(), ImageError> {
let data = vec![0u8, 1, 2, 3, 4, 5];
let tensor = Tensor::<u8, 2, _>::from_shape_vec([2, 3], data, CpuAllocator)?;
let image = Image::<u8, 1, CpuAllocator>::try_from(tensor.clone())?;
assert_eq!(image.size().width, 3);
assert_eq!(image.size().height, 2);
assert_eq!(image.num_channels(), 1);
let image_2: Image<u8, 1, CpuAllocator> = tensor.try_into()?;
assert_eq!(image_2.size().width, 3);
assert_eq!(image_2.size().height, 2);
assert_eq!(image_2.num_channels(), 1);
Ok(())
}
#[test]
fn test_image_from_tensor_3d() -> Result<(), ImageError> {
let tensor = Tensor::<u8, 3, CpuAllocator>::from_shape_vec(
[2, 3, 4],
vec![0u8; 2 * 3 * 4],
CpuAllocator,
)?;
let image = Image::<u8, 4, CpuAllocator>::try_from(tensor.clone())?;
assert_eq!(image.size().width, 3);
assert_eq!(image.size().height, 2);
assert_eq!(image.num_channels(), 4);
let image_2: Image<u8, 4, CpuAllocator> = tensor.try_into()?;
assert_eq!(image_2.size().width, 3);
assert_eq!(image_2.size().height, 2);
assert_eq!(image_2.num_channels(), 4);
Ok(())
}
#[test]
fn test_image_from_raw_parts() -> Result<(), ImageError> {
let data = vec![0u8, 1, 2, 3, 4, 5];
let image = unsafe {
Image::<_, 1, CpuAllocator>::from_raw_parts(
[2, 3].into(),
data.as_ptr(),
data.len(),
CpuAllocator,
)?
};
std::mem::forget(data);
assert_eq!(image.size().width, 2);
assert_eq!(image.size().height, 3);
assert_eq!(image.num_channels(), 1);
Ok(())
}
#[test]
fn test_get_pixel() -> Result<(), ImageError> {
let image = Image::<u8, 3, CpuAllocator>::new(
ImageSize {
height: 2,
width: 1,
},
vec![1, 2, 5, 19, 255, 128],
CpuAllocator,
)?;
assert_eq!(image.get_pixel(0, 0, 0)?, &1);
assert_eq!(image.get_pixel(0, 0, 1)?, &2);
assert_eq!(image.get_pixel(0, 0, 2)?, &5);
assert_eq!(image.get_pixel(0, 1, 0)?, &19);
assert_eq!(image.get_pixel(0, 1, 1)?, &255);
assert_eq!(image.get_pixel(0, 1, 2)?, &128);
Ok(())
}
#[test]
fn test_set_pixel() -> Result<(), ImageError> {
let mut image = Image::<u8, 3, CpuAllocator>::new(
ImageSize {
height: 2,
width: 1,
},
vec![1, 2, 5, 19, 255, 128],
CpuAllocator,
)?;
image.set_pixel(0, 0, 0, 128)?;
image.set_pixel(0, 1, 1, 25)?;
assert_eq!(image.get_pixel(0, 0, 0)?, &128);
assert_eq!(image.get_pixel(0, 1, 1)?, &25);
Ok(())
}
#[test]
fn test_image_map() -> Result<(), ImageError> {
let image_u8 = Image::<u8, 1, CpuAllocator>::new(
ImageSize {
height: 2,
width: 1,
},
vec![0, 128],
CpuAllocator,
)?;
let image_f32 = image_u8.map(|x| (x + 2) as f32)?;
assert_eq!(image_f32.size().width, 1);
assert_eq!(image_f32.size().height, 2);
assert_eq!(image_f32.num_channels(), 1);
assert_eq!(image_f32.get([0, 0, 0]), Some(&2.0f32));
assert_eq!(image_f32.get([1, 0, 0]), Some(&130.0f32));
Ok(())
}
#[test]
fn test_cast_round_trip() -> Result<(), ImageError> {
let data_f32 = vec![0.0f32, 128.0, 255.0, 10.0, 20.0, 30.0];
let image_f32 = Image::<f32, 3, CpuAllocator>::new(
ImageSize {
height: 2,
width: 1,
},
data_f32.clone(),
CpuAllocator,
)?;
let image_u8 = image_f32.cast::<u8>()?;
let image_f32_rt = image_u8.cast::<f32>()?;
assert_eq!(image_f32_rt.size(), image_f32.size());
assert_eq!(image_f32_rt.num_channels(), 3);
for (original, round_tripped) in data_f32.iter().zip(image_f32_rt.as_slice()) {
assert!((*original - round_tripped).abs() < 1.0);
}
Ok(())
}
#[test]
fn test_cast_out_of_range() -> Result<(), ImageError> {
let image_f32 = Image::<f32, 1, CpuAllocator>::new(
ImageSize {
height: 1,
width: 1,
},
vec![f32::MAX],
CpuAllocator,
)?;
let result = image_f32.cast::<u8>();
assert!(
matches!(result, Err(ImageError::CastError)),
"expected CastError for out-of-range f32 value"
);
Ok(())
}
#[test]
fn test_cast_shape_preservation() -> Result<(), ImageError> {
let data = vec![0u8, 64, 128, 192, 200, 255];
let image_u8 = Image::<u8, 3, CpuAllocator>::new(
ImageSize {
height: 2,
width: 1,
},
data.clone(),
CpuAllocator,
)?;
let image_f32 = image_u8.cast::<f32>()?;
assert_eq!(image_f32.size(), image_u8.size());
assert_eq!(image_f32.num_channels(), 3);
for (original, casted) in data.iter().zip(image_f32.as_slice()) {
assert_eq!(*casted, *original as f32);
}
Ok(())
}
}