pub mod engine;
pub mod strategy;
pub mod equalize;
pub mod otsu;
pub use engine::Histogram;
pub use strategy::{BinIndex, BinningStrategy, CustomBins, LinearBins, NaturalBins};
pub use equalize::{equalization_lut, equalize_image, equalize_image_into};
pub use otsu::{otsu_binary_mask, otsu_threshold};
use crate::Error;
use crate::image::RasterImage;
use crate::pixel::HomogeneousPixel;
pub trait HistogramOutput<S, V>: Sized {
fn collect(
channel_count: usize,
compute: impl FnMut(usize) -> Result<Histogram<S, V>, Error>,
) -> Result<Self, Error>;
}
impl<S, V> HistogramOutput<S, V> for Histogram<S, V> {
fn collect(
channel_count: usize,
mut compute: impl FnMut(usize) -> Result<Histogram<S, V>, Error>,
) -> Result<Self, Error> {
assert_eq!(
channel_count, 1,
"histogram() called with output type `Histogram<S, V>` on a pixel with {} \
channels; use `Vec<Histogram<S, V>>` or `[Histogram<S, V>; N]` instead",
channel_count
);
compute(0)
}
}
impl<S, V> HistogramOutput<S, V> for Vec<Histogram<S, V>> {
fn collect(
channel_count: usize,
mut compute: impl FnMut(usize) -> Result<Histogram<S, V>, Error>,
) -> Result<Self, Error> {
let mut out = Vec::with_capacity(channel_count);
for c in 0..channel_count {
out.push(compute(c)?);
}
Ok(out)
}
}
impl<S, V, const N: usize> HistogramOutput<S, V> for [Histogram<S, V>; N] {
fn collect(
channel_count: usize,
mut compute: impl FnMut(usize) -> Result<Histogram<S, V>, Error>,
) -> Result<Self, Error> {
assert_eq!(
channel_count, N,
"histogram() called with output type `[Histogram<S, V>; {}]` on a pixel with \
{} channels",
N, channel_count
);
let mut tmp: Vec<Histogram<S, V>> = Vec::with_capacity(N);
for c in 0..N {
tmp.push(compute(c)?);
}
Ok(tmp.try_into().unwrap_or_else(|_| {
unreachable!("internal error: vec length != N after collect")
}))
}
}
pub fn histogram<P, S, O>(image: &impl RasterImage<Pixel = P>, strategy: &S) -> Result<O, Error>
where
P: HomogeneousPixel,
S: BinningStrategy<P::Channel> + Clone,
O: HistogramOutput<S, P::Channel>,
{
strategy.validate()?;
O::collect(P::CHANNEL_COUNT, |channel| {
Ok(compute_channel_histogram(image, channel, strategy))
})
}
fn compute_channel_histogram<P, S>(
image: &impl RasterImage<Pixel = P>,
channel: usize,
strategy: &S,
) -> Histogram<S, P::Channel>
where
P: HomogeneousPixel,
S: BinningStrategy<P::Channel> + Clone,
{
let bin_count = strategy.bin_count();
let mut bins = vec![0u64; bin_count];
let mut nan_count: u64 = 0;
let mut underflow_count: u64 = 0;
let mut overflow_count: u64 = 0;
for y in 0..image.height() {
let row = image.row(y);
for px in row {
let value = px.channel(channel);
match strategy.bin_index(value) {
BinIndex::In(i) => {
debug_assert!(
i < bins.len(),
"BinningStrategy::bin_index returned In({}) but bin_count = {}",
i,
bins.len()
);
bins[i] += 1;
}
BinIndex::Underflow => underflow_count += 1,
BinIndex::Overflow => overflow_count += 1,
BinIndex::Nan => nan_count += 1,
}
}
}
Histogram::new(
strategy.clone(),
bins,
nan_count,
underflow_count,
overflow_count,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image::{Image, SubView};
use crate::pixel::{Indexed8, Mono8, MonoF32, Rgb8};
use std::num::Saturating;
#[allow(unused_imports)]
use crate::image::SubView as _;
#[test]
fn mono8_natural_single_histogram() {
let img = Image::from_vec(
2,
2,
vec![Mono8::new(0), Mono8::new(1), Mono8::new(1), Mono8::new(255)],
)
.unwrap();
let h: Histogram<NaturalBins, Saturating<u8>> = histogram(&img, &NaturalBins).unwrap();
assert_eq!(h.count_at_bin(0), 1);
assert_eq!(h.count_at_bin(1), 2);
assert_eq!(h.count_at_bin(255), 1);
assert_eq!(h.total_count, 4);
assert_eq!(h.nan_count, 0);
assert_eq!(h.underflow_count, 0);
assert_eq!(h.overflow_count, 0);
}
#[test]
fn indexed8_natural_uses_u8_channel_type() {
let img = Image::from_vec(
2,
2,
vec![Indexed8(3), Indexed8(3), Indexed8(7), Indexed8(200)],
)
.unwrap();
let h: Histogram<NaturalBins, u8> = histogram(&img, &NaturalBins).unwrap();
assert_eq!(h.count_at_bin(3), 2);
assert_eq!(h.count_at_bin(7), 1);
assert_eq!(h.count_at_bin(200), 1);
assert_eq!(h.total_count, 4);
}
#[test]
fn rgb8_natural_array_output() {
let img = Image::from_vec(1, 1, vec![Rgb8::new(10, 20, 30)]).unwrap();
let [r, g, b]: [Histogram<NaturalBins, Saturating<u8>>; 3] =
histogram(&img, &NaturalBins).unwrap();
assert_eq!(r.count_at_bin(10), 1);
assert_eq!(g.count_at_bin(20), 1);
assert_eq!(b.count_at_bin(30), 1);
}
#[test]
fn rgb8_natural_vec_output() {
let img =
Image::from_vec(1, 2, vec![Rgb8::new(10, 20, 30), Rgb8::new(10, 21, 30)]).unwrap();
let v: Vec<Histogram<NaturalBins, Saturating<u8>>> = histogram(&img, &NaturalBins).unwrap();
assert_eq!(v.len(), 3);
assert_eq!(v[0].count_at_bin(10), 2); assert_eq!(v[1].count_at_bin(20), 1); assert_eq!(v[1].count_at_bin(21), 1); assert_eq!(v[2].count_at_bin(30), 2); }
#[test]
#[should_panic(expected = "channels")]
fn rgb8_requested_as_single_histogram_panics() {
let img = Image::from_vec(1, 1, vec![Rgb8::new(0, 0, 0)]).unwrap();
let _: Histogram<NaturalBins, Saturating<u8>> = histogram(&img, &NaturalBins).unwrap();
}
#[test]
#[should_panic(expected = "channels")]
fn rgb8_requested_as_4_array_panics() {
let img = Image::from_vec(1, 1, vec![Rgb8::new(0, 0, 0)]).unwrap();
let _: [Histogram<NaturalBins, Saturating<u8>>; 4] = histogram(&img, &NaturalBins).unwrap();
}
#[test]
fn monof32_linear_counts_in_range_nan_underflow_overflow() {
let img = Image::from_vec(
5,
1,
vec![
MonoF32::new(0.0),
MonoF32::new(0.25),
MonoF32::new(f32::NAN),
MonoF32::new(-0.5),
MonoF32::new(2.0),
],
)
.unwrap();
let s = LinearBins {
min: 0.0,
max: 1.0,
bin_count: 4,
};
let h: Histogram<LinearBins, f32> = histogram(&img, &s).unwrap();
assert_eq!(h.count_at_bin(0), 1); assert_eq!(h.count_at_bin(1), 1); assert_eq!(h.nan_count, 1);
assert_eq!(h.underflow_count, 1);
assert_eq!(h.overflow_count, 1);
assert_eq!(h.total_count, 5);
}
#[test]
fn invalid_linear_returns_error() {
let img = Image::from_vec(1, 1, vec![MonoF32::new(0.0)]).unwrap();
let bad = LinearBins {
min: 1.0,
max: 0.0,
bin_count: 4,
};
let r: Result<Histogram<LinearBins, f32>, _> = histogram(&img, &bad);
match r {
Err(Error::InvalidBinningStrategy(_)) => {}
other => panic!("expected InvalidBinningStrategy, got {:?}", other),
}
}
#[test]
fn invalid_custom_returns_error() {
let img = Image::from_vec(1, 1, vec![MonoF32::new(0.0)]).unwrap();
let bad = CustomBins { edges: vec![1.0] };
let r: Result<Histogram<CustomBins, f32>, _> = histogram(&img, &bad);
assert!(matches!(r, Err(Error::InvalidBinningStrategy(_))));
}
#[test]
fn subview_histogram_only_counts_selected_region() {
let img = Image::from_vec(
4,
1,
vec![
Mono8::new(10),
Mono8::new(20),
Mono8::new(30),
Mono8::new(40),
],
)
.unwrap();
let roi = SubView::roi(
&img,
crate::Rectangle::new(crate::Coordinate::new(1, 0), crate::Size::new(2, 1)),
)
.unwrap();
let h: Histogram<NaturalBins, Saturating<u8>> = histogram(&roi, &NaturalBins).unwrap();
assert_eq!(h.total_count, 2);
assert_eq!(h.count_at_bin(20), 1);
assert_eq!(h.count_at_bin(30), 1);
assert_eq!(h.count_at_bin(10), 0);
assert_eq!(h.count_at_bin(40), 0);
}
#[test]
fn engine_total_count_matches_pixel_count() {
let pixels: Vec<Mono8> = (0..100u16).map(|v| Mono8::new((v % 256) as u8)).collect();
let img = Image::from_vec(10, 10, pixels).unwrap();
let h: Histogram<NaturalBins, Saturating<u8>> = histogram(&img, &NaturalBins).unwrap();
assert_eq!(h.total_count, 100);
assert_eq!(h.bins().iter().sum::<u64>(), 100);
}
}