1#![allow(unsafe_code)]
6
7pub(crate) const RAYON_THRESHOLD: usize = 4096; pub(crate) mod gcd {
15 #[inline]
17 #[cfg(target_os = "macos")]
18 pub fn parallel_for<F: Fn(usize) + Sync + Send>(n: usize, f: F) {
19 if n <= 1 {
20 for i in 0..n {
21 f(i);
22 }
23 return;
24 }
25 use rayon::prelude::*;
26 (0..n).into_par_iter().for_each(f);
27 }
28
29 #[inline]
34 #[cfg(not(target_os = "macos"))]
35 pub fn parallel_for<F: Fn(usize) + Sync>(n: usize, f: F) {
36 let cpus = std::thread::available_parallelism()
37 .map(|n| n.get())
38 .unwrap_or(1);
39 if cpus <= 1 || n <= 1 {
40 for i in 0..n {
41 f(i);
42 }
43 return;
44 }
45 let threads = cpus.min(n);
46 let chunk = n.div_ceil(threads);
47 std::thread::scope(|s| {
48 for t in 0..threads {
49 let start = t * chunk;
50 let end = (start + chunk).min(n);
51 if start >= end {
52 continue;
53 }
54 let f = &f;
55 s.spawn(move || {
56 for i in start..end {
57 f(i);
58 }
59 });
60 }
61 });
62 }
63}
64
65#[derive(Clone, Debug)]
67pub struct ImageU8 {
68 data: Vec<u8>,
69 height: usize,
70 width: usize,
71 channels: usize,
72}
73
74impl ImageU8 {
75 pub fn new(data: Vec<u8>, height: usize, width: usize, channels: usize) -> Option<Self> {
77 if data.len() != height * width * channels {
78 return None;
79 }
80 Some(Self {
81 data,
82 height,
83 width,
84 channels,
85 })
86 }
87
88 pub fn zeros(height: usize, width: usize, channels: usize) -> Self {
90 Self {
91 data: vec![0u8; height * width * channels],
92 height,
93 width,
94 channels,
95 }
96 }
97
98 pub fn data(&self) -> &[u8] {
99 &self.data
100 }
101 pub fn data_mut(&mut self) -> &mut [u8] {
102 &mut self.data
103 }
104 pub fn height(&self) -> usize {
105 self.height
106 }
107 pub fn width(&self) -> usize {
108 self.width
109 }
110 pub fn channels(&self) -> usize {
111 self.channels
112 }
113 pub fn len(&self) -> usize {
114 self.data.len()
115 }
116 pub fn is_empty(&self) -> bool {
117 self.data.is_empty()
118 }
119
120 pub fn from_tensor(tensor: &yscv_tensor::Tensor) -> Option<Self> {
122 let shape = tensor.shape();
123 if shape.len() != 3 {
124 return None;
125 }
126 let (h, w, c) = (shape[0], shape[1], shape[2]);
127 let data: Vec<u8> = tensor
128 .data()
129 .iter()
130 .map(|&v| (v * 255.0).clamp(0.0, 255.0) as u8)
131 .collect();
132 Some(Self {
133 data,
134 height: h,
135 width: w,
136 channels: c,
137 })
138 }
139
140 pub fn to_tensor(&self) -> yscv_tensor::Tensor {
142 let data: Vec<f32> = self.data.iter().map(|&v| v as f32 / 255.0).collect();
143 yscv_tensor::Tensor::from_vec(vec![self.height, self.width, self.channels], data)
145 .expect("ImageU8::to_tensor: shape mismatch (bug)")
146 }
147}
148
149#[derive(Clone, Debug)]
155pub struct ImageF32 {
156 data: Vec<f32>,
157 height: usize,
158 width: usize,
159 channels: usize,
160}
161
162impl ImageF32 {
163 pub fn new(data: Vec<f32>, height: usize, width: usize, channels: usize) -> Option<Self> {
165 if data.len() != height * width * channels {
166 return None;
167 }
168 Some(Self {
169 data,
170 height,
171 width,
172 channels,
173 })
174 }
175
176 pub fn zeros(height: usize, width: usize, channels: usize) -> Self {
178 Self {
179 data: vec![0.0f32; height * width * channels],
180 height,
181 width,
182 channels,
183 }
184 }
185
186 pub fn data(&self) -> &[f32] {
187 &self.data
188 }
189 pub fn data_mut(&mut self) -> &mut [f32] {
190 &mut self.data
191 }
192 pub fn height(&self) -> usize {
193 self.height
194 }
195 pub fn width(&self) -> usize {
196 self.width
197 }
198 pub fn channels(&self) -> usize {
199 self.channels
200 }
201 pub fn len(&self) -> usize {
202 self.data.len()
203 }
204 pub fn is_empty(&self) -> bool {
205 self.data.is_empty()
206 }
207
208 pub fn to_tensor(&self) -> yscv_tensor::Tensor {
210 yscv_tensor::Tensor::from_vec(
211 vec![self.height, self.width, self.channels],
212 self.data.clone(),
213 )
214 .expect("ImageF32::to_tensor: shape mismatch")
215 }
216
217 pub fn from_tensor(tensor: &yscv_tensor::Tensor) -> Option<Self> {
219 let shape = tensor.shape();
220 if shape.len() != 3 {
221 return None;
222 }
223 Self::new(tensor.data().to_vec(), shape[0], shape[1], shape[2])
224 }
225}
226
227#[cfg(test)]
232mod tests {
233 use super::super::f32_ops::*;
234 use super::super::u8_canny::*;
235 use super::super::u8_features::*;
236 use super::super::u8_filters::*;
237 use super::super::u8_resize::*;
238 use super::*;
239
240 #[test]
241 fn test_grayscale_u8() {
242 let mut data = vec![0u8; 4 * 4 * 3];
243 data[0] = 255;
244 data[1] = 255;
245 data[2] = 255;
246 let img = ImageU8::new(data, 4, 4, 3).unwrap();
247 let gray = grayscale_u8(&img).unwrap();
248 assert_eq!(gray.channels(), 1);
249 assert_eq!(gray.height(), 4);
250 assert_eq!(gray.width(), 4);
251 assert!(gray.data()[0] >= 250, "got {}", gray.data()[0]);
252 assert_eq!(gray.data()[1], 0);
253 }
254
255 #[test]
256 fn test_dilate_u8() {
257 let mut data = vec![0u8; 8 * 8];
258 data[4 * 8 + 4] = 200;
259 let img = ImageU8::new(data, 8, 8, 1).unwrap();
260 let dilated = dilate_3x3_u8(&img).unwrap();
261 for dy in -1i32..=1 {
262 for dx in -1i32..=1 {
263 let y = (4 + dy) as usize;
264 let x = (4 + dx) as usize;
265 assert_eq!(dilated.data()[y * 8 + x], 200, "at ({},{})", y, x);
266 }
267 }
268 }
269
270 #[test]
271 fn test_erode_u8() {
272 let data = vec![200u8; 8 * 8];
273 let img = ImageU8::new(data, 8, 8, 1).unwrap();
274 let eroded = erode_3x3_u8(&img).unwrap();
275 assert_eq!(eroded.data()[4 * 8 + 4], 200);
276 }
277
278 #[test]
279 fn test_gaussian_blur_u8() {
280 let mut data = vec![128u8; 16 * 16];
281 data[8 * 16 + 8] = 255;
282 let img = ImageU8::new(data, 16, 16, 1).unwrap();
283 let blurred = gaussian_blur_3x3_u8(&img).unwrap();
284 assert_eq!(blurred.channels(), 1);
285 let center = blurred.data()[8 * 16 + 8];
286 assert!(center > 128 && center < 255, "got {}", center);
287 }
288
289 #[test]
290 fn test_box_blur_u8() {
291 let data = vec![100u8; 16 * 16];
292 let img = ImageU8::new(data, 16, 16, 1).unwrap();
293 let blurred = box_blur_3x3_u8(&img).unwrap();
294 let center = blurred.data()[8 * 16 + 8];
295 assert!((99..=101).contains(¢er), "got {}", center);
296 }
297
298 #[test]
299 fn test_sobel_u8() {
300 let mut data = vec![0u8; 16 * 16];
301 for y in 0..16 {
302 for x in 8..16 {
303 data[y * 16 + x] = 255;
304 }
305 }
306 let img = ImageU8::new(data, 16, 16, 1).unwrap();
307 let edges = sobel_3x3_magnitude_u8(&img).unwrap();
308 let edge_val = edges.data()[8 * 16 + 8];
309 assert!(edge_val > 100, "edge value = {}", edge_val);
310 assert_eq!(edges.data()[8 * 16 + 2], 0);
311 }
312
313 #[test]
314 fn test_image_u8_tensor_roundtrip() {
315 let data = vec![128u8; 4 * 4 * 3];
316 let img = ImageU8::new(data, 4, 4, 3).unwrap();
317 let tensor = img.to_tensor();
318 assert_eq!(tensor.shape(), &[4, 4, 3]);
319 let back = ImageU8::from_tensor(&tensor).unwrap();
320 for (a, b) in img.data().iter().zip(back.data().iter()) {
321 assert!((*a as i16 - *b as i16).unsigned_abs() <= 1);
322 }
323 }
324
325 #[test]
326 fn test_median_blur_u8() {
327 let data = vec![100u8; 16 * 16];
329 let img = ImageU8::new(data, 16, 16, 1).unwrap();
330 let blurred = median_blur_3x3_u8(&img).unwrap();
331 let center = blurred.data()[8 * 16 + 8];
332 assert_eq!(center, 100);
333
334 let mut data2 = vec![128u8; 16 * 16];
336 data2[8 * 16 + 8] = 255; let img2 = ImageU8::new(data2, 16, 16, 1).unwrap();
338 let blurred2 = median_blur_3x3_u8(&img2).unwrap();
339 assert_eq!(blurred2.data()[8 * 16 + 8], 128);
341 }
342
343 #[test]
344 fn test_canny_u8() {
345 let mut data = vec![0u8; 32 * 32];
347 for y in 0..32 {
348 for x in 16..32 {
349 data[y * 32 + x] = 255;
350 }
351 }
352 let img = ImageU8::new(data, 32, 32, 1).unwrap();
353 let edges = canny_u8(&img, 30, 100).unwrap();
354 assert_eq!(edges.channels(), 1);
355 let edge_count: usize = edges.data().iter().filter(|&&v| v == 255).count();
357 assert!(edge_count > 5, "too few edges: {}", edge_count);
358 assert_eq!(edges.data()[16 * 32 + 2], 0);
360 }
361
362 #[test]
363 fn test_resize_bilinear_u8() {
364 let data = vec![128u8; 16 * 16];
366 let img = ImageU8::new(data, 16, 16, 1).unwrap();
367 let resized = resize_bilinear_u8(&img, 8, 8).unwrap();
368 assert_eq!(resized.height(), 8);
369 assert_eq!(resized.width(), 8);
370 assert_eq!(resized.channels(), 1);
371 for &v in resized.data() {
373 assert_eq!(v, 128);
374 }
375
376 let data3 = vec![100u8; 4 * 4 * 3];
378 let img3 = ImageU8::new(data3, 4, 4, 3).unwrap();
379 let up = resize_bilinear_u8(&img3, 8, 8).unwrap();
380 assert_eq!(up.height(), 8);
381 assert_eq!(up.width(), 8);
382 assert_eq!(up.channels(), 3);
383 for &v in up.data() {
384 assert_eq!(v, 100);
385 }
386 }
387
388 #[test]
389 fn test_resize_nearest_u8() {
390 let data = vec![128u8; 16 * 16];
392 let img = ImageU8::new(data, 16, 16, 1).unwrap();
393 let resized = resize_nearest_u8(&img, 8, 8).unwrap();
394 assert_eq!(resized.height(), 8);
395 assert_eq!(resized.width(), 8);
396 assert_eq!(resized.channels(), 1);
397 for &v in resized.data() {
398 assert_eq!(v, 128);
399 }
400
401 let data4: Vec<u8> = (0..16).collect();
403 let img4 = ImageU8::new(data4, 4, 4, 1).unwrap();
404 let up = resize_nearest_u8(&img4, 8, 8).unwrap();
405 assert_eq!(up.height(), 8);
406 assert_eq!(up.width(), 8);
407 assert_eq!(up.data()[0], 0);
409 assert_eq!(up.data()[1], 0);
410 assert_eq!(up.data()[8], 0);
411 assert_eq!(up.data()[9], 0);
412
413 let data3 = vec![100u8; 4 * 4 * 3];
415 let img3 = ImageU8::new(data3, 4, 4, 3).unwrap();
416 let up3 = resize_nearest_u8(&img3, 8, 8).unwrap();
417 assert_eq!(up3.height(), 8);
418 assert_eq!(up3.width(), 8);
419 assert_eq!(up3.channels(), 3);
420 for &v in up3.data() {
421 assert_eq!(v, 100);
422 }
423
424 assert!(resize_nearest_u8(&img, 0, 8).is_none());
426 assert!(resize_nearest_u8(&img, 8, 0).is_none());
427 }
428
429 #[test]
434 fn test_image_f32_new() {
435 let img = ImageF32::new(vec![1.0; 12], 2, 2, 3).unwrap();
436 assert_eq!(img.height(), 2);
437 assert_eq!(img.width(), 2);
438 assert_eq!(img.channels(), 3);
439 assert_eq!(img.len(), 12);
440 assert!(!img.is_empty());
441 assert!(ImageF32::new(vec![1.0; 10], 2, 2, 3).is_none());
443 }
444
445 #[test]
446 fn test_image_f32_tensor_roundtrip() {
447 let data: Vec<f32> = (0..48).map(|i| i as f32 / 48.0).collect();
448 let img = ImageF32::new(data.clone(), 4, 4, 3).unwrap();
449 let tensor = img.to_tensor();
450 assert_eq!(tensor.shape(), &[4, 4, 3]);
451 let back = ImageF32::from_tensor(&tensor).unwrap();
452 assert_eq!(back.data(), img.data());
453 }
454
455 #[test]
456 fn test_grayscale_f32_known_values() {
457 let data = vec![1.0f32; 4 * 4 * 3];
459 let img = ImageF32::new(data, 4, 4, 3).unwrap();
460 let gray = grayscale_f32(&img).unwrap();
461 assert_eq!(gray.channels(), 1);
462 assert_eq!(gray.height(), 4);
463 assert_eq!(gray.width(), 4);
464 for &v in gray.data() {
465 assert!((v - 1.0).abs() < 0.01, "white pixel gray = {}", v);
466 }
467
468 let mut red_data = vec![0.0f32; 4 * 4 * 3];
470 for i in 0..16 {
471 red_data[i * 3] = 1.0;
472 }
473 let red_img = ImageF32::new(red_data, 4, 4, 3).unwrap();
474 let red_gray = grayscale_f32(&red_img).unwrap();
475 for &v in red_gray.data() {
476 assert!((v - 0.299).abs() < 0.01, "red pixel gray = {}", v);
477 }
478
479 let gray_img = ImageF32::zeros(4, 4, 1);
481 assert!(grayscale_f32(&gray_img).is_none());
482 }
483
484 #[test]
485 fn test_gaussian_blur_f32_uniform() {
486 let data = vec![0.5f32; 32 * 32];
489 let img = ImageF32::new(data, 32, 32, 1).unwrap();
490 let blurred = gaussian_blur_3x3_f32(&img).unwrap();
491 for y in 1..31 {
493 for x in 1..31 {
494 let v = blurred.data()[y * 32 + x];
495 assert!(
496 (v - 0.5).abs() < 1e-4,
497 "uniform gaussian at ({},{}) got {}",
498 x,
499 y,
500 v
501 );
502 }
503 }
504 }
505
506 #[test]
507 fn test_gaussian_blur_f32_smoothing() {
508 let mut data = vec![0.5f32; 16 * 16];
509 data[8 * 16 + 8] = 1.0; let img = ImageF32::new(data, 16, 16, 1).unwrap();
511 let blurred = gaussian_blur_3x3_f32(&img).unwrap();
512 let center = blurred.data()[8 * 16 + 8];
513 assert!(center > 0.5 && center < 1.0, "gaussian center = {}", center);
514 }
515
516 #[test]
517 fn test_box_blur_f32_uniform() {
518 let data = vec![0.75f32; 32 * 32];
519 let img = ImageF32::new(data, 32, 32, 1).unwrap();
520 let blurred = box_blur_3x3_f32(&img).unwrap();
521 for y in 1..31 {
523 for x in 1..31 {
524 let v = blurred.data()[y * 32 + x];
525 assert!(
526 (v - 0.75).abs() < 1e-4,
527 "uniform box at ({},{}) got {}",
528 x,
529 y,
530 v
531 );
532 }
533 }
534 }
535
536 #[test]
537 fn test_dilate_f32_known_pattern() {
538 let mut data = vec![0.0f32; 8 * 8];
540 data[4 * 8 + 4] = 0.9;
541 let img = ImageF32::new(data, 8, 8, 1).unwrap();
542 let dilated = dilate_3x3_f32(&img).unwrap();
543 for dy in -1i32..=1 {
544 for dx in -1i32..=1 {
545 let y = (4 + dy) as usize;
546 let x = (4 + dx) as usize;
547 assert!(
548 (dilated.data()[y * 8 + x] - 0.9).abs() < 1e-6,
549 "dilate at ({},{}) = {}",
550 y,
551 x,
552 dilated.data()[y * 8 + x]
553 );
554 }
555 }
556 assert!((dilated.data()[0] - 0.0).abs() < 1e-6);
558 }
559
560 #[test]
561 fn test_sobel_f32_flat_is_zero() {
562 let data = vec![0.5f32; 16 * 16];
564 let img = ImageF32::new(data, 16, 16, 1).unwrap();
565 let edges = sobel_3x3_f32(&img).unwrap();
566 for y in 1..15 {
568 for x in 1..15 {
569 assert!(
570 (edges.data()[y * 16 + x]).abs() < 1e-5,
571 "sobel flat at ({},{}) = {}",
572 y,
573 x,
574 edges.data()[y * 16 + x]
575 );
576 }
577 }
578 }
579
580 #[test]
581 fn test_sobel_f32_edge_detection() {
582 let mut data = vec![0.0f32; 16 * 16];
584 for y in 0..16 {
585 for x in 8..16 {
586 data[y * 16 + x] = 1.0;
587 }
588 }
589 let img = ImageF32::new(data, 16, 16, 1).unwrap();
590 let edges = sobel_3x3_f32(&img).unwrap();
591 let edge_val = edges.data()[8 * 16 + 8];
593 assert!(edge_val > 0.5, "sobel edge = {}", edge_val);
594 assert!(edges.data()[8 * 16 + 2].abs() < 1e-5);
596 }
597
598 #[test]
599 fn test_threshold_binary_f32() {
600 let data = vec![0.0, 0.3, 0.5, 0.7, 0.8, 1.0, 0.1, 0.6, 0.9];
601 let img = ImageF32::new(data, 3, 3, 1).unwrap();
602 let result = threshold_binary_f32(&img, 0.5, 1.0).unwrap();
603 let expected = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0];
604 for (i, (&got, &exp)) in result.data().iter().zip(expected.iter()).enumerate() {
605 assert!(
606 (got - exp).abs() < 1e-6,
607 "threshold at {} = {}, expected {}",
608 i,
609 got,
610 exp
611 );
612 }
613 }
614
615 #[test]
620 fn test_fast9_detect_u8_empty() {
621 let img = ImageU8::zeros(32, 32, 1);
623 let corners = fast9_detect_u8(&img, 20, false);
624 assert!(
625 corners.is_empty(),
626 "uniform image should have 0 corners, got {}",
627 corners.len()
628 );
629 }
630
631 #[test]
632 fn test_fast9_detect_u8_bright_spot() {
633 let mut data = vec![0u8; 32 * 32];
634 data[16 * 32 + 16] = 255;
635 let img = ImageU8::new(data, 32, 32, 1).unwrap();
636 let corners = fast9_detect_u8(&img, 20, false);
637 assert!(
638 !corners.is_empty(),
639 "bright spot should produce at least one corner"
640 );
641 }
642
643 #[test]
644 fn test_fast9_detect_u8_too_small() {
645 let img = ImageU8::zeros(5, 5, 1);
646 let corners = fast9_detect_u8(&img, 20, false);
647 assert!(corners.is_empty());
648 }
649
650 #[test]
651 fn test_fast9_detect_u8_wrong_channels() {
652 let img = ImageU8::zeros(32, 32, 3);
653 let corners = fast9_detect_u8(&img, 20, false);
654 assert!(corners.is_empty());
655 }
656
657 #[test]
658 fn test_fast9_detect_u8_nms_reduces() {
659 let mut data = vec![50u8; 64 * 64];
660 for i in 0..5 {
661 let y = 10 + i * 10;
662 let x = 10 + i * 10;
663 data[y * 64 + x] = 255;
664 }
665 let img = ImageU8::new(data, 64, 64, 1).unwrap();
666 let no_nms = fast9_detect_u8(&img, 20, false);
667 let with_nms = fast9_detect_u8(&img, 20, true);
668 assert!(
669 with_nms.len() <= no_nms.len(),
670 "NMS should not increase corners: {} > {}",
671 with_nms.len(),
672 no_nms.len()
673 );
674 }
675
676 #[test]
681 fn test_distance_transform_u8_all_nonzero() {
682 let data = vec![255u8; 16 * 16];
683 let img = ImageU8::new(data, 16, 16, 1).unwrap();
684 let dt = distance_transform_u8(&img);
685 assert_eq!(dt.len(), 256);
686 for &d in &dt {
687 assert_eq!(d, 0);
688 }
689 }
690
691 #[test]
692 fn test_distance_transform_u8_single_source() {
693 let mut data = vec![0u8; 16 * 16];
694 data[8 * 16 + 8] = 255;
695 let img = ImageU8::new(data, 16, 16, 1).unwrap();
696 let dt = distance_transform_u8(&img);
697 assert_eq!(dt[8 * 16 + 8], 0);
698 assert_eq!(dt[8 * 16 + 9], 1);
699 assert_eq!(dt[7 * 16 + 8], 1);
700 assert_eq!(dt[7 * 16 + 7], 2);
701 assert_eq!(dt[0], 16);
702 }
703
704 #[test]
705 fn test_distance_transform_u8_wrong_channels() {
706 let img = ImageU8::zeros(16, 16, 3);
707 let dt = distance_transform_u8(&img);
708 assert!(dt.is_empty());
709 }
710
711 #[test]
712 fn test_distance_transform_u8_row_edge() {
713 let mut data = vec![0u8; 8 * 8];
714 data[4 * 8] = 255;
715 let img = ImageU8::new(data, 8, 8, 1).unwrap();
716 let dt = distance_transform_u8(&img);
717 assert_eq!(dt[4 * 8], 0);
718 assert_eq!(dt[4 * 8 + 1], 1);
719 assert_eq!(dt[4 * 8 + 2], 2);
720 }
721
722 #[test]
727 fn test_warp_perspective_u8_identity() {
728 let mut data = vec![0u8; 16 * 16];
729 data[8 * 16 + 8] = 200;
730 let img = ImageU8::new(data.clone(), 16, 16, 1).unwrap();
731 let identity: [f64; 9] = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
732 let result = warp_perspective_u8(&img, &identity, 16, 16);
733 assert_eq!(result.height(), 16);
734 assert_eq!(result.width(), 16);
735 assert_eq!(result.channels(), 1);
736 assert_eq!(result.data()[8 * 16 + 8], 200);
737 }
738
739 #[test]
740 fn test_warp_perspective_u8_translation() {
741 let mut data = vec![0u8; 16 * 16];
742 data[8 * 16 + 8] = 200;
743 let img = ImageU8::new(data, 16, 16, 1).unwrap();
744 let h_translate: [f64; 9] = [1.0, 0.0, 2.0, 0.0, 1.0, 3.0, 0.0, 0.0, 1.0];
745 let result = warp_perspective_u8(&img, &h_translate, 16, 16);
746 assert_eq!(result.data()[5 * 16 + 6], 200);
747 }
748
749 #[test]
750 fn test_warp_perspective_u8_out_of_bounds() {
751 let data = vec![128u8; 8 * 8];
752 let img = ImageU8::new(data, 8, 8, 1).unwrap();
753 let h_big: [f64; 9] = [1.0, 0.0, 1000.0, 0.0, 1.0, 1000.0, 0.0, 0.0, 1.0];
754 let result = warp_perspective_u8(&img, &h_big, 8, 8);
755 for &v in result.data() {
756 assert_eq!(v, 0);
757 }
758 }
759
760 #[test]
761 fn test_warp_perspective_u8_different_output_size() {
762 let data = vec![100u8; 8 * 8];
763 let img = ImageU8::new(data, 8, 8, 1).unwrap();
764 let identity: [f64; 9] = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
765 let result = warp_perspective_u8(&img, &identity, 4, 4);
766 assert_eq!(result.height(), 4);
767 assert_eq!(result.width(), 4);
768 assert_eq!(result.data()[2 * 4 + 2], 100);
769 }
770
771 #[test]
772 fn test_bilateral_filter_u8_flat_image() {
773 let data = vec![128u8; 32 * 32];
774 let result = bilateral_filter_u8(&data, 32, 32, 1, 5, 75.0, 75.0);
775 assert_eq!(result.len(), 32 * 32);
776 for &v in &result {
777 assert_eq!(v, 128, "flat image should be preserved");
778 }
779 }
780
781 #[test]
782 fn test_bilateral_filter_u8_preserves_edges() {
783 let mut data = vec![0u8; 64 * 64];
784 for y in 0..64 {
785 for x in 0..64 {
786 data[y * 64 + x] = if x < 32 { 50 } else { 200 };
787 }
788 }
789 let result = bilateral_filter_u8(&data, 64, 64, 1, 5, 20.0, 75.0);
790 assert!(
791 (result[32 * 64 + 5] as i16 - 50).abs() <= 2,
792 "left interior preserved"
793 );
794 assert!(
795 (result[32 * 64 + 58] as i16 - 200).abs() <= 2,
796 "right interior preserved"
797 );
798 }
799
800 #[test]
801 fn test_bilateral_filter_u8_dimensions() {
802 let data = vec![100u8; 480 * 640];
803 let result = bilateral_filter_u8(&data, 640, 480, 1, 5, 75.0, 75.0);
804 assert_eq!(result.len(), 480 * 640);
805 }
806}