pub mod moments;
pub mod summary;
#[doc(inline)]
pub use moments::{CentralMoments, ImageMoments, NormalizedMoments, image_moments};
#[doc(inline)]
pub use summary::{ChannelStatistics, StatisticsChannel};
use crate::image::RasterImage;
use crate::pixel::{HomogeneousPixel, SingleChannel};
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a statistics output shape for the pixel type `{P}`",
label = "the single-record `ChannelStatistics<C>` shape requires a single-channel pixel",
note = "use `Vec<ChannelStatistics<C>>` or `[ChannelStatistics<C>; N]` for a pixel type \
with more than one channel"
)]
pub trait StatisticsOutput<C, P>: Sized {
fn collect(channel_count: usize, compute: impl FnMut(usize) -> ChannelStatistics<C>) -> Self;
}
impl<P> StatisticsOutput<P::Channel, P> for ChannelStatistics<P::Channel>
where
P: SingleChannel,
{
fn collect(
channel_count: usize,
mut compute: impl FnMut(usize) -> ChannelStatistics<P::Channel>,
) -> Self {
debug_assert_eq!(channel_count, 1);
compute(0)
}
}
impl<C, P> StatisticsOutput<C, P> for Vec<ChannelStatistics<C>> {
fn collect(
channel_count: usize,
mut compute: impl FnMut(usize) -> ChannelStatistics<C>,
) -> Self {
(0..channel_count).map(&mut compute).collect()
}
}
impl<C, P, const N: usize> StatisticsOutput<C, P> for [ChannelStatistics<C>; N] {
fn collect(channel_count: usize, compute: impl FnMut(usize) -> ChannelStatistics<C>) -> Self {
assert_eq!(
channel_count, N,
"image_statistics() called with output type `[ChannelStatistics<C>; {N}]` on a pixel \
with {channel_count} channels",
);
core::array::from_fn(compute)
}
}
#[must_use]
pub fn image_statistics<I, P, O>(image: &I) -> O
where
I: RasterImage<Pixel = P>,
P: HomogeneousPixel,
P::Channel: StatisticsChannel,
O: StatisticsOutput<P::Channel, P>,
{
O::collect(P::CHANNEL_COUNT, |channel| {
let mut stats = ChannelStatistics::empty();
for y in 0..image.height() {
for pixel in image.row(y) {
stats.push(pixel.channel(channel));
}
}
stats
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image::Image;
use crate::pixel::{Mono8, Mono16, MonoF32, Rgb8, RgbF32};
#[test]
fn a_uniform_image_has_zero_spread() {
let image = Image::fill(10, 7, Mono8::new(42));
let stats: ChannelStatistics<_> = image_statistics(&image);
assert_eq!(stats.count, 70);
assert_eq!(stats.min().map(|c| c.0), Some(42));
assert_eq!(stats.max().map(|c| c.0), Some(42));
assert_eq!(stats.mean(), Some(42.0));
assert_eq!(stats.variance(), Some(0.0));
assert_eq!(stats.std_dev(), Some(0.0));
}
#[test]
fn an_empty_image_reports_absence() {
let image: Image<Mono8> = Image::generate(0, 0, |_, _| Mono8::new(0));
let stats: ChannelStatistics<_> = image_statistics(&image);
assert_eq!(stats.count, 0);
assert_eq!(stats.mean(), None);
assert_eq!(stats.min(), None);
}
#[test]
fn a_zero_height_image_reports_absence_too() {
let image: Image<Mono8> = Image::generate(16, 0, |_, _| Mono8::new(0));
let stats: ChannelStatistics<_> = image_statistics(&image);
assert_eq!(stats.count, 0);
assert_eq!(stats.mean(), None);
}
#[test]
fn each_channel_is_summarised_independently() {
let image = Image::generate(4, 4, |x, y| Rgb8::new((x * 10) as u8, (y * 20) as u8, 200));
let [r, g, b]: [ChannelStatistics<_>; 3] = image_statistics(&image);
assert_eq!(r.min().map(|c| c.0), Some(0));
assert_eq!(r.max().map(|c| c.0), Some(30));
assert_eq!(r.mean(), Some(15.0));
assert_eq!(g.min().map(|c| c.0), Some(0));
assert_eq!(g.max().map(|c| c.0), Some(60));
assert_eq!(g.mean(), Some(30.0));
assert_eq!(b.min().map(|c| c.0), Some(200));
assert_eq!(b.variance(), Some(0.0));
}
#[test]
fn the_vec_shape_matches_the_array_shape() {
let image = Image::generate(3, 3, |x, _| RgbF32::new(x as f32, 1.0, -1.0));
let listed: Vec<ChannelStatistics<_>> = image_statistics(&image);
let fixed: [ChannelStatistics<_>; 3] = image_statistics(&image);
assert_eq!(listed.len(), 3);
for (a, b) in listed.iter().zip(fixed.iter()) {
assert_eq!(a, b);
}
}
#[test]
#[should_panic(expected = "with 3 channels")]
fn a_wrong_array_length_panics() {
let image = Image::fill(2, 2, Rgb8::new(1, 2, 3));
let _stats: [ChannelStatistics<_>; 4] = image_statistics(&image);
}
#[test]
fn nan_pixels_are_counted_not_averaged_in() {
let image = Image::generate(4, 1, |x, _| {
MonoF32::new(if x == 2 { f32::NAN } else { 2.0 })
});
let stats: ChannelStatistics<_> = image_statistics(&image);
assert_eq!(stats.count, 3);
assert_eq!(stats.nan_count, 1);
assert_eq!(stats.mean(), Some(2.0));
}
#[test]
fn sixteen_bit_input_keeps_its_precision_in_the_mean() {
let image = Image::generate(1024, 1024, |x, _| {
Mono16::new(if x % 2 == 0 { 65_535 } else { 65_533 })
});
let stats: ChannelStatistics<_> = image_statistics(&image);
assert_eq!(stats.mean(), Some(65_534.0));
assert_eq!(stats.min().map(|c| c.0), Some(65_533));
assert_eq!(stats.max().map(|c| c.0), Some(65_535));
assert_eq!(stats.variance(), Some(1.0));
}
#[test]
fn the_generic_mono_family_is_accepted() {
use crate::pixel::Mono;
let image = Image::generate(8, 8, |x, _| Mono::<12>::new((x * 500) as u16));
let stats: ChannelStatistics<_> = image_statistics(&image);
assert_eq!(stats.count, 64);
assert_eq!(stats.min().map(|c| c.0), Some(0));
assert_eq!(stats.max().map(|c| c.0), Some(3500));
}
}