use std::ops::{Add, Sub};
use crate::Error;
use crate::image::{ImageView, ImageViewMut, RasterImage};
use crate::pixel::{IntegralPixel, IntegralSquaredPixel, ZeroablePixel};
use super::output::IntegralImage;
use super::preflight::{self, IntegralCapacity, IntegralSquaredCapacity};
#[allow(private_bounds)] pub fn integral_image<I, A>(image: &I) -> Result<IntegralImage<A>, Error>
where
I: RasterImage,
I::Pixel: IntegralPixel<A>,
A: Copy + ZeroablePixel + Add<Output = A> + Sub<Output = A> + IntegralCapacity,
{
let mut out = IntegralImage::<A>::new_zero(image.size());
integral_image_into(image, &mut out)?;
Ok(out)
}
#[allow(private_bounds)] pub fn integral_image_into<I, A>(image: &I, out: &mut IntegralImage<A>) -> Result<(), Error>
where
I: RasterImage,
I::Pixel: IntegralPixel<A>,
A: Copy + ZeroablePixel + Add<Output = A> + Sub<Output = A> + IntegralCapacity,
{
let w = image.width();
let h = image.height();
preflight::check::<I::Pixel, A>(w, h)?;
assert_eq!(
out.source_size(),
image.size(),
"integral_image_into: output source size does not match input",
);
fill_engine(image, out, |p| p.to_integral());
Ok(())
}
#[allow(private_bounds)] pub fn integral_squared_image<I, A>(image: &I) -> Result<IntegralImage<A>, Error>
where
I: RasterImage,
I::Pixel: IntegralSquaredPixel<A>,
A: Copy + ZeroablePixel + Add<Output = A> + Sub<Output = A> + IntegralSquaredCapacity,
{
let mut out = IntegralImage::<A>::new_zero(image.size());
integral_squared_image_into(image, &mut out)?;
Ok(out)
}
#[allow(private_bounds)] pub fn integral_squared_image_into<I, A>(image: &I, out: &mut IntegralImage<A>) -> Result<(), Error>
where
I: RasterImage,
I::Pixel: IntegralSquaredPixel<A>,
A: Copy + ZeroablePixel + Add<Output = A> + Sub<Output = A> + IntegralSquaredCapacity,
{
let w = image.width();
let h = image.height();
preflight::check_squared::<I::Pixel, A>(w, h)?;
assert_eq!(
out.source_size(),
image.size(),
"integral_squared_image_into: output source size does not match input",
);
fill_engine(image, out, |p| p.to_integral_squared());
Ok(())
}
#[inline]
fn fill_engine<I, A, F>(image: &I, out: &mut IntegralImage<A>, project: F)
where
I: RasterImage,
A: Copy + ZeroablePixel + Add<Output = A> + Sub<Output = A>,
F: Fn(I::Pixel) -> A,
{
let w = image.width();
let h = image.height();
let table = out.data_image_mut();
for y in 1..=h {
let src_row = image.row(y - 1);
for x in 1..=w {
let s = project(src_row[x - 1]);
let left = table.pixel_at(x - 1, y);
let up = table.pixel_at(x, y - 1);
let ul = table.pixel_at(x - 1, y - 1);
let lu = left + up;
let lu_minus_ul = lu - ul; *table.pixel_at_mut(x, y) = s + lu_minus_ul;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image::{Image, SubView};
use crate::pixel::{Mono8, Mono16, Mono32, Mono64, MonoF32, MonoF64, Rgb8, Rgb32};
use crate::{Coordinate, Rectangle, Size};
#[test]
fn mono8_to_mono32_4x4_constant_10() {
let img: Image<Mono8> = Image::fill(4, 4, Mono8::new(10));
let sat = integral_image::<_, Mono32>(&img).unwrap();
for y in 0..4 {
for x in 0..4 {
for h in 1..=(4 - y) {
for w in 1..=(4 - x) {
let rect = Rectangle::new(Coordinate::new(x, y), Size::new(w, h));
assert_eq!(
sat.region_sum(rect),
Mono32::new((w * h * 10) as u32),
"rect = {:?}",
rect,
);
}
}
}
}
}
fn naive_sum_mono(img: &Image<Mono8>, rect: Rectangle) -> u64 {
let mut s = 0u64;
for y in rect.top()..rect.bottom() {
for x in rect.left()..rect.right() {
s += img.pixel_at(x, y).value() as u64;
}
}
s
}
#[test]
fn mono8_to_mono64_matches_naive_for_deterministic_pattern() {
let img = Image::<Mono8>::generate(8, 6, |x, y| {
Mono8::new(((x.wrapping_mul(37).wrapping_add(y.wrapping_mul(91))) & 0xFF) as u8)
});
let sat = integral_image::<_, Mono64>(&img).unwrap();
for y in 0..6 {
for x in 0..8 {
for h in 1..=(6 - y) {
for w in 1..=(8 - x) {
let rect = Rectangle::new(Coordinate::new(x, y), Size::new(w, h));
let expected = naive_sum_mono(&img, rect);
assert_eq!(
sat.region_sum(rect),
Mono64::new(expected),
"rect = {:?}",
rect,
);
}
}
}
}
}
#[test]
fn mono8_to_mono32_preflight_overflow_returns_err() {
let img = Image::<Mono8>::zero(5000, 5000);
let err = integral_image::<_, Mono32>(&img).unwrap_err();
let Error::AccumulatorOverflow {
required_capacity,
accumulator_capacity,
} = err
else {
panic!("expected AccumulatorOverflow, got {:?}", err);
};
assert_eq!(required_capacity, 255u128 * 5000 * 5000);
assert_eq!(accumulator_capacity, u32::MAX as u128);
}
#[test]
fn monof32_to_monof64_matches_naive() {
let img =
Image::<MonoF32>::generate(5, 4, |x, y| MonoF32::new(((x * 4 + y) as f32) / 32.0));
let sat = integral_image::<_, MonoF64>(&img).unwrap();
let rects = [
Rectangle::new(Coordinate::new(0, 0), Size::new(5, 4)),
Rectangle::new(Coordinate::new(1, 1), Size::new(3, 2)),
Rectangle::new(Coordinate::new(2, 0), Size::new(2, 4)),
];
for rect in rects {
let mut expected = 0.0f64;
for y in rect.top()..rect.bottom() {
for x in rect.left()..rect.right() {
expected += img.pixel_at(x, y).value() as f64;
}
}
let got = sat.region_sum(rect).value();
assert!(
(got - expected).abs() < 1e-9,
"rect = {:?}, expected = {}, got = {}",
rect,
expected,
got,
);
}
}
#[test]
fn rgb8_to_rgb32_per_channel() {
let img: Image<Rgb8> = Image::fill(3, 3, Rgb8::new(2, 5, 11));
let sat = integral_image::<_, Rgb32>(&img).unwrap();
let rect = Rectangle::new(Coordinate::new(0, 0), Size::new(3, 3));
let s = sat.region_sum(rect);
assert_eq!(s.r.0, 2 * 9);
assert_eq!(s.g.0, 5 * 9);
assert_eq!(s.b.0, 11 * 9);
}
#[test]
#[should_panic(expected = "source size does not match")]
fn integral_image_into_size_mismatch_panics() {
let img: Image<Mono8> = Image::fill(4, 4, Mono8::new(1));
let mut out = IntegralImage::<Mono32>::new_zero(Size::new(5, 5));
let _ = integral_image_into(&img, &mut out);
}
#[test]
fn integral_squared_mono8_to_mono64_3x3_constant() {
let img: Image<Mono8> = Image::fill(3, 3, Mono8::new(4));
let sat = integral_squared_image::<_, Mono64>(&img).unwrap();
let rect = Rectangle::new(Coordinate::new(0, 0), Size::new(3, 3));
assert_eq!(sat.region_sum(rect), Mono64::new(144));
}
#[test]
fn integral_squared_mono8_partial_rectangles() {
let img = Image::<Mono8>::generate(4, 3, |x, y| Mono8::new((x + y + 1) as u8));
let sat = integral_squared_image::<_, Mono64>(&img).unwrap();
let rect = Rectangle::new(Coordinate::new(0, 0), Size::new(2, 2));
assert_eq!(sat.region_sum(rect), Mono64::new(18));
}
#[test]
fn subview_input_carries_through() {
let outer = Image::<Mono8>::generate(6, 6, |x, y| Mono8::new((x + y * 6 + 1) as u8));
let rect = Rectangle::new(Coordinate::new(1, 1), Size::new(4, 4));
let view = outer.roi(rect).expect("roi in bounds");
let sat = integral_image::<_, Mono64>(&view).unwrap();
assert_eq!(sat.source_size(), Size::new(4, 4));
let single = Rectangle::new(Coordinate::new(0, 0), Size::new(1, 1));
assert_eq!(sat.region_sum(single), Mono64::new(8));
}
#[test]
fn integral_image_into_reuses_buffer() {
let mut buf = IntegralImage::<Mono32>::new_zero(Size::new(3, 3));
let a: Image<Mono8> = Image::fill(3, 3, Mono8::new(1));
integral_image_into(&a, &mut buf).unwrap();
let r = Rectangle::new(Coordinate::new(0, 0), Size::new(3, 3));
assert_eq!(buf.region_sum(r), Mono32::new(9));
let b: Image<Mono8> = Image::fill(3, 3, Mono8::new(5));
integral_image_into(&b, &mut buf).unwrap();
assert_eq!(buf.region_sum(r), Mono32::new(45));
}
#[allow(dead_code)]
fn type_gate_compile_time_fixture(img: &Image<Mono8>) {
let _: IntegralImage<Mono32> = integral_image::<_, Mono32>(img).unwrap();
let _: IntegralImage<Mono64> = integral_image::<_, Mono64>(img).unwrap();
let _: IntegralImage<MonoF64> = integral_image::<_, MonoF64>(img).unwrap();
}
#[allow(dead_code)]
fn _touch_mono16(_: Mono16) {}
}