use std::time::Instant;
use image::{DynamicImage, GenericImageView, GrayImage, ImageBuffer};
use log::{debug, info, trace};
pub fn edge_detection_filter(img: DynamicImage, hysteresis: bool) -> DynamicImage {
let blurred_img = blur(img, 6.4f32);
let sobel_img = apply_sobel_kernel(blurred_img);
if hysteresis {
edge_tracking(sobel_img)
} else {
sobel_img
}
}
fn blur(img: DynamicImage, sigma: f32) -> DynamicImage {
info!("Blurring image");
trace!("Started time tracking for blurring");
let now = Instant::now();
debug!("Creating gauss kernel");
let kernel = create_gauss_kernel(sigma);
let offset = (kernel.len() / 2) as u32;
let (width, height) = img.dimensions();
debug!("Creating target blur image");
let mut destination_img = ImageBuffer::new(width, height);
img.pixels().into_iter().for_each(|(x, y, _)| {
let mut kernel_values_red = 0f32;
let mut kernel_values_green = 0f32;
let mut kernel_values_blue = 0f32;
for (k_y, row) in kernel.iter().enumerate() {
for (k_x, kernel_value) in row.iter().enumerate() {
let pixel_pos_x = (x + k_x as u32).saturating_sub(offset).clamp(0, width - 1);
let pixel_pos_y = (y + k_y as u32).saturating_sub(offset).clamp(0, height - 1);
if destination_img.in_bounds(pixel_pos_x, pixel_pos_y) {
let pixel = img.get_pixel(pixel_pos_x, pixel_pos_y);
kernel_values_red += pixel.0[0] as f32 * kernel_value;
kernel_values_green += pixel.0[1] as f32 * kernel_value;
kernel_values_blue += pixel.0[2] as f32 * kernel_value;
}
}
}
destination_img.put_pixel(
x,
y,
image::Rgb([
(kernel_values_red as u8),
(kernel_values_green as u8),
(kernel_values_blue as u8),
]),
);
});
info!(
"Successfully blurred image in {:3} ms",
now.elapsed().as_millis()
);
DynamicImage::ImageRgb8(destination_img)
}
#[cfg(test)]
mod test_blur {
use super::*;
#[test]
#[should_panic]
fn panic_sigma_0() {
let img = DynamicImage::ImageRgb8(ImageBuffer::new(3, 3));
blur(img, 0f32);
}
#[test]
#[should_panic]
fn panic_sigma_negative() {
let img = DynamicImage::ImageRgb8(ImageBuffer::new(3, 3));
blur(img, -1f32);
}
#[test]
fn black_img_remains_black() {
let img = DynamicImage::ImageRgb8(ImageBuffer::new(3, 3));
let blur = blur(img.clone(), 1.4f32);
assert_eq!(img, blur);
}
#[test]
fn img_middle_white() {
let img = DynamicImage::ImageRgb8(ImageBuffer::from_fn(3, 3, |x, y| {
if y == 1 && x == 1 {
image::Rgb([255, 255, 255])
} else {
image::Rgb([0, 0, 0])
}
}));
let blur = blur(img.clone(), 1.4f32);
assert_ne!(img, blur);
let result = DynamicImage::ImageRgb8(ImageBuffer::from_fn(3, 3, |x, y| {
if y == 1 && x == 1 {
image::Rgb([39, 39, 39])
} else if y != 1 && x != 1 {
image::Rgb([23, 23, 23])
} else {
image::Rgb([30, 30, 30])
}
}));
assert_eq!(result, blur);
}
}
fn create_gauss_kernel(sigma: f32) -> [[f32; 3]; 3] {
if sigma <= 0f32 {
panic!("The given sigma {} was smaller or equal to zero", sigma)
}
let mut kernel = [[0f32; 3]; 3];
let mut r = 2f32 * sigma * sigma;
let s = r;
let mut sum = 0f32;
for x in -1..=1isize {
for y in -1..=1isize {
r = ((x * x + y * y) as f32).sqrt();
let value = (f32::exp(-(r * r) / s)) / (std::f32::consts::PI * s);
kernel[(x + 1) as usize][(y + 1) as usize] = value;
sum += value;
}
}
for row in kernel.iter_mut() {
for value in row.iter_mut() {
*value /= sum;
}
}
kernel
}
#[cfg(test)]
mod test_create_gauss_kernel {
use super::*;
#[test]
#[should_panic]
fn sigma_zero_panics() {
create_gauss_kernel(0f32);
}
#[test]
#[should_panic]
fn sigma_minus_one_panics() {
create_gauss_kernel(-1f32);
}
#[test]
fn sigma_1_4() {
assert_eq!(
[
[0.09235313, 0.119190335, 0.09235313],
[0.119190335, 0.15382625, 0.119190335],
[0.09235313, 0.119190335, 0.09235313]
],
create_gauss_kernel(1.4f32)
)
}
}
fn apply_sobel_kernel(img: DynamicImage) -> DynamicImage {
info!("Creating outline image");
trace!("Started time tracking for sobel");
let now = Instant::now();
let kernel_x = &[
[1f32, 2f32, 1f32],
[0f32, 0f32, 0f32],
[-1f32, -2f32, -1f32],
];
let kernel_y = &[
[1f32, 0f32, -1f32],
[2f32, 0f32, -2f32],
[1f32, 0f32, -1f32],
];
let kernel_length = kernel_x.len();
trace!("Length: {}", kernel_length);
let offset = (kernel_length / 2) as u32;
let (width, height) = img.dimensions();
debug!("Creating target sobel image");
let mut destination_img = ImageBuffer::new(width, height);
img.pixels().into_iter().for_each(|(x, y, _)| {
let mut kernel_values_x = 0f32;
let mut kernel_values_y = 0f32;
for k_y in 0..kernel_length {
for k_x in 0..kernel_length {
let pixel_pos_x = (x + k_x as u32).saturating_sub(offset).clamp(0, width - 1);
let pixel_pos_y = (y + k_y as u32).saturating_sub(offset).clamp(0, height - 1);
let pixel = img.get_pixel(pixel_pos_x, pixel_pos_y);
let pixel_gray = crate::pixel::luminosity(pixel.0[0], pixel.0[1], pixel.0[2]);
kernel_values_x += pixel_gray as f32 * kernel_x[k_x][k_y];
kernel_values_y += pixel_gray as f32 * kernel_y[k_x][k_y];
}
}
destination_img.put_pixel(
x,
y,
image::Luma([
(((kernel_values_x * kernel_values_x + kernel_values_y * kernel_values_y).sqrt())
.round() as u8)
.saturating_mul(3),
]),
);
});
info!(
"Successfully outlined image in {:3} ms",
now.elapsed().as_millis()
);
DynamicImage::ImageLuma8(destination_img)
}
#[cfg(test)]
mod test_sobel {
use super::*;
#[test]
fn no_edge() {
let img = DynamicImage::ImageLuma8(ImageBuffer::new(3, 3));
let edge_img = apply_sobel_kernel(img.clone());
assert_eq!(img, edge_img);
}
#[test]
fn edge_vertical() {
let img = DynamicImage::ImageLuma8(ImageBuffer::from_fn(3, 3, |x, _| {
if x == 1 {
image::Luma([0u8])
} else {
image::Luma([255u8])
}
}));
let edge_img = apply_sobel_kernel(img.clone());
assert_eq!(img, edge_img);
}
#[test]
fn edge_horizontal() {
let img = DynamicImage::ImageLuma8(ImageBuffer::from_fn(3, 3, |_, y| {
if y == 1 {
image::Luma([0u8])
} else {
image::Luma([255u8])
}
}));
let edge_img = apply_sobel_kernel(img.clone());
assert_eq!(img, edge_img);
}
}
fn edge_tracking(img: DynamicImage) -> DynamicImage {
trace!("Started time tracking for hysteresis");
let now = Instant::now();
debug!("Creating target hysteresis image");
let mut destination_img: GrayImage = ImageBuffer::new(img.width(), img.height());
let upper_threshold = u8::MAX as f32 * 0.5;
debug!("Upper threshold: {}", upper_threshold);
let lower_threshold = u8::MAX as f32 * 0.3;
debug!("Lower threshold: {}", lower_threshold);
img.pixels().into_iter().for_each(|(x, y, pixel)| {
let grayscale_pixel = crate::pixel::luminosity(pixel.0[0], pixel.0[1], pixel.0[2]);
if grayscale_pixel >= upper_threshold {
destination_img.put_pixel(x, y, image::Luma([255]));
} else if grayscale_pixel >= lower_threshold {
let mut strong = false;
'outer: for k_y in 0..3 {
for k_x in 0..3 {
let pixel_pos_x = (x + k_x as u32).saturating_sub(1).clamp(0, img.width() - 1);
let pixel_pos_y = (y + k_y as u32)
.saturating_sub(1)
.clamp(0, img.height() - 1);
let pixel = img.get_pixel(pixel_pos_x, pixel_pos_y);
let pixel_gray = crate::pixel::luminosity(pixel.0[0], pixel.0[1], pixel.0[2]);
if pixel_gray >= upper_threshold {
strong = true;
break 'outer;
}
}
}
if strong {
destination_img.put_pixel(x, y, image::Luma([255]))
} else {
destination_img.put_pixel(x, y, image::Luma([0]))
}
} else {
destination_img.put_pixel(x, y, image::Luma([0]))
}
});
info!(
"Successfully applied hysteresis to target image in {:3} ms",
now.elapsed().as_millis()
);
DynamicImage::ImageLuma8(destination_img)
}
#[cfg(test)]
mod test_edge_detection {
use super::*;
#[test]
fn no_strong_results_in_black_img() {
let img = DynamicImage::ImageLuma8(ImageBuffer::new(3, 3));
let result = edge_tracking(img.clone());
assert_eq!(img, result);
}
#[test]
fn strong_pixels_stay() {
let img = DynamicImage::ImageLuma8(ImageBuffer::from_fn(3, 3, |x, y| {
if x == 1 && y == 1 {
image::Luma([255u8])
} else {
image::Luma([0u8])
}
}));
let result = edge_tracking(img.clone());
assert_eq!(img, result);
}
#[test]
fn weak_pixel_removed() {
let img = DynamicImage::ImageLuma8(ImageBuffer::from_fn(3, 3, |x, y| {
if x == 1 && y == 1 {
image::Luma([126u8])
} else {
image::Luma([0u8])
}
}));
let result = edge_tracking(img.clone());
assert_eq!(DynamicImage::ImageLuma8(ImageBuffer::new(3, 3)), result);
}
#[test]
fn irrelevant_pixel_removed() {
let img = DynamicImage::ImageLuma8(ImageBuffer::from_fn(3, 3, |x, y| {
if x == 1 && y == 1 {
image::Luma([76u8])
} else {
image::Luma([0u8])
}
}));
let result = edge_tracking(img.clone());
assert_eq!(DynamicImage::ImageLuma8(ImageBuffer::new(3, 3)), result);
}
#[test]
fn weak_pixel_with_strong_neighbor_is_converted() {
let img = DynamicImage::ImageLuma8(ImageBuffer::from_fn(3, 3, |x, y| {
if x == 1 && y == 1 {
image::Luma([126u8])
} else if x == 2 && y == 1 {
image::Luma([255u8])
} else {
image::Luma([0u8])
}
}));
let desired_result = DynamicImage::ImageLuma8(ImageBuffer::from_fn(3, 3, |x, y| {
if x == 1 && y == 1 {
image::Luma([255u8])
} else if x == 2 && y == 1 {
image::Luma([255u8])
} else {
image::Luma([0u8])
}
}));
let result = edge_tracking(img.clone());
assert_eq!(desired_result, result);
}
#[test]
fn irrelevant_pixel_with_strong_neighbor_is_removed() {
let img = DynamicImage::ImageLuma8(ImageBuffer::from_fn(3, 3, |x, y| {
if x == 1 && y == 1 {
image::Luma([255u8])
} else if x == 2 && y == 1 {
image::Luma([40u8])
} else {
image::Luma([0u8])
}
}));
let desired_result = DynamicImage::ImageLuma8(ImageBuffer::from_fn(3, 3, |x, y| {
if x == 1 && y == 1 {
image::Luma([255u8])
} else {
image::Luma([0u8])
}
}));
let result = edge_tracking(img.clone());
assert_eq!(desired_result, result);
}
}