use image::RgbaImage;
use super::super::config::{VISUAL_COLOR_CHANNEL_TOLERANCE, VISUAL_COLOR_JND};
use super::super::geom::Mask;
use super::color::{ciede2000, srgb_to_lab};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum PixelClass {
Match,
ColorErr,
Missing,
Extra,
}
impl PixelClass {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Match => "Match",
Self::ColorErr => "ColorErr",
Self::Missing => "Missing",
Self::Extra => "Extra",
}
}
}
pub(crate) struct ClassMap {
pub(crate) w: u32,
pub(crate) h: u32,
pub(crate) px: Vec<PixelClass>,
}
pub(crate) fn classify_pixels(
cand: &RgbaImage,
reference: &RgbaImage,
mask_c: &Mask,
mask_r: &Mask,
) -> ClassMap {
let (w, h) = cand.dimensions();
let mut px = Vec::with_capacity((w as usize) * (h as usize));
for y in 0..h {
for x in 0..w {
let c = cand.get_pixel(x, y);
let r = reference.get_pixel(x, y);
let ink_c = mask_c.get(x, y);
let ink_r = mask_r.get(x, y);
let class = if c == r {
PixelClass::Match
} else if ciede2000(
srgb_to_lab([c[0], c[1], c[2]]),
srgb_to_lab([r[0], r[1], r[2]]),
) <= VISUAL_COLOR_JND
{
PixelClass::ColorErr
} else if ink_r && !ink_c {
PixelClass::Missing
} else if ink_c && !ink_r {
PixelClass::Extra
} else {
PixelClass::ColorErr
};
px.push(class);
}
}
ClassMap { w, h, px }
}
pub(crate) fn classify_visible_colors(
raw: &ClassMap,
cand: &RgbaImage,
reference: &RgbaImage,
) -> ClassMap {
let px = raw
.px
.iter()
.enumerate()
.map(|(index, class)| {
if *class != PixelClass::ColorErr {
return *class;
}
let x = index as u32 % raw.w;
let y = index as u32 / raw.w;
if rgb_delta_exceeds_channel_tolerance(
cand.get_pixel(x, y).0,
reference.get_pixel(x, y).0,
) {
PixelClass::ColorErr
} else {
PixelClass::Match
}
})
.collect();
ClassMap {
w: raw.w,
h: raw.h,
px,
}
}
pub(crate) fn rgb_delta_exceeds_channel_tolerance(candidate: [u8; 4], reference: [u8; 4]) -> bool {
candidate[..3]
.iter()
.zip(reference[..3].iter())
.any(|(&candidate, &reference)| {
candidate.abs_diff(reference) > VISUAL_COLOR_CHANNEL_TOLERANCE
})
}
#[cfg(test)]
mod tests {
use super::*;
use image::{ImageBuffer, Rgba};
#[test]
fn only_byte_identical_pixels_match() {
let reference = ImageBuffer::from_pixel(1, 1, Rgba([100, 100, 100, 255]));
let candidate = ImageBuffer::from_pixel(1, 1, Rgba([101, 100, 100, 255]));
let mask_c = super::super::super::geom::content_mask(&candidate);
let mask_r = super::super::super::geom::content_mask(&reference);
let classes = classify_pixels(&candidate, &reference, &mask_c, &mask_r);
assert_eq!(classes.px, vec![PixelClass::ColorErr]);
}
#[test]
fn visibility_map_applies_channel_tolerance_without_changing_raw_classes() {
let reference = ImageBuffer::from_pixel(3, 1, Rgba([100, 100, 100, 255]));
let candidate = ImageBuffer::from_fn(3, 1, |x, _| {
if x == 0 {
Rgba([101, 100, 100, 255])
} else if x == 1 {
Rgba([102, 100, 100, 255])
} else {
Rgba([103, 100, 100, 255])
}
});
let mask_c = super::super::super::geom::content_mask(&candidate);
let mask_r = super::super::super::geom::content_mask(&reference);
let raw = classify_pixels(&candidate, &reference, &mask_c, &mask_r);
let visible = classify_visible_colors(&raw, &candidate, &reference);
assert_eq!(raw.px, vec![PixelClass::ColorErr; 3]);
assert_eq!(
visible.px,
vec![PixelClass::Match, PixelClass::Match, PixelClass::ColorErr]
);
}
#[test]
fn adjacent_swapped_colors_remain_same_coordinate_color_errors() {
let red = Rgba([255, 0, 0, 255]);
let blue = Rgba([0, 0, 255, 255]);
let reference = ImageBuffer::from_fn(2, 1, |x, _| if x == 0 { red } else { blue });
let candidate = ImageBuffer::from_fn(2, 1, |x, _| if x == 0 { blue } else { red });
let mask_c = super::super::super::geom::content_mask(&candidate);
let mask_r = super::super::super::geom::content_mask(&reference);
let classes = classify_pixels(&candidate, &reference, &mask_c, &mask_r);
assert_eq!(classes.px, vec![PixelClass::ColorErr, PixelClass::ColorErr]);
}
#[test]
fn sub_jnd_paper_boundary_stays_a_color_error() {
let pair = (0u8..=255)
.flat_map(|candidate| (0u8..=255).map(move |reference| (candidate, reference)))
.find(|(candidate, reference)| {
let candidate = Rgba([*candidate; 4]);
let reference = Rgba([*reference; 4]);
super::super::super::geom::is_content(&candidate)
!= super::super::super::geom::is_content(&reference)
&& ciede2000(
srgb_to_lab([candidate[0], candidate[1], candidate[2]]),
srgb_to_lab([reference[0], reference[1], reference[2]]),
) <= VISUAL_COLOR_JND
})
.expect("a JND boundary must have adjacent sub-JND grayscale samples");
let candidate = ImageBuffer::from_pixel(1, 1, Rgba([pair.0; 4]));
let reference = ImageBuffer::from_pixel(1, 1, Rgba([pair.1; 4]));
let classes = classify_pixels(
&candidate,
&reference,
&super::super::super::geom::content_mask(&candidate),
&super::super::super::geom::content_mask(&reference),
);
assert_eq!(classes.px, vec![PixelClass::ColorErr]);
}
}