use image::Rgb;
use palette::color_difference::{Ciede2000, EuclideanDistance};
use palette::{IntoColor, Lab, Srgb};
pub use crate::session::ColorDistance;
pub fn distance_sq(a: Rgb<u8>, b: Rgb<u8>, mode: ColorDistance) -> f64 {
match mode {
ColorDistance::Rgb => {
let dr = a[0] as f64 - b[0] as f64;
let dg = a[1] as f64 - b[1] as f64;
let db = a[2] as f64 - b[2] as f64;
dr * dr + dg * dg + db * db
}
ColorDistance::LabCie76 => {
let la = rgb_to_lab(a);
let lb = rgb_to_lab(b);
la.distance_squared(lb) as f64
}
ColorDistance::LabCie2000 => {
let la = rgb_to_lab(a);
let lb = rgb_to_lab(b);
let de = la.difference(lb) as f64;
de * de
}
}
}
pub fn threshold_sq(tolerance: u8, mode: ColorDistance) -> f64 {
let t = tolerance as f64;
match mode {
ColorDistance::Rgb => t * t,
ColorDistance::LabCie76 | ColorDistance::LabCie2000 => {
let scaled = t / 4.0;
scaled * scaled
}
}
}
fn rgb_to_lab(c: Rgb<u8>) -> Lab {
let srgb = Srgb::new(c[0], c[1], c[2]).into_format::<f32>();
srgb.into_color()
}
pub fn sample_window(img: &image::RgbImage, x: i32, y: i32, radius: u32) -> Option<Rgb<u8>> {
let (w, h) = img.dimensions();
if x < 0 || y < 0 || x >= w as i32 || y >= h as i32 {
return None;
}
if radius == 0 {
return Some(*img.get_pixel(x as u32, y as u32));
}
let r = radius as i32;
let x0 = (x - r).max(0) as u32;
let y0 = (y - r).max(0) as u32;
let x1 = (x + r).min(w as i32 - 1) as u32;
let y1 = (y + r).min(h as i32 - 1) as u32;
let mut sum_r: u32 = 0;
let mut sum_g: u32 = 0;
let mut sum_b: u32 = 0;
let mut n: u32 = 0;
for yy in y0..=y1 {
for xx in x0..=x1 {
let p = img.get_pixel(xx, yy);
sum_r += p[0] as u32;
sum_g += p[1] as u32;
sum_b += p[2] as u32;
n += 1;
}
}
if n == 0 {
return Some(*img.get_pixel(x as u32, y as u32));
}
Some(Rgb([
(sum_r / n) as u8,
(sum_g / n) as u8,
(sum_b / n) as u8,
]))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rgb_distance_identical_is_zero() {
let p = Rgb([100, 100, 100]);
assert_eq!(distance_sq(p, p, ColorDistance::Rgb), 0.0);
assert_eq!(distance_sq(p, p, ColorDistance::LabCie76), 0.0);
assert!(distance_sq(p, p, ColorDistance::LabCie2000) < 1e-6);
}
#[test]
fn lab_finds_blue_yellow_less_extreme_than_rgb() {
let blue = Rgb([20, 30, 220]);
let yellow = Rgb([220, 220, 30]);
let rgb_dist = distance_sq(blue, yellow, ColorDistance::Rgb);
let lab_dist = distance_sq(blue, yellow, ColorDistance::LabCie76);
assert!(rgb_dist > 0.0 && lab_dist > 0.0);
}
#[test]
fn sample_window_radius_zero_is_single_pixel() {
let mut img = image::RgbImage::new(10, 10);
img.put_pixel(5, 5, Rgb([100, 110, 120]));
let s = sample_window(&img, 5, 5, 0).unwrap();
assert_eq!(s, Rgb([100, 110, 120]));
}
#[test]
fn sample_window_averages_neighbours() {
let mut img = image::RgbImage::new(5, 5);
for x in 0..5 {
for y in 0..5 {
img.put_pixel(x, y, Rgb([100, 100, 100]));
}
}
img.put_pixel(2, 2, Rgb([200, 200, 200]));
let s = sample_window(&img, 2, 2, 1).unwrap();
assert!((s[0] as i32 - 111).abs() <= 1);
}
#[test]
fn sample_window_clips_to_image_bounds() {
let mut img = image::RgbImage::new(3, 3);
for x in 0..3 {
for y in 0..3 {
img.put_pixel(x, y, Rgb([50, 60, 70]));
}
}
let s = sample_window(&img, 0, 0, 5).unwrap();
assert_eq!(s, Rgb([50, 60, 70]));
}
#[test]
fn sample_window_returns_none_for_out_of_bounds_centre() {
let img = image::RgbImage::new(3, 3);
assert!(sample_window(&img, -1, 0, 1).is_none());
assert!(sample_window(&img, 0, 5, 1).is_none());
}
}