Skip to main content

yscv_imgproc/ops/
contours.rs

1use yscv_tensor::Tensor;
2
3use super::super::ImgProcError;
4use super::super::shape::hwc_shape;
5
6/// A contour as an ordered list of (x, y) boundary pixel coordinates.
7#[derive(Debug, Clone, PartialEq)]
8pub struct Contour {
9    pub points: Vec<(usize, usize)>,
10}
11
12/// Finds external contours in a binary single-channel `[H, W, 1]` image.
13///
14/// Pixels > 0.5 are foreground. Returns a list of contours where each contour
15/// is an ordered sequence of border pixel coordinates using 8-connected
16/// Moore boundary tracing.
17pub fn find_contours(input: &Tensor) -> Result<Vec<Contour>, ImgProcError> {
18    let (h, w, c) = hwc_shape(input)?;
19    if c != 1 {
20        return Err(ImgProcError::InvalidChannelCount {
21            expected: 1,
22            got: c,
23        });
24    }
25    let data = input.data();
26    let mut visited = vec![false; h * w];
27    let mut contours = Vec::new();
28
29    // 8-connected Moore neighborhood (clockwise from east)
30    const DIRS: [(i32, i32); 8] = [
31        (1, 0),
32        (1, 1),
33        (0, 1),
34        (-1, 1),
35        (-1, 0),
36        (-1, -1),
37        (0, -1),
38        (1, -1),
39    ];
40
41    for y in 0..h {
42        for x in 0..w {
43            if data[y * w + x] <= 0.5 || visited[y * w + x] {
44                continue;
45            }
46            // Check if this is a border pixel (has at least one background 4-neighbor)
47            let is_border = x == 0
48                || x == w - 1
49                || y == 0
50                || y == h - 1
51                || data[y * w + x - 1] <= 0.5
52                || data[y * w + x + 1] <= 0.5
53                || data[(y - 1) * w + x] <= 0.5
54                || data[(y + 1) * w + x] <= 0.5;
55            if !is_border {
56                continue;
57            }
58
59            // Moore boundary trace
60            let mut contour_points = Vec::new();
61            let start = (x, y);
62            let mut cur = start;
63            let mut dir = 0usize; // start looking east
64            let max_steps = h * w * 2;
65            let mut steps = 0;
66
67            loop {
68                contour_points.push(cur);
69                visited[cur.1 * w + cur.0] = true;
70
71                let mut found = false;
72                let start_dir = (dir + 5) % 8; // backtrack: turn right from where we came
73                for i in 0..8 {
74                    let d = (start_dir + i) % 8;
75                    let (dx, dy) = DIRS[d];
76                    let nx = cur.0 as i32 + dx;
77                    let ny = cur.1 as i32 + dy;
78                    if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
79                        let (ux, uy) = (nx as usize, ny as usize);
80                        if data[uy * w + ux] > 0.5 {
81                            cur = (ux, uy);
82                            dir = d;
83                            found = true;
84                            break;
85                        }
86                    }
87                }
88
89                if !found || cur == start || steps > max_steps {
90                    break;
91                }
92                steps += 1;
93            }
94
95            if contour_points.len() >= 2 {
96                contours.push(Contour {
97                    points: contour_points,
98                });
99            }
100        }
101    }
102
103    Ok(contours)
104}
105
106/// Computes the convex hull of 2D points using Andrew's monotone chain algorithm.
107///
108/// Input points are `(x, y)` pairs. Returns hull vertices in counter-clockwise order.
109pub fn convex_hull(points: &[(f32, f32)]) -> Vec<(f32, f32)> {
110    if points.len() < 3 {
111        return points.to_vec();
112    }
113
114    let mut pts: Vec<(f32, f32)> = points.to_vec();
115    pts.sort_unstable_by(|a, b| {
116        a.0.partial_cmp(&b.0)
117            .unwrap_or(std::cmp::Ordering::Equal)
118            .then(a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
119    });
120
121    let n = pts.len();
122    let mut hull: Vec<(f32, f32)> = Vec::with_capacity(2 * n);
123
124    for &p in &pts {
125        while hull.len() >= 2 && cross_2d(hull[hull.len() - 2], hull[hull.len() - 1], p) <= 0.0 {
126            hull.pop();
127        }
128        hull.push(p);
129    }
130
131    let lower_len = hull.len() + 1;
132    for &p in pts.iter().rev().skip(1) {
133        while hull.len() >= lower_len
134            && cross_2d(hull[hull.len() - 2], hull[hull.len() - 1], p) <= 0.0
135        {
136            hull.pop();
137        }
138        hull.push(p);
139    }
140    hull.pop();
141    hull
142}
143
144fn cross_2d(o: (f32, f32), a: (f32, f32), b: (f32, f32)) -> f32 {
145    (a.0 - o.0) * (b.1 - o.1) - (a.1 - o.1) * (b.0 - o.0)
146}
147
148/// Computes the minimum-area bounding rectangle for a set of 2D points
149/// using the rotating calipers approach on the convex hull.
150///
151/// Returns `(center_x, center_y, width, height, angle_radians)`.
152pub fn min_area_rect(points: &[(f32, f32)]) -> Option<(f32, f32, f32, f32, f32)> {
153    let hull = convex_hull(points);
154    if hull.len() < 2 {
155        return hull.first().map(|p| (p.0, p.1, 0.0, 0.0, 0.0));
156    }
157
158    let n = hull.len();
159    let mut best_area = f32::MAX;
160    let mut best_rect = (0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32);
161
162    for i in 0..n {
163        let j = (i + 1) % n;
164        let edge_x = hull[j].0 - hull[i].0;
165        let edge_y = hull[j].1 - hull[i].1;
166        let edge_len = (edge_x * edge_x + edge_y * edge_y).sqrt();
167        if edge_len < 1e-12 {
168            continue;
169        }
170        let ux = edge_x / edge_len;
171        let uy = edge_y / edge_len;
172
173        let mut min_proj = f32::MAX;
174        let mut max_proj = f32::MIN;
175        let mut min_perp = f32::MAX;
176        let mut max_perp = f32::MIN;
177
178        for &p in &hull {
179            let dx = p.0 - hull[i].0;
180            let dy = p.1 - hull[i].1;
181            let proj = dx * ux + dy * uy;
182            let perp = -dx * uy + dy * ux;
183            min_proj = min_proj.min(proj);
184            max_proj = max_proj.max(proj);
185            min_perp = min_perp.min(perp);
186            max_perp = max_perp.max(perp);
187        }
188
189        let width = max_proj - min_proj;
190        let height = max_perp - min_perp;
191        let area = width * height;
192        if area < best_area {
193            best_area = area;
194            let mid_proj = (min_proj + max_proj) * 0.5;
195            let mid_perp = (min_perp + max_perp) * 0.5;
196            let cx = hull[i].0 + ux * mid_proj - uy * mid_perp;
197            let cy = hull[i].1 + uy * mid_proj + ux * mid_perp;
198            let angle = uy.atan2(ux);
199            best_rect = (cx, cy, width, height, angle);
200        }
201    }
202
203    Some(best_rect)
204}
205
206/// Computes a 3x3 homography matrix from 4 source/destination point correspondences
207/// using the Direct Linear Transform (DLT) algorithm.
208///
209/// `src` and `dst` must each contain exactly 4 points `(x, y)`.
210/// Returns the 9-element matrix in row-major order.
211pub fn homography_4pt(
212    src: &[(f32, f32); 4],
213    dst: &[(f32, f32); 4],
214) -> Result<[f32; 9], ImgProcError> {
215    let mut a = [[0.0f64; 8]; 8];
216    let mut b = [0.0f64; 8];
217
218    for i in 0..4 {
219        let (sx, sy) = (src[i].0 as f64, src[i].1 as f64);
220        let (dx, dy) = (dst[i].0 as f64, dst[i].1 as f64);
221        let r = i * 2;
222        a[r] = [sx, sy, 1.0, 0.0, 0.0, 0.0, -dx * sx, -dx * sy];
223        b[r] = dx;
224        a[r + 1] = [0.0, 0.0, 0.0, sx, sy, 1.0, -dy * sx, -dy * sy];
225        b[r + 1] = dy;
226    }
227
228    let h =
229        solve_8x8(&a, &b).ok_or(ImgProcError::InvalidOutputDimensions { out_h: 0, out_w: 0 })?;
230
231    Ok([
232        h[0] as f32,
233        h[1] as f32,
234        h[2] as f32,
235        h[3] as f32,
236        h[4] as f32,
237        h[5] as f32,
238        h[6] as f32,
239        h[7] as f32,
240        1.0,
241    ])
242}
243
244#[allow(clippy::needless_range_loop)]
245fn solve_8x8(a: &[[f64; 8]; 8], b: &[f64; 8]) -> Option<[f64; 8]> {
246    let mut m = [[0.0f64; 9]; 8];
247    for i in 0..8 {
248        for j in 0..8 {
249            m[i][j] = a[i][j];
250        }
251        m[i][8] = b[i];
252    }
253
254    for col in 0..8 {
255        let mut pivot = col;
256        let mut max_val = m[col][col].abs();
257        for row in (col + 1)..8 {
258            if m[row][col].abs() > max_val {
259                max_val = m[row][col].abs();
260                pivot = row;
261            }
262        }
263        if max_val < 1e-12 {
264            return None;
265        }
266        if pivot != col {
267            m.swap(pivot, col);
268        }
269        let diag = m[col][col];
270        for j in col..9 {
271            m[col][j] /= diag;
272        }
273        for row in 0..8 {
274            if row != col {
275                let factor = m[row][col];
276                for j in col..9 {
277                    m[row][j] -= factor * m[col][j];
278                }
279            }
280        }
281    }
282
283    let mut result = [0.0f64; 8];
284    for i in 0..8 {
285        result[i] = m[i][8];
286    }
287    Some(result)
288}
289
290/// RANSAC-based homography estimation from point correspondences.
291///
292/// Iteratively samples 4-point subsets, computes candidate homographies via DLT,
293/// and selects the model with the most inliers under `inlier_threshold`.
294/// Returns `(homography [9], inlier_mask)`.
295pub fn ransac_homography(
296    src: &[(f32, f32)],
297    dst: &[(f32, f32)],
298    iterations: usize,
299    inlier_threshold: f32,
300    rng_seed: u64,
301) -> Option<([f32; 9], Vec<bool>)> {
302    if src.len() < 4 || src.len() != dst.len() {
303        return None;
304    }
305    let n = src.len();
306    let mut best_h = [0.0f32; 9];
307    let mut best_inliers: Vec<bool> = vec![false; n];
308    let mut best_count = 0usize;
309    let mut rng_state = rng_seed;
310
311    for _ in 0..iterations {
312        let mut indices = [0usize; 4];
313        for slot in &mut indices {
314            rng_state = rng_state
315                .wrapping_mul(6364136223846793005)
316                .wrapping_add(1442695040888963407);
317            *slot = (rng_state >> 33) as usize % n;
318        }
319        let src4: [(f32, f32); 4] = [
320            src[indices[0]],
321            src[indices[1]],
322            src[indices[2]],
323            src[indices[3]],
324        ];
325        let dst4: [(f32, f32); 4] = [
326            dst[indices[0]],
327            dst[indices[1]],
328            dst[indices[2]],
329            dst[indices[3]],
330        ];
331        let h = match homography_4pt(&src4, &dst4) {
332            Ok(h) => h,
333            Err(_) => continue,
334        };
335        let mut inliers = vec![false; n];
336        let mut count = 0;
337        for i in 0..n {
338            let (sx, sy) = src[i];
339            let denom = h[6] * sx + h[7] * sy + h[8];
340            if denom.abs() < 1e-12 {
341                continue;
342            }
343            let px = (h[0] * sx + h[1] * sy + h[2]) / denom;
344            let py = (h[3] * sx + h[4] * sy + h[5]) / denom;
345            let err = ((px - dst[i].0).powi(2) + (py - dst[i].1).powi(2)).sqrt();
346            if err < inlier_threshold {
347                inliers[i] = true;
348                count += 1;
349            }
350        }
351        if count > best_count {
352            best_count = count;
353            best_h = h;
354            best_inliers = inliers;
355        }
356    }
357
358    if best_count >= 4 {
359        Some((best_h, best_inliers))
360    } else {
361        None
362    }
363}
364
365/// Fits an axis-aligned ellipse to a set of 2D points using the method of moments.
366///
367/// Returns `(center_x, center_y, semi_axis_a, semi_axis_b, rotation_angle_radians)`.
368pub fn fit_ellipse(points: &[(f32, f32)]) -> Option<(f32, f32, f32, f32, f32)> {
369    if points.len() < 5 {
370        return None;
371    }
372    let n = points.len() as f32;
373    let cx: f32 = points.iter().map(|p| p.0).sum::<f32>() / n;
374    let cy: f32 = points.iter().map(|p| p.1).sum::<f32>() / n;
375
376    let mut cov_xx = 0.0f32;
377    let mut cov_xy = 0.0f32;
378    let mut cov_yy = 0.0f32;
379    for &(x, y) in points {
380        let dx = x - cx;
381        let dy = y - cy;
382        cov_xx += dx * dx;
383        cov_xy += dx * dy;
384        cov_yy += dy * dy;
385    }
386    cov_xx /= n;
387    cov_xy /= n;
388    cov_yy /= n;
389
390    let trace = cov_xx + cov_yy;
391    let det = cov_xx * cov_yy - cov_xy * cov_xy;
392    let disc = (trace * trace / 4.0 - det).max(0.0).sqrt();
393    let lambda1 = trace / 2.0 + disc;
394    let lambda2 = (trace / 2.0 - disc).max(1e-12);
395
396    let angle = cov_xy.atan2(lambda1 - cov_yy);
397    let a = (2.0 * lambda1).sqrt();
398    let b = (2.0 * lambda2).sqrt();
399
400    Some((cx, cy, a, b, angle))
401}
402
403/// Douglas-Peucker contour approximation.
404///
405/// Simplifies a polyline by recursively removing points within `epsilon` distance
406/// from the line segment connecting endpoints.
407pub fn approx_poly_dp(contour: &[(f32, f32)], epsilon: f32) -> Vec<(f32, f32)> {
408    if contour.len() <= 2 {
409        return contour.to_vec();
410    }
411    let n = contour.len();
412    let (first, last) = (contour[0], contour[n - 1]);
413
414    let mut max_dist = 0.0f32;
415    let mut max_idx = 0;
416    for (i, &pt) in contour.iter().enumerate().skip(1).take(n - 2) {
417        let d = point_line_dist_f32(pt, first, last);
418        if d > max_dist {
419            max_dist = d;
420            max_idx = i;
421        }
422    }
423
424    if max_dist > epsilon {
425        let mut left = approx_poly_dp(&contour[..=max_idx], epsilon);
426        let right = approx_poly_dp(&contour[max_idx..], epsilon);
427        left.pop();
428        left.extend(right);
429        left
430    } else {
431        vec![first, last]
432    }
433}
434
435/// Statistics for a single connected component.
436#[derive(Debug, Clone, PartialEq)]
437pub struct ComponentStats {
438    pub label: usize,
439    pub area: usize,
440    pub bbox: (usize, usize, usize, usize), // (x, y, w, h)
441    pub centroid: (f32, f32),               // (cx, cy)
442}
443
444/// Properties for a labelled region.
445#[derive(Debug, Clone, PartialEq)]
446pub struct RegionProp {
447    pub label: usize,
448    pub area: usize,
449    pub centroid: (f32, f32),
450    pub bbox: (usize, usize, usize, usize), // (x, y, w, h)
451    pub perimeter: f32,
452}
453
454/// Connected-component labelling with per-component statistics.
455///
456/// Input: single-channel binary image `[H, W, 1]` (pixels > 0.5 are foreground).
457/// Returns `(label_image, stats)` where `label_image` has shape `[H, W, 1]` with
458/// label 0 for background and labels 1..N for components. Uses BFS with 4-connectivity.
459pub fn connected_components_with_stats(
460    img: &Tensor,
461) -> Result<(Tensor, Vec<ComponentStats>), ImgProcError> {
462    let (h, w, c) = hwc_shape(img)?;
463    if c != 1 {
464        return Err(ImgProcError::InvalidChannelCount {
465            expected: 1,
466            got: c,
467        });
468    }
469    let data = img.data();
470    let mut labels = vec![0u32; h * w];
471    let mut next_label = 1u32;
472    let mut stats_list: Vec<ComponentStats> = Vec::new();
473
474    for y in 0..h {
475        for x in 0..w {
476            let idx = y * w + x;
477            if data[idx] <= 0.5 || labels[idx] != 0 {
478                continue;
479            }
480            // BFS flood-fill
481            let current_label = next_label;
482            next_label += 1;
483            labels[idx] = current_label;
484
485            let mut queue = std::collections::VecDeque::new();
486            queue.push_back((x, y));
487
488            let mut area = 0usize;
489            let mut sum_x = 0.0f64;
490            let mut sum_y = 0.0f64;
491            let mut min_x = x;
492            let mut max_x = x;
493            let mut min_y = y;
494            let mut max_y = y;
495
496            while let Some((cx, cy)) = queue.pop_front() {
497                area += 1;
498                sum_x += cx as f64;
499                sum_y += cy as f64;
500                if cx < min_x {
501                    min_x = cx;
502                }
503                if cx > max_x {
504                    max_x = cx;
505                }
506                if cy < min_y {
507                    min_y = cy;
508                }
509                if cy > max_y {
510                    max_y = cy;
511                }
512
513                for &(dx, dy) in &[(0isize, -1isize), (0, 1), (-1, 0), (1, 0)] {
514                    let nx = cx as isize + dx;
515                    let ny = cy as isize + dy;
516                    if nx >= 0 && nx < w as isize && ny >= 0 && ny < h as isize {
517                        let nidx = ny as usize * w + nx as usize;
518                        if data[nidx] > 0.5 && labels[nidx] == 0 {
519                            labels[nidx] = current_label;
520                            queue.push_back((nx as usize, ny as usize));
521                        }
522                    }
523                }
524            }
525
526            stats_list.push(ComponentStats {
527                label: current_label as usize,
528                area,
529                bbox: (min_x, min_y, max_x - min_x + 1, max_y - min_y + 1),
530                centroid: ((sum_x / area as f64) as f32, (sum_y / area as f64) as f32),
531            });
532        }
533    }
534
535    let label_data: Vec<f32> = labels.iter().map(|&l| l as f32).collect();
536    let label_tensor = Tensor::from_vec(vec![h, w, 1], label_data)?;
537    Ok((label_tensor, stats_list))
538}
539
540/// Compute region properties from a label image.
541///
542/// Input: label image `[H, W, 1]` (e.g. from `connected_components_with_stats`).
543/// For each non-zero label, computes area, centroid, bounding box, and perimeter.
544/// Perimeter counts pixels that are adjacent to a different label or to the image edge.
545pub fn region_props(labels: &Tensor) -> Result<Vec<RegionProp>, ImgProcError> {
546    let (h, w, c) = hwc_shape(labels)?;
547    if c != 1 {
548        return Err(ImgProcError::InvalidChannelCount {
549            expected: 1,
550            got: c,
551        });
552    }
553    let data = labels.data();
554
555    // Find all unique non-zero labels
556    let mut max_label = 0u32;
557    for &v in data.iter() {
558        let l = v as u32;
559        if l > max_label {
560            max_label = l;
561        }
562    }
563    if max_label == 0 {
564        return Ok(Vec::new());
565    }
566
567    // Accumulators per label (1-indexed, slot 0 unused)
568    let n = max_label as usize;
569    let mut area = vec![0usize; n + 1];
570    let mut sum_x = vec![0.0f64; n + 1];
571    let mut sum_y = vec![0.0f64; n + 1];
572    let mut min_x = vec![usize::MAX; n + 1];
573    let mut max_x = vec![0usize; n + 1];
574    let mut min_y = vec![usize::MAX; n + 1];
575    let mut max_y = vec![0usize; n + 1];
576    let mut perim = vec![0usize; n + 1];
577
578    for y in 0..h {
579        for x in 0..w {
580            let l = data[y * w + x] as u32;
581            if l == 0 {
582                continue;
583            }
584            let li = l as usize;
585            area[li] += 1;
586            sum_x[li] += x as f64;
587            sum_y[li] += y as f64;
588            if x < min_x[li] {
589                min_x[li] = x;
590            }
591            if x > max_x[li] {
592                max_x[li] = x;
593            }
594            if y < min_y[li] {
595                min_y[li] = y;
596            }
597            if y > max_y[li] {
598                max_y[li] = y;
599            }
600
601            // Boundary pixel: adjacent to a different label or at the image edge
602            let is_boundary = x == 0
603                || x == w - 1
604                || y == 0
605                || y == h - 1
606                || data[y * w + (x - 1)] as u32 != l
607                || data[y * w + (x + 1)] as u32 != l
608                || data[(y - 1) * w + x] as u32 != l
609                || data[(y + 1) * w + x] as u32 != l;
610            if is_boundary {
611                perim[li] += 1;
612            }
613        }
614    }
615
616    let mut props = Vec::new();
617    for li in 1..=n {
618        if area[li] == 0 {
619            continue;
620        }
621        props.push(RegionProp {
622            label: li,
623            area: area[li],
624            centroid: (
625                (sum_x[li] / area[li] as f64) as f32,
626                (sum_y[li] / area[li] as f64) as f32,
627            ),
628            bbox: (
629                min_x[li],
630                min_y[li],
631                max_x[li] - min_x[li] + 1,
632                max_y[li] - min_y[li] + 1,
633            ),
634            perimeter: perim[li] as f32,
635        });
636    }
637
638    Ok(props)
639}
640
641/// Computes the 7 Hu invariant moments from a single-channel `[H, W, 1]` image.
642///
643/// Hu moments are invariant to translation, scale, and rotation. They are
644/// derived from the normalised central moments of the image.
645pub fn hu_moments(img: &Tensor) -> Result<[f64; 7], ImgProcError> {
646    let (h, w, c) = hwc_shape(img)?;
647    if c != 1 {
648        return Err(ImgProcError::InvalidChannelCount {
649            expected: 1,
650            got: c,
651        });
652    }
653    let data = img.data();
654
655    // Raw moments m00, m10, m01
656    let mut m00 = 0.0f64;
657    let mut m10 = 0.0f64;
658    let mut m01 = 0.0f64;
659    for y in 0..h {
660        for x in 0..w {
661            let v = data[y * w + x] as f64;
662            m00 += v;
663            m10 += x as f64 * v;
664            m01 += y as f64 * v;
665        }
666    }
667    if m00.abs() < 1e-15 {
668        return Ok([0.0; 7]);
669    }
670    let cx = m10 / m00;
671    let cy = m01 / m00;
672
673    // Central moments up to order 3
674    let mut mu20 = 0.0f64;
675    let mut mu02 = 0.0f64;
676    let mut mu11 = 0.0f64;
677    let mut mu30 = 0.0f64;
678    let mut mu03 = 0.0f64;
679    let mut mu21 = 0.0f64;
680    let mut mu12 = 0.0f64;
681    for y in 0..h {
682        for x in 0..w {
683            let v = data[y * w + x] as f64;
684            let dx = x as f64 - cx;
685            let dy = y as f64 - cy;
686            mu20 += dx * dx * v;
687            mu02 += dy * dy * v;
688            mu11 += dx * dy * v;
689            mu30 += dx * dx * dx * v;
690            mu03 += dy * dy * dy * v;
691            mu21 += dx * dx * dy * v;
692            mu12 += dx * dy * dy * v;
693        }
694    }
695
696    // Normalised central moments: eta_pq = mu_pq / m00^((p+q)/2 + 1)
697    let n20 = mu20 / m00.powf(2.0);
698    let n02 = mu02 / m00.powf(2.0);
699    let n11 = mu11 / m00.powf(2.0);
700    let n30 = mu30 / m00.powf(2.5);
701    let n03 = mu03 / m00.powf(2.5);
702    let n21 = mu21 / m00.powf(2.5);
703    let n12 = mu12 / m00.powf(2.5);
704
705    // Hu's 7 invariants
706    let h1 = n20 + n02;
707    let h2 = (n20 - n02).powi(2) + 4.0 * n11 * n11;
708    let h3 = (n30 - 3.0 * n12).powi(2) + (3.0 * n21 - n03).powi(2);
709    let h4 = (n30 + n12).powi(2) + (n21 + n03).powi(2);
710    let h5 = (n30 - 3.0 * n12) * (n30 + n12) * ((n30 + n12).powi(2) - 3.0 * (n21 + n03).powi(2))
711        + (3.0 * n21 - n03) * (n21 + n03) * (3.0 * (n30 + n12).powi(2) - (n21 + n03).powi(2));
712    let h6 = (n20 - n02) * ((n30 + n12).powi(2) - (n21 + n03).powi(2))
713        + 4.0 * n11 * (n30 + n12) * (n21 + n03);
714    let h7 = (3.0 * n21 - n03) * (n30 + n12) * ((n30 + n12).powi(2) - 3.0 * (n21 + n03).powi(2))
715        - (n30 - 3.0 * n12) * (n21 + n03) * (3.0 * (n30 + n12).powi(2) - (n21 + n03).powi(2));
716
717    Ok([h1, h2, h3, h4, h5, h6, h7])
718}
719
720fn point_line_dist_f32(p: (f32, f32), a: (f32, f32), b: (f32, f32)) -> f32 {
721    let dx = b.0 - a.0;
722    let dy = b.1 - a.1;
723    let len_sq = dx * dx + dy * dy;
724    if len_sq < 1e-12 {
725        return ((p.0 - a.0).powi(2) + (p.1 - a.1).powi(2)).sqrt();
726    }
727    let cross = ((p.0 - a.0) * dy - (p.1 - a.1) * dx).abs();
728    cross / len_sq.sqrt()
729}
730
731/// Computes the area of a polygon defined by its vertices using the Shoelace formula.
732///
733/// The contour should be an ordered sequence of `(x, y)` vertex coordinates.
734/// Returns the absolute area of the polygon.
735pub fn contour_area(contour: &[(usize, usize)]) -> f64 {
736    if contour.len() < 3 {
737        return 0.0;
738    }
739    let n = contour.len();
740    let mut sum = 0.0f64;
741    for i in 0..n {
742        let j = (i + 1) % n;
743        let (x1, y1) = (contour[i].0 as f64, contour[i].1 as f64);
744        let (x2, y2) = (contour[j].0 as f64, contour[j].1 as f64);
745        sum += x1 * y2 - x2 * y1;
746    }
747    sum.abs() / 2.0
748}
749
750/// Computes the perimeter (arc length) of a contour.
751///
752/// Sums the Euclidean distances between consecutive points.
753/// If `closed` is true, also adds the distance from the last point back to the first.
754pub fn arc_length(contour: &[(usize, usize)], closed: bool) -> f64 {
755    if contour.len() < 2 {
756        return 0.0;
757    }
758    let mut length = 0.0f64;
759    for i in 0..contour.len() - 1 {
760        let dx = contour[i + 1].0 as f64 - contour[i].0 as f64;
761        let dy = contour[i + 1].1 as f64 - contour[i].1 as f64;
762        length += (dx * dx + dy * dy).sqrt();
763    }
764    if closed {
765        let dx = contour[0].0 as f64 - contour[contour.len() - 1].0 as f64;
766        let dy = contour[0].1 as f64 - contour[contour.len() - 1].1 as f64;
767        length += (dx * dx + dy * dy).sqrt();
768    }
769    length
770}
771
772/// Computes the axis-aligned bounding rectangle of a contour.
773///
774/// Returns `(x, y, width, height)` where `(x, y)` is the top-left corner.
775pub fn bounding_rect(contour: &[(usize, usize)]) -> (usize, usize, usize, usize) {
776    if contour.is_empty() {
777        return (0, 0, 0, 0);
778    }
779    let mut min_x = usize::MAX;
780    let mut min_y = usize::MAX;
781    let mut max_x = 0usize;
782    let mut max_y = 0usize;
783    for &(x, y) in contour {
784        min_x = min_x.min(x);
785        min_y = min_y.min(y);
786        max_x = max_x.max(x);
787        max_y = max_y.max(y);
788    }
789    (min_x, min_y, max_x - min_x, max_y - min_y)
790}