use std::num::Saturating;
use crate::Error;
use crate::analyze::histogram::strategy::BinningStrategy;
use crate::analyze::histogram::{Histogram, NaturalBins, histogram};
use crate::image::{Image, RasterImage, RasterImageMut};
use crate::pixel::{Array, HomogeneousPixel, ZeroablePixel};
use crate::transform::ChannelLut;
pub fn equalization_lut<V: Copy>(hist: &Histogram<NaturalBins, V>) -> ChannelLut
where
NaturalBins: BinningStrategy<V>,
{
let cdf = hist.cumulative();
debug_assert_eq!(cdf.len(), 256);
let total: u64 = *cdf.last().expect("NaturalBins yields 256 bins");
let cdf_min: u64 = cdf.iter().copied().find(|&c| c != 0).unwrap_or(0);
if total <= cdf_min {
return ChannelLut::from_fn(|i| i);
}
let denom = (total - cdf_min) as f64;
let mut table = [0u8; 256];
for i in 0..256 {
let num = cdf[i].saturating_sub(cdf_min) as f64;
let v = (num * 255.0 / denom).round();
table[i] = v.clamp(0.0, 255.0) as u8;
}
ChannelLut::new(table)
}
pub fn equalize_image<I, P>(image: &I) -> Result<Image<P>, Error>
where
I: RasterImage<Pixel = P>,
P: HomogeneousPixel<Channel = Saturating<u8>> + ZeroablePixel,
NaturalBins: BinningStrategy<P::Channel>,
{
let mut out = Image::<P>::zero(image.width(), image.height());
equalize_image_into(image, &mut out)?;
Ok(out)
}
pub fn equalize_image_into<I, O, P>(image: &I, out: &mut O) -> Result<(), Error>
where
I: RasterImage<Pixel = P>,
O: RasterImageMut<Pixel = P>,
P: HomogeneousPixel<Channel = Saturating<u8>>,
NaturalBins: BinningStrategy<P::Channel>,
{
assert_eq!(
image.size(),
out.size(),
"equalize_image_into: input size {:?} does not match output size {:?}",
image.size(),
out.size()
);
let hists: Vec<Histogram<NaturalBins, P::Channel>> = histogram(image, &NaturalBins)?;
let luts: Vec<ChannelLut> = hists.iter().map(equalization_lut).collect();
debug_assert_eq!(luts.len(), P::CHANNEL_COUNT);
for y in 0..image.height() {
let in_row = image.row(y);
let out_row = out.row_mut(y);
for (src, dst) in in_row.iter().zip(out_row.iter_mut()) {
let channels = <P::Channels as Array<P::Channel>>::from_fn(|c| {
let v: Saturating<u8> = src.channel(c);
Saturating(luts[c].lookup(v.0))
});
*dst = P::from_channels(channels.as_ref());
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image::{Image, ImageView};
use crate::pixel::{Mono8, Rgb8};
use crate::transform::ConvertPixel;
use std::num::Saturating as S;
fn hist_from_bins(bins: Vec<u64>) -> Histogram<NaturalBins, S<u8>> {
assert_eq!(bins.len(), 256);
Histogram::new(NaturalBins, bins, 0, 0, 0)
}
#[test]
fn equalization_lut_uniform_is_identity() {
let h = hist_from_bins(vec![1u64; 256]);
let lut = equalization_lut(&h);
for i in 0..=255u8 {
assert_eq!(lut.lookup(i), i, "expected identity at i={i}");
}
}
#[test]
fn equalization_lut_single_bin_is_identity() {
let mut bins = vec![0u64; 256];
bins[100] = 50;
let h = hist_from_bins(bins);
let lut = equalization_lut(&h);
for i in [0u8, 50, 100, 200, 255] {
assert_eq!(lut.lookup(i), i);
}
}
#[test]
fn equalization_lut_empty_histogram_is_identity() {
let h = hist_from_bins(vec![0u64; 256]);
let lut = equalization_lut(&h);
for i in [0u8, 64, 200, 255] {
assert_eq!(lut.lookup(i), i);
}
}
#[test]
fn equalization_lut_known_reference() {
let mut bins = vec![0u64; 256];
bins[0] = 2;
bins[1] = 3;
bins[3] = 5;
let h = hist_from_bins(bins);
let lut = equalization_lut(&h);
assert_eq!(lut.lookup(0), 0);
assert_eq!(lut.lookup(1), 96);
assert_eq!(lut.lookup(2), 96);
assert_eq!(lut.lookup(3), 255);
assert_eq!(lut.lookup(50), 255);
assert_eq!(lut.lookup(255), 255);
}
#[test]
fn equalization_lut_endpoints_are_zero_and_255() {
let mut bins = vec![0u64; 256];
bins[10] = 7;
bins[20] = 3;
bins[200] = 9;
let h = hist_from_bins(bins);
let lut = equalization_lut(&h);
assert_eq!(lut.lookup(10), 0);
assert_eq!(lut.lookup(200), 255);
}
#[test]
fn equalization_lut_is_monotonic_non_decreasing() {
let mut bins = vec![0u64; 256];
for (i, c) in [10u8, 30, 80, 120, 200]
.iter()
.zip([5u64, 9, 13, 8, 3].iter())
{
bins[*i as usize] = *c;
}
let h = hist_from_bins(bins);
let lut = equalization_lut(&h);
for i in 1..=255u8 {
assert!(
lut.lookup(i) >= lut.lookup(i - 1),
"non-monotonic at i={i}: {} < {}",
lut.lookup(i),
lut.lookup(i - 1)
);
}
}
#[test]
fn equalize_image_mono8_preserves_size_and_dtype() {
let img: Image<Mono8> = Image::generate(8, 2, |x, _| Mono8::new((x * 30) as u8));
let out = equalize_image(&img).unwrap();
assert_eq!(out.size(), img.size());
}
#[test]
fn equalize_image_mono8_uniform_image_is_unchanged() {
let img: Image<Mono8> = Image::fill(4, 3, Mono8::new(123));
let out = equalize_image(&img).unwrap();
for y in 0..img.height() {
for x in 0..img.width() {
assert_eq!(out.pixel_at(x, y), img.pixel_at(x, y));
}
}
}
#[test]
fn equalize_image_mono8_stretches_low_contrast() {
let img: Image<Mono8> = Image::generate(11, 1, |x, _| Mono8::new(100 + x as u8));
let out = equalize_image(&img).unwrap();
assert_eq!(u8::from(out.pixel_at(0, 0)), 0);
assert_eq!(u8::from(out.pixel_at(10, 0)), 255);
}
#[test]
fn equalize_image_mono8_is_idempotent_on_already_uniform_histogram() {
let img: Image<Mono8> = Image::generate(256, 1, |x, _| Mono8::new(x as u8));
let once = equalize_image(&img).unwrap();
let twice = equalize_image(&once).unwrap();
for x in 0..256 {
assert_eq!(once.pixel_at(x, 0), twice.pixel_at(x, 0));
}
}
#[test]
fn equalize_image_rgb8_independent_per_channel() {
let w = 16usize;
let img: Image<Rgb8> = Image::generate(w, 1, |x, _| {
Rgb8::new(
(x as u8) * 4,
((x as u8) * 2) + 50,
if x < w / 2 { 10 } else { 240 },
)
});
let out = equalize_image(&img).unwrap();
let hists: Vec<Histogram<NaturalBins, S<u8>>> = histogram(&img, &NaturalBins).unwrap();
let lut_r = equalization_lut(&hists[0]);
let lut_g = equalization_lut(&hists[1]);
let lut_b = equalization_lut(&hists[2]);
for x in 0..w {
let p_in = img.pixel_at(x, 0);
let p_out = out.pixel_at(x, 0);
let expected = Rgb8::new(
lut_r.lookup(p_in.r.0),
lut_g.lookup(p_in.g.0),
lut_b.lookup(p_in.b.0),
);
assert_eq!(p_out, expected, "mismatch at x={x}");
}
}
#[test]
fn equalize_image_rgb8_uses_per_channel_distinct_luts() {
let img: Image<Rgb8> = Image::generate(8, 1, |x, _| {
Rgb8::new((x * 30) as u8, ((x * 3) + 10) as u8, 255 - (x * 30) as u8)
});
let out = equalize_image(&img).unwrap();
let hists: Vec<Histogram<NaturalBins, S<u8>>> = histogram(&img, &NaturalBins).unwrap();
let lut_r = equalization_lut(&hists[0]);
let mut any_disagree = false;
for x in 0..8 {
let in_p = img.pixel_at(x, 0);
let out_p = out.pixel_at(x, 0);
let single = lut_r.convert(&in_p);
if out_p.g.0 != single.g.0 || out_p.b.0 != single.b.0 {
any_disagree = true;
break;
}
}
assert!(any_disagree, "expected per-channel-distinct equalization");
}
#[test]
fn equalize_image_into_writes_into_provided_output() {
let img: Image<Mono8> = Image::generate(4, 4, |x, y| Mono8::new(((x + y) * 16) as u8));
let expected = equalize_image(&img).unwrap();
let mut out: Image<Mono8> = Image::zero(4, 4);
equalize_image_into(&img, &mut out).unwrap();
for y in 0..4 {
for x in 0..4 {
assert_eq!(out.pixel_at(x, y), expected.pixel_at(x, y));
}
}
}
#[test]
#[should_panic(expected = "does not match")]
fn equalize_image_into_size_mismatch_panics() {
let img: Image<Mono8> = Image::fill(4, 4, Mono8::new(0));
let mut out: Image<Mono8> = Image::zero(3, 4);
let _ = equalize_image_into(&img, &mut out);
}
}