1#[cfg(feature = "no-std")]
7use core::f32::consts;
8#[cfg(not(feature = "no-std"))]
9use std::f32::consts;
10
11#[cfg(feature = "no-std")]
12use core::cmp::Ordering;
13#[cfg(not(feature = "no-std"))]
14use std::cmp::Ordering;
15
16pub mod convolution {
18 #[cfg(feature = "no-std")]
19 use alloc::{vec, vec::Vec};
20 #[cfg(not(feature = "no-std"))]
21 use std::{vec, vec::Vec};
22
23 pub fn convolve_2d(
25 image: &[f32],
26 width: usize,
27 height: usize,
28 kernel: &[f32],
29 kernel_size: usize,
30 ) -> Vec<f32> {
31 assert_eq!(image.len(), width * height);
32 assert_eq!(kernel.len(), kernel_size * kernel_size);
33 assert!(kernel_size % 2 == 1, "Kernel size must be odd");
34
35 let mut output = vec![0.0; width * height];
36 let half_kernel = kernel_size / 2;
37
38 for y in 0..height {
39 for x in 0..width {
40 let mut sum = 0.0;
41
42 for ky in 0..kernel_size {
43 for kx in 0..kernel_size {
44 let img_y = y as i32 + ky as i32 - half_kernel as i32;
45 let img_x = x as i32 + kx as i32 - half_kernel as i32;
46
47 if img_y >= 0 && img_y < height as i32 && img_x >= 0 && img_x < width as i32
48 {
49 let img_idx = img_y as usize * width + img_x as usize;
50 let kernel_idx = ky * kernel_size + kx;
51 sum += image[img_idx] * kernel[kernel_idx];
52 }
53 }
54 }
55
56 output[y * width + x] = sum;
57 }
58 }
59
60 output
61 }
62
63 pub fn separable_convolve_2d(
65 image: &[f32],
66 width: usize,
67 height: usize,
68 kernel_x: &[f32],
69 kernel_y: &[f32],
70 ) -> Vec<f32> {
71 let temp = convolve_horizontal(image, width, height, kernel_x);
73 convolve_vertical(&temp, width, height, kernel_y)
75 }
76
77 fn convolve_horizontal(image: &[f32], width: usize, height: usize, kernel: &[f32]) -> Vec<f32> {
78 let mut output = vec![0.0; width * height];
79 let half_kernel = kernel.len() / 2;
80
81 for y in 0..height {
82 for x in 0..width {
83 let mut sum = 0.0;
84
85 for (k, &kernel_val) in kernel.iter().enumerate() {
86 let img_x = x as i32 + k as i32 - half_kernel as i32;
87
88 if img_x >= 0 && img_x < width as i32 {
89 let img_idx = y * width + img_x as usize;
90 sum += image[img_idx] * kernel_val;
91 }
92 }
93
94 output[y * width + x] = sum;
95 }
96 }
97
98 output
99 }
100
101 fn convolve_vertical(image: &[f32], width: usize, height: usize, kernel: &[f32]) -> Vec<f32> {
102 let mut output = vec![0.0; width * height];
103 let half_kernel = kernel.len() / 2;
104
105 for y in 0..height {
106 for x in 0..width {
107 let mut sum = 0.0;
108
109 for (k, &kernel_val) in kernel.iter().enumerate() {
110 let img_y = y as i32 + k as i32 - half_kernel as i32;
111
112 if img_y >= 0 && img_y < height as i32 {
113 let img_idx = img_y as usize * width + x;
114 sum += image[img_idx] * kernel_val;
115 }
116 }
117
118 output[y * width + x] = sum;
119 }
120 }
121
122 output
123 }
124}
125
126pub mod edge_detection {
128 #[cfg(feature = "no-std")]
129 use alloc::{vec, vec::Vec};
130 #[cfg(not(feature = "no-std"))]
131 use std::{vec, vec::Vec};
132
133 use super::*;
134
135 pub fn sobel(image: &[f32], width: usize, height: usize) -> Vec<f32> {
137 let sobel_x = vec![-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0];
138
139 let sobel_y = vec![-1.0, -2.0, -1.0, 0.0, 0.0, 0.0, 1.0, 2.0, 1.0];
140
141 let grad_x = convolution::convolve_2d(image, width, height, &sobel_x, 3);
142 let grad_y = convolution::convolve_2d(image, width, height, &sobel_y, 3);
143
144 grad_x
146 .iter()
147 .zip(grad_y.iter())
148 .map(|(&gx, &gy)| (gx * gx + gy * gy).sqrt())
149 .collect()
150 }
151
152 pub fn prewitt(image: &[f32], width: usize, height: usize) -> Vec<f32> {
154 let prewitt_x = vec![-1.0, 0.0, 1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0];
155
156 let prewitt_y = vec![-1.0, -1.0, -1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
157
158 let grad_x = convolution::convolve_2d(image, width, height, &prewitt_x, 3);
159 let grad_y = convolution::convolve_2d(image, width, height, &prewitt_y, 3);
160
161 grad_x
162 .iter()
163 .zip(grad_y.iter())
164 .map(|(&gx, &gy)| (gx * gx + gy * gy).sqrt())
165 .collect()
166 }
167
168 pub fn laplacian(image: &[f32], width: usize, height: usize) -> Vec<f32> {
170 let laplacian_kernel = vec![0.0, -1.0, 0.0, -1.0, 4.0, -1.0, 0.0, -1.0, 0.0];
171
172 convolution::convolve_2d(image, width, height, &laplacian_kernel, 3)
173 }
174
175 pub fn canny(
177 image: &[f32],
178 width: usize,
179 height: usize,
180 low_threshold: f32,
181 high_threshold: f32,
182 ) -> Vec<f32> {
183 let gaussian_kernel = [1.0, 2.0, 1.0, 2.0, 4.0, 2.0, 1.0, 2.0, 1.0];
185 let gaussian_sum: f32 = gaussian_kernel.iter().sum();
186 let normalized_gaussian: Vec<f32> =
187 gaussian_kernel.iter().map(|&x| x / gaussian_sum).collect();
188
189 let blurred = convolution::convolve_2d(image, width, height, &normalized_gaussian, 3);
190
191 let edges = sobel(&blurred, width, height);
193
194 edges
196 .iter()
197 .map(|&magnitude| {
198 if magnitude >= high_threshold {
199 255.0
200 } else if magnitude >= low_threshold {
201 128.0
202 } else {
203 0.0
204 }
205 })
206 .collect()
207 }
208}
209
210pub mod filters {
212 #[cfg(feature = "no-std")]
213 use alloc::{vec, vec::Vec};
214 #[cfg(not(feature = "no-std"))]
215 use std::{vec, vec::Vec};
216
217 use super::*;
218
219 pub fn gaussian_blur(image: &[f32], width: usize, height: usize, sigma: f32) -> Vec<f32> {
221 let kernel_size = (6.0 * sigma).ceil() as usize | 1; let half_size = kernel_size / 2;
224 let mut kernel = Vec::with_capacity(kernel_size);
225
226 let sigma_sq_2 = 2.0 * sigma * sigma;
227 let norm_factor = 1.0 / (sigma * (2.0 * consts::PI).sqrt());
228
229 for i in 0..kernel_size {
230 let x = (i as i32 - half_size as i32) as f32;
231 let value = norm_factor * (-x * x / sigma_sq_2).exp();
232 kernel.push(value);
233 }
234
235 let kernel_sum: f32 = kernel.iter().sum();
237 for k in &mut kernel {
238 *k /= kernel_sum;
239 }
240
241 convolution::separable_convolve_2d(image, width, height, &kernel, &kernel)
243 }
244
245 pub fn box_blur(image: &[f32], width: usize, height: usize, kernel_size: usize) -> Vec<f32> {
247 let kernel_val = 1.0 / (kernel_size * kernel_size) as f32;
248 let kernel = vec![kernel_val; kernel_size * kernel_size];
249 convolution::convolve_2d(image, width, height, &kernel, kernel_size)
250 }
251
252 pub fn median_filter(
254 image: &[f32],
255 width: usize,
256 height: usize,
257 kernel_size: usize,
258 ) -> Vec<f32> {
259 assert!(kernel_size % 2 == 1, "Kernel size must be odd");
260
261 let mut output = vec![0.0; width * height];
262 let half_kernel = kernel_size / 2;
263
264 for y in 0..height {
265 for x in 0..width {
266 let mut window = Vec::new();
267
268 for ky in 0..kernel_size {
269 for kx in 0..kernel_size {
270 let img_y = y as i32 + ky as i32 - half_kernel as i32;
271 let img_x = x as i32 + kx as i32 - half_kernel as i32;
272
273 if img_y >= 0 && img_y < height as i32 && img_x >= 0 && img_x < width as i32
274 {
275 let img_idx = img_y as usize * width + img_x as usize;
276 window.push(image[img_idx]);
277 }
278 }
279 }
280
281 window.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
282 let median = if window.len() % 2 == 0 && !window.is_empty() {
283 (window[window.len() / 2 - 1] + window[window.len() / 2]) / 2.0
284 } else if !window.is_empty() {
285 window[window.len() / 2]
286 } else {
287 0.0
288 };
289
290 output[y * width + x] = median;
291 }
292 }
293
294 output
295 }
296
297 pub fn unsharp_mask(
299 image: &[f32],
300 width: usize,
301 height: usize,
302 amount: f32,
303 sigma: f32,
304 ) -> Vec<f32> {
305 let blurred = gaussian_blur(image, width, height, sigma);
306
307 image
308 .iter()
309 .zip(blurred.iter())
310 .map(|(&original, &blur)| {
311 let detail = original - blur;
312 original + amount * detail
313 })
314 .collect()
315 }
316}
317
318pub mod morphology {
320 #[cfg(feature = "no-std")]
321 use alloc::{vec, vec::Vec};
322 #[cfg(not(feature = "no-std"))]
323 use std::{vec, vec::Vec};
324
325 pub fn erosion(
327 image: &[f32],
328 width: usize,
329 height: usize,
330 structuring_element: &[bool],
331 se_size: usize,
332 ) -> Vec<f32> {
333 assert_eq!(structuring_element.len(), se_size * se_size);
334 assert!(se_size % 2 == 1, "Structuring element size must be odd");
335
336 let mut output = vec![0.0; width * height];
337 let half_se = se_size / 2;
338
339 for y in 0..height {
340 for x in 0..width {
341 let mut min_val = f32::INFINITY;
342
343 for sy in 0..se_size {
344 for sx in 0..se_size {
345 if structuring_element[sy * se_size + sx] {
346 let img_y = y as i32 + sy as i32 - half_se as i32;
347 let img_x = x as i32 + sx as i32 - half_se as i32;
348
349 if img_y >= 0
350 && img_y < height as i32
351 && img_x >= 0
352 && img_x < width as i32
353 {
354 let img_idx = img_y as usize * width + img_x as usize;
355 min_val = min_val.min(image[img_idx]);
356 }
357 }
358 }
359 }
360
361 output[y * width + x] = if min_val == f32::INFINITY {
362 0.0
363 } else {
364 min_val
365 };
366 }
367 }
368
369 output
370 }
371
372 pub fn dilation(
374 image: &[f32],
375 width: usize,
376 height: usize,
377 structuring_element: &[bool],
378 se_size: usize,
379 ) -> Vec<f32> {
380 assert_eq!(structuring_element.len(), se_size * se_size);
381 assert!(se_size % 2 == 1, "Structuring element size must be odd");
382
383 let mut output = vec![0.0; width * height];
384 let half_se = se_size / 2;
385
386 for y in 0..height {
387 for x in 0..width {
388 let mut max_val = f32::NEG_INFINITY;
389
390 for sy in 0..se_size {
391 for sx in 0..se_size {
392 if structuring_element[sy * se_size + sx] {
393 let img_y = y as i32 + sy as i32 - half_se as i32;
394 let img_x = x as i32 + sx as i32 - half_se as i32;
395
396 if img_y >= 0
397 && img_y < height as i32
398 && img_x >= 0
399 && img_x < width as i32
400 {
401 let img_idx = img_y as usize * width + img_x as usize;
402 max_val = max_val.max(image[img_idx]);
403 }
404 }
405 }
406 }
407
408 output[y * width + x] = if max_val == f32::NEG_INFINITY {
409 0.0
410 } else {
411 max_val
412 };
413 }
414 }
415
416 output
417 }
418
419 pub fn opening(
421 image: &[f32],
422 width: usize,
423 height: usize,
424 structuring_element: &[bool],
425 se_size: usize,
426 ) -> Vec<f32> {
427 let eroded = erosion(image, width, height, structuring_element, se_size);
428 dilation(&eroded, width, height, structuring_element, se_size)
429 }
430
431 pub fn closing(
433 image: &[f32],
434 width: usize,
435 height: usize,
436 structuring_element: &[bool],
437 se_size: usize,
438 ) -> Vec<f32> {
439 let dilated = dilation(image, width, height, structuring_element, se_size);
440 erosion(&dilated, width, height, structuring_element, se_size)
441 }
442
443 pub fn circular_structuring_element(radius: usize) -> (Vec<bool>, usize) {
445 let size = 2 * radius + 1;
446 let mut element = vec![false; size * size];
447 let center = radius as i32;
448
449 for y in 0..size {
450 for x in 0..size {
451 let dy = y as i32 - center;
452 let dx = x as i32 - center;
453 let distance = ((dx * dx + dy * dy) as f32).sqrt();
454
455 if distance <= radius as f32 {
456 element[y * size + x] = true;
457 }
458 }
459 }
460
461 (element, size)
462 }
463
464 pub fn square_structuring_element(size: usize) -> (Vec<bool>, usize) {
466 (vec![true; size * size], size)
467 }
468}
469
470pub mod features {
472 #[cfg(feature = "no-std")]
473 use alloc::{vec, vec::Vec};
474 #[cfg(not(feature = "no-std"))]
475 use std::{vec, vec::Vec};
476
477 use super::*;
478
479 pub fn local_binary_pattern(
481 image: &[f32],
482 width: usize,
483 height: usize,
484 radius: usize,
485 num_points: usize,
486 ) -> Vec<u8> {
487 let mut output = vec![0u8; width * height];
488
489 for y in radius..height - radius {
490 for x in radius..width - radius {
491 let center_val = image[y * width + x];
492 let mut lbp_code = 0u8;
493
494 for p in 0..num_points {
495 let angle = 2.0 * consts::PI * p as f32 / num_points as f32;
496 let dy = (radius as f32 * angle.sin()).round() as i32;
497 let dx = (radius as f32 * angle.cos()).round() as i32;
498
499 let ny = y as i32 + dy;
500 let nx = x as i32 + dx;
501
502 if ny >= 0 && ny < height as i32 && nx >= 0 && nx < width as i32 {
503 let neighbor_val = image[ny as usize * width + nx as usize];
504 if neighbor_val >= center_val {
505 lbp_code |= 1 << p;
506 }
507 }
508 }
509
510 output[y * width + x] = lbp_code;
511 }
512 }
513
514 output
515 }
516
517 pub fn harris_corners(
519 image: &[f32],
520 width: usize,
521 height: usize,
522 k: f32,
523 threshold: f32,
524 ) -> Vec<(usize, usize)> {
525 let sobel_x = vec![-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0];
527 let sobel_y = vec![-1.0, -2.0, -1.0, 0.0, 0.0, 0.0, 1.0, 2.0, 1.0];
528
529 let grad_x = convolution::convolve_2d(image, width, height, &sobel_x, 3);
530 let grad_y = convolution::convolve_2d(image, width, height, &sobel_y, 3);
531
532 let mut ixx = vec![0.0; width * height];
534 let mut iyy = vec![0.0; width * height];
535 let mut ixy = vec![0.0; width * height];
536
537 for i in 0..width * height {
538 ixx[i] = grad_x[i] * grad_x[i];
539 iyy[i] = grad_y[i] * grad_y[i];
540 ixy[i] = grad_x[i] * grad_y[i];
541 }
542
543 let gaussian_kernel = [1.0, 2.0, 1.0];
545 let gaussian_sum: f32 = gaussian_kernel.iter().sum();
546 let normalized_gaussian: Vec<f32> =
547 gaussian_kernel.iter().map(|&x| x / gaussian_sum).collect();
548
549 ixx = convolution::separable_convolve_2d(
550 &ixx,
551 width,
552 height,
553 &normalized_gaussian,
554 &normalized_gaussian,
555 );
556 iyy = convolution::separable_convolve_2d(
557 &iyy,
558 width,
559 height,
560 &normalized_gaussian,
561 &normalized_gaussian,
562 );
563 ixy = convolution::separable_convolve_2d(
564 &ixy,
565 width,
566 height,
567 &normalized_gaussian,
568 &normalized_gaussian,
569 );
570
571 let mut corners = Vec::new();
573 for y in 1..height - 1 {
574 for x in 1..width - 1 {
575 let idx = y * width + x;
576 let det = ixx[idx] * iyy[idx] - ixy[idx] * ixy[idx];
577 let trace = ixx[idx] + iyy[idx];
578 let harris_response = det - k * trace * trace;
579
580 if harris_response > threshold {
581 corners.push((x, y));
582 }
583 }
584 }
585
586 corners
587 }
588}
589
590#[allow(non_snake_case)]
591#[cfg(all(test, not(feature = "no-std")))]
592mod tests {
593 use super::*;
594
595 #[cfg(feature = "no-std")]
596 use alloc::{
597 string::{String, ToString},
598 vec,
599 vec::Vec,
600 };
601
602 #[test]
603 fn test_2d_convolution() {
604 let image = vec![0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0];
605
606 let kernel = vec![0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0];
607
608 let result = convolution::convolve_2d(&image, 3, 3, &kernel, 3);
609
610 assert!(result[4] > result[0]);
612 assert!(result[4] > result[8]);
613 }
614
615 #[test]
616 fn test_sobel_edge_detection() {
617 let image = vec![
618 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
619 ];
620
621 let edges = edge_detection::sobel(&image, 4, 4);
622
623 assert!(edges[4] > 0.0 || edges[5] > 0.0 || edges[6] > 0.0 || edges[7] > 0.0);
625 }
626
627 #[test]
628 fn test_gaussian_blur() {
629 let image = vec![0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0];
630
631 let blurred = filters::gaussian_blur(&image, 3, 3, 1.0);
632
633 assert!(blurred[4] < 1.0);
635 assert!(blurred[1] > 0.0);
637 assert!(blurred[3] > 0.0);
638 }
639
640 #[test]
641 fn test_median_filter() {
642 let image = vec![
643 1.0, 1.0, 1.0, 1.0, 9.0, 1.0, 1.0, 1.0, 1.0,
645 ];
646
647 let filtered = filters::median_filter(&image, 3, 3, 3);
648
649 assert!(filtered[4] < 9.0);
651 assert!(filtered[4] <= 1.0);
652 }
653
654 #[test]
655 fn test_erosion_dilation() {
656 let image = vec![
657 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0,
658 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
659 ];
660
661 let (se, se_size) = morphology::square_structuring_element(3);
662
663 let eroded = morphology::erosion(&image, 5, 5, &se, se_size);
664 let dilated = morphology::dilation(&image, 5, 5, &se, se_size);
665
666 assert!(eroded.iter().sum::<f32>() <= image.iter().sum::<f32>());
668
669 assert!(dilated.iter().sum::<f32>() >= image.iter().sum::<f32>());
671 }
672
673 #[test]
674 fn test_circular_structuring_element() {
675 let (se, size) = morphology::circular_structuring_element(2);
676 assert_eq!(size, 5);
677
678 assert!(se[2 * 5 + 2]);
680
681 assert!(!se[0]);
683 assert!(!se[4]);
684 assert!(!se[20]);
685 assert!(!se[24]);
686 }
687
688 #[test]
689 fn test_local_binary_pattern() {
690 let image = vec![
691 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
692 ];
693
694 let lbp = features::local_binary_pattern(&image, 4, 4, 1, 8);
695
696 assert!(lbp[5] > 0 || lbp[6] > 0);
698 }
699
700 #[test]
701 fn test_harris_corners() {
702 let image = vec![
704 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0,
705 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
706 ];
707
708 let _corners = features::harris_corners(&image, 5, 5, 0.04, 0.01);
709
710 }
712
713 #[test]
714 fn test_unsharp_mask() {
715 let image = vec![0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0];
716
717 let sharpened = filters::unsharp_mask(&image, 3, 3, 1.0, 1.0);
718
719 assert!(sharpened[4] >= image[4]);
721 }
722}