#[derive(Debug, Clone)]
pub struct CannyEdge {
low: f32,
high: f32,
}
impl CannyEdge {
#[must_use]
pub fn new(low: f32, high: f32) -> Self {
Self { low, high }
}
#[must_use]
pub fn run(&self, img: &[u8], w: u32, h: u32) -> Vec<bool> {
detect(img, w, h, self.low, self.high)
}
#[must_use]
pub fn detect(img: &[u8], w: u32, h: u32, low: f32, high: f32) -> Vec<bool> {
detect(img, w, h, low, high)
}
}
fn detect(img: &[u8], w: u32, h: u32, low: f32, high: f32) -> Vec<bool> {
let width = w as usize;
let height = h as usize;
let n = width * height;
if n == 0 || img.len() < n {
return vec![false; n];
}
let blurred = gaussian_blur_5x5(img, width, height);
let (mag, dir) = sobel_gradients(&blurred, width, height);
let suppressed = non_maximum_suppression(&mag, &dir, width, height);
hysteresis(&suppressed, width, height, low, high)
}
fn gaussian_blur_5x5(src: &[u8], w: usize, h: usize) -> Vec<f32> {
let kernel: [f32; 5] = [1.0, 4.0, 6.0, 4.0, 1.0];
let norm = 16.0_f32; let n = w * h;
let mut tmp = vec![0.0f32; n];
for y in 0..h {
for x in 0..w {
let mut acc = 0.0f32;
for (k, &kv) in kernel.iter().enumerate() {
let xi = (x as i64 + k as i64 - 2).clamp(0, w as i64 - 1) as usize;
acc += src[y * w + xi] as f32 * kv;
}
tmp[y * w + x] = acc / norm;
}
}
let mut out = vec![0.0f32; n];
for y in 0..h {
for x in 0..w {
let mut acc = 0.0f32;
for (k, &kv) in kernel.iter().enumerate() {
let yi = (y as i64 + k as i64 - 2).clamp(0, h as i64 - 1) as usize;
acc += tmp[yi * w + x] * kv;
}
out[y * w + x] = acc / norm;
}
}
out
}
fn sobel_gradients(src: &[f32], w: usize, h: usize) -> (Vec<f32>, Vec<u8>) {
let n = w * h;
let mut mag = vec![0.0f32; n];
let mut dir = vec![0u8; n];
for y in 1..h.saturating_sub(1) {
for x in 1..w.saturating_sub(1) {
let tl = src[(y - 1) * w + (x - 1)];
let t = src[(y - 1) * w + x];
let tr = src[(y - 1) * w + (x + 1)];
let l = src[y * w + (x - 1)];
let r = src[y * w + (x + 1)];
let bl = src[(y + 1) * w + (x - 1)];
let b = src[(y + 1) * w + x];
let br = src[(y + 1) * w + (x + 1)];
let gx = -tl - 2.0 * l - bl + tr + 2.0 * r + br;
let gy = -tl - 2.0 * t - tr + bl + 2.0 * b + br;
mag[y * w + x] = (gx * gx + gy * gy).sqrt();
let angle = gy.atan2(gx).to_degrees();
let angle = if angle < 0.0 { angle + 180.0 } else { angle };
dir[y * w + x] = if angle < 22.5 || angle >= 157.5 {
0 } else if angle < 67.5 {
1 } else if angle < 112.5 {
2 } else {
3 };
}
}
(mag, dir)
}
fn non_maximum_suppression(mag: &[f32], dir: &[u8], w: usize, h: usize) -> Vec<f32> {
let n = w * h;
let mut out = vec![0.0f32; n];
for y in 1..h.saturating_sub(1) {
for x in 1..w.saturating_sub(1) {
let idx = y * w + x;
let m = mag[idx];
let (n1, n2) = match dir[idx] {
0 => (mag[y * w + x + 1], mag[y * w + x - 1]), 1 => (mag[(y + 1) * w + x + 1], mag[(y - 1) * w + x - 1]), 2 => (mag[(y + 1) * w + x], mag[(y - 1) * w + x]), _ => (mag[(y + 1) * w + x - 1], mag[(y - 1) * w + x + 1]), };
if m >= n1 && m >= n2 {
out[idx] = m;
}
}
}
out
}
fn hysteresis(mag: &[f32], w: usize, h: usize, low: f32, high: f32) -> Vec<bool> {
let n = w * h;
let mut state = vec![0u8; n];
for (i, &m) in mag.iter().enumerate().take(n) {
if m > high {
state[i] = 2;
} else if m > low {
state[i] = 1;
}
}
let mut visited = vec![false; n];
let mut stack: Vec<usize> = Vec::new();
for i in 0..n {
if state[i] == 2 && !visited[i] {
stack.push(i);
visited[i] = true;
}
}
while let Some(idx) = stack.pop() {
let y = idx / w;
let x = idx % w;
for dy in -1_i64..=1 {
for dx in -1_i64..=1 {
if dx == 0 && dy == 0 {
continue;
}
let ny = y as i64 + dy;
let nx = x as i64 + dx;
if ny < 0 || ny >= h as i64 || nx < 0 || nx >= w as i64 {
continue;
}
let ni = ny as usize * w + nx as usize;
if !visited[ni] && state[ni] == 1 {
state[ni] = 2; visited[ni] = true;
stack.push(ni);
}
}
}
}
state.iter().map(|&s| s == 2).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_output_length() {
let img = vec![128u8; 16];
let edges = CannyEdge::detect(&img, 4, 4, 10.0, 30.0);
assert_eq!(edges.len(), 16);
}
#[test]
fn test_uniform_image_no_edges() {
let img = vec![200u8; 64];
let edges = CannyEdge::detect(&img, 8, 8, 10.0, 30.0);
assert!(edges.iter().all(|&e| !e));
}
#[test]
fn test_horizontal_step_edge() {
let w = 8usize;
let h = 8usize;
let mut img = vec![0u8; w * h];
for y in 0..4 {
for x in 0..w {
img[y * w + x] = 200;
}
}
let edges = CannyEdge::detect(&img, w as u32, h as u32, 20.0, 60.0);
assert_eq!(edges.len(), w * h);
assert!(edges.iter().any(|&e| e), "Expected at least one edge pixel");
}
#[test]
fn test_zero_size_image() {
let img: Vec<u8> = vec![];
let edges = CannyEdge::detect(&img, 0, 0, 10.0, 30.0);
assert_eq!(edges.len(), 0);
}
#[test]
fn test_new_and_run_equivalent_to_detect() {
let img: Vec<u8> = (0..64).map(|i| (i * 4) as u8).collect();
let direct = CannyEdge::detect(&img, 8, 8, 15.0, 40.0);
let via_struct = CannyEdge::new(15.0, 40.0).run(&img, 8, 8);
assert_eq!(direct, via_struct);
}
#[test]
fn test_high_threshold_suppresses_all() {
let img: Vec<u8> = (0..64).map(|i| (i % 255) as u8).collect();
let edges = CannyEdge::detect(&img, 8, 8, 1_000_000.0, 2_000_000.0);
assert!(edges.iter().all(|&e| !e));
}
}