Skip to main content

yscv_imgproc/ops/
u8ops.rs

1//! u8 image processing operations.
2//!
3//! These operate directly on `&[u8]` pixel data in HWC layout, giving ~4x throughput
4//! over f32 paths (16 u8 per NEON register vs 4 f32).
5#![allow(unsafe_code)]
6
7// WHY 4096: below 4096 pixels, thread dispatch overhead (~1-3us) exceeds the compute saved by parallelism.
8pub(crate) const RAYON_THRESHOLD: usize = 4096; // min pixels before we go parallel
9
10/// Low-overhead parallel-for.
11///
12/// On macOS uses rayon (which meshes well with GCD), on other platforms uses
13/// `std::thread::scope` for ~1us dispatch latency (vs rayon's 3-5us).
14pub(crate) mod gcd {
15    /// Execute `f(0), f(1), ..., f(n-1)` in parallel.
16    #[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    /// Execute `f(0), f(1), ..., f(n-1)` in parallel using `std::thread::scope`.
30    ///
31    /// Lower dispatch overhead than rayon (~1us vs 3-5us) which matters for
32    /// small-to-medium workloads common in image processing.
33    #[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/// Simple u8 image wrapper (HWC layout, row-major).
66#[derive(Clone, Debug)]
67pub struct ImageU8 {
68    data: Vec<u8>,
69    height: usize,
70    width: usize,
71    channels: usize,
72}
73
74impl ImageU8 {
75    /// Creates a new image from raw bytes. Returns `None` if length doesn't match.
76    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    /// Creates a zero-filled image.
89    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    /// Convert from f32 Tensor `[H,W,C]` to u8 image (clamp to `[0,255]`).
121    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    /// Convert to f32 Tensor `[H,W,C]` with values in `[0,1]`.
141    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        // Shape always matches data length for a valid ImageU8, so this cannot fail.
144        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// ============================================================================
150// ImageF32 — zero-overhead f32 image wrapper (HWC layout, row-major)
151// ============================================================================
152
153/// Simple f32 image wrapper (HWC layout, row-major). Zero overhead.
154#[derive(Clone, Debug)]
155pub struct ImageF32 {
156    data: Vec<f32>,
157    height: usize,
158    width: usize,
159    channels: usize,
160}
161
162impl ImageF32 {
163    /// Creates a new image from raw f32 data. Returns `None` if length doesn't match.
164    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    /// Creates a zero-filled f32 image.
177    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    /// Convert to Tensor `[H,W,C]`.
209    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    /// Convert from Tensor `[H,W,C]`.
218    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// ============================================================================
228// Tests
229// ============================================================================
230
231#[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(&center), "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        // Uniform image should stay the same
328        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        // Salt & pepper: single outlier should be removed by median
335        let mut data2 = vec![128u8; 16 * 16];
336        data2[8 * 16 + 8] = 255; // outlier
337        let img2 = ImageU8::new(data2, 16, 16, 1).unwrap();
338        let blurred2 = median_blur_3x3_u8(&img2).unwrap();
339        // Median of 8x128 + 1x255 = 128
340        assert_eq!(blurred2.data()[8 * 16 + 8], 128);
341    }
342
343    #[test]
344    fn test_canny_u8() {
345        // Vertical edge
346        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        // Should have some edge pixels
356        let edge_count: usize = edges.data().iter().filter(|&&v| v == 255).count();
357        assert!(edge_count > 5, "too few edges: {}", edge_count);
358        // Interior of uniform region should be 0
359        assert_eq!(edges.data()[16 * 32 + 2], 0);
360    }
361
362    #[test]
363    fn test_resize_bilinear_u8() {
364        // Simple downscale
365        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        // All pixels should be 128 for uniform input
372        for &v in resized.data() {
373            assert_eq!(v, 128);
374        }
375
376        // Upscale RGB
377        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        // Uniform downscale
391        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        // Upscale: each pixel should be replicated
402        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        // Top-left 2x2 block should all be pixel (0,0) = 0
408        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        // RGB uniform
414        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        // Zero target returns None
425        assert!(resize_nearest_u8(&img, 0, 8).is_none());
426        assert!(resize_nearest_u8(&img, 8, 0).is_none());
427    }
428
429    // ========================================================================
430    // ImageF32 and f32 ops tests
431    // ========================================================================
432
433    #[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        // Mismatched length returns None
442        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        // Pure white: 0.299*1 + 0.587*1 + 0.114*1 = 1.0
458        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        // Pure red: 0.299*1 + 0.587*0 + 0.114*0 = 0.299
469        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        // Wrong channel count returns None
480        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        // Uniform image should be unchanged by gaussian blur.
487        // Use a larger image to exercise both SIMD and scalar paths.
488        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        // Check interior pixels (borders may differ due to clamping)
492        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; // spike
510        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        // Check interior pixels (borders may differ due to clamping)
522        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        // Single bright pixel should spread to 3x3
539        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        // Far corner should still be 0
557        assert!((dilated.data()[0] - 0.0).abs() < 1e-6);
558    }
559
560    #[test]
561    fn test_sobel_f32_flat_is_zero() {
562        // Flat image: all gradients should be zero
563        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        // Interior pixels should be zero (borders are always zero)
567        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        // Vertical step edge at x=8
583        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        // Edge magnitude at (8,8) should be nonzero
592        let edge_val = edges.data()[8 * 16 + 8];
593        assert!(edge_val > 0.5, "sobel edge = {}", edge_val);
594        // Interior uniform region should be ~0
595        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    // ====================================================================
616    // FAST-9 tests
617    // ====================================================================
618
619    #[test]
620    fn test_fast9_detect_u8_empty() {
621        // Uniform image: no corners
622        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    // ====================================================================
677    // Distance transform tests
678    // ====================================================================
679
680    #[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    // ====================================================================
723    // Warp perspective tests
724    // ====================================================================
725
726    #[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}