diff --git a/CHANGELOG.md b/CHANGELOG.md index 92048cd..7a4c7ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,14 @@ * Padding strategies (`NoPadding`, `ConstantPadding`, `ZeroPadding`) * Threshold module with Otsu and Mean threshold algorithms * Image transformations and functions to create affine transform matrices +* Type alias `Image` for `ImageBase, _>` replicated old `Image` type +* Type alias `ImageView` for `ImageBase, _>` ### Changed * Integrated Padding strategies into convolutions * Updated `ndarray-stats` to 0.2.0 adding `noisy_float` for median change * [INTERNAL] Disabled code coverage due to issues with tarpaulin and native libraries +* Renamed `Image` to `ImageBase` which can take any implementor of the ndaray `Data` trait ### Removed diff --git a/src/core/colour_models.rs b/src/core/colour_models.rs index 36e795a..13c33b5 100644 --- a/src/core/colour_models.rs +++ b/src/core/colour_models.rs @@ -1,6 +1,6 @@ use crate::core::traits::*; -use crate::core::{normalise_pixel_value, Image}; -use ndarray::{prelude::*, s, Zip}; +use crate::core::*; +use ndarray::{prelude::*, s, Data, Zip}; use num_traits::cast::{FromPrimitive, NumCast}; use num_traits::{Num, NumAssignOps}; use std::convert::From; @@ -182,8 +182,9 @@ where (red, green, blue) } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Clone + FromPrimitive @@ -194,8 +195,8 @@ where + Display + PixelBound, { - fn from(image: Image) -> Self { - let mut res = Array3::::zeros((image.rows(), image.cols(), HSV::channels())); + fn from(image: ImageBase) -> Self { + let mut res = Array3::<_>::zeros((image.rows(), image.cols(), HSV::channels())); let window = image.data.windows((1, 1, image.channels())); Zip::indexed(window).apply(|(i, j, _), pix| { @@ -210,8 +211,9 @@ where } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Clone + FromPrimitive @@ -222,7 +224,7 @@ where + Display + PixelBound, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let mut res = Array3::::zeros((image.rows(), image.cols(), RGB::channels())); let window = image.data.windows((1, 1, image.channels())); @@ -238,8 +240,9 @@ where } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Clone + FromPrimitive @@ -250,7 +253,7 @@ where + Display + PixelBound, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let mut res = Array3::::zeros((image.rows(), image.cols(), Gray::channels())); let window = image.data.windows((1, 1, image.channels())); @@ -268,8 +271,9 @@ where } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Clone + FromPrimitive @@ -280,7 +284,7 @@ where + Display + PixelBound, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let mut res = Array3::::zeros((image.rows(), image.cols(), RGB::channels())); let window = image.data.windows((1, 1, image.channels())); @@ -294,8 +298,9 @@ where } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Clone + FromPrimitive @@ -306,7 +311,7 @@ where + Display + PixelBound, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let mut res = Array3::::zeros((image.rows(), image.cols(), CIEXYZ::channels())); let window = image.data.windows((1, 1, image.channels())); @@ -332,8 +337,9 @@ where } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Clone + FromPrimitive @@ -344,7 +350,7 @@ where + Display + PixelBound, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let mut res = Array3::::zeros((image.rows(), image.cols(), RGB::channels())); let window = image.data.windows((1, 1, image.channels())); @@ -370,241 +376,312 @@ where } } -impl From> for Image { - fn from(image: Image) -> Self { - Self::from_data(image.data) +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { + image.into_type_raw() } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image { - fn from(image: Image) -> Self { +impl From> for ImageBase +where + T: Data, +{ + fn from(image: ImageBase) -> Self { Self::from_data(image.data) } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic4::channels()); let data = Array3::from_shape_fn(shape, |(i, j, k)| image.data[[i, j, k]]); Self::from_data(data) } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic3::channels()); let data = Array3::from_shape_fn(shape, |(i, j, k)| image.data[[i, j, k]]); Self::from_data(data) } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic2::channels()); let data = Array3::from_shape_fn(shape, |(i, j, k)| image.data[[i, j, k]]); Self::from_data(data) } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic1::channels()); let data = Array3::from_shape_fn(shape, |(i, j, k)| image.data[[i, j, k]]); Self::from_data(data) } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic3::channels()); let data = Array3::from_shape_fn(shape, |(i, j, k)| image.data[[i, j, k]]); Self::from_data(data) } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic2::channels()); let data = Array3::from_shape_fn(shape, |(i, j, k)| image.data[[i, j, k]]); Self::from_data(data) } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic1::channels()); let data = Array3::from_shape_fn(shape, |(i, j, k)| image.data[[i, j, k]]); Self::from_data(data) } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic2::channels()); let data = Array3::from_shape_fn(shape, |(i, j, k)| image.data[[i, j, k]]); Self::from_data(data) } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic1::channels()); let data = Array3::from_shape_fn(shape, |(i, j, k)| image.data[[i, j, k]]); Self::from_data(data) } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic1::channels()); let data = Array3::from_shape_fn(shape, |(i, j, k)| image.data[[i, j, k]]); Self::from_data(data) } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Num, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic5::channels()); let mut data = Array3::zeros(shape); data.slice_mut(s![.., .., 0..Generic1::channels()]) @@ -613,11 +690,12 @@ where } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Num, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic5::channels()); let mut data = Array3::zeros(shape); data.slice_mut(s![.., .., 0..Generic2::channels()]) @@ -626,11 +704,12 @@ where } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Num, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic5::channels()); let mut data = Array3::zeros(shape); data.slice_mut(s![.., .., 0..Generic3::channels()]) @@ -639,11 +718,12 @@ where } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Num, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic5::channels()); let mut data = Array3::zeros(shape); data.slice_mut(s![.., .., 0..Generic4::channels()]) @@ -652,11 +732,12 @@ where } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Num, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic4::channels()); let mut data = Array3::zeros(shape); data.slice_mut(s![.., .., 0..Generic1::channels()]) @@ -665,11 +746,12 @@ where } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Num, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic4::channels()); let mut data = Array3::zeros(shape); data.slice_mut(s![.., .., 0..Generic2::channels()]) @@ -678,11 +760,12 @@ where } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Num, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic4::channels()); let mut data = Array3::zeros(shape); data.slice_mut(s![.., .., 0..Generic3::channels()]) @@ -691,11 +774,12 @@ where } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Num, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic3::channels()); let mut data = Array3::zeros(shape); data.slice_mut(s![.., .., 0..Generic1::channels()]) @@ -704,11 +788,12 @@ where } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Num, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic3::channels()); let mut data = Array3::zeros(shape); data.slice_mut(s![.., .., 0..Generic2::channels()]) @@ -717,11 +802,12 @@ where } } -impl From> for Image +impl From> for Image where + U: Data, T: Copy + Num, { - fn from(image: Image) -> Self { + fn from(image: ImageBase) -> Self { let shape = (image.rows(), image.cols(), Generic2::channels()); let mut data = Array3::zeros(shape); data.slice_mut(s![.., .., 0..Generic1::channels()]) diff --git a/src/core/image.rs b/src/core/image.rs index c01f75d..3c9c4f2 100644 --- a/src/core/image.rs +++ b/src/core/image.rs @@ -1,16 +1,19 @@ use crate::core::colour_models::*; use crate::core::traits::PixelBound; use ndarray::prelude::*; -use ndarray::s; +use ndarray::{s, Data, DataMut, OwnedRepr, RawDataClone, ViewRepr}; use num_traits::cast::{FromPrimitive, NumCast}; use num_traits::Num; -use std::marker::PhantomData; +use std::{fmt, hash, marker::PhantomData}; + +pub type Image = ImageBase, C>; +pub type ImageView<'a, T, C> = ImageBase, C>; /// Basic structure containing an image. -#[derive(Clone, Eq, PartialEq, Hash, Debug)] -pub struct Image +pub struct ImageBase where C: ColourModel, + T: Data, { /// Images are always going to be 3D to handle rows, columns and colour /// channels @@ -20,13 +23,14 @@ where /// number of channels in an image as this may cause other functionality to /// perform incorrectly. Use conversions to one of the `Generic` colour models /// instead. - pub data: Array3, + pub data: ArrayBase, /// Representation of how colour is encoded in the image pub(crate) model: PhantomData, } -impl Image +impl ImageBase where + U: Data, T: Copy + Clone + FromPrimitive + Num + NumCast + PixelBound, C: ColourModel, { @@ -43,7 +47,21 @@ where T2::from_f64(scaled).unwrap_or_else(T2::zero) + T2::min_pixel() }; let data = self.data.map(rescale); - Image::::from_data(data) + Image::<_, C>::from_data(data) + } +} + +impl ImageBase +where + S: Data, + T: Clone, + C: ColourModel, +{ + pub fn to_owned(&self) -> Image { + Image { + data: self.data.to_owned(), + model: PhantomData, + } } } @@ -56,7 +74,7 @@ where /// a colour model pub fn new(rows: usize, columns: usize) -> Self { Image { - data: Array3::::zeros((rows, columns, C::channels())), + data: Array3::zeros((rows, columns, C::channels())), model: PhantomData, } } @@ -65,7 +83,7 @@ where /// the data sizes don't match a zero filled image will be returned instead /// of panicking pub fn from_shape_data(rows: usize, cols: usize, data: Vec) -> Self { - let data = Array3::::from_shape_vec((rows, cols, C::channels()), data) + let data = Array3::from_shape_vec((rows, cols, C::channels()), data) .unwrap_or_else(|_| Array3::::zeros((rows, cols, C::channels()))); Image { @@ -75,13 +93,28 @@ where } } -impl Image +impl ImageBase where + T: Data, + C: ColourModel, +{ + /// Create an image given an existing ndarray + pub fn from_array(data: ArrayBase) -> Self { + Self { + data, + model: PhantomData, + } + } +} + +impl ImageBase +where + T: Data, C: ColourModel, { /// Construct the image from a given Array3 - pub fn from_data(data: Array3) -> Self { - Image { + pub fn from_data(data: ArrayBase) -> Self { + Self { data, model: PhantomData, } @@ -101,16 +134,82 @@ where } /// Get a view of all colour channels at a pixels location - pub fn pixel(&self, row: usize, col: usize) -> ArrayView { + pub fn pixel(&self, row: usize, col: usize) -> ArrayView { self.data.slice(s![row, col, ..]) } + pub fn into_type_raw(self) -> ImageBase + where + C2: ColourModel, + { + assert_eq!(C2::channels(), C::channels()); + ImageBase::::from_data(self.data) + } +} + +impl ImageBase +where + T: DataMut, + C: ColourModel, +{ /// Get a mutable view of a pixels colour channels given a location - pub fn pixel_mut(&mut self, row: usize, col: usize) -> ArrayViewMut { + pub fn pixel_mut(&mut self, row: usize, col: usize) -> ArrayViewMut { self.data.slice_mut(s![row, col, ..]) } } +impl fmt::Debug for ImageBase +where + U: Data, + T: fmt::Debug, + C: ColourModel, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ColourModel={:?} Data={:?}", self.model, self.data)?; + Ok(()) + } +} + +impl PartialEq> for ImageBase +where + U: Data, + T: PartialEq, + C: ColourModel, +{ + fn eq(&self, other: &Self) -> bool { + self.model == other.model && self.data == other.data + } +} + +impl Clone for ImageBase +where + S: RawDataClone + Data, + C: ColourModel, +{ + fn clone(&self) -> Self { + Self { + data: self.data.clone(), + model: PhantomData, + } + } + + fn clone_from(&mut self, other: &Self) { + self.data.clone_from(&other.data) + } +} + +impl<'a, S, C> hash::Hash for ImageBase +where + S: Data, + S::Elem: hash::Hash, + C: ColourModel, +{ + fn hash(&self, state: &mut H) { + self.model.hash(state); + self.data.hash(state); + } +} + /// Returns a normalised pixel value or 0 if it can't convert the types. /// This should never fail if your types are good. pub fn normalise_pixel_value(t: T) -> f64 diff --git a/src/core/padding.rs b/src/core/padding.rs index f9a8e40..28c8350 100644 --- a/src/core/padding.rs +++ b/src/core/padding.rs @@ -1,5 +1,5 @@ -use crate::core::{ColourModel, Image}; -use ndarray::{prelude::*, s}; +use crate::core::{ColourModel, Image, ImageBase}; +use ndarray::{prelude::*, s, Data, OwnedRepr}; use num_traits::identities::Zero; use std::marker::PhantomData; @@ -11,7 +11,11 @@ where { /// Taking in the image data and the margin to apply to rows and columns /// returns a padded image - fn pad(&self, image: ArrayView3, padding: (usize, usize)) -> Array3; + fn pad( + &self, + image: ArrayView, + padding: (usize, usize), + ) -> ArrayBase, Ix3>; } /// Doesn't apply any padding to the image returning it unaltered regardless @@ -33,7 +37,11 @@ impl PaddingStrategy for NoPadding where T: Copy, { - fn pad(&self, image: ArrayView3, _padding: (usize, usize)) -> Array3 { + fn pad( + &self, + image: ArrayView, + _padding: (usize, usize), + ) -> ArrayBase, Ix3> { image.to_owned() } } @@ -42,7 +50,11 @@ impl PaddingStrategy for ConstantPadding where T: Copy, { - fn pad(&self, image: ArrayView3, padding: (usize, usize)) -> Array3 { + fn pad( + &self, + image: ArrayView, + padding: (usize, usize), + ) -> ArrayBase, Ix3> { let shape = ( image.shape()[0] + padding.0 * 2, image.shape()[1] + padding.1 * 2, @@ -66,40 +78,46 @@ impl PaddingStrategy for ZeroPadding where T: Copy + Zero, { - fn pad(&self, image: ArrayView3, padding: (usize, usize)) -> Array3 { + fn pad( + &self, + image: ArrayView, + padding: (usize, usize), + ) -> ArrayBase, Ix3> { let padder = ConstantPadding(T::zero()); padder.pad(image, padding) } } /// Padding extension for images -pub trait PaddingExt { - /// Data type for container - type Data; +pub trait PaddingExt { + /// Type of the output image + type Output; /// Pad the object with the given padding and strategy - fn pad(&self, padding: (usize, usize), strategy: &dyn PaddingStrategy) -> Self; + fn pad(&self, padding: (usize, usize), strategy: &dyn PaddingStrategy) -> Self::Output; } -impl PaddingExt for Array3 +impl PaddingExt for ArrayBase where + U: Data, T: Copy, { - type Data = T; + type Output = ArrayBase, Ix3>; - fn pad(&self, padding: (usize, usize), strategy: &dyn PaddingStrategy) -> Self { + fn pad(&self, padding: (usize, usize), strategy: &dyn PaddingStrategy) -> Self::Output { strategy.pad(self.view(), padding) } } -impl PaddingExt for Image +impl PaddingExt for ImageBase where + U: Data, T: Copy, C: ColourModel, { - type Data = T; + type Output = Image; - fn pad(&self, padding: (usize, usize), strategy: &dyn PaddingStrategy) -> Self { - Self { + fn pad(&self, padding: (usize, usize), strategy: &dyn PaddingStrategy) -> Self::Output { + Self::Output { data: strategy.pad(self.data.view(), padding), model: PhantomData, } diff --git a/src/enhancement/histogram_equalisation.rs b/src/enhancement/histogram_equalisation.rs index e2e172a..dae2bdf 100644 --- a/src/enhancement/histogram_equalisation.rs +++ b/src/enhancement/histogram_equalisation.rs @@ -1,5 +1,5 @@ use crate::core::*; -use ndarray::prelude::*; +use ndarray::{prelude::*, DataMut}; use ndarray_stats::{histogram::Grid, HistogramExt}; use num_traits::cast::{FromPrimitive, ToPrimitive}; use num_traits::{Num, NumAssignOps}; @@ -10,21 +10,25 @@ pub trait HistogramEqExt where A: Ord, { + type Output; /// Equalises an image histogram returning a new image. /// Grids should be for a 1xN image as the image is flattened during processing - fn equalise_hist(&self, grid: Grid) -> Self; + fn equalise_hist(&self, grid: Grid) -> Self::Output; /// Equalises an image histogram inplace /// Grids should be for a 1xN image as the image is flattened during processing fn equalise_hist_inplace(&mut self, grid: Grid); } -impl HistogramEqExt for Array3 +impl HistogramEqExt for ArrayBase where + U: DataMut, T: Copy + Clone + Ord + Num + NumAssignOps + ToPrimitive + FromPrimitive + PixelBound, { - fn equalise_hist(&self, grid: Grid) -> Self { - let mut result = self.clone(); + type Output = Array; + + fn equalise_hist(&self, grid: Grid) -> Self::Output { + let mut result = self.to_owned(); result.equalise_hist_inplace(grid); result } @@ -71,14 +75,16 @@ where } } -impl HistogramEqExt for Image +impl HistogramEqExt for ImageBase where - Image: Clone, + U: DataMut, T: Copy + Clone + Ord + Num + NumAssignOps + ToPrimitive + FromPrimitive + PixelBound, C: ColourModel, { - fn equalise_hist(&self, grid: Grid) -> Self { - let mut result = self.clone(); + type Output = Image; + + fn equalise_hist(&self, grid: Grid) -> Self::Output { + let mut result = self.to_owned(); result.equalise_hist_inplace(grid); result } diff --git a/src/format/mod.rs b/src/format/mod.rs index 55a1dd3..f3c5c8a 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -1,5 +1,6 @@ use crate::core::traits::PixelBound; use crate::core::*; +use ndarray::Data; use num_traits::cast::{FromPrimitive, NumCast}; use num_traits::{Num, NumAssignOps}; use std::fmt::Display; @@ -8,17 +9,22 @@ use std::io::prelude::*; use std::path::Path; /// Trait for an image encoder -pub trait Encoder +pub trait Encoder where + U: Data, T: Copy + Clone + Num + NumAssignOps + NumCast + PartialOrd + Display + PixelBound, C: ColourModel, { /// Encode an image into a sequence of bytes for the given format - fn encode(&self, image: &Image) -> Vec; + fn encode(&self, image: &ImageBase) -> Vec; /// Encode an image saving it to the file at filename. This function shouldn't /// add an extension preferring the user to do that instead. - fn encode_file>(&self, image: &Image, filename: P) -> std::io::Result<()> { + fn encode_file>( + &self, + image: &ImageBase, + filename: P, + ) -> std::io::Result<()> { let mut file = File::create(filename)?; file.write_all(&self.encode(image))?; Ok(()) diff --git a/src/format/netpbm.rs b/src/format/netpbm.rs index 3f540af..a87e6f5 100644 --- a/src/format/netpbm.rs +++ b/src/format/netpbm.rs @@ -1,5 +1,6 @@ -use crate::core::{normalise_pixel_value, Image, PixelBound, RGB}; +use crate::core::{normalise_pixel_value, Image, ImageBase, PixelBound, RGB}; use crate::format::{Decoder, Encoder}; +use ndarray::Data; use num_traits::cast::{FromPrimitive, NumCast}; use num_traits::{Num, NumAssignOps}; use std::fmt::Display; @@ -32,8 +33,9 @@ impl Default for PpmEncoder { /// The ColourModel type argument is locked to RGB - this prevents calling /// RGB::into::() unnecessarily which is unavoidable until trait specialisation is /// stabilised. -impl Encoder for PpmEncoder +impl Encoder for PpmEncoder where + U: Data, T: Copy + Clone + Num @@ -44,7 +46,7 @@ where + PixelBound + FromPrimitive, { - fn encode(&self, image: &Image) -> Vec { + fn encode(&self, image: &ImageBase) -> Vec { use EncodingType::*; match self.encoding { Plaintext => self.encode_plaintext(image), @@ -71,8 +73,9 @@ impl PpmEncoder { /// Gets the maximum pixel value in the image across all channels. This is /// used in the PPM header - fn get_max_value(image: &Image) -> Option + fn get_max_value(image: &ImageBase) -> Option where + U: Data, T: Copy + Clone + Num + NumAssignOps + NumCast + PartialOrd + Display + PixelBound, { image @@ -92,8 +95,9 @@ impl PpmEncoder { } /// Encode the image into the binary PPM format (P6) returning the bytes - fn encode_binary(self, image: &Image) -> Vec + fn encode_binary(self, image: &ImageBase) -> Vec where + U: Data, T: Copy + Clone + Num + NumAssignOps + NumCast + PartialOrd + Display + PixelBound, { let max_val = Self::get_max_value(image).unwrap_or_else(|| 255); @@ -113,8 +117,9 @@ impl PpmEncoder { /// Encode the image into the plaintext PPM format (P3) returning the text as /// an array of bytes - fn encode_plaintext(self, image: &Image) -> Vec + fn encode_plaintext(self, image: &ImageBase) -> Vec where + U: Data, T: Copy + Clone + Num + NumAssignOps + NumCast + PartialOrd + Display + PixelBound, { let max_val = 255; diff --git a/src/processing/canny.rs b/src/processing/canny.rs index 8841a6d..bf56327 100644 --- a/src/processing/canny.rs +++ b/src/processing/canny.rs @@ -1,7 +1,7 @@ -use crate::core::{ColourModel, Image}; +use crate::core::{ColourModel, Image, ImageBase}; use crate::processing::*; use ndarray::prelude::*; -use ndarray::IntoDimension; +use ndarray::{DataMut, IntoDimension}; use num_traits::{cast::FromPrimitive, real::Real, Num, NumAssignOps}; use std::collections::HashSet; use std::marker::PhantomData; @@ -38,8 +38,9 @@ pub struct CannyParameters { pub t2: T, } -impl CannyEdgeDetectorExt for Image +impl CannyEdgeDetectorExt for ImageBase where + U: DataMut, T: Copy + Clone + FromPrimitive + Real + Num + NumAssignOps, C: ColourModel, { @@ -54,8 +55,9 @@ where } } -impl CannyEdgeDetectorExt for Array3 +impl CannyEdgeDetectorExt for ArrayBase where + U: DataMut, T: Copy + Clone + FromPrimitive + Real + Num + NumAssignOps, { type Output = Array3; diff --git a/src/processing/conv.rs b/src/processing/conv.rs index 18cd90b..14f53dd 100644 --- a/src/processing/conv.rs +++ b/src/processing/conv.rs @@ -1,8 +1,8 @@ use crate::core::padding::*; -use crate::core::{ColourModel, Image}; +use crate::core::{ColourModel, Image, ImageBase}; use crate::processing::Error; use ndarray::prelude::*; -use ndarray::{s, Zip}; +use ndarray::{s, DataMut, Zip}; use num_traits::{Num, NumAssignOps}; use std::marker::PhantomData; use std::marker::Sized; @@ -14,10 +14,12 @@ where { /// Underlying data type to perform the convolution on type Data; + /// Type for the output as data will have to be allocated + type Output; /// Perform a convolution returning the resultant data /// applies the default padding of zero padding - fn conv2d(&self, kernel: ArrayView3) -> Result; + fn conv2d(&self, kernel: ArrayView3) -> Result; /// Performs the convolution inplace mutating the containers data /// applies the default padding of zero padding fn conv2d_inplace(&mut self, kernel: ArrayView3) -> Result<(), Error>; @@ -27,7 +29,7 @@ where &self, kernel: ArrayView3, strategy: &dyn PaddingStrategy, - ) -> Result; + ) -> Result; /// Performs the convolution inplace mutating the containers data /// applies the default padding of zero padding fn conv2d_inplace_with_padding( @@ -43,18 +45,20 @@ fn kernel_centre(rows: usize, cols: usize) -> (usize, usize) { (row_offset, col_offset) } -impl ConvolutionExt for Array3 +impl ConvolutionExt for ArrayBase where + U: DataMut, T: Copy + Clone + Num + NumAssignOps, { type Data = T; + type Output = Array; - fn conv2d(&self, kernel: ArrayView3) -> Result { + fn conv2d(&self, kernel: ArrayView3) -> Result { self.conv2d_with_padding(kernel, &ZeroPadding {}) } fn conv2d_inplace(&mut self, kernel: ArrayView3) -> Result<(), Error> { - *self = self.conv2d_with_padding(kernel, &ZeroPadding {})?; + self.assign(&self.conv2d_with_padding(kernel, &ZeroPadding {})?); Ok(()) } @@ -62,7 +66,7 @@ where &self, kernel: ArrayView3, strategy: &dyn PaddingStrategy, - ) -> Result { + ) -> Result { if self.shape()[2] != kernel.shape()[2] { Err(Error::ChannelDimensionMismatch) } else { @@ -73,7 +77,7 @@ where let shape = (self.shape()[0], self.shape()[1], self.shape()[2]); if shape.0 > 0 && shape.1 > 0 { - let mut result = Self::zeros(shape); + let mut result = Self::Output::zeros(shape); let tmp = self.pad((row_offset, col_offset), strategy); Zip::indexed(tmp.windows(kernel.dim())).apply(|(i, j, _), window| { @@ -93,20 +97,23 @@ where kernel: ArrayView3, strategy: &dyn PaddingStrategy, ) -> Result<(), Error> { - *self = self.conv2d_with_padding(kernel, strategy)?; + self.assign(&self.conv2d_with_padding(kernel, strategy)?); Ok(()) } } -impl ConvolutionExt for Image +impl ConvolutionExt for ImageBase where + U: DataMut, T: Copy + Clone + Num + NumAssignOps, C: ColourModel, { type Data = T; - fn conv2d(&self, kernel: ArrayView3) -> Result { + type Output = Image; + + fn conv2d(&self, kernel: ArrayView3) -> Result { let data = self.data.conv2d(kernel)?; - Ok(Self { + Ok(Self::Output { data, model: PhantomData, }) @@ -120,9 +127,9 @@ where &self, kernel: ArrayView3, strategy: &dyn PaddingStrategy, - ) -> Result { + ) -> Result { let data = self.data.conv2d_with_padding(kernel, strategy)?; - Ok(Self { + Ok(Self::Output { data, model: PhantomData, }) diff --git a/src/processing/filter.rs b/src/processing/filter.rs index f0a3f40..3c97ec6 100644 --- a/src/processing/filter.rs +++ b/src/processing/filter.rs @@ -1,6 +1,6 @@ -use crate::core::{ColourModel, Image}; +use crate::core::{ColourModel, Image, ImageBase}; use ndarray::prelude::*; -use ndarray::{IntoDimension, Zip}; +use ndarray::{Data, IntoDimension, OwnedRepr, Zip}; use ndarray_stats::interpolate::*; use ndarray_stats::Quantile1dExt; use noisy_float::types::n64; @@ -11,18 +11,22 @@ use std::marker::PhantomData; /// Median filter, given a region to move over the image, each pixel is given /// the median value of itself and it's neighbours pub trait MedianFilterExt { + type Output; /// Run the median filter given the region. Median is assumed to be calculated /// independently for each channel. - fn median_filter(&self, region: E) -> Self + fn median_filter(&self, region: E) -> Self::Output where E: IntoDimension; } -impl MedianFilterExt for Array3 +impl MedianFilterExt for ArrayBase where + U: Data, T: Copy + Clone + FromPrimitive + ToPrimitive + Num + Ord, { - fn median_filter(&self, region: E) -> Self + type Output = ArrayBase, Ix3>; + + fn median_filter(&self, region: E) -> Self::Output where E: IntoDimension, { @@ -43,12 +47,15 @@ where } } -impl MedianFilterExt for Image +impl MedianFilterExt for ImageBase where + U: Data, T: Copy + Clone + FromPrimitive + ToPrimitive + Num + Ord, C: ColourModel, { - fn median_filter(&self, region: E) -> Self + type Output = Image; + + fn median_filter(&self, region: E) -> Self::Output where E: IntoDimension, { diff --git a/src/processing/kernels.rs b/src/processing/kernels.rs index 19d9faf..f7ccf63 100644 --- a/src/processing/kernels.rs +++ b/src/processing/kernels.rs @@ -44,7 +44,7 @@ pub struct LaplaceFilter; #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub enum LaplaceType { /// Standard filter and the default parameter choice, for a 3x3x1 matrix it is: - /// ```ignore + /// ```text /// [0, -1, 0] /// [-1, 4, -1] /// [0, -1, 0] @@ -52,7 +52,7 @@ pub enum LaplaceType { Standard, /// The diagonal filter also contains derivatives for diagonal lines and /// for a 3x3x1 matrix is given by: - /// ```ignore + /// ```text /// [-1, -1, -1] /// [-1, 8, -1] /// [-1, -1, -1] @@ -102,7 +102,7 @@ where { /// The parameter for the Gaussian filter is the horizontal and vertical /// covariances to form the covariance matrix. - /// ```ignore + /// ```text /// [ Params[0], 0] /// [ 0, Params[1]] /// ``` diff --git a/src/processing/sobel.rs b/src/processing/sobel.rs index a4d6ecf..0842fc4 100644 --- a/src/processing/sobel.rs +++ b/src/processing/sobel.rs @@ -1,7 +1,7 @@ use crate::core::*; use crate::processing::*; use core::ops::Neg; -use ndarray::prelude::*; +use ndarray::{prelude::*, DataMut, OwnedRepr}; use num_traits::{cast::FromPrimitive, real::Real, Num, NumAssignOps}; use std::marker::Sized; @@ -13,15 +13,16 @@ where /// Type to output type Output; /// Returns the magnitude output of the sobel - an image of only lines - fn apply_sobel(&self) -> Result; + fn apply_sobel(&self) -> Result; /// Returns the magntitude and rotation outputs for use in other algorithms /// like the Canny edge detector. Rotation is in radians fn full_sobel(&self) -> Result<(Self::Output, Self::Output), Error>; } -fn get_edge_images(mat: &Array3) -> Result<(Array3, Array3), Error> +fn get_edge_images(mat: &ArrayBase) -> Result<(Array3, Array3), Error> where + U: DataMut, T: Copy + Clone + Num + NumAssignOps + Neg + FromPrimitive + Real, { let v_temp: Array3 = SobelFilter::build_with_params(Orientation::Vertical).unwrap(); @@ -36,13 +37,14 @@ where Ok((h_deriv, v_deriv)) } -impl SobelExt for Array3 +impl SobelExt for ArrayBase where + U: DataMut, T: Copy + Clone + Num + NumAssignOps + Neg + FromPrimitive + Real, { - type Output = Self; + type Output = ArrayBase, Ix3>; - fn apply_sobel(&self) -> Result { + fn apply_sobel(&self) -> Result { let (h_deriv, v_deriv) = get_edge_images(self)?; let h_deriv = h_deriv.mapv(|x| x.powi(2)); @@ -71,19 +73,22 @@ where } } -impl SobelExt for Image +impl SobelExt for ImageBase where + U: DataMut, T: Copy + Clone + Num + NumAssignOps + Neg + FromPrimitive + Real, C: ColourModel, { - type Output = Array3; + type Output = Image; - fn apply_sobel(&self) -> Result { + fn apply_sobel(&self) -> Result { let data = self.data.apply_sobel()?; Ok(Image::from_data(data)) } fn full_sobel(&self) -> Result<(Self::Output, Self::Output), Error> { - self.data.full_sobel() + self.data + .full_sobel() + .map(|(m, r)| (Image::from_data(m), Image::from_data(r))) } } diff --git a/src/processing/threshold.rs b/src/processing/threshold.rs index 49304f0..f4ea498 100644 --- a/src/processing/threshold.rs +++ b/src/processing/threshold.rs @@ -1,7 +1,7 @@ use crate::core::PixelBound; -use crate::core::{ColourModel, Image}; +use crate::core::{ColourModel, Image, ImageBase}; use crate::processing::*; -use ndarray::prelude::*; +use ndarray::{prelude::*, Data}; use ndarray_stats::histogram::{Bins, Edges, Grid}; use ndarray_stats::HistogramExt; use ndarray_stats::QuantileExt; @@ -41,9 +41,10 @@ pub trait ThresholdMeanExt { fn threshold_mean(&self) -> Result; } -impl ThresholdOtsuExt for Image +impl ThresholdOtsuExt for ImageBase where - Image: Clone, + U: Data, + Image: Clone, T: Copy + Clone + Ord + Num + NumAssignOps + ToPrimitive + FromPrimitive + PixelBound, C: ColourModel, { @@ -58,8 +59,9 @@ where } } -impl ThresholdOtsuExt for Array3 +impl ThresholdOtsuExt for ArrayBase where + U: Data, T: Copy + Clone + Ord + Num + NumAssignOps + ToPrimitive + FromPrimitive, { type Output = Array3; @@ -68,7 +70,7 @@ where if self.shape()[2] > 1 { Err(Error::ChannelDimensionMismatch) } else { - let value = calculate_threshold_otsu(&self)?; + let value = calculate_threshold_otsu(self)?; let mask = apply_threshold(self, value); Ok(mask) } @@ -82,8 +84,9 @@ where /// i.e. single channel; otherwise we need to output all 3 threshold values). /// Todo: Add optional nbins /// -fn calculate_threshold_otsu(mat: &Array3) -> Result +fn calculate_threshold_otsu(mat: &ArrayBase) -> Result where + U: Data, T: Copy + Clone + Ord + Num + NumAssignOps + ToPrimitive + FromPrimitive, { let mut threshold = 0.0; @@ -132,9 +135,10 @@ where Ok(threshold) } -impl ThresholdMeanExt for Image +impl ThresholdMeanExt for ImageBase where - Image: Clone, + U: Data, + Image: Clone, T: Copy + Clone + Ord + Num + NumAssignOps + ToPrimitive + FromPrimitive + PixelBound, C: ColourModel, { @@ -149,8 +153,9 @@ where } } -impl ThresholdMeanExt for Array3 +impl ThresholdMeanExt for ArrayBase where + U: Data, T: Copy + Clone + Ord + Num + NumAssignOps + ToPrimitive + FromPrimitive, { type Output = Array3; @@ -159,22 +164,24 @@ where if self.shape()[2] > 1 { Err(Error::ChannelDimensionMismatch) } else { - let value = calculate_threshold_mean(&self)?; + let value = calculate_threshold_mean(self)?; let mask = apply_threshold(self, value); Ok(mask) } } } -fn calculate_threshold_mean(array: &Array3) -> Result +fn calculate_threshold_mean(array: &ArrayBase) -> Result where + U: Data, T: Copy + Clone + Num + NumAssignOps + ToPrimitive + FromPrimitive, { Ok(array.sum().to_f64().unwrap() / array.len() as f64) } -fn apply_threshold(data: &Array3, threshold: f64) -> Array3 +fn apply_threshold(data: &ArrayBase, threshold: f64) -> Array3 where + U: Data, T: Copy + Clone + Num + NumAssignOps + ToPrimitive + FromPrimitive, { let result = data.mapv(|x| x.to_f64().unwrap() >= threshold); diff --git a/src/transform/mod.rs b/src/transform/mod.rs index 9db811c..c97789d 100644 --- a/src/transform/mod.rs +++ b/src/transform/mod.rs @@ -1,10 +1,9 @@ -use crate::core::{ColourModel, Image}; +use crate::core::{ColourModel, Image, ImageBase}; use crate::transform::affine::translation; use ndarray::{array, prelude::*, s, Data}; use ndarray_linalg::solve::Inverse; use num_traits::{Num, NumAssignOps}; use std::cmp::{max, min}; -use std::marker::PhantomData; pub mod affine; @@ -88,7 +87,7 @@ where T: Copy + Clone + Num + NumAssignOps, U: Data, { - type Output = Array3; + type Output = Array; fn transform( &self, @@ -139,12 +138,13 @@ where } } -impl TransformExt for Image +impl TransformExt for ImageBase where + U: Data, T: Copy + Clone + Num + NumAssignOps, C: ColourModel, { - type Output = Self; + type Output = Image; fn transform( &self, @@ -152,10 +152,8 @@ where output_size: Option<(usize, usize)>, ) -> Result { let data = self.data.transform(transform, output_size)?; - Ok(Self { - data, - model: PhantomData, - }) + let result = Self::Output::from_array(data).to_owned(); + Ok(result) } }