1pub mod gradients;
2
3use crate::error::Result;
4use crate::image::Image;
5use burn::tensor::{Tensor, TensorData, backend::Backend};
6
7pub type LineSegment = ((usize, usize), (usize, usize));
9
10impl<B: Backend> Image<B> {
11 pub fn sobel(&self) -> Result<Self> {
14 let gray = self.grayscale()?;
15 let dims = gray.tensor.dims();
16 let _c = dims[0]; 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 pub fn canny(&self, low_threshold: f32, high_threshold: f32) -> Result<Self> {
53 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 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 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 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 let (mag1, mag2) =
109 if (0.0..22.5).contains(&angle) || (157.5..=180.0).contains(&angle) {
110 (mag_vals[y * w + (x + 1)], mag_vals[y * w + (x - 1)])
112 } else if (22.5..67.5).contains(&angle) {
113 (
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 (mag_vals[(y - 1) * w + x], mag_vals[(y + 1) * w + x])
121 } else {
122 (
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 let mut final_vals = vec![0.0f32; h * w];
139 let mut strong_edges = Vec::new();
140
141 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; } else if val >= low_threshold {
154 labels[idx] = 1;
155 }
156 }
157 }
158
159 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 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 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 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 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 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 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 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 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 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 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 let edge_thresh = param1 * 0.5;
341 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 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 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 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 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 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 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 let _ = circles;
471 }
472}