use crate::definitions::{HasBlack, HasWhite};
use crate::filter::gaussian_blur_f32;
use crate::gradients::{horizontal_sobel, vertical_sobel};
use image::{GenericImageView, GrayImage, ImageBuffer, Luma};
use std::f32;
pub fn canny(image: &GrayImage, low_threshold: f32, high_threshold: f32) -> GrayImage {
assert!(high_threshold >= low_threshold);
const SIGMA: f32 = 1.4;
let blurred = gaussian_blur_f32(image, SIGMA);
let gx = horizontal_sobel(&blurred);
let gy = vertical_sobel(&blurred);
let g: Vec<f32> = gx
.iter()
.zip(gy.iter())
.map(|(h, v)| (*h as f32).hypot(*v as f32))
.collect::<Vec<f32>>();
let g = ImageBuffer::from_raw(image.width(), image.height(), g).unwrap();
let thinned = non_maximum_suppression(&g, &gx, &gy);
hysteresis(&thinned, low_threshold, high_threshold)
}
fn non_maximum_suppression(
g: &ImageBuffer<Luma<f32>, Vec<f32>>,
gx: &ImageBuffer<Luma<i16>, Vec<i16>>,
gy: &ImageBuffer<Luma<i16>, Vec<i16>>,
) -> ImageBuffer<Luma<f32>, Vec<f32>> {
const RADIANS_TO_DEGREES: f32 = 180f32 / f32::consts::PI;
let mut out = ImageBuffer::from_pixel(g.width(), g.height(), Luma([0.0]));
for y in 1..g.height() - 1 {
for x in 1..g.width() - 1 {
let x_gradient = gx[(x, y)][0] as f32;
let y_gradient = gy[(x, y)][0] as f32;
let mut angle = (y_gradient).atan2(x_gradient) * RADIANS_TO_DEGREES;
if angle < 0.0 {
angle += 180.0
}
let clamped_angle = if angle >= 157.5 || angle < 22.5 {
0
} else if angle >= 22.5 && angle < 67.5 {
45
} else if angle >= 67.5 && angle < 112.5 {
90
} else if angle >= 112.5 && angle < 157.5 {
135
} else {
unreachable!()
};
let (cmp1, cmp2) = unsafe {
match clamped_angle {
0 => (g.unsafe_get_pixel(x - 1, y), g.unsafe_get_pixel(x + 1, y)),
45 => (
g.unsafe_get_pixel(x + 1, y + 1),
g.unsafe_get_pixel(x - 1, y - 1),
),
90 => (g.unsafe_get_pixel(x, y - 1), g.unsafe_get_pixel(x, y + 1)),
135 => (
g.unsafe_get_pixel(x - 1, y + 1),
g.unsafe_get_pixel(x + 1, y - 1),
),
_ => unreachable!(),
}
};
let pixel = *g.get_pixel(x, y);
if pixel[0] < cmp1[0] || pixel[0] < cmp2[0] {
out.put_pixel(x, y, Luma([0.0]));
} else {
out.put_pixel(x, y, pixel);
}
}
}
out
}
fn hysteresis(
input: &ImageBuffer<Luma<f32>, Vec<f32>>,
low_thresh: f32,
high_thresh: f32,
) -> ImageBuffer<Luma<u8>, Vec<u8>> {
let max_brightness = Luma::white();
let min_brightness = Luma::black();
let mut out = ImageBuffer::from_pixel(input.width(), input.height(), min_brightness);
let mut edges = Vec::with_capacity(((input.width() * input.height()) / 2) as usize);
for y in 1..input.height() - 1 {
for x in 1..input.width() - 1 {
let inp_pix = *input.get_pixel(x, y);
let out_pix = *out.get_pixel(x, y);
if inp_pix[0] >= high_thresh && out_pix[0] == 0 {
out.put_pixel(x, y, max_brightness);
edges.push((x, y));
while !edges.is_empty() {
let (nx, ny) = edges.pop().unwrap();
let neighbor_indices = [
(nx + 1, ny),
(nx + 1, ny + 1),
(nx, ny + 1),
(nx - 1, ny - 1),
(nx - 1, ny),
(nx - 1, ny + 1),
];
for neighbor_idx in &neighbor_indices {
let in_neighbor = *input.get_pixel(neighbor_idx.0, neighbor_idx.1);
let out_neighbor = *out.get_pixel(neighbor_idx.0, neighbor_idx.1);
if in_neighbor[0] >= low_thresh && out_neighbor[0] == 0 {
out.put_pixel(neighbor_idx.0, neighbor_idx.1, max_brightness);
edges.push((neighbor_idx.0, neighbor_idx.1));
}
}
}
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::canny;
use crate::drawing::draw_filled_rect_mut;
use crate::rect::Rect;
use ::test;
use image::{GrayImage, Luma};
fn edge_detect_bench_image(width: u32, height: u32) -> GrayImage {
let mut image = GrayImage::new(width, height);
let (w, h) = (width as i32, height as i32);
let large = Rect::at(w / 4, h / 4).of_size(width / 2, height / 2);
let small = Rect::at(9, 9).of_size(3, 3);
draw_filled_rect_mut(&mut image, large, Luma([255]));
draw_filled_rect_mut(&mut image, small, Luma([255]));
image
}
#[bench]
fn bench_canny(b: &mut test::Bencher) {
let image = edge_detect_bench_image(250, 250);
b.iter(|| {
let output = canny(&image, 250.0, 300.0);
test::black_box(output);
});
}
}