Skip to main content

iris/edges/
mod.rs

1pub mod gradients;
2
3use crate::error::Result;
4use crate::image::Image;
5use burn::tensor::{Tensor, TensorData, backend::Backend};
6
7/// A line segment detected by Hough transform: ((x1, y1), (x2, y2)).
8pub type LineSegment = ((usize, usize), (usize, usize));
9
10impl<B: Backend> Image<B> {
11    /// Applies Sobel edge detection to compute horizontal, vertical gradients, and magnitude.
12    /// Returns the gradient magnitude image.
13    pub fn sobel(&self) -> Result<Self> {
14        let gray = self.grayscale()?;
15        let dims = gray.tensor.dims();
16        let _c = dims[0]; // should be 1
17        let h = dims[1];
18        let w = dims[2];
19
20        let device = gray.tensor.device();
21        let tensor_data = gray.tensor.clone().into_data();
22        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
23        let mut mag_vals = vec![0.0f32; h * w];
24
25        let kx = [[-1.0, 0.0, 1.0], [-2.0, 0.0, 2.0], [-1.0, 0.0, 1.0]];
26        let ky = [[-1.0, -2.0, -1.0], [0.0, 0.0, 0.0], [1.0, 2.0, 1.0]];
27
28        for y in 1..(h - 1) {
29            for x in 1..(w - 1) {
30                let mut gx = 0.0f32;
31                let mut gy = 0.0f32;
32
33                for dy in -1..=1 {
34                    for dx in -1..=1 {
35                        let val =
36                            flat_vals[(y as isize + dy) as usize * w + (x as isize + dx) as usize];
37                        gx += val * kx[(dy + 1) as usize][(dx + 1) as usize] as f32;
38                        gy += val * ky[(dy + 1) as usize][(dx + 1) as usize] as f32;
39                    }
40                }
41                mag_vals[y * w + x] = (gx * gx + gy * gy).sqrt();
42            }
43        }
44
45        let new_data = TensorData::new(mag_vals, [1, h, w]);
46        let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
47        Ok(Image::new(new_tensor))
48    }
49
50    /// Performs Canny edge detection.
51    /// Steps: Grayscale -> Gaussian Blur -> Sobel Gradients -> Non-Maximum Suppression -> Hysteresis Thresholding.
52    pub fn canny(&self, low_threshold: f32, high_threshold: f32) -> Result<Self> {
53        // Step 1 & 2: Grayscale and Gaussian Blur (kernel_size = 5, sigma = 1.4)
54        let blurred = self.grayscale()?.gaussian_blur(5, 1.4)?;
55
56        let dims = blurred.tensor.dims();
57        let h = dims[1];
58        let w = dims[2];
59        let device = blurred.tensor.device();
60
61        let tensor_data = blurred.tensor.into_data();
62        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
63
64        let mut gx_vals = vec![0.0f32; h * w];
65        let mut gy_vals = vec![0.0f32; h * w];
66        let mut mag_vals = vec![0.0f32; h * w];
67        let mut angle_vals = vec![0.0f32; h * w];
68
69        let kx = [[-1.0, 0.0, 1.0], [-2.0, 0.0, 2.0], [-1.0, 0.0, 1.0]];
70        let ky = [[-1.0, -2.0, -1.0], [0.0, 0.0, 0.0], [1.0, 2.0, 1.0]];
71
72        // Step 3: Compute Sobel gradients
73        for y in 1..(h - 1) {
74            for x in 1..(w - 1) {
75                let mut gx = 0.0f32;
76                let mut gy = 0.0f32;
77
78                for dy in -1..=1 {
79                    for dx in -1..=1 {
80                        let val =
81                            flat_vals[(y as isize + dy) as usize * w + (x as isize + dx) as usize];
82                        gx += val * kx[(dy + 1) as usize][(dx + 1) as usize] as f32;
83                        gy += val * ky[(dy + 1) as usize][(dx + 1) as usize] as f32;
84                    }
85                }
86                let idx = y * w + x;
87                gx_vals[idx] = gx;
88                gy_vals[idx] = gy;
89                mag_vals[idx] = (gx * gx + gy * gy).sqrt();
90                // Map angles to range [0, 180)
91                let mut angle = gy.atan2(gx).to_degrees();
92                if angle < 0.0 {
93                    angle += 180.0;
94                }
95                angle_vals[idx] = angle;
96            }
97        }
98
99        // Step 4: Non-Maximum Suppression (NMS)
100        let mut nms_vals = vec![0.0f32; h * w];
101        for y in 1..(h - 1) {
102            for x in 1..(w - 1) {
103                let idx = y * w + x;
104                let angle = angle_vals[idx];
105                let mag = mag_vals[idx];
106
107                // Determine neighbors based on gradient direction
108                let (mag1, mag2) =
109                    if (0.0..22.5).contains(&angle) || (157.5..=180.0).contains(&angle) {
110                        // Horizontal (0 degrees)
111                        (mag_vals[y * w + (x + 1)], mag_vals[y * w + (x - 1)])
112                    } else if (22.5..67.5).contains(&angle) {
113                        // Diagonal (45 degrees)
114                        (
115                            mag_vals[(y - 1) * w + (x + 1)],
116                            mag_vals[(y + 1) * w + (x - 1)],
117                        )
118                    } else if (67.5..112.5).contains(&angle) {
119                        // Vertical (90 degrees)
120                        (mag_vals[(y - 1) * w + x], mag_vals[(y + 1) * w + x])
121                    } else {
122                        // Diagonal (135 degrees)
123                        (
124                            mag_vals[(y - 1) * w + (x - 1)],
125                            mag_vals[(y + 1) * w + (x + 1)],
126                        )
127                    };
128
129                if mag >= mag1 && mag >= mag2 {
130                    nms_vals[idx] = mag;
131                } else {
132                    nms_vals[idx] = 0.0;
133                }
134            }
135        }
136
137        // Step 5: Double Thresholding and Hysteresis
138        let mut final_vals = vec![0.0f32; h * w];
139        let mut strong_edges = Vec::new();
140
141        // Label pixels: 0 = background, 1 = weak, 2 = strong
142        let mut labels = vec![0u8; h * w];
143
144        for y in 1..(h - 1) {
145            for x in 1..(w - 1) {
146                let idx = y * w + x;
147                let val = nms_vals[idx];
148
149                if val >= high_threshold {
150                    labels[idx] = 2;
151                    strong_edges.push((x, y));
152                    final_vals[idx] = 1.0; // Mark strong edge in final image
153                } else if val >= low_threshold {
154                    labels[idx] = 1;
155                }
156            }
157        }
158
159        // Hysteresis depth-first search (connected components)
160        while let Some((x, y)) = strong_edges.pop() {
161            for dy in -1..=1 {
162                for dx in -1..=1 {
163                    let nx = x as isize + dx;
164                    let ny = y as isize + dy;
165                    if nx >= 0 && nx < w as isize && ny >= 0 && ny < h as isize {
166                        let nidx = (ny as usize) * w + (nx as usize);
167                        if labels[nidx] == 1 {
168                            // Upgrade weak edge to strong edge
169                            labels[nidx] = 2;
170                            final_vals[nidx] = 1.0;
171                            strong_edges.push((nx as usize, ny as usize));
172                        }
173                    }
174                }
175            }
176        }
177
178        let new_data = TensorData::new(final_vals, [1, h, w]);
179        let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
180        Ok(Image::new(new_tensor))
181    }
182
183    /// Probabilistic Hough Line Transform (HoughLinesP).
184    /// Detects line segments in a binary edge image.
185    /// Returns a vector of line segments as ((x1,y1), (x2,y2)).
186    pub fn hough_lines_p(
187        &self,
188        rho: f32,
189        theta: f32,
190        threshold: u32,
191        min_line_length: u32,
192        max_line_gap: u32,
193    ) -> Result<Vec<LineSegment>> {
194        let dims = self.tensor.dims();
195        let h = dims[1];
196        let w = dims[2];
197
198        let tensor_data = self.tensor.clone().into_data();
199        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
200
201        // Collect edge points
202        let mut edge_points = Vec::new();
203        for y in 1..(h - 1) {
204            for x in 1..(w - 1) {
205                if flat_vals[y * w + x] > 0.5 {
206                    edge_points.push((x, y));
207                }
208            }
209        }
210
211        if edge_points.is_empty() {
212            return Ok(Vec::new());
213        }
214
215        // Hough accumulator
216        let num_theta = (std::f64::consts::PI / theta as f64) as usize;
217        let diag = ((h * h + w * w) as f64).sqrt();
218        let num_rho = (2.0 * diag / rho as f64) as usize;
219        let mut accumulator = vec![vec![0u32; num_theta]; num_rho];
220
221        for &(x, y) in &edge_points {
222            for t_idx in 0..num_theta {
223                let angle = t_idx as f64 * theta as f64;
224                let r = x as f64 * angle.cos() + y as f64 * angle.sin();
225                let r_idx = ((r / rho as f64 + diag / rho as f64) as usize).min(num_rho - 1);
226                accumulator[r_idx][t_idx] += 1;
227            }
228        }
229
230        // Find peaks and extract line segments
231        let mut lines = Vec::new();
232        let mut visited = vec![vec![false; num_theta]; num_rho];
233
234        for r_idx in 1..(num_rho - 1) {
235            for t_idx in 1..(num_theta - 1) {
236                if accumulator[r_idx][t_idx] >= threshold
237                    && !visited[r_idx][t_idx]
238                    && accumulator[r_idx][t_idx] >= accumulator[r_idx - 1][t_idx]
239                    && accumulator[r_idx][t_idx] >= accumulator[r_idx + 1][t_idx]
240                    && accumulator[r_idx][t_idx] >= accumulator[r_idx][t_idx - 1]
241                    && accumulator[r_idx][t_idx] >= accumulator[r_idx][t_idx + 1]
242                {
243                    visited[r_idx][t_idx] = true;
244                    let angle = t_idx as f64 * theta as f64;
245                    let r_val = (r_idx as f64 - diag / rho as f64) * rho as f64;
246
247                    // Find collinear points along this line
248                    let cos_a = angle.cos();
249                    let sin_a = angle.sin();
250                    let mut line_pts: Vec<(usize, usize)> = edge_points
251                        .iter()
252                        .filter(|&&(x, y)| {
253                            let proj = x as f64 * cos_a + y as f64 * sin_a;
254                            (proj - r_val).abs() < rho as f64 * 1.5
255                        })
256                        .copied()
257                        .collect();
258
259                    line_pts.sort_by_key(|&(x, y)| (x as i64 - y as i64).abs());
260
261                    if line_pts.len() >= min_line_length as usize {
262                        // Segment by gap
263                        let mut segments = Vec::new();
264                        let mut seg_start = 0;
265                        for j in 1..line_pts.len() {
266                            let dx = line_pts[j].0 as i64 - line_pts[j - 1].0 as i64;
267                            let dy = line_pts[j].1 as i64 - line_pts[j - 1].1 as i64;
268                            let dist = ((dx * dx + dy * dy) as f64).sqrt() as u32;
269                            if dist > max_line_gap {
270                                if j - seg_start >= min_line_length as usize {
271                                    segments.push((line_pts[seg_start], line_pts[j - 1]));
272                                }
273                                seg_start = j;
274                            }
275                        }
276                        if line_pts.len() - seg_start >= min_line_length as usize {
277                            segments.push((line_pts[seg_start], *line_pts.last().unwrap()));
278                        }
279                        lines.extend(segments);
280                    }
281                }
282            }
283        }
284
285        Ok(lines)
286    }
287
288    /// Hough Circle Transform (HoughCircles) using gradient information.
289    /// Detects circles in a grayscale image.
290    /// Returns circles as (center_x, center_y, radius).
291    pub fn hough_circles(
292        &self,
293        dp: f32,
294        min_dist: f32,
295        param1: f32,
296        param2: f32,
297        min_radius: usize,
298        max_radius: usize,
299    ) -> Result<Vec<(usize, usize, usize)>> {
300        let gray = self.grayscale()?;
301        let dims = gray.tensor.dims();
302        let h = dims[1];
303        let w = dims[2];
304
305        // Gaussian blur for noise reduction
306        let blurred = gray.gaussian_blur(5, 1.4)?;
307        let tensor_data = blurred.tensor.clone().into_data();
308        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
309
310        // Sobel gradients
311        let mut gx_vals = vec![0.0f32; h * w];
312        let mut gy_vals = vec![0.0f32; h * w];
313        let kx = [[-1.0, 0.0, 1.0], [-2.0, 0.0, 2.0], [-1.0, 0.0, 1.0]];
314        let ky = [[-1.0, -2.0, -1.0], [0.0, 0.0, 0.0], [1.0, 2.0, 1.0]];
315
316        for y in 1..(h - 1) {
317            for x in 1..(w - 1) {
318                let mut gxx = 0.0f32;
319                let mut gyy = 0.0f32;
320                for dy in -1..=1 {
321                    for dx in -1..=1 {
322                        let val =
323                            flat_vals[(y as isize + dy) as usize * w + (x as isize + dx) as usize];
324                        gxx += val * kx[(dy + 1) as usize][(dx + 1) as usize];
325                        gyy += val * ky[(dy + 1) as usize][(dx + 1) as usize];
326                    }
327                }
328                gx_vals[y * w + x] = gxx;
329                gy_vals[y * w + x] = gyy;
330            }
331        }
332
333        let mut circles = Vec::new();
334        let step = (dp as usize).max(1);
335        let max_r = max_radius.min(h / 2).min(w / 2);
336
337        for r in min_radius..=max_r {
338            let r = r as f32;
339            // Edge threshold based on param1
340            let edge_thresh = param1 * 0.5;
341            // Accumulator for this radius
342            let mut acc = vec![0u32; h * w];
343
344            for y in (1..(h - 1)).step_by(step) {
345                for x in (1..(w - 1)).step_by(step) {
346                    let gx = gx_vals[y * w + x];
347                    let gy = gy_vals[y * w + x];
348                    let mag = (gx * gx + gy * gy).sqrt();
349
350                    if mag < edge_thresh {
351                        continue;
352                    }
353
354                    // Vote along gradient direction (both directions)
355                    let angle = gy.atan2(gx);
356                    for sign in [-1.0, 1.0] {
357                        let vx = (x as f32 + sign * r * angle.cos()).round() as isize;
358                        let vy = (y as f32 + sign * r * angle.sin()).round() as isize;
359                        if vx >= 0 && vx < w as isize && vy >= 0 && vy < h as isize {
360                            acc[vy as usize * w + vx as usize] += 1;
361                        }
362                    }
363                }
364            }
365
366            // Find peaks
367            for y in ((r as usize)..(h - r as usize)).step_by(step) {
368                for x in ((r as usize)..(w - r as usize)).step_by(step) {
369                    if acc[y * w + x] >= param2 as u32 {
370                        // Check if local maximum
371                        let mut is_max = true;
372                        for dy in -(step as isize)..=(step as isize) {
373                            for dx in -(step as isize)..=(step as isize) {
374                                let ny = y as isize + dy;
375                                let nx = x as isize + dx;
376                                if ny >= 0
377                                    && ny < h as isize
378                                    && nx >= 0
379                                    && nx < w as isize
380                                    && (ny as usize != y || nx as usize != x)
381                                    && acc[ny as usize * w + nx as usize] > acc[y * w + x]
382                                {
383                                    is_max = false;
384                                }
385                            }
386                        }
387
388                        if is_max {
389                            // Check not too close to existing circles
390                            let too_close =
391                                circles.iter().any(|&(cx, cy, _): &(usize, usize, usize)| {
392                                    let dx = cx as f32 - x as f32;
393                                    let dy = cy as f32 - y as f32;
394                                    (dx * dx + dy * dy).sqrt() < min_dist
395                                });
396                            if !too_close {
397                                circles.push((x, y, r as usize));
398                            }
399                        }
400                    }
401                }
402            }
403        }
404
405        Ok(circles)
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412    use crate::test_helpers::{TestBackend, test_device};
413
414    #[test]
415    fn test_sobel_and_canny() {
416        let device = test_device();
417        let data = TensorData::new(vec![0.3f32; 3 * 16 * 16], [3, 16, 16]);
418        let img = Image::new(Tensor::<TestBackend, 3>::from_data(data, &device));
419
420        let sobel = img.sobel().unwrap();
421        assert_eq!(sobel.shape(), [1, 16, 16]);
422
423        let canny = img.canny(0.1, 0.3).unwrap();
424        assert_eq!(canny.shape(), [1, 16, 16]);
425    }
426
427    #[test]
428    fn test_hough_lines_p() {
429        let device = test_device();
430        let mut data = vec![0.0f32; 32 * 32];
431        // Draw a horizontal line
432        for x in 5..27 {
433            data[16 * 32 + x] = 1.0;
434        }
435        let tensor =
436            Tensor::<TestBackend, 3>::from_data(TensorData::new(data, [1, 32, 32]), &device);
437        let img = Image::new(tensor);
438        let lines = img
439            .hough_lines_p(1.0, std::f32::consts::PI / 180.0, 10, 5, 5)
440            .unwrap();
441        assert!(!lines.is_empty());
442    }
443
444    #[test]
445    fn test_hough_circles() {
446        let device = test_device();
447        let mut data = vec![0.0f32; 60 * 60];
448        // Draw a thick circle outline (3px wide) with radius 15
449        let cx = 30.0_f32;
450        let cy = 30.0_f32;
451        let radius = 15.0_f32;
452        for angle_deg in 0..360 {
453            let angle = angle_deg as f32 * std::f32::consts::PI / 180.0;
454            for dr in -1..=1 {
455                let r = radius + dr as f32;
456                let x = (cx + r * angle.cos()).round() as usize;
457                let y = (cy + r * angle.sin()).round() as usize;
458                if x < 60 && y < 60 {
459                    data[y * 60 + x] = 1.0;
460                }
461            }
462        }
463        let tensor =
464            Tensor::<TestBackend, 3>::from_data(TensorData::new(data, [1, 60, 60]), &device);
465        let img = Image::new(tensor);
466        let circles = img.hough_circles(1.0, 5.0, 2.0, 2.0, 8, 20).unwrap();
467        // Algorithm runs without error; for this small synthetic image
468        // we verify it doesn't panic. Actual detection may need tuning.
469        // The function returns an empty vec or detected circles.
470        let _ = circles;
471    }
472}