use std::ops::Add as StdAdd;
use std::ops::Mul as StdMul;
use std::ops::Sub as StdSub;
use crate::error::Error;
use crate::image::{Image, RasterImage, RasterImageMut};
use crate::pixel::{HomogeneousPixel, LinearPixel, LinearSpace, ZeroablePixel};
pub trait CombinePixels<A, B> {
type Output;
fn combine(&self, a: &A, b: &B) -> Self::Output;
}
pub struct ClosureCombine<F>(pub F);
impl<A, B, Out, F> CombinePixels<A, B> for ClosureCombine<F>
where
F: Fn(&A, &B) -> Out,
{
type Output = Out;
#[inline(always)]
fn combine(&self, a: &A, b: &B) -> Out {
(self.0)(a, b)
}
}
pub fn combine_images_into<IA, IB, O, M>(
a: &IA,
b: &IB,
out: &mut O,
method: M,
) -> Result<(), Error>
where
IA: RasterImage,
IB: RasterImage,
O: RasterImageMut<Pixel = M::Output>,
M: CombinePixels<IA::Pixel, IB::Pixel>,
{
if a.size() != b.size() {
return Err(Error::SizeMismatch {
expected: a.size(),
actual: b.size(),
});
}
assert_eq!(
a.size(),
out.size(),
"combine_images_into: input size {:?} does not match output size {:?}",
a.size(),
out.size()
);
for y in 0..a.height() {
let row_a = a.row(y);
let row_b = b.row(y);
let row_out = out.row_mut(y);
for ((pa, pb), dst) in row_a.iter().zip(row_b.iter()).zip(row_out.iter_mut()) {
*dst = method.combine(pa, pb);
}
}
Ok(())
}
#[must_use]
pub fn combine_images<IA, IB, M>(a: &IA, b: &IB, method: M) -> Result<Image<M::Output>, Error>
where
IA: RasterImage,
IB: RasterImage,
M: CombinePixels<IA::Pixel, IB::Pixel>,
M::Output: ZeroablePixel,
{
if a.size() != b.size() {
return Err(Error::SizeMismatch {
expected: a.size(),
actual: b.size(),
});
}
let mut out = Image::<M::Output>::zero(a.width(), a.height());
combine_images_into(a, b, &mut out, method).unwrap();
Ok(out)
}
pub fn combine_images_fn_into<IA, IB, O, Out, F>(
a: &IA,
b: &IB,
out: &mut O,
f: F,
) -> Result<(), Error>
where
IA: RasterImage,
IB: RasterImage,
O: RasterImageMut<Pixel = Out>,
F: Fn(&IA::Pixel, &IB::Pixel) -> Out,
{
combine_images_into(a, b, out, ClosureCombine(f))
}
#[must_use]
pub fn combine_images_fn<IA, IB, Out, F>(a: &IA, b: &IB, f: F) -> Result<Image<Out>, Error>
where
IA: RasterImage,
IB: RasterImage,
Out: ZeroablePixel,
F: Fn(&IA::Pixel, &IB::Pixel) -> Out,
{
combine_images(a, b, ClosureCombine(f))
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PixelAdd;
impl<P: Copy + StdAdd<Output = P>> CombinePixels<P, P> for PixelAdd {
type Output = P;
#[inline(always)]
fn combine(&self, a: &P, b: &P) -> P {
*a + *b
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PixelSubtract;
impl<P: Copy + StdSub<Output = P>> CombinePixels<P, P> for PixelSubtract {
type Output = P;
#[inline(always)]
fn combine(&self, a: &P, b: &P) -> P {
*a - *b
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PixelMultiply;
impl<P: Copy + StdMul<Output = P>> CombinePixels<P, P> for PixelMultiply {
type Output = P;
#[inline(always)]
fn combine(&self, a: &P, b: &P) -> P {
*a * *b
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct AbsDiff;
impl<P> CombinePixels<P, P> for AbsDiff
where
P: HomogeneousPixel,
P::Channel: PartialOrd + StdSub<Output = P::Channel>,
{
type Output = P;
fn combine(&self, a: &P, b: &P) -> P {
let mut result = *a;
for i in 0..P::CHANNEL_COUNT {
let ac = a.channel(i);
let bc = b.channel(i);
result.set_channel(i, if ac >= bc { ac - bc } else { bc - ac });
}
result
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Max;
impl<P> CombinePixels<P, P> for Max
where
P: HomogeneousPixel,
P::Channel: Ord,
{
type Output = P;
fn combine(&self, a: &P, b: &P) -> P {
let mut result = *a;
for i in 0..P::CHANNEL_COUNT {
result.set_channel(i, a.channel(i).max(b.channel(i)));
}
result
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Min;
impl<P> CombinePixels<P, P> for Min
where
P: HomogeneousPixel,
P::Channel: Ord,
{
type Output = P;
fn combine(&self, a: &P, b: &P) -> P {
let mut result = *a;
for i in 0..P::CHANNEL_COUNT {
result.set_channel(i, a.channel(i).min(b.channel(i)));
}
result
}
}
pub struct LinearCombine {
pub wa: f32,
pub wb: f32,
}
impl<P: LinearPixel + LinearSpace> CombinePixels<P, P> for LinearCombine {
type Output = P::Accumulator;
#[inline(always)]
fn combine(&self, a: &P, b: &P) -> P::Accumulator {
b.scale_add(self.wb, a.scale(self.wa))
}
}
pub struct Blend {
pub alpha: f32,
}
impl<P: LinearPixel + LinearSpace> CombinePixels<P, P> for Blend {
type Output = P::Accumulator;
#[inline(always)]
fn combine(&self, a: &P, b: &P) -> P::Accumulator {
b.scale_add(self.alpha, a.scale(1.0 - self.alpha))
}
}
mod magnitude_sealed {
pub trait Sealed: Copy {}
}
pub trait MagnitudeChannel: magnitude_sealed::Sealed + Copy {
fn magnitude(a: Self, b: Self) -> Self;
}
impl magnitude_sealed::Sealed for f32 {}
impl MagnitudeChannel for f32 {
#[inline(always)]
fn magnitude(a: f32, b: f32) -> f32 {
f32::hypot(a, b)
}
}
impl magnitude_sealed::Sealed for f64 {}
impl MagnitudeChannel for f64 {
#[inline(always)]
fn magnitude(a: f64, b: f64) -> f64 {
f64::hypot(a, b)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Magnitude;
impl<P> CombinePixels<P, P> for Magnitude
where
P: HomogeneousPixel,
P::Channel: MagnitudeChannel,
{
type Output = P;
fn combine(&self, a: &P, b: &P) -> P {
let mut result = *a;
for i in 0..P::CHANNEL_COUNT {
result.set_channel(i, MagnitudeChannel::magnitude(a.channel(i), b.channel(i)));
}
result
}
}
#[must_use]
pub fn add<IA, IB>(a: &IA, b: &IB) -> Result<Image<IA::Pixel>, Error>
where
IA: RasterImage,
IB: RasterImage<Pixel = IA::Pixel>,
IA::Pixel: Copy + StdAdd<Output = IA::Pixel> + ZeroablePixel,
{
combine_images(a, b, PixelAdd)
}
#[must_use]
pub fn subtract<IA, IB>(a: &IA, b: &IB) -> Result<Image<IA::Pixel>, Error>
where
IA: RasterImage,
IB: RasterImage<Pixel = IA::Pixel>,
IA::Pixel: Copy + StdSub<Output = IA::Pixel> + ZeroablePixel,
{
combine_images(a, b, PixelSubtract)
}
#[must_use]
pub fn abs_diff<IA, IB>(a: &IA, b: &IB) -> Result<Image<IA::Pixel>, Error>
where
IA: RasterImage,
IB: RasterImage<Pixel = IA::Pixel>,
IA::Pixel: HomogeneousPixel + ZeroablePixel,
<IA::Pixel as HomogeneousPixel>::Channel:
PartialOrd + StdSub<Output = <IA::Pixel as HomogeneousPixel>::Channel>,
{
combine_images(a, b, AbsDiff)
}
#[must_use]
pub fn image_min<IA, IB>(a: &IA, b: &IB) -> Result<Image<IA::Pixel>, Error>
where
IA: RasterImage,
IB: RasterImage<Pixel = IA::Pixel>,
IA::Pixel: HomogeneousPixel + ZeroablePixel,
<IA::Pixel as HomogeneousPixel>::Channel: Ord,
{
combine_images(a, b, Min)
}
#[must_use]
pub fn image_max<IA, IB>(a: &IA, b: &IB) -> Result<Image<IA::Pixel>, Error>
where
IA: RasterImage,
IB: RasterImage<Pixel = IA::Pixel>,
IA::Pixel: HomogeneousPixel + ZeroablePixel,
<IA::Pixel as HomogeneousPixel>::Channel: Ord,
{
combine_images(a, b, Max)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Rectangle;
use crate::image::{Image, ImageArray, ImageView, SubView};
use crate::pixel::*;
#[test]
fn combine_pixels_closure_mono8() {
let strategy =
ClosureCombine(|a: &Mono8, b: &Mono8| Mono8::new(a.value().wrapping_add(b.value())));
let result = strategy.combine(&Mono8::new(100), &Mono8::new(50));
assert_eq!(result, Mono8::new(150));
}
#[test]
fn combine_pixels_closure_cross_type() {
let strategy =
ClosureCombine(|a: &Mono8, b: &Mono8| Mono32::new(a.value() as u32 * b.value() as u32));
let result = strategy.combine(&Mono8::new(200), &Mono8::new(200));
assert_eq!(result, Mono32::new(40000));
}
#[test]
fn combine_pixels_closure_rgb8() {
let strategy = ClosureCombine(|a: &Rgb8, b: &Rgb8| {
Rgb8::new(a.r.0.max(b.r.0), a.g.0.max(b.g.0), a.b.0.max(b.b.0))
});
let result = strategy.combine(&Rgb8::new(100, 200, 50), &Rgb8::new(150, 100, 250));
assert_eq!(result, Rgb8::new(150, 200, 250));
}
#[test]
fn combine_pixels_custom_struct_strategy() {
struct SatAdd;
impl CombinePixels<Mono8, Mono8> for SatAdd {
type Output = Mono8;
fn combine(&self, a: &Mono8, b: &Mono8) -> Mono8 {
Mono8::new(a.value().saturating_add(b.value()))
}
}
let result = SatAdd.combine(&Mono8::new(200), &Mono8::new(100));
assert_eq!(result, Mono8::new(255));
}
#[test]
fn combine_pixels_asymmetric_types() {
let strategy = ClosureCombine(|a: &Mono8, b: &MonoF32| {
MonoF32::new(a.value() as f32 / 255.0 + b.value())
});
let result = strategy.combine(&Mono8::new(255), &MonoF32::new(0.5));
assert!((result.value() - 1.5).abs() < 1e-6);
}
#[test]
fn combine_images_same_size_mono8() {
let a = Image::fill(4, 4, Mono8::new(100));
let b = Image::fill(4, 4, Mono8::new(50));
let result = combine_images(
&a,
&b,
ClosureCombine(|a: &Mono8, b: &Mono8| Mono8::new(a.value().saturating_add(b.value()))),
);
let out = result.unwrap();
assert_eq!(out.width(), 4);
assert_eq!(out.height(), 4);
for y in 0..4 {
for x in 0..4 {
assert_eq!(out.pixel_at(x, y), Mono8::new(150));
}
}
}
#[test]
fn combine_images_size_mismatch_returns_none() {
let a = Image::fill(3, 3, Mono8::new(100));
let b = Image::fill(4, 3, Mono8::new(50));
let result = combine_images(
&a,
&b,
ClosureCombine(|a: &Mono8, b: &Mono8| Mono8::new(a.value().saturating_add(b.value()))),
);
assert!(result.is_err());
}
#[test]
fn combine_images_size_mismatch_height_returns_none() {
let a = Image::fill(3, 3, Mono8::new(100));
let b = Image::fill(3, 4, Mono8::new(50));
let result = combine_images(&a, &b, ClosureCombine(|_: &Mono8, _: &Mono8| Mono8::new(0)));
assert!(result.is_err());
}
#[test]
fn combine_images_into_same_size() {
let a = Image::fill(3, 3, Mono8::new(200));
let b = Image::fill(3, 3, Mono8::new(100));
let mut out = Image::<Mono8>::zero(3, 3);
let result = combine_images_into(
&a,
&b,
&mut out,
ClosureCombine(|a: &Mono8, b: &Mono8| *a - *b),
);
assert!(result.is_ok());
for y in 0..3 {
for x in 0..3 {
assert_eq!(out.pixel_at(x, y), Mono8::new(100));
}
}
}
#[test]
fn combine_images_into_returns_none_on_input_mismatch() {
let a = Image::fill(3, 3, Mono8::new(100));
let b = Image::fill(4, 3, Mono8::new(50));
let mut out = Image::<Mono8>::zero(3, 3);
let result = combine_images_into(
&a,
&b,
&mut out,
ClosureCombine(|_: &Mono8, _: &Mono8| Mono8::new(0)),
);
assert!(result.is_err());
}
#[test]
#[should_panic(expected = "combine_images_into")]
fn combine_images_into_panics_on_output_mismatch() {
let a = Image::fill(3, 3, Mono8::new(100));
let b = Image::fill(3, 3, Mono8::new(50));
let mut out = Image::<Mono8>::zero(2, 2); let _ = combine_images_into(
&a,
&b,
&mut out,
ClosureCombine(|_: &Mono8, _: &Mono8| Mono8::new(0)),
);
}
#[test]
fn combine_images_into_matches_allocating_variant() {
let a = Image::generate(5, 5, |x, y| Mono8::new((x + y * 5) as u8));
let b = Image::generate(5, 5, |x, y| Mono8::new((x * y) as u8));
let strategy =
ClosureCombine(|a: &Mono8, b: &Mono8| Mono8::new(a.value().saturating_add(b.value())));
let allocating = combine_images(
&a,
&b,
ClosureCombine(|a: &Mono8, b: &Mono8| Mono8::new(a.value().saturating_add(b.value()))),
)
.unwrap();
let mut into = Image::<Mono8>::zero(5, 5);
combine_images_into(&a, &b, &mut into, strategy).unwrap();
for y in 0..5 {
for x in 0..5 {
assert_eq!(allocating.pixel_at(x, y), into.pixel_at(x, y));
}
}
}
#[test]
fn combine_images_zero_size() {
let a = Image::fill(0, 0, Mono8::new(0));
let b = Image::fill(0, 0, Mono8::new(0));
let result = combine_images(&a, &b, ClosureCombine(|_: &Mono8, _: &Mono8| Mono8::new(0)));
let out = result.unwrap();
assert_eq!(out.width(), 0);
assert_eq!(out.height(), 0);
}
#[test]
fn combine_images_1x1() {
let a = Image::fill(1, 1, Mono8::new(42));
let b = Image::fill(1, 1, Mono8::new(10));
let result = combine_images(
&a,
&b,
ClosureCombine(|a: &Mono8, b: &Mono8| Mono8::new(a.value() + b.value())),
);
let out = result.unwrap();
assert_eq!(out.pixel_at(0, 0), Mono8::new(52));
}
#[test]
fn combine_images_with_image_array() {
let a: ImageArray<Mono8, 3, 3> = ImageArray::generate(|x, y| Mono8::new((x + y) as u8));
let b: ImageArray<Mono8, 3, 3> = ImageArray::generate(|x, y| Mono8::new((x * y) as u8));
let result = combine_images(
&a,
&b,
ClosureCombine(|a: &Mono8, b: &Mono8| Mono8::new(a.value().saturating_add(b.value()))),
);
let out = result.unwrap();
assert_eq!(out.width(), 3);
assert_eq!(out.height(), 3);
assert_eq!(out.pixel_at(1, 1), Mono8::new(3));
}
#[test]
fn combine_images_mixed_image_types() {
let a = Image::fill(3, 3, Mono8::new(10));
let b: ImageArray<Mono8, 3, 3> = ImageArray::generate(|_, _| Mono8::new(20));
let result = combine_images(
&a,
&b,
ClosureCombine(|a: &Mono8, b: &Mono8| Mono8::new(a.value() + b.value())),
);
let out = result.unwrap();
for y in 0..3 {
for x in 0..3 {
assert_eq!(out.pixel_at(x, y), Mono8::new(30));
}
}
}
#[test]
fn combine_images_with_roi_view() {
let a = Image::generate(6, 6, |x, y| Mono8::new((x + y) as u8));
let b = Image::generate(6, 6, |_x, _y| Mono8::new(1));
let roi_a = a.roi(Rectangle::new((1, 1), (3, 3))).unwrap();
let roi_b = b.roi(Rectangle::new((2, 2), (3, 3))).unwrap();
let result = combine_images(
&roi_a,
&roi_b,
ClosureCombine(|a: &Mono8, b: &Mono8| Mono8::new(a.value().saturating_add(b.value()))),
);
let out = result.unwrap();
assert_eq!(out.width(), 3);
assert_eq!(out.height(), 3);
assert_eq!(out.pixel_at(0, 0), Mono8::new(3));
}
#[test]
fn combine_images_rgb8_channel_wise() {
let a = Image::fill(2, 2, Rgb8::new(200, 100, 50));
let b = Image::fill(2, 2, Rgb8::new(100, 200, 250));
let result = combine_images(&a, &b, ClosureCombine(|a: &Rgb8, b: &Rgb8| *a + *b));
let out = result.unwrap();
assert_eq!(out.pixel_at(0, 0), Rgb8::new(255, 255, 255));
}
#[test]
fn combine_images_varying_pixels() {
let a = Image::generate(4, 4, |x, y| Mono8::new((x * 10 + y * 40) as u8));
let b = Image::generate(4, 4, |x, y| Mono8::new((x * 5 + y * 20) as u8));
let result = combine_images(
&a,
&b,
ClosureCombine(|a: &Mono8, b: &Mono8| Mono8::new(a.value().saturating_add(b.value()))),
)
.unwrap();
for y in 0..4 {
for x in 0..4 {
let expected = ((x * 10 + y * 40) + (x * 5 + y * 20)).min(255) as u8;
assert_eq!(
result.pixel_at(x, y).value(),
expected,
"mismatch at ({}, {})",
x,
y
);
}
}
}
#[test]
fn combine_images_fn_widening_multiply() {
let a = Image::fill(3, 3, Mono8::new(200));
let b = Image::fill(3, 3, Mono8::new(200));
let result = combine_images_fn(&a, &b, |a: &Mono8, b: &Mono8| {
Mono32::new(a.value() as u32 * b.value() as u32)
});
let out = result.unwrap();
assert_eq!(out.pixel_at(0, 0), Mono32::new(40000));
}
#[test]
fn combine_images_fn_cross_type() {
let a = Image::fill(2, 2, Mono8::new(128));
let b = Image::fill(2, 2, MonoF32::new(0.5));
let result = combine_images_fn(&a, &b, |a: &Mono8, b: &MonoF32| {
MonoF32::new(a.value() as f32 / 255.0 + b.value())
});
let out = result.unwrap();
let expected = 128.0 / 255.0 + 0.5;
assert!((out.pixel_at(0, 0).value() - expected).abs() < 1e-5);
}
#[test]
fn combine_images_fn_same_type_custom_logic() {
let a = Image::fill(2, 2, Mono8::new(100));
let b = Image::fill(2, 2, Mono8::new(60));
let result = combine_images_fn(
&a,
&b,
|a: &Mono8, b: &Mono8| {
if a.value() > b.value() { *a } else { *b }
},
);
let out = result.unwrap();
assert_eq!(out.pixel_at(0, 0), Mono8::new(100));
}
#[test]
fn combine_images_fn_size_mismatch_returns_none() {
let a = Image::fill(3, 3, Mono8::new(0));
let b = Image::fill(4, 3, Mono8::new(0));
let result = combine_images_fn(&a, &b, |_: &Mono8, _: &Mono8| Mono8::new(0));
assert!(result.is_err());
}
#[test]
fn combine_images_fn_into_basic() {
let a = Image::fill(2, 2, Mono8::new(10));
let b = Image::fill(2, 2, Mono8::new(20));
let mut out: Image<Mono32> = Image::zero(2, 2);
let result = combine_images_fn_into(&a, &b, &mut out, |a: &Mono8, b: &Mono8| {
Mono32::new(a.value() as u32 + b.value() as u32)
});
assert!(result.is_ok());
assert_eq!(out.pixel_at(0, 0), Mono32::new(30));
}
#[test]
fn combine_images_fn_into_size_mismatch_returns_none() {
let a = Image::fill(3, 3, Mono8::new(0));
let b = Image::fill(4, 3, Mono8::new(0));
let mut out: Image<Mono8> = Image::zero(3, 3);
let result = combine_images_fn_into(&a, &b, &mut out, |_: &Mono8, _: &Mono8| Mono8::new(0));
assert!(result.is_err());
}
#[test]
fn combine_images_fn_matches_strategy_variant() {
struct DoubleAdd;
impl CombinePixels<Mono8, Mono8> for DoubleAdd {
type Output = Mono16;
fn combine(&self, a: &Mono8, b: &Mono8) -> Mono16 {
Mono16::new(a.value() as u16 + b.value() as u16)
}
}
let a = Image::generate(3, 3, |x, y| Mono8::new((x + y * 3) as u8));
let b = Image::generate(3, 3, |x, y| Mono8::new((x * y) as u8));
let strategy_result = combine_images(&a, &b, DoubleAdd).unwrap();
let closure_result = combine_images_fn(&a, &b, |a: &Mono8, b: &Mono8| {
Mono16::new(a.value() as u16 + b.value() as u16)
})
.unwrap();
for y in 0..3 {
for x in 0..3 {
assert_eq!(
strategy_result.pixel_at(x, y),
closure_result.pixel_at(x, y),
"mismatch at ({}, {})",
x,
y
);
}
}
}
#[test]
fn combine_images_fn_into_matches_allocating() {
let a = Image::generate(4, 4, |x, y| Mono8::new((x + y) as u8));
let b = Image::generate(4, 4, |x, y| Mono8::new((x * y) as u8));
let allocating = combine_images_fn(&a, &b, |a: &Mono8, b: &Mono8| {
Mono32::new(a.value() as u32 + b.value() as u32)
})
.unwrap();
let mut into = Image::<Mono32>::zero(4, 4);
combine_images_fn_into(&a, &b, &mut into, |a: &Mono8, b: &Mono8| {
Mono32::new(a.value() as u32 + b.value() as u32)
})
.unwrap();
for y in 0..4 {
for x in 0..4 {
assert_eq!(allocating.pixel_at(x, y), into.pixel_at(x, y));
}
}
}
#[test]
fn combine_images_subtraction_saturating() {
let a = Image::fill(2, 2, Mono8::new(100));
let b = Image::fill(2, 2, Mono8::new(150));
let result = combine_images_fn(&a, &b, |a: &Mono8, b: &Mono8| *a - *b).unwrap();
assert_eq!(result.pixel_at(0, 0), Mono8::new(0));
}
#[test]
fn combine_images_rgb8_subtraction() {
let a = Image::fill(2, 2, Rgb8::new(200, 50, 100));
let b = Image::fill(2, 2, Rgb8::new(100, 100, 50));
let result = combine_images_fn(&a, &b, |a: &Rgb8, b: &Rgb8| *a - *b).unwrap();
assert_eq!(result.pixel_at(0, 0), Rgb8::new(100, 0, 50));
}
#[test]
fn combine_images_monof32_subtraction() {
let a = Image::fill(2, 2, MonoF32::new(1.0));
let b = Image::fill(2, 2, MonoF32::new(0.3));
let result = combine_images_fn(&a, &b, |a: &MonoF32, b: &MonoF32| *a - *b).unwrap();
assert!((result.pixel_at(0, 0).value() - 0.7).abs() < 1e-6);
}
#[test]
fn combine_images_monof64_subtraction() {
let a = Image::fill(2, 2, MonoF64::new(1.0));
let b = Image::fill(2, 2, MonoF64::new(0.3));
let result = combine_images_fn(&a, &b, |a: &MonoF64, b: &MonoF64| *a - *b).unwrap();
assert!((result.pixel_at(0, 0).value() - 0.7).abs() < 1e-12);
}
#[test]
fn combine_images_with_roi() {
let a = Image::generate(6, 6, |x, y| Mono8::new((x + y) as u8));
let a_roi = a.roi(Rectangle::new((1, 1), (3, 3))).unwrap();
let b: ImageArray<Mono8, 3, 3> = ImageArray::generate(|_, _| Mono8::new(1));
let result = combine_images_fn(&a_roi, &b, |a: &Mono8, b: &Mono8| {
Mono8::new(a.value() + b.value())
})
.unwrap();
assert_eq!(result.width(), 3);
assert_eq!(result.height(), 3);
assert_eq!(result.pixel_at(0, 0), Mono8::new(3));
}
#[test]
fn add_mono8_saturates() {
assert_eq!(
PixelAdd.combine(&Mono8::new(200), &Mono8::new(100)),
Mono8::new(255)
);
}
#[test]
fn add_mono8_no_overflow() {
assert_eq!(
PixelAdd.combine(&Mono8::new(100), &Mono8::new(50)),
Mono8::new(150)
);
}
#[test]
fn add_mono8_zero_identity() {
assert_eq!(
PixelAdd.combine(&Mono8::new(42), &Mono8::new(0)),
Mono8::new(42)
);
assert_eq!(
PixelAdd.combine(&Mono8::new(0), &Mono8::new(42)),
Mono8::new(42)
);
}
#[test]
fn add_mono8_max_plus_max() {
assert_eq!(
PixelAdd.combine(&Mono8::new(255), &Mono8::new(255)),
Mono8::new(255)
);
}
#[test]
fn add_monof32_ieee() {
let result = PixelAdd.combine(&MonoF32::new(0.5), &MonoF32::new(0.3));
assert!((result.value() - 0.8).abs() < 1e-6);
}
#[test]
fn add_monof64_ieee() {
let result = PixelAdd.combine(&MonoF64::new(0.5), &MonoF64::new(0.3));
assert!((result.value() - 0.8).abs() < 1e-12);
}
#[test]
fn add_rgb8_channel_wise_saturating() {
let result = PixelAdd.combine(&Rgb8::new(200, 100, 50), &Rgb8::new(100, 200, 250));
assert_eq!(result, Rgb8::new(255, 255, 255));
}
#[test]
fn add_rgb8_no_overflow() {
let result = PixelAdd.combine(&Rgb8::new(10, 20, 30), &Rgb8::new(5, 10, 15));
assert_eq!(result, Rgb8::new(15, 30, 45));
}
#[test]
fn add_image_mono8() {
let a = Image::fill(3, 3, Mono8::new(100));
let b = Image::fill(3, 3, Mono8::new(50));
let out = combine_images(&a, &b, PixelAdd).unwrap();
assert_eq!(out.width(), 3);
assert_eq!(out.height(), 3);
assert_eq!(out.pixel_at(0, 0), Mono8::new(150));
assert_eq!(out.pixel_at(2, 2), Mono8::new(150));
}
#[test]
fn add_image_size_mismatch_returns_none() {
let a = Image::fill(4, 4, Mono8::new(10));
let b = Image::fill(3, 3, Mono8::new(10));
assert!(combine_images(&a, &b, PixelAdd).is_err());
}
#[test]
fn subtract_mono8_saturates_to_zero() {
assert_eq!(
PixelSubtract.combine(&Mono8::new(50), &Mono8::new(100)),
Mono8::new(0)
);
}
#[test]
fn subtract_mono8_no_underflow() {
assert_eq!(
PixelSubtract.combine(&Mono8::new(200), &Mono8::new(50)),
Mono8::new(150)
);
}
#[test]
fn subtract_mono8_both_zero() {
assert_eq!(
PixelSubtract.combine(&Mono8::new(0), &Mono8::new(0)),
Mono8::new(0)
);
}
#[test]
fn subtract_mono8_same_value() {
assert_eq!(
PixelSubtract.combine(&Mono8::new(128), &Mono8::new(128)),
Mono8::new(0)
);
}
#[test]
fn subtract_monof32_ieee() {
let result = PixelSubtract.combine(&MonoF32::new(1.0), &MonoF32::new(0.3));
assert!((result.value() - 0.7).abs() < 1e-6);
}
#[test]
fn subtract_monof32_goes_negative() {
let result = PixelSubtract.combine(&MonoF32::new(0.3), &MonoF32::new(1.0));
assert!((result.value() - (-0.7)).abs() < 1e-6);
}
#[test]
fn subtract_rgb8_channel_wise_saturating() {
let result = PixelSubtract.combine(&Rgb8::new(100, 200, 50), &Rgb8::new(150, 100, 100));
assert_eq!(result, Rgb8::new(0, 100, 0));
}
#[test]
fn subtract_image_mono8() {
let a = Image::fill(3, 3, Mono8::new(200));
let b = Image::fill(3, 3, Mono8::new(50));
let out = combine_images(&a, &b, PixelSubtract).unwrap();
assert_eq!(out.pixel_at(0, 0), Mono8::new(150));
}
#[test]
fn subtract_image_size_mismatch_returns_none() {
let a = Image::fill(4, 4, Mono8::new(100));
let b = Image::fill(3, 3, Mono8::new(50));
assert!(combine_images(&a, &b, PixelSubtract).is_err());
}
#[test]
fn multiply_mono8_basic() {
assert_eq!(
PixelMultiply.combine(&Mono8::new(10), &Mono8::new(3)),
Mono8::new(30)
);
}
#[test]
fn multiply_mono8_saturates() {
assert_eq!(
PixelMultiply.combine(&Mono8::new(100), &Mono8::new(4)),
Mono8::new(255)
);
}
#[test]
fn multiply_mono8_by_zero() {
assert_eq!(
PixelMultiply.combine(&Mono8::new(255), &Mono8::new(0)),
Mono8::new(0)
);
assert_eq!(
PixelMultiply.combine(&Mono8::new(0), &Mono8::new(255)),
Mono8::new(0)
);
}
#[test]
fn multiply_mono8_by_one() {
assert_eq!(
PixelMultiply.combine(&Mono8::new(123), &Mono8::new(1)),
Mono8::new(123)
);
}
#[test]
fn multiply_monof32_ieee() {
let result = PixelMultiply.combine(&MonoF32::new(0.5), &MonoF32::new(0.5));
assert!((result.value() - 0.25).abs() < 1e-6);
}
#[test]
fn multiply_monof64_ieee() {
let result = PixelMultiply.combine(&MonoF64::new(0.5), &MonoF64::new(0.5));
assert!((result.value() - 0.25).abs() < 1e-12);
}
#[test]
fn multiply_monof32_by_zero() {
let result = PixelMultiply.combine(&MonoF32::new(0.75), &MonoF32::new(0.0));
assert_eq!(result.value(), 0.0);
}
#[test]
fn multiply_monof32_by_one() {
let result = PixelMultiply.combine(&MonoF32::new(0.75), &MonoF32::new(1.0));
assert!((result.value() - 0.75).abs() < 1e-6);
}
#[test]
fn multiply_image_mono8() {
let a = Image::fill(3, 3, Mono8::new(5));
let b = Image::fill(3, 3, Mono8::new(3));
let out = combine_images(&a, &b, PixelMultiply).unwrap();
assert_eq!(out.pixel_at(0, 0), Mono8::new(15));
}
#[test]
fn multiply_image_size_mismatch_returns_none() {
let a = Image::fill(4, 4, Mono8::new(5));
let b = Image::fill(3, 3, Mono8::new(3));
assert!(combine_images(&a, &b, PixelMultiply).is_err());
}
#[test]
fn abs_diff_mono8_a_greater() {
assert_eq!(
AbsDiff.combine(&Mono8::new(150), &Mono8::new(100)),
Mono8::new(50)
);
}
#[test]
fn abs_diff_mono8_b_greater() {
assert_eq!(
AbsDiff.combine(&Mono8::new(100), &Mono8::new(150)),
Mono8::new(50)
);
}
#[test]
fn abs_diff_mono8_max_range() {
assert_eq!(
AbsDiff.combine(&Mono8::new(0), &Mono8::new(255)),
Mono8::new(255)
);
assert_eq!(
AbsDiff.combine(&Mono8::new(255), &Mono8::new(0)),
Mono8::new(255)
);
}
#[test]
fn abs_diff_mono8_same_value() {
assert_eq!(
AbsDiff.combine(&Mono8::new(128), &Mono8::new(128)),
Mono8::new(0)
);
assert_eq!(
AbsDiff.combine(&Mono8::new(0), &Mono8::new(0)),
Mono8::new(0)
);
}
#[test]
fn abs_diff_mono8_commutative() {
for v in [0u8, 1, 100, 127, 128, 200, 254, 255] {
for u in [0u8, 1, 100, 127, 128, 200, 254, 255] {
let a = Mono8::new(v);
let b = Mono8::new(u);
assert_eq!(
AbsDiff.combine(&a, &b),
AbsDiff.combine(&b, &a),
"commutativity failed for v={v}, u={u}"
);
}
}
}
#[test]
fn abs_diff_monof32_a_greater() {
let result = AbsDiff.combine(&MonoF32::new(1.0), &MonoF32::new(0.3));
assert!((result.value() - 0.7).abs() < 1e-6);
}
#[test]
fn abs_diff_monof32_b_greater() {
let result = AbsDiff.combine(&MonoF32::new(0.3), &MonoF32::new(1.0));
assert!((result.value() - 0.7).abs() < 1e-6);
}
#[test]
fn abs_diff_monof32_negative_inputs() {
let result = AbsDiff.combine(&MonoF32::new(-0.5), &MonoF32::new(0.5));
assert!((result.value() - 1.0).abs() < 1e-6);
}
#[test]
fn abs_diff_monof64() {
let result = AbsDiff.combine(&MonoF64::new(2.0), &MonoF64::new(3.5));
assert!((result.value() - 1.5).abs() < 1e-12);
}
#[test]
fn abs_diff_mono16() {
assert_eq!(
AbsDiff.combine(&Mono16::new(1000), &Mono16::new(300)),
Mono16::new(700)
);
assert_eq!(
AbsDiff.combine(&Mono16::new(300), &Mono16::new(1000)),
Mono16::new(700)
);
}
#[test]
fn abs_diff_rgb8_channel_wise() {
let result = AbsDiff.combine(&Rgb8::new(200, 50, 100), &Rgb8::new(100, 150, 100));
assert_eq!(result, Rgb8::new(100, 100, 0));
}
#[test]
fn abs_diff_image_mono8() {
let a = Image::fill(3, 3, Mono8::new(200));
let b = Image::fill(3, 3, Mono8::new(50));
let out = combine_images(&a, &b, AbsDiff).unwrap();
assert_eq!(out.pixel_at(0, 0), Mono8::new(150));
}
#[test]
fn abs_diff_image_reversed_same_result() {
let a = Image::fill(3, 3, Mono8::new(50));
let b = Image::fill(3, 3, Mono8::new(200));
let out = combine_images(&a, &b, AbsDiff).unwrap();
assert_eq!(out.pixel_at(0, 0), Mono8::new(150));
}
#[test]
fn abs_diff_image_size_mismatch_returns_none() {
let a = Image::fill(4, 4, Mono8::new(100));
let b = Image::fill(3, 3, Mono8::new(50));
assert!(combine_images(&a, &b, AbsDiff).is_err());
}
#[test]
fn max_mono8_picks_larger() {
assert_eq!(
Max.combine(&Mono8::new(100), &Mono8::new(200)),
Mono8::new(200)
);
assert_eq!(
Max.combine(&Mono8::new(200), &Mono8::new(100)),
Mono8::new(200)
);
}
#[test]
fn max_mono8_equal_values() {
assert_eq!(
Max.combine(&Mono8::new(128), &Mono8::new(128)),
Mono8::new(128)
);
}
#[test]
fn max_mono8_extremes() {
assert_eq!(
Max.combine(&Mono8::new(0), &Mono8::new(255)),
Mono8::new(255)
);
assert_eq!(
Max.combine(&Mono8::new(255), &Mono8::new(0)),
Mono8::new(255)
);
}
#[test]
fn max_mono16() {
assert_eq!(
Max.combine(&Mono16::new(500), &Mono16::new(1000)),
Mono16::new(1000)
);
assert_eq!(
Max.combine(&Mono16::new(1000), &Mono16::new(500)),
Mono16::new(1000)
);
}
#[test]
fn max_rgb8_channel_wise() {
let result = Max.combine(&Rgb8::new(200, 50, 100), &Rgb8::new(100, 150, 100));
assert_eq!(result, Rgb8::new(200, 150, 100));
}
#[test]
fn max_image_mono8() {
let a = Image::fill(3, 3, Mono8::new(100));
let b = Image::fill(3, 3, Mono8::new(200));
let out = combine_images(&a, &b, Max).unwrap();
assert_eq!(out.pixel_at(0, 0), Mono8::new(200));
}
#[test]
fn max_image_size_mismatch_returns_none() {
let a = Image::fill(4, 4, Mono8::new(100));
let b = Image::fill(3, 3, Mono8::new(200));
assert!(combine_images(&a, &b, Max).is_err());
}
#[test]
fn min_mono8_picks_smaller() {
assert_eq!(
Min.combine(&Mono8::new(100), &Mono8::new(200)),
Mono8::new(100)
);
assert_eq!(
Min.combine(&Mono8::new(200), &Mono8::new(100)),
Mono8::new(100)
);
}
#[test]
fn min_mono8_equal_values() {
assert_eq!(
Min.combine(&Mono8::new(128), &Mono8::new(128)),
Mono8::new(128)
);
}
#[test]
fn min_mono8_extremes() {
assert_eq!(Min.combine(&Mono8::new(0), &Mono8::new(255)), Mono8::new(0));
assert_eq!(Min.combine(&Mono8::new(255), &Mono8::new(0)), Mono8::new(0));
}
#[test]
fn min_mono16() {
assert_eq!(
Min.combine(&Mono16::new(500), &Mono16::new(1000)),
Mono16::new(500)
);
assert_eq!(
Min.combine(&Mono16::new(1000), &Mono16::new(500)),
Mono16::new(500)
);
}
#[test]
fn min_rgb8_channel_wise() {
let result = Min.combine(&Rgb8::new(200, 50, 100), &Rgb8::new(100, 150, 100));
assert_eq!(result, Rgb8::new(100, 50, 100));
}
#[test]
fn min_image_mono8() {
let a = Image::fill(3, 3, Mono8::new(100));
let b = Image::fill(3, 3, Mono8::new(200));
let out = combine_images(&a, &b, Min).unwrap();
assert_eq!(out.pixel_at(0, 0), Mono8::new(100));
}
#[test]
fn min_image_size_mismatch_returns_none() {
let a = Image::fill(4, 4, Mono8::new(100));
let b = Image::fill(3, 3, Mono8::new(200));
assert!(combine_images(&a, &b, Min).is_err());
}
#[test]
fn max_min_ordering_invariant() {
for v in [0u8, 50, 100, 200, 255] {
for u in [0u8, 50, 100, 200, 255] {
let a = Mono8::new(v);
let b = Mono8::new(u);
let mx = Max.combine(&a, &b);
let mn = Min.combine(&a, &b);
assert!(
mx.value() >= mn.value(),
"max={} < min={} for v={v}, u={u}",
mx.value(),
mn.value()
);
}
}
}
#[test]
fn linear_combine_half_half_mono8() {
let result = LinearCombine { wa: 0.5, wb: 0.5 }.combine(&Mono8::new(100), &Mono8::new(200));
assert!((result - MonoF32(150.0)).abs().0 < 1e-5);
}
#[test]
fn linear_combine_passthrough_a() {
let result = LinearCombine { wa: 1.0, wb: 0.0 }.combine(&Mono8::new(80), &Mono8::new(200));
assert!((result - MonoF32(80.0)).abs().0 < 1e-5);
}
#[test]
fn linear_combine_passthrough_b() {
let result = LinearCombine { wa: 0.0, wb: 1.0 }.combine(&Mono8::new(80), &Mono8::new(200));
assert!((result - MonoF32(200.0)).abs().0 < 1e-5);
}
#[test]
fn linear_combine_double_a() {
let result = LinearCombine { wa: 2.0, wb: 0.0 }.combine(&Mono8::new(50), &Mono8::new(0));
assert!((result - MonoF32(100.0)).abs().0 < 1e-5);
}
#[test]
fn linear_combine_equals_blend() {
let a = Mono8::new(40);
let b = Mono8::new(160);
let alpha = 0.75_f32;
let lc = LinearCombine {
wa: 1.0 - alpha,
wb: alpha,
}
.combine(&a, &b);
let bl = Blend { alpha }.combine(&a, &b);
assert!((lc - bl).abs().0 < 1e-5);
}
#[test]
fn linear_combine_rgbf32() {
let a = RgbF32::new(0.0, 0.5, 1.0);
let b = RgbF32::new(1.0, 0.5, 0.0);
let result = LinearCombine { wa: 0.5, wb: 0.5 }.combine(&a, &b);
assert!((result.r - 0.5).abs() < 1e-6);
assert!((result.g - 0.5).abs() < 1e-6);
assert!((result.b - 0.5).abs() < 1e-6);
}
#[test]
fn linear_combine_image_mono8() {
let a = Image::fill(3, 3, Mono8::new(100));
let b = Image::fill(3, 3, Mono8::new(200));
let out: Image<MonoF32> =
combine_images(&a, &b, LinearCombine { wa: 0.5, wb: 0.5 }).unwrap();
assert!((out.pixel_at(0, 0) - MonoF32(150.0)).abs().0 < 1e-5);
}
#[test]
fn linear_combine_image_size_mismatch_returns_none() {
let a = Image::fill(4, 4, Mono8::new(100));
let b = Image::fill(3, 3, Mono8::new(100));
assert!(combine_images(&a, &b, LinearCombine { wa: 0.5, wb: 0.5 }).is_err());
}
#[test]
fn blend_alpha_zero_returns_a() {
let result = Blend { alpha: 0.0 }.combine(&Mono8::new(100), &Mono8::new(200));
assert!((result - MonoF32(100.0)).abs().0 < 1e-5);
}
#[test]
fn blend_alpha_one_returns_b() {
let result = Blend { alpha: 1.0 }.combine(&Mono8::new(100), &Mono8::new(200));
assert!((result - MonoF32(200.0)).abs().0 < 1e-5);
}
#[test]
fn blend_midpoint_mono8() {
let result = Blend { alpha: 0.5 }.combine(&Mono8::new(0), &Mono8::new(200));
assert!((result - MonoF32(100.0)).abs().0 < 1e-5);
}
#[test]
fn blend_monof32_alpha_half() {
let result = Blend { alpha: 0.5 }.combine(&MonoF32::new(0.0), &MonoF32::new(1.0));
assert!((result.value() - 0.5).abs() < 1e-6);
}
#[test]
fn blend_rgbf32() {
let black = RgbF32::new(0.0, 0.0, 0.0);
let white = RgbF32::new(1.0, 1.0, 1.0);
let result = Blend { alpha: 0.25 }.combine(&black, &white);
assert!((result.r - 0.25).abs() < 1e-6);
assert!((result.g - 0.25).abs() < 1e-6);
assert!((result.b - 0.25).abs() < 1e-6);
}
#[test]
fn blend_rgb8_accumulator_is_rgbf32() {
let a = Rgb8::new(0, 100, 200);
let b = Rgb8::new(100, 0, 0);
let result = Blend { alpha: 0.5 }.combine(&a, &b);
assert!((result.r - 50.0).abs() < 1.0);
assert!((result.g - 50.0).abs() < 1.0);
assert!((result.b - 100.0).abs() < 1.0);
}
#[test]
fn blend_image_mono8() {
let a = Image::fill(3, 3, Mono8::new(0));
let b = Image::fill(3, 3, Mono8::new(200));
let out: Image<MonoF32> = combine_images(&a, &b, Blend { alpha: 0.5 }).unwrap();
assert!((out.pixel_at(1, 1) - MonoF32(100.0)).abs().0 < 1e-5);
}
#[test]
fn blend_image_size_mismatch_returns_none() {
let a = Image::fill(4, 4, Mono8::new(0));
let b = Image::fill(3, 3, Mono8::new(200));
assert!(combine_images(&a, &b, Blend { alpha: 0.5 }).is_err());
}
#[test]
fn magnitude_monof32_pythagorean_3_4_5() {
let result = Magnitude.combine(&MonoF32::new(3.0), &MonoF32::new(4.0));
assert!((result.value() - 5.0).abs() < 1e-5);
}
#[test]
fn magnitude_monof32_unit_axes() {
let r1 = Magnitude.combine(&MonoF32::new(1.0), &MonoF32::new(0.0));
let r2 = Magnitude.combine(&MonoF32::new(0.0), &MonoF32::new(1.0));
assert!((r1.value() - 1.0).abs() < 1e-6);
assert!((r2.value() - 1.0).abs() < 1e-6);
}
#[test]
fn magnitude_monof32_zeros() {
let result = Magnitude.combine(&MonoF32::new(0.0), &MonoF32::new(0.0));
assert_eq!(result.value(), 0.0);
}
#[test]
fn magnitude_monof32_commutative() {
let r1 = Magnitude.combine(&MonoF32::new(3.0), &MonoF32::new(4.0));
let r2 = Magnitude.combine(&MonoF32::new(4.0), &MonoF32::new(3.0));
assert!((r1.value() - r2.value()).abs() < 1e-6);
}
#[test]
fn magnitude_monof64_pythagorean_5_12_13() {
let result = Magnitude.combine(&MonoF64::new(5.0), &MonoF64::new(12.0));
assert!((result.value() - 13.0).abs() < 1e-12);
}
#[test]
fn magnitude_rgbf32_channel_wise() {
let a = RgbF32::new(3.0, 0.0, 1.0);
let b = RgbF32::new(4.0, 1.0, 0.0);
let result = Magnitude.combine(&a, &b);
assert!((result.r - 5.0).abs() < 1e-5);
assert!((result.g - 1.0).abs() < 1e-5);
assert!((result.b - 1.0).abs() < 1e-5);
}
#[test]
fn magnitude_image_monof32_sobel_gradient() {
let gx = Image::fill(3, 3, MonoF32::new(3.0));
let gy = Image::fill(3, 3, MonoF32::new(4.0));
let mag = combine_images(&gx, &gy, Magnitude).unwrap();
assert!((mag.pixel_at(1, 1).value() - 5.0).abs() < 1e-5);
assert!((mag.pixel_at(0, 0).value() - 5.0).abs() < 1e-5);
}
#[test]
fn magnitude_image_size_mismatch_returns_none() {
let gx = Image::fill(4, 4, MonoF32::new(1.0));
let gy = Image::fill(3, 3, MonoF32::new(1.0));
assert!(combine_images(&gx, &gy, Magnitude).is_err());
}
#[test]
fn magnitude_channel_f32_3_4_5() {
assert!((MagnitudeChannel::magnitude(3.0_f32, 4.0_f32) - 5.0).abs() < 1e-5);
}
#[test]
fn magnitude_channel_f32_commutative() {
let r1 = MagnitudeChannel::magnitude(3.0_f32, 4.0_f32);
let r2 = MagnitudeChannel::magnitude(4.0_f32, 3.0_f32);
assert!((r1 - r2).abs() < 1e-6);
}
#[test]
fn magnitude_channel_f32_zero_inputs() {
assert_eq!(MagnitudeChannel::magnitude(0.0_f32, 0.0_f32), 0.0_f32);
assert!((MagnitudeChannel::magnitude(0.0_f32, 1.0_f32) - 1.0).abs() < 1e-6);
assert!((MagnitudeChannel::magnitude(1.0_f32, 0.0_f32) - 1.0).abs() < 1e-6);
}
#[test]
fn magnitude_channel_f64_5_12_13() {
assert!((MagnitudeChannel::magnitude(5.0_f64, 12.0_f64) - 13.0).abs() < 1e-12);
}
#[test]
fn blend_is_special_case_of_linear_combine() {
for alpha in [0.0_f32, 0.25, 0.5, 0.75, 1.0] {
let a = Mono8::new(40);
let b = Mono8::new(200);
let blend_result = Blend { alpha }.combine(&a, &b);
let lc_result = LinearCombine {
wa: 1.0 - alpha,
wb: alpha,
}
.combine(&a, &b);
assert!(
(blend_result - lc_result).abs().0 < 1e-5,
"Blend and LinearCombine disagree at alpha={alpha}"
);
}
}
#[test]
fn abs_diff_always_nonnegative_mono8() {
for v in [0u8, 1, 50, 127, 200, 255] {
for u in [0u8, 1, 50, 127, 200, 255] {
let result = AbsDiff.combine(&Mono8::new(v), &Mono8::new(u));
assert_eq!(result.value(), v.abs_diff(u), "v={v}, u={u}");
}
}
}
#[test]
fn max_ge_min_for_all_mono8_pairs() {
for v in [0u8, 50, 128, 255] {
for u in [0u8, 50, 128, 255] {
let a = Mono8::new(v);
let b = Mono8::new(u);
assert!(Max.combine(&a, &b).value() >= Min.combine(&a, &b).value());
}
}
}
#[test]
fn subtract_then_abs_diff_consistent() {
let a = Mono8::new(200);
let b = Mono8::new(50);
assert_eq!(PixelSubtract.combine(&a, &b), AbsDiff.combine(&a, &b));
}
}