use std::ops::Sub;
use crate::Rectangle;
use crate::Size;
use crate::image::{Image, ImageRef, ImageView};
use crate::pixel::ZeroablePixel;
#[derive(Clone)]
pub struct IntegralImage<A: Copy> {
data: Image<A>,
source: Size,
}
impl<A: Copy + std::fmt::Debug> std::fmt::Debug for IntegralImage<A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use crate::image::ContiguousImage;
f.debug_struct("IntegralImage")
.field("source", &self.source)
.field("table_size", &self.table_size())
.field("data", &self.data.as_slice())
.finish()
}
}
impl<A: Copy + PartialEq> PartialEq for IntegralImage<A> {
fn eq(&self, other: &Self) -> bool {
use crate::image::ContiguousImage;
self.source == other.source && self.data.as_slice() == other.data.as_slice()
}
}
impl<A: Copy> IntegralImage<A> {
#[inline]
pub(crate) fn new_zero(source: Size) -> Self
where
A: ZeroablePixel,
{
let table_w = source.width.checked_add(1).unwrap_or_else(|| {
panic!(
"IntegralImage::new_zero: source.width + 1 overflows usize \
(source.width = {})",
source.width
)
});
let table_h = source.height.checked_add(1).unwrap_or_else(|| {
panic!(
"IntegralImage::new_zero: source.height + 1 overflows usize \
(source.height = {})",
source.height
)
});
let data = Image::<A>::zero(table_w, table_h);
IntegralImage { data, source }
}
#[inline]
pub fn source_size(&self) -> Size {
self.source
}
#[inline]
pub fn table_size(&self) -> Size {
Size::new(self.source.width + 1, self.source.height + 1)
}
#[inline]
pub fn region_sum(&self, rect: Rectangle) -> A
where
A: Sub<Output = A>,
{
assert!(
rect.right() <= self.source.width && rect.bottom() <= self.source.height,
"IntegralImage::region_sum: rectangle {:?} out of bounds for source {:?}",
rect,
self.source,
);
self.region_sum_unchecked(rect)
}
#[inline]
#[must_use]
pub fn get_region_sum(&self, rect: Rectangle) -> Option<A>
where
A: Sub<Output = A>,
{
let right = rect.checked_right()?;
let bottom = rect.checked_bottom()?;
if right <= self.source.width && bottom <= self.source.height {
Some(self.region_sum_unchecked(rect))
} else {
None
}
}
#[inline]
pub fn as_table_view(&self) -> ImageRef<'_, A> {
let w = self.data.width();
let h = self.data.height();
ImageRef::new(w, h, image_slice(&self.data)).expect("table data length matches dimensions")
}
#[inline]
pub(crate) fn data_image_mut(&mut self) -> &mut Image<A> {
&mut self.data
}
#[inline]
fn region_sum_unchecked(&self, rect: Rectangle) -> A
where
A: Sub<Output = A>,
{
let left = rect.left();
let top = rect.top();
let right = rect.right();
let bottom = rect.bottom();
let a = self.data.pixel_at(right, bottom);
let b = self.data.pixel_at(left, top);
let c = self.data.pixel_at(right, top);
let d = self.data.pixel_at(left, bottom);
(a - c) - (d - b)
}
}
#[inline]
fn image_slice<A: Copy>(img: &Image<A>) -> &[A] {
use crate::image::ContiguousImage;
img.as_slice()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Coordinate;
use crate::image::{ImageView, ImageViewMut};
use crate::pixel::Mono32;
fn fixture_const_10() -> IntegralImage<Mono32> {
let mut sat = IntegralImage::<Mono32>::new_zero(Size::new(4, 4));
let table = sat.data_image_mut();
for y in 0..=4usize {
for x in 0..=4usize {
*table.pixel_at_mut(x, y) = Mono32::new((10 * x * y) as u32);
}
}
sat
}
#[test]
fn source_and_table_sizes() {
let sat = fixture_const_10();
assert_eq!(sat.source_size(), Size::new(4, 4));
assert_eq!(sat.table_size(), Size::new(5, 5));
}
#[test]
fn region_sum_corners() {
let sat = fixture_const_10();
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,
);
}
}
}
}
}
#[test]
fn region_sum_single_pixel() {
let sat = fixture_const_10();
let rect = Rectangle::new(Coordinate::new(2, 3), Size::new(1, 1));
assert_eq!(sat.region_sum(rect), Mono32::new(10));
}
#[test]
fn region_sum_full_image_no_plus_one_adjustment() {
let sat = fixture_const_10();
let rect = Rectangle::new(Coordinate::new(0, 0), Size::new(4, 4));
assert_eq!(sat.region_sum(rect), Mono32::new(4 * 4 * 10));
}
#[test]
fn get_region_sum_returns_some_in_bounds() {
let sat = fixture_const_10();
let rect = Rectangle::new(Coordinate::new(1, 1), Size::new(2, 2));
assert_eq!(sat.get_region_sum(rect), Some(Mono32::new(40)));
}
#[test]
fn get_region_sum_returns_none_out_of_bounds() {
let sat = fixture_const_10();
let rect = Rectangle::new(Coordinate::new(0, 0), Size::new(5, 4));
assert_eq!(sat.get_region_sum(rect), None);
}
#[test]
#[should_panic(expected = "out of bounds")]
fn region_sum_panics_out_of_bounds() {
let sat = fixture_const_10();
let rect = Rectangle::new(Coordinate::new(0, 0), Size::new(4, 5));
let _ = sat.region_sum(rect);
}
#[test]
fn region_sum_evaluation_order_under_saturating() {
let v = u32::MAX / 4;
let mut sat = IntegralImage::<Mono32>::new_zero(Size::new(2, 2));
let table = sat.data_image_mut();
*table.pixel_at_mut(1, 1) = Mono32::new(v);
*table.pixel_at_mut(2, 1) = Mono32::new(2 * v);
*table.pixel_at_mut(1, 2) = Mono32::new(2 * v);
*table.pixel_at_mut(2, 2) = Mono32::new(4 * v);
let rect = Rectangle::new(Coordinate::new(0, 0), Size::new(2, 2));
assert_eq!(sat.region_sum(rect), Mono32::new(4 * v));
let rect = Rectangle::new(Coordinate::new(1, 0), Size::new(1, 2));
assert_eq!(sat.region_sum(rect), Mono32::new(2 * v));
}
#[test]
fn as_table_view_reports_w_plus_one_h_plus_one() {
let sat = fixture_const_10();
let view = sat.as_table_view();
assert_eq!(view.size(), Size::new(5, 5));
}
#[test]
fn as_table_view_top_row_and_left_column_are_zero() {
let sat = fixture_const_10();
let view = sat.as_table_view();
for x in 0..5 {
assert_eq!(view.pixel_at(x, 0), Mono32::new(0), "top row at x={}", x);
}
for y in 0..5 {
assert_eq!(
view.pixel_at(0, y),
Mono32::new(0),
"left column at y={}",
y
);
}
}
#[test]
#[should_panic]
fn integral_table_size_overflow_panics_tier3() {
let _ = IntegralImage::<Mono32>::new_zero(Size::new(usize::MAX, 1));
}
#[test]
fn get_region_sum_overflowing_rect_returns_none() {
let sat = fixture_const_10();
let rect = Rectangle::new(Coordinate::new(usize::MAX - 1, 0), Size::new(10, 1));
assert_eq!(sat.get_region_sum(rect), None);
let rect = Rectangle::new(Coordinate::new(0, usize::MAX - 1), Size::new(1, 10));
assert_eq!(sat.get_region_sum(rect), None);
}
}