Skip to main content

scirs2_ndimage/segmentation/
contours.rs

1//! Contour finding and analysis
2//!
3//! This module provides functionality similar to OpenCV's `findContours`,
4//! implementing the Suzuki-Abe border following algorithm for extracting
5//! contours from binary images.
6//!
7//! # Example
8//!
9//! ```
10//! use scirs2_ndimage::segmentation::contours::{
11//!     find_contours, RetrievalMode, ApproximationMethod,
12//! };
13//! use scirs2_core::ndarray::Array2;
14//!
15//! // Create a simple binary image with a square
16//! let mut image = Array2::<u8>::zeros((10, 10));
17//! for i in 2..8 {
18//!     for j in 2..8 {
19//!         image[[i, j]] = 255;
20//!     }
21//! }
22//!
23//! // Find contours
24//! let contours = find_contours(
25//!     &image.view(),
26//!     RetrievalMode::External,
27//!     ApproximationMethod::Simple,
28//! ).unwrap();
29//!
30//! assert!(!contours.is_empty());
31//! ```
32
33use scirs2_core::ndarray::{Array2, ArrayView2};
34use std::collections::VecDeque;
35
36use crate::error::{NdimageError, NdimageResult};
37
38/// Contour retrieval mode
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum RetrievalMode {
41    /// Retrieve only the outermost contours
42    #[default]
43    External,
44    /// Retrieve all contours without establishing hierarchy
45    List,
46    /// Retrieve all contours and organize them into a two-level hierarchy
47    /// (external contours and holes)
48    CComp,
49    /// Retrieve all contours and reconstruct full hierarchy of nested contours
50    Tree,
51}
52
53/// Contour approximation method
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
55pub enum ApproximationMethod {
56    /// Store all contour points (no approximation)
57    None,
58    /// Compress horizontal, vertical, and diagonal segments
59    /// (keep only endpoints)
60    #[default]
61    Simple,
62    /// Apply Teh-Chin chain approximation (L1 distance)
63    TehChinL1,
64    /// Apply Teh-Chin chain approximation (k-cosine curvature)
65    TehChinKCos,
66}
67
68/// Contour hierarchy information
69///
70/// Each contour can have:
71/// - `next`: Index of the next contour at the same hierarchy level
72/// - `previous`: Index of the previous contour at the same hierarchy level
73/// - `first_child`: Index of the first child contour
74/// - `parent`: Index of the parent contour
75///
76/// A value of -1 indicates no corresponding contour exists.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct ContourHierarchy {
79    /// Index of the next contour at the same level
80    pub next: i32,
81    /// Index of the previous contour at the same level
82    pub previous: i32,
83    /// Index of the first child contour
84    pub first_child: i32,
85    /// Index of the parent contour
86    pub parent: i32,
87}
88
89impl Default for ContourHierarchy {
90    fn default() -> Self {
91        Self {
92            next: -1,
93            previous: -1,
94            first_child: -1,
95            parent: -1,
96        }
97    }
98}
99
100/// A contour extracted from a binary image
101#[derive(Debug, Clone)]
102pub struct Contour {
103    /// Points forming the contour, in order
104    pub points: Vec<(i32, i32)>,
105    /// Hierarchy information for this contour
106    pub hierarchy: ContourHierarchy,
107    /// Whether this is an outer (external) contour or a hole
108    pub is_hole: bool,
109}
110
111impl Contour {
112    /// Create a new contour
113    pub fn new(points: Vec<(i32, i32)>) -> Self {
114        Self {
115            points,
116            hierarchy: ContourHierarchy::default(),
117            is_hole: false,
118        }
119    }
120
121    /// Calculate the area of the contour using the shoelace formula
122    pub fn area(&self) -> f64 {
123        if self.points.len() < 3 {
124            return 0.0;
125        }
126
127        let mut sum = 0i64;
128        let n = self.points.len();
129
130        for i in 0..n {
131            let j = (i + 1) % n;
132            sum += (self.points[i].0 as i64) * (self.points[j].1 as i64);
133            sum -= (self.points[j].0 as i64) * (self.points[i].1 as i64);
134        }
135
136        (sum.abs() as f64) / 2.0
137    }
138
139    /// Calculate the perimeter (arc length) of the contour
140    pub fn perimeter(&self) -> f64 {
141        if self.points.len() < 2 {
142            return 0.0;
143        }
144
145        let mut length = 0.0;
146        let n = self.points.len();
147
148        for i in 0..n {
149            let j = (i + 1) % n;
150            let dx = (self.points[j].0 - self.points[i].0) as f64;
151            let dy = (self.points[j].1 - self.points[i].1) as f64;
152            length += (dx * dx + dy * dy).sqrt();
153        }
154
155        length
156    }
157
158    /// Calculate the bounding rectangle of the contour
159    pub fn bounding_rect(&self) -> (i32, i32, i32, i32) {
160        if self.points.is_empty() {
161            return (0, 0, 0, 0);
162        }
163
164        let mut min_x = i32::MAX;
165        let mut min_y = i32::MAX;
166        let mut max_x = i32::MIN;
167        let mut max_y = i32::MIN;
168
169        for &(x, y) in &self.points {
170            min_x = min_x.min(x);
171            min_y = min_y.min(y);
172            max_x = max_x.max(x);
173            max_y = max_y.max(y);
174        }
175
176        (min_x, min_y, max_x - min_x + 1, max_y - min_y + 1)
177    }
178
179    /// Calculate the centroid of the contour
180    pub fn centroid(&self) -> (f64, f64) {
181        if self.points.is_empty() {
182            return (0.0, 0.0);
183        }
184
185        let sum_x: i64 = self.points.iter().map(|p| p.0 as i64).sum();
186        let sum_y: i64 = self.points.iter().map(|p| p.1 as i64).sum();
187        let n = self.points.len() as f64;
188
189        (sum_x as f64 / n, sum_y as f64 / n)
190    }
191}
192
193/// Direction offsets for 8-connectivity border following
194/// Order: E, NE, N, NW, W, SW, S, SE (clockwise from East)
195const DIRECTIONS: [(i32, i32); 8] = [
196    (1, 0),   // 0: East
197    (1, -1),  // 1: NE
198    (0, -1),  // 2: North
199    (-1, -1), // 3: NW
200    (-1, 0),  // 4: West
201    (-1, 1),  // 5: SW
202    (0, 1),   // 6: South
203    (1, 1),   // 7: SE
204];
205
206/// Find contours in a binary image using the Suzuki-Abe algorithm
207///
208/// This function extracts contours from a binary image, similar to OpenCV's
209/// `findContours` function. It implements the border following algorithm
210/// described in Suzuki & Abe (1985).
211///
212/// # Arguments
213///
214/// * `image` - Binary image (0 = background, non-zero = foreground)
215/// * `mode` - Contour retrieval mode
216/// * `method` - Contour approximation method
217///
218/// # Returns
219///
220/// A vector of contours, each with points and hierarchy information
221///
222/// # Example
223///
224/// ```
225/// use scirs2_ndimage::segmentation::contours::{
226///     find_contours, RetrievalMode, ApproximationMethod,
227/// };
228/// use scirs2_core::ndarray::Array2;
229///
230/// // Create a binary image with a filled rectangle
231/// let mut image = Array2::<u8>::zeros((20, 20));
232/// for i in 5..15 {
233///     for j in 5..15 {
234///         image[[i, j]] = 255;
235///     }
236/// }
237///
238/// let contours = find_contours(
239///     &image.view(),
240///     RetrievalMode::List,
241///     ApproximationMethod::Simple,
242/// ).unwrap();
243///
244/// assert_eq!(contours.len(), 1);
245/// ```
246pub fn find_contours(
247    image: &ArrayView2<u8>,
248    mode: RetrievalMode,
249    method: ApproximationMethod,
250) -> NdimageResult<Vec<Contour>> {
251    let (height, width) = image.dim();
252
253    if height < 2 || width < 2 {
254        return Ok(Vec::new());
255    }
256
257    // Create a working copy with border padding
258    // Values: 0 = background, 1 = foreground, 2+ = labels
259    let mut labels = Array2::<i32>::zeros((height + 2, width + 2));
260
261    // Copy image to labels (with 1-pixel border)
262    for i in 0..height {
263        for j in 0..width {
264            if image[[i, j]] != 0 {
265                labels[[i + 1, j + 1]] = 1;
266            }
267        }
268    }
269
270    let mut contours = Vec::new();
271    let mut nbd = 1; // Current border number
272    let mut lnbd; // Last encountered border number
273
274    // Parent tracking for hierarchy
275    let mut parent_stack: Vec<i32> = Vec::new();
276    let mut border_types: Vec<bool> = Vec::new(); // true = outer, false = hole
277
278    for i in 1..=height {
279        lnbd = 1;
280
281        for j in 1..=width {
282            let fij = labels[[i, j]];
283
284            // Check for outer border starting point
285            // Condition: f(i,j) = 1 and f(i,j-1) = 0
286            let is_outer_start = fij == 1 && labels[[i, j - 1]] == 0;
287
288            // Check for hole border starting point
289            // Condition: f(i,j) >= 1 and f(i,j+1) = 0
290            let is_hole_start = fij >= 1 && labels[[i, j + 1]] == 0;
291
292            if is_outer_start || is_hole_start {
293                let is_outer = is_outer_start;
294
295                // Determine parent based on LNBD and border type
296                let parent = if lnbd <= 1 {
297                    -1
298                } else {
299                    let lnbd_idx = (lnbd - 2) as usize;
300                    if lnbd_idx < border_types.len() {
301                        let lnbd_is_outer = border_types[lnbd_idx];
302                        if is_outer == lnbd_is_outer {
303                            // Same type: parent is LNBD's parent
304                            if lnbd_idx < parent_stack.len() {
305                                parent_stack[lnbd_idx]
306                            } else {
307                                -1
308                            }
309                        } else {
310                            // Different type: parent is LNBD itself
311                            lnbd as i32 - 2
312                        }
313                    } else {
314                        -1
315                    }
316                };
317
318                nbd += 1;
319
320                // Follow the border
321                let start_dir = if is_outer { 0 } else { 4 }; // Start from East for outer, West for hole
322                let border_points = follow_border(&mut labels, i, j, start_dir, nbd, is_outer)?;
323
324                if !border_points.is_empty() {
325                    // Apply approximation
326                    let approx_points = approximate_contour(&border_points, method);
327
328                    // Adjust coordinates (remove padding offset)
329                    let adjusted_points: Vec<(i32, i32)> = approx_points
330                        .into_iter()
331                        .map(|(x, y)| (x - 1, y - 1))
332                        .collect();
333
334                    // Create contour based on retrieval mode
335                    let should_add = match mode {
336                        RetrievalMode::External => is_outer && parent == -1,
337                        RetrievalMode::List => true,
338                        RetrievalMode::CComp => true,
339                        RetrievalMode::Tree => true,
340                    };
341
342                    if should_add && !adjusted_points.is_empty() {
343                        let mut contour = Contour::new(adjusted_points);
344                        contour.is_hole = !is_outer;
345                        contour.hierarchy.parent = parent;
346                        contours.push(contour);
347                    }
348                }
349
350                parent_stack.push(parent);
351                border_types.push(is_outer);
352            }
353
354            // Update LNBD
355            let abs_fij = labels[[i, j]].abs();
356            if abs_fij > 1 {
357                lnbd = abs_fij;
358            }
359        }
360    }
361
362    // Build hierarchy relationships
363    build_hierarchy(&mut contours, mode);
364
365    Ok(contours)
366}
367
368/// Follow a border starting from (i, j) in the given direction
369fn follow_border(
370    labels: &mut Array2<i32>,
371    start_i: usize,
372    start_j: usize,
373    start_dir: usize,
374    nbd: i32,
375    is_outer: bool,
376) -> NdimageResult<Vec<(i32, i32)>> {
377    let (height, width) = labels.dim();
378    let mut points = Vec::new();
379
380    // Find the first non-zero neighbor
381    let mut dir = start_dir;
382    let mut found = false;
383
384    for _ in 0..8 {
385        let ni = (start_i as i32 + DIRECTIONS[dir].1) as usize;
386        let nj = (start_j as i32 + DIRECTIONS[dir].0) as usize;
387
388        if ni < height && nj < width && labels[[ni, nj]] != 0 {
389            found = true;
390            break;
391        }
392        dir = (dir + 1) % 8;
393    }
394
395    if !found {
396        // Isolated point
397        points.push((start_j as i32, start_i as i32));
398        labels[[start_i, start_j]] = -nbd;
399        return Ok(points);
400    }
401
402    let mut i = start_i;
403    let mut j = start_j;
404    let mut prev_dir = dir;
405
406    loop {
407        points.push((j as i32, i as i32));
408
409        // Search for next border point
410        let search_start = (prev_dir + if is_outer { 6 } else { 2 }) % 8;
411        let mut next_found = false;
412        let mut next_i = i;
413        let mut next_j = j;
414        let mut examined_background = false;
415
416        for k in 0..8 {
417            let d = (search_start + k) % 8;
418            let ni = (i as i32 + DIRECTIONS[d].1) as usize;
419            let nj = (j as i32 + DIRECTIONS[d].0) as usize;
420
421            if ni >= height || nj >= width {
422                examined_background = true;
423                continue;
424            }
425
426            if labels[[ni, nj]] == 0 {
427                examined_background = true;
428            } else {
429                next_i = ni;
430                next_j = nj;
431                prev_dir = d;
432                next_found = true;
433                break;
434            }
435        }
436
437        // Update label
438        if examined_background {
439            labels[[i, j]] = -nbd;
440        } else if labels[[i, j]] == 1 {
441            labels[[i, j]] = nbd;
442        }
443
444        if !next_found || (next_i == start_i && next_j == start_j && i == start_i && j == start_j) {
445            break;
446        }
447
448        // Check for return to start
449        if next_i == start_i && next_j == start_j {
450            // We've completed the loop
451            break;
452        }
453
454        i = next_i;
455        j = next_j;
456
457        // Safety check to prevent infinite loops
458        if points.len() > (height * width * 4) {
459            return Err(NdimageError::ComputationError(
460                "Contour following exceeded maximum iterations".to_string(),
461            ));
462        }
463    }
464
465    Ok(points)
466}
467
468/// Apply contour approximation
469fn approximate_contour(points: &[(i32, i32)], method: ApproximationMethod) -> Vec<(i32, i32)> {
470    match method {
471        ApproximationMethod::None => points.to_vec(),
472        ApproximationMethod::Simple => approximate_simple(points),
473        ApproximationMethod::TehChinL1 => approximate_teh_chin(points, false),
474        ApproximationMethod::TehChinKCos => approximate_teh_chin(points, true),
475    }
476}
477
478/// Simple approximation: keep only endpoints of horizontal/vertical/diagonal runs
479fn approximate_simple(points: &[(i32, i32)]) -> Vec<(i32, i32)> {
480    if points.len() <= 2 {
481        return points.to_vec();
482    }
483
484    let mut result = Vec::new();
485    result.push(points[0]);
486
487    let mut prev_dx = 0i32;
488    let mut prev_dy = 0i32;
489
490    for i in 1..points.len() {
491        let dx = (points[i].0 - points[i - 1].0).signum();
492        let dy = (points[i].1 - points[i - 1].1).signum();
493
494        if dx != prev_dx || dy != prev_dy {
495            if i > 1 {
496                result.push(points[i - 1]);
497            }
498            prev_dx = dx;
499            prev_dy = dy;
500        }
501    }
502
503    // Always include the last point
504    if result.last() != points.last() {
505        result.push(*points.last().unwrap_or(&(0, 0)));
506    }
507
508    result
509}
510
511/// Teh-Chin chain approximation algorithm
512fn approximate_teh_chin(points: &[(i32, i32)], use_kcos: bool) -> Vec<(i32, i32)> {
513    if points.len() <= 4 {
514        return points.to_vec();
515    }
516
517    // Calculate curvature at each point
518    let n = points.len();
519    let mut curvatures = vec![0.0f64; n];
520    let region = 3; // Region size for curvature calculation
521
522    for i in 0..n {
523        let prev_idx = (i + n - region) % n;
524        let next_idx = (i + region) % n;
525
526        let p1 = points[prev_idx];
527        let p2 = points[i];
528        let p3 = points[next_idx];
529
530        if use_kcos {
531            // K-cosine curvature
532            let v1 = ((p2.0 - p1.0) as f64, (p2.1 - p1.1) as f64);
533            let v2 = ((p3.0 - p2.0) as f64, (p3.1 - p2.1) as f64);
534
535            let dot = v1.0 * v2.0 + v1.1 * v2.1;
536            let len1 = (v1.0 * v1.0 + v1.1 * v1.1).sqrt();
537            let len2 = (v2.0 * v2.0 + v2.1 * v2.1).sqrt();
538
539            if len1 > 0.0 && len2 > 0.0 {
540                curvatures[i] = 1.0 - dot / (len1 * len2);
541            }
542        } else {
543            // L1 distance-based curvature
544            let direct_dist = ((p3.0 - p1.0).abs() + (p3.1 - p1.1).abs()) as f64;
545            let path_dist = ((p2.0 - p1.0).abs()
546                + (p2.1 - p1.1).abs()
547                + (p3.0 - p2.0).abs()
548                + (p3.1 - p2.1).abs()) as f64;
549
550            if path_dist > 0.0 {
551                curvatures[i] = 1.0 - direct_dist / path_dist;
552            }
553        }
554    }
555
556    // Non-maximum suppression
557    let threshold = 0.1;
558    let mut is_corner = vec![false; n];
559
560    for i in 0..n {
561        if curvatures[i] > threshold {
562            let prev = (i + n - 1) % n;
563            let next = (i + 1) % n;
564
565            if curvatures[i] >= curvatures[prev] && curvatures[i] >= curvatures[next] {
566                is_corner[i] = true;
567            }
568        }
569    }
570
571    // Collect corner points
572    let mut result: Vec<(i32, i32)> = points
573        .iter()
574        .enumerate()
575        .filter(|(i, _)| is_corner[*i])
576        .map(|(_, p)| *p)
577        .collect();
578
579    // Ensure we have at least the first and last points
580    if result.is_empty() {
581        result = approximate_simple(points);
582    }
583
584    result
585}
586
587/// Build hierarchy relationships between contours
588fn build_hierarchy(contours: &mut [Contour], mode: RetrievalMode) {
589    if contours.is_empty() {
590        return;
591    }
592
593    let n = contours.len();
594
595    match mode {
596        RetrievalMode::External | RetrievalMode::List => {
597            // Simple linear list
598            for i in 0..n {
599                contours[i].hierarchy.next = if i + 1 < n { (i + 1) as i32 } else { -1 };
600                contours[i].hierarchy.previous = if i > 0 { (i - 1) as i32 } else { -1 };
601                contours[i].hierarchy.first_child = -1;
602                contours[i].hierarchy.parent = -1;
603            }
604        }
605        RetrievalMode::CComp | RetrievalMode::Tree => {
606            // Build parent-child relationships based on stored parent info
607            // First, update first_child for parents
608            for i in 0..n {
609                let parent = contours[i].hierarchy.parent;
610                if parent >= 0 && (parent as usize) < n {
611                    let parent_idx = parent as usize;
612                    if contours[parent_idx].hierarchy.first_child == -1 {
613                        contours[parent_idx].hierarchy.first_child = i as i32;
614                    }
615                }
616            }
617
618            // Build sibling relationships (next/previous at same level)
619            // Group contours by parent
620            let mut by_parent: std::collections::HashMap<i32, Vec<usize>> =
621                std::collections::HashMap::new();
622
623            for i in 0..n {
624                by_parent
625                    .entry(contours[i].hierarchy.parent)
626                    .or_default()
627                    .push(i);
628            }
629
630            for siblings in by_parent.values() {
631                for (idx, &i) in siblings.iter().enumerate() {
632                    contours[i].hierarchy.previous = if idx > 0 {
633                        siblings[idx - 1] as i32
634                    } else {
635                        -1
636                    };
637                    contours[i].hierarchy.next = if idx + 1 < siblings.len() {
638                        siblings[idx + 1] as i32
639                    } else {
640                        -1
641                    };
642                }
643            }
644        }
645    }
646}
647
648/// Draw contours on an image
649///
650/// # Arguments
651///
652/// * `image` - Mutable image to draw on
653/// * `contours` - Contours to draw
654/// * `contour_idx` - Index of contour to draw (-1 for all)
655/// * `color` - Color value to use
656/// * `thickness` - Line thickness (1 for single pixel, -1 for filled)
657pub fn draw_contours(
658    image: &mut Array2<u8>,
659    contours: &[Contour],
660    contour_idx: i32,
661    color: u8,
662    thickness: i32,
663) -> NdimageResult<()> {
664    let (height, width) = image.dim();
665
666    let indices: Vec<usize> = if contour_idx < 0 {
667        (0..contours.len()).collect()
668    } else {
669        vec![contour_idx as usize]
670    };
671
672    for &idx in &indices {
673        if idx >= contours.len() {
674            continue;
675        }
676
677        let contour = &contours[idx];
678
679        if thickness == -1 {
680            // Fill contour
681            fill_contour(image, contour)?;
682        } else {
683            // Draw contour outline
684            for i in 0..contour.points.len() {
685                let p1 = contour.points[i];
686                let p2 = contour.points[(i + 1) % contour.points.len()];
687
688                draw_line(image, p1, p2, color, thickness.max(1) as usize)?;
689            }
690        }
691    }
692
693    Ok(())
694}
695
696/// Fill a contour using scanline algorithm
697fn fill_contour(image: &mut Array2<u8>, contour: &Contour) -> NdimageResult<()> {
698    if contour.points.len() < 3 {
699        return Ok(());
700    }
701
702    let (height, width) = image.dim();
703    let (_, min_y, _, h) = contour.bounding_rect();
704    let max_y = min_y + h;
705
706    for y in min_y.max(0)..max_y.min(height as i32) {
707        let mut intersections = Vec::new();
708
709        // Find all intersections with the scanline
710        for i in 0..contour.points.len() {
711            let p1 = contour.points[i];
712            let p2 = contour.points[(i + 1) % contour.points.len()];
713
714            if (p1.1 <= y && p2.1 > y) || (p2.1 <= y && p1.1 > y) {
715                let x = p1.0 + (y - p1.1) * (p2.0 - p1.0) / (p2.1 - p1.1);
716                intersections.push(x);
717            }
718        }
719
720        intersections.sort();
721
722        // Fill between pairs of intersections
723        for pair in intersections.chunks(2) {
724            if pair.len() == 2 {
725                let x1 = pair[0].max(0) as usize;
726                let x2 = (pair[1] as usize).min(width - 1);
727                for x in x1..=x2 {
728                    if y >= 0 && (y as usize) < height {
729                        image[[y as usize, x]] = 255;
730                    }
731                }
732            }
733        }
734    }
735
736    Ok(())
737}
738
739/// Draw a line using Bresenham's algorithm
740fn draw_line(
741    image: &mut Array2<u8>,
742    p1: (i32, i32),
743    p2: (i32, i32),
744    color: u8,
745    thickness: usize,
746) -> NdimageResult<()> {
747    let (height, width) = image.dim();
748
749    let dx = (p2.0 - p1.0).abs();
750    let dy = (p2.1 - p1.1).abs();
751    let sx = if p1.0 < p2.0 { 1 } else { -1 };
752    let sy = if p1.1 < p2.1 { 1 } else { -1 };
753    let mut err = dx - dy;
754
755    let mut x = p1.0;
756    let mut y = p1.1;
757
758    loop {
759        // Draw point with thickness
760        let half_t = (thickness / 2) as i32;
761        for dy in -half_t..=half_t {
762            for dx in -half_t..=half_t {
763                let px = x + dx;
764                let py = y + dy;
765                if px >= 0 && py >= 0 && (px as usize) < width && (py as usize) < height {
766                    image[[py as usize, px as usize]] = color;
767                }
768            }
769        }
770
771        if x == p2.0 && y == p2.1 {
772            break;
773        }
774
775        let e2 = 2 * err;
776        if e2 > -dy {
777            err -= dy;
778            x += sx;
779        }
780        if e2 < dx {
781            err += dx;
782            y += sy;
783        }
784    }
785
786    Ok(())
787}
788
789/// Check if a point is inside a contour using ray casting
790pub fn point_in_contour(point: (i32, i32), contour: &Contour) -> bool {
791    if contour.points.len() < 3 {
792        return false;
793    }
794
795    let (x, y) = point;
796    let n = contour.points.len();
797    let mut inside = false;
798
799    let mut j = n - 1;
800    for i in 0..n {
801        let (xi, yi) = contour.points[i];
802        let (xj, yj) = contour.points[j];
803
804        if ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
805            inside = !inside;
806        }
807        j = i;
808    }
809
810    inside
811}
812
813/// Calculate moments of a contour
814#[derive(Debug, Clone, Default)]
815pub struct ContourMoments {
816    /// Spatial moments m00, m10, m01, m20, m11, m02, m30, m21, m12, m03
817    pub m: [[f64; 4]; 4],
818    /// Central moments mu20, mu11, mu02, mu30, mu21, mu12, mu03
819    pub mu: [[f64; 4]; 4],
820    /// Normalized central moments nu20, nu11, nu02, nu30, nu21, nu12, nu03
821    pub nu: [[f64; 4]; 4],
822}
823
824impl ContourMoments {
825    /// Calculate moments from contour points
826    pub fn from_contour(contour: &Contour) -> Self {
827        let mut moments = Self::default();
828
829        if contour.points.is_empty() {
830            return moments;
831        }
832
833        // Calculate spatial moments
834        for &(x, y) in &contour.points {
835            let xf = x as f64;
836            let yf = y as f64;
837
838            for i in 0..4 {
839                for j in 0..4 {
840                    if i + j <= 3 {
841                        moments.m[i][j] += xf.powi(i as i32) * yf.powi(j as i32);
842                    }
843                }
844            }
845        }
846
847        // Calculate centroid
848        let cx = if moments.m[0][0] != 0.0 {
849            moments.m[1][0] / moments.m[0][0]
850        } else {
851            0.0
852        };
853        let cy = if moments.m[0][0] != 0.0 {
854            moments.m[0][1] / moments.m[0][0]
855        } else {
856            0.0
857        };
858
859        // Calculate central moments
860        for &(x, y) in &contour.points {
861            let dx = x as f64 - cx;
862            let dy = y as f64 - cy;
863
864            for i in 0..4 {
865                for j in 0..4 {
866                    if i + j >= 2 && i + j <= 3 {
867                        moments.mu[i][j] += dx.powi(i as i32) * dy.powi(j as i32);
868                    }
869                }
870            }
871        }
872
873        // Calculate normalized central moments
874        let m00 = moments.m[0][0];
875        if m00 > 0.0 {
876            for i in 0..4 {
877                for j in 0..4 {
878                    if i + j >= 2 && i + j <= 3 {
879                        let exp = ((i + j) as f64 / 2.0) + 1.0;
880                        moments.nu[i][j] = moments.mu[i][j] / m00.powf(exp);
881                    }
882                }
883            }
884        }
885
886        moments
887    }
888}
889
890#[cfg(test)]
891mod tests {
892    use super::*;
893    use scirs2_core::ndarray::Array2;
894
895    #[test]
896    fn test_find_contours_simple_square() {
897        let mut image = Array2::<u8>::zeros((10, 10));
898
899        // Create a filled square
900        for i in 2..8 {
901            for j in 2..8 {
902                image[[i, j]] = 255;
903            }
904        }
905
906        let contours = find_contours(
907            &image.view(),
908            RetrievalMode::External,
909            ApproximationMethod::Simple,
910        )
911        .expect("find_contours should succeed for valid image");
912
913        assert!(!contours.is_empty());
914        assert!(contours[0].points.len() >= 4); // At least 4 corners
915    }
916
917    #[test]
918    fn test_find_contours_with_hole() {
919        let mut image = Array2::<u8>::zeros((20, 20));
920
921        // Outer square
922        for i in 2..18 {
923            for j in 2..18 {
924                image[[i, j]] = 255;
925            }
926        }
927
928        // Inner hole
929        for i in 6..14 {
930            for j in 6..14 {
931                image[[i, j]] = 0;
932            }
933        }
934
935        let contours = find_contours(
936            &image.view(),
937            RetrievalMode::Tree,
938            ApproximationMethod::None,
939        )
940        .expect("find_contours should succeed for image with hole");
941
942        // Should find at least 2 contours (outer and hole)
943        assert!(!contours.is_empty());
944    }
945
946    #[test]
947    fn test_contour_area() {
948        // 10x10 square contour
949        let points = vec![(0, 0), (10, 0), (10, 10), (0, 10)];
950        let contour = Contour::new(points);
951
952        let area = contour.area();
953        assert!((area - 100.0).abs() < 1.0);
954    }
955
956    #[test]
957    fn test_contour_perimeter() {
958        // 10x10 square contour
959        let points = vec![(0, 0), (10, 0), (10, 10), (0, 10)];
960        let contour = Contour::new(points);
961
962        let perimeter = contour.perimeter();
963        assert!((perimeter - 40.0).abs() < 1.0);
964    }
965
966    #[test]
967    fn test_point_in_contour() {
968        let points = vec![(0, 0), (10, 0), (10, 10), (0, 10)];
969        let contour = Contour::new(points);
970
971        assert!(point_in_contour((5, 5), &contour));
972        assert!(!point_in_contour((15, 15), &contour));
973    }
974
975    #[test]
976    fn test_empty_image() {
977        let image = Array2::<u8>::zeros((10, 10));
978
979        let contours = find_contours(
980            &image.view(),
981            RetrievalMode::List,
982            ApproximationMethod::Simple,
983        )
984        .expect("find_contours should succeed for empty image");
985
986        assert!(contours.is_empty());
987    }
988}