pineapple_core/mp/
form.rs

1// Copyright (c) 2025, Tom Ouellette
2// Licensed under the BSD 3-Clause License
3
4use crate::cv::ellipse::fit_ellipse_lstsq;
5use crate::cv::points::{convex_hull, point_to_segment_distance};
6
7#[inline]
8pub fn area(points: &[[f32; 2]]) -> f32 {
9    let mut area = 0.0;
10
11    let n = points.len();
12    for i in 0..n - 1 {
13        let p1 = points[i];
14        let p2 = points[i + 1];
15        area += p1[0] * p2[1] - p2[0] * p1[1];
16    }
17
18    if points[0] != points[n - 1] {
19        let p1 = points[n - 1];
20        let p2 = points[0];
21        area += p1[0] * p2[1] - p2[0] * p1[1];
22    }
23
24    area.abs() / 2.0
25}
26
27#[inline]
28pub fn area_bbox(points: &[[f32; 2]]) -> f32 {
29    let (mut xmin, mut ymin) = (points[0][0], points[0][1]);
30    let (mut xmax, mut ymax) = (points[0][0], points[0][1]);
31
32    for point in points.iter().skip(1) {
33        xmin = if point[0] < xmin { point[0] } else { xmin };
34        ymin = if point[1] < ymin { point[1] } else { ymin };
35        xmax = if point[0] > xmax { point[0] } else { xmax };
36        ymax = if point[1] > ymax { point[1] } else { ymax };
37    }
38
39    (xmax - xmin) * (ymax - ymin)
40}
41
42#[inline]
43pub fn area_convex(points: &[[f32; 2]]) -> f32 {
44    area(&convex_hull(points))
45}
46
47#[inline]
48pub fn perimeter(points: &[[f32; 2]]) -> f32 {
49    let n_points = points.len();
50
51    let mut perimeter = 0.0;
52
53    for i in 0..n_points - 1 {
54        let dx = points[i][0] - points[i + 1][0];
55        let dy = points[i][1] - points[i + 1][1];
56        perimeter += (dx * dx + dy * dy).sqrt();
57    }
58
59    if points[0] != points[n_points - 1] {
60        let dx = points[points.len() - 1][0] - points[0][0];
61        let dy = points[points.len() - 1][1] - points[0][1];
62        perimeter += (dx * dx + dy * dy).sqrt();
63    }
64
65    perimeter
66}
67
68#[inline]
69pub fn centroid(points: &[[f32; 2]]) -> [f32; 2] {
70    let mut sum_x = 0.0;
71    let mut sum_y = 0.0;
72    let mut area = 0.0;
73
74    let n = points.len();
75    let is_closed = points[0] == points[n - 1];
76    let n_end = if is_closed { n - 1 } else { n };
77
78    for i in 0..n_end {
79        let j = (i + 1) % n_end;
80        let p1 = points[i];
81        let p2 = points[j];
82        let cross = p1[0] * p2[1] - p2[0] * p1[1];
83        sum_x += (p1[0] + p2[0]) * cross;
84        sum_y += (p1[1] + p2[1]) * cross;
85        area += cross;
86    }
87
88    area /= 2.0;
89    [sum_x / (6.0 * area.abs()), sum_y / (6.0 * area.abs())]
90}
91
92#[inline]
93pub fn center(points: &[[f32; 2]]) -> [f32; 2] {
94    let mut sum_x = 0.0;
95    let mut sum_y = 0.0;
96
97    let n = points.len();
98    let is_closed = points[0] == points[n - 1];
99    let n_end = if is_closed { n - 1 } else { n };
100
101    for point in points.iter().take(n_end) {
102        sum_x += point[0];
103        sum_y += point[1];
104    }
105
106    [sum_x / n_end as f32, sum_y / n_end as f32]
107}
108
109#[inline]
110pub fn elongation(points: &[[f32; 2]]) -> f32 {
111    let (mut xmin, mut ymin) = (points[0][0], points[0][1]);
112    let (mut xmax, mut ymax) = (points[0][0], points[0][1]);
113
114    for point in points.iter().skip(1) {
115        xmin = if point[0] < xmin { point[0] } else { xmin };
116        ymin = if point[1] < ymin { point[1] } else { ymin };
117        xmax = if point[0] > xmax { point[0] } else { xmax };
118        ymax = if point[1] > ymax { point[1] } else { ymax };
119    }
120
121    let elongation = (xmax - xmin) / (ymax - ymin);
122
123    if elongation > 1.0 {
124        1.0 / elongation
125    } else {
126        elongation
127    }
128}
129
130#[inline]
131pub fn thread_length(points: &[[f32; 2]]) -> f32 {
132    let perimeter = perimeter(points);
133    let area = area(points);
134
135    let left = perimeter.powi(2);
136    let right = 16.0 * area;
137
138    let coefficient = if left <= right {
139        0.0
140    } else {
141        (left - right).sqrt()
142    };
143
144    (perimeter + coefficient) / 4.0
145}
146
147#[inline]
148pub fn thread_width(points: &[[f32; 2]]) -> f32 {
149    let perimeter = perimeter(points);
150    let area = area(points);
151
152    let left = perimeter.powi(2);
153    let right = 16.0 * area;
154
155    let coefficient = if left <= right {
156        0.0
157    } else {
158        (left - right).sqrt()
159    };
160
161    let thread_length = (perimeter + coefficient) / 4.0;
162    area / thread_length
163}
164
165#[inline]
166pub fn solidity(points: &[[f32; 2]]) -> f32 {
167    let area = area(points);
168    let area_convex = area_convex(points);
169
170    area / area_convex
171}
172
173#[inline]
174pub fn extent(points: &[[f32; 2]]) -> f32 {
175    let area = area(points);
176    let area_bbox = area_bbox(points);
177
178    area / area_bbox
179}
180
181#[inline]
182pub fn form_factor(points: &[[f32; 2]]) -> f32 {
183    let perimeter = perimeter(points);
184    let area = area(points);
185
186    (4.0 * std::f32::consts::PI * area) / (perimeter * perimeter)
187}
188
189#[inline]
190pub fn equivalent_diameter(points: &[[f32; 2]]) -> f32 {
191    (area(points) / std::f32::consts::PI).sqrt() * 2.0
192}
193
194#[inline]
195pub fn eccentricity(points: &[[f32; 2]]) -> f32 {
196    let ellipse = fit_ellipse_lstsq(points);
197    ellipse[2]
198}
199
200#[inline]
201pub fn major_axis_length(points: &[[f32; 2]]) -> f32 {
202    let ellipse = fit_ellipse_lstsq(points);
203    ellipse[0]
204}
205
206#[inline]
207pub fn minor_axis_length(points: &[[f32; 2]]) -> f32 {
208    let ellipse = fit_ellipse_lstsq(points);
209    ellipse[1]
210}
211
212#[inline]
213pub fn min_radius(points: &[[f32; 2]]) -> f32 {
214    let [cx, cy] = centroid(points);
215    let mut min_radius = f32::MAX;
216
217    for i in 0..points.len() {
218        let p1 = points[i];
219        let p2 = points[(i + 1) % points.len()];
220
221        let distance = point_to_segment_distance(cx, cy, p1, p2);
222        if distance < min_radius {
223            min_radius = distance;
224        }
225    }
226
227    min_radius
228}
229
230#[inline]
231pub fn max_radius(points: &[[f32; 2]]) -> f32 {
232    let [x_centroid, y_centroid] = centroid(points);
233    let mut maximum_radius = 0.0;
234
235    for point in points.iter() {
236        let (x, y) = (point[0], point[1]);
237        let distance = (x_centroid - x) * (x_centroid - x) + (y_centroid - y) * (y_centroid - y);
238        if distance > maximum_radius {
239            maximum_radius = distance;
240        }
241    }
242
243    maximum_radius.sqrt()
244}
245
246#[inline]
247pub fn mean_radius(points: &[[f32; 2]]) -> f32 {
248    let [x_centroid, y_centroid] = centroid(points);
249    let include_last = (points.last().unwrap() == &points[0]) as usize;
250
251    let mut mean_radius = 0.0;
252    for point in points.iter().take(points.len() - include_last) {
253        let (x, y) = (point[0], point[1]);
254        let distance = (x_centroid - x) * (x_centroid - x) + (y_centroid - y) * (y_centroid - y);
255        mean_radius += distance.sqrt();
256    }
257
258    mean_radius / (points.len() - include_last) as f32
259}
260
261#[inline]
262pub fn min_feret(points: &[[f32; 2]]) -> f32 {
263    let norm = |x: [f32; 2]| -> f32 { (x[0] * x[0] + x[1] * x[1]).sqrt() };
264    let cross_product = |x: [f32; 2], y: [f32; 2]| -> f32 { x[0] * y[1] - x[1] * y[0] };
265    let n_points = points.len();
266
267    let mut feret_diameter_minimum = f32::MAX;
268    for i in 0..n_points {
269        let p1 = points[i];
270        let p2 = points[(i + 1) % n_points];
271        let diff_a = [p2[0] - p1[0], p2[1] - p1[1]];
272
273        let mut distance = 0.0;
274        for point in points.iter().take(n_points) {
275            let diff_b = [point[0] - p1[0], point[1] - p1[1]];
276            let d = (cross_product(diff_a, diff_b) / norm(diff_a)).abs();
277
278            if d > distance {
279                distance = d;
280            }
281        }
282
283        if distance < feret_diameter_minimum && distance > f32::EPSILON {
284            feret_diameter_minimum = distance;
285        }
286    }
287
288    feret_diameter_minimum
289}
290
291#[inline]
292pub fn max_feret(points: &[[f32; 2]]) -> f32 {
293    let mut max_diameter = 0f32;
294    let n_points = points.len();
295
296    for i in 0..n_points {
297        for j in (i + 1)..n_points {
298            let dx = points[j][0] - points[i][0];
299            let dy = points[j][1] - points[i][1];
300
301            if dx == 0.0 && dy == 0.0 {
302                continue;
303            }
304
305            let mut min_proj = f32::INFINITY;
306            let mut max_proj = f32::NEG_INFINITY;
307
308            for k in 0..n_points {
309                let proj = (points[k][0] - points[i][0]) * dx + (points[k][1] - points[i][1]) * dy;
310
311                min_proj = min_proj.min(proj);
312                max_proj = max_proj.max(proj);
313            }
314
315            let diameter = ((max_proj - min_proj) / ((dx * dx + dy * dy).sqrt())).abs();
316            max_diameter = max_diameter.max(diameter);
317        }
318    }
319
320    max_diameter
321}
322
323#[inline]
324pub fn descriptors(points: &[[f32; 2]]) -> [f32; 23] {
325    let n = points.len();
326    let is_closed = points[0] == points[n - 1];
327    let n_end = if is_closed { n - 1 } else { n };
328
329    let mut area = 0f32;
330    let mut perimeter = 0f32;
331
332    let mut sum_x = 0f32;
333    let mut sum_y = 0f32;
334    let mut mean_x = 0f32;
335    let mut mean_y = 0f32;
336
337    let (mut xmin, mut ymin) = (points[0][0], points[0][1]);
338    let (mut xmax, mut ymax) = (points[0][0], points[0][1]);
339
340    for i in 0..n {
341        let p1 = points[i];
342        let p2 = points[(i + 1) % n];
343
344        // Area and centroid
345        if i < n_end {
346            let cross = p1[0] * p2[1] - p2[0] * p1[1];
347            area += cross;
348            sum_x += (p1[0] + p2[0]) * cross;
349            sum_y += (p1[1] + p2[1]) * cross;
350        }
351
352        // Perimeter
353        if !is_closed || (i + 1) % n != 0 {
354            let dx = p1[0] - p2[0];
355            let dy = p1[1] - p2[1];
356            perimeter += (dx * dx + dy * dy).sqrt();
357        }
358
359        // Bounding box
360        xmin = xmin.min(p1[0]);
361        ymin = ymin.min(p1[1]);
362        xmax = xmax.max(p1[0]);
363        ymax = ymax.max(p1[1]);
364
365        // Center
366        if i < n_end {
367            mean_x += p1[0];
368            mean_y += p1[1];
369        }
370    }
371
372    let area = area.abs() / 2.0;
373    let area_bbox = (xmax - xmin) * (ymax - ymin);
374
375    let centroid_x = sum_x / (6.0 * area.abs());
376    let centroid_y = sum_y / (6.0 * area.abs());
377    let center_x = mean_x / n_end as f32;
378    let center_y = mean_y / n_end as f32;
379
380    let elongation = {
381        let e = (xmax - xmin) / (ymax - ymin);
382        if e > 1.0 { 1.0 / e } else { e }
383    };
384
385    let mut minimum_radius = f32::MAX;
386    let mut maximum_radius = 0f32;
387    let mut mean_radius = 0f32;
388
389    let mut min_feret = f32::MAX;
390    let mut max_feret = 0f32;
391
392    for i in 0..n {
393        let p1 = points[i];
394        let p2 = points[(i + 1) % n];
395
396        // Min radius
397        minimum_radius =
398            minimum_radius.min(point_to_segment_distance(centroid_x, centroid_y, p1, p2));
399
400        // Max and mean radius
401        if !is_closed || i < n - 1 {
402            let dx = centroid_x - p1[0];
403            let dy = centroid_y - p1[1];
404            let distance_sq = dx * dx + dy * dy;
405            maximum_radius = maximum_radius.max(distance_sq);
406            mean_radius += distance_sq.sqrt();
407        }
408
409        // Feret diameters
410        let diff_a = [p2[0] - p1[0], p2[1] - p1[1]];
411        let norm_a = (diff_a[0] * diff_a[0] + diff_a[1] * diff_a[1]).sqrt();
412
413        let mut max_distance = 0f32;
414        for point in points {
415            let diff_b = [point[0] - p1[0], point[1] - p1[1]];
416            let d = (diff_a[0] * diff_b[1] - diff_a[1] * diff_b[0]).abs() / norm_a;
417            max_distance = max_distance.max(d);
418        }
419
420        if max_distance < min_feret && max_distance > f32::EPSILON {
421            min_feret = max_distance;
422        }
423    }
424
425    maximum_radius = maximum_radius.sqrt();
426    mean_radius /= n_end as f32;
427
428    // Max feret
429    for i in 0..n {
430        for j in (i + 1)..n {
431            let dx = points[j][0] - points[i][0];
432            let dy = points[j][1] - points[i][1];
433
434            if dx == 0.0 && dy == 0.0 {
435                continue;
436            }
437
438            let norm = (dx * dx + dy * dy).sqrt();
439
440            let mut min_proj = f32::INFINITY;
441            let mut max_proj = f32::NEG_INFINITY;
442
443            for point in points {
444                let proj =
445                    ((point[0] - points[i][0]) * dx + ((point[1] - points[i][1]) * dy)) / norm;
446                min_proj = min_proj.min(proj);
447                max_proj = max_proj.max(proj);
448            }
449
450            max_feret = max_feret.max((max_proj - min_proj).abs());
451        }
452    }
453
454    // Convex hull
455    let area_convex = {
456        let convex_hull_points = convex_hull(points);
457
458        let mut area = 0.0;
459        let n_hull = convex_hull_points.len();
460        for i in 0..n_hull - 1 {
461            let p1 = convex_hull_points[i];
462            let p2 = convex_hull_points[i + 1];
463            area += p1[0] * p2[1] - p2[0] * p1[1];
464        }
465
466        if convex_hull_points[0] != convex_hull_points[n_hull - 1] {
467            let p1 = convex_hull_points[n_hull - 1];
468            let p2 = convex_hull_points[0];
469            area += p1[0] * p2[1] - p2[0] * p1[1];
470        }
471
472        area.abs() / 2.0
473    };
474
475    // Ellipse fitting
476    let ellipse = fit_ellipse_lstsq(points);
477    let major_axis = ellipse[0];
478    let minor_axis = ellipse[1];
479    let eccentricity = ellipse[2];
480
481    // Thread width and height
482    let thread_left = perimeter.powi(2);
483    let thread_right = 16.0 * area;
484    let thread_coefficient = if thread_left <= thread_right {
485        0.0
486    } else {
487        (thread_left - thread_right).sqrt()
488    };
489    let thread_length = (perimeter + thread_coefficient) / 4.0;
490    let thread_width = area / thread_length;
491
492    // Refine
493    let solidity = area / area_convex;
494    let extent = area / area_bbox;
495    let form_factor = (4.0 * std::f32::consts::PI * area) / (perimeter * perimeter);
496    let equivalent_diameter = (area / std::f32::consts::PI).sqrt() * 2.0;
497
498    [
499        centroid_x,
500        centroid_y,
501        center_x,
502        center_y,
503        area,
504        area_bbox,
505        area_convex,
506        perimeter,
507        elongation,
508        thread_length,
509        thread_width,
510        solidity,
511        extent,
512        form_factor,
513        equivalent_diameter,
514        eccentricity,
515        major_axis,
516        minor_axis,
517        minimum_radius,
518        maximum_radius,
519        mean_radius,
520        min_feret,
521        max_feret,
522    ]
523}
524
525#[cfg(test)]
526mod test {
527    use super::*;
528
529    const EPSILON: f32 = 1e-5;
530
531    fn unit_circle(close: bool) -> Vec<[f32; 2]> {
532        let mut points = Vec::with_capacity(360);
533        for i in 0..360 {
534            let t = 2.0 * std::f32::consts::PI * i as f32 / 360f32;
535            points.push([t.cos(), t.sin()]);
536        }
537
538        if close {
539            points.push(points[0]);
540        }
541
542        points
543    }
544
545    fn unit_circle_shifted(close: bool) -> Vec<[f32; 2]> {
546        let mut points = Vec::with_capacity(360);
547        for i in 0..360 {
548            let t = 2.0 * std::f32::consts::PI * i as f32 / 360f32;
549            points.push([t.cos() + 1.0, t.sin() + 2.0]);
550        }
551
552        if close {
553            points.push(points[0]);
554        }
555
556        points
557    }
558
559    fn unit_square(close: bool) -> Vec<[f32; 2]> {
560        let mut points = vec![[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]];
561        if close {
562            points.push(points[0]);
563        }
564        points
565    }
566
567    fn test_equivalence(f: fn(&[[f32; 2]]) -> f32) {
568        let open_circle = unit_circle(false);
569        let open_square = unit_square(false);
570        let closed_circle = unit_circle(true);
571        let closed_square = unit_square(true);
572
573        assert_eq!(f(&open_circle), f(&closed_circle));
574        assert_eq!(f(&open_square), f(&closed_square));
575    }
576
577    #[test]
578    fn test_centroid() {
579        for close in [true, false] {
580            let circle = unit_circle(close);
581            let xy = centroid(&circle);
582            assert!((xy[0] - 0.0).abs() < EPSILON);
583            assert!((xy[1] - 0.0).abs() < EPSILON);
584
585            let square = unit_square(close);
586            let xy = centroid(&square);
587            assert!((xy[0] - 0.0).abs() < EPSILON);
588            assert!((xy[1] - 0.0).abs() < EPSILON);
589        }
590    }
591
592    #[test]
593    fn test_centroid_shifted() {
594        for close in [true, false] {
595            let circle = unit_circle_shifted(close);
596
597            let xy = centroid(&circle);
598            assert!((xy[0] - 1.0).abs() < EPSILON);
599            assert!((xy[1] - 2.0).abs() < EPSILON);
600        }
601    }
602
603    #[test]
604    fn test_center() {
605        for close in [true, false] {
606            let circle = unit_circle(close);
607            let xy = center(&circle);
608            assert!((xy[0] - 0.0).abs() < EPSILON);
609            assert!((xy[1] - 0.0).abs() < EPSILON);
610
611            let square = unit_square(close);
612            let xy = center(&square);
613            assert!((xy[0] - 0.0).abs() < EPSILON);
614            assert!((xy[1] - 0.0).abs() < EPSILON);
615        }
616    }
617
618    #[test]
619    fn test_area() {
620        for close in [true, false] {
621            let circle = unit_circle(close);
622            let area_polygon = area(&circle);
623            let circle_area = 0.5 * 360f32 * (2.0 * std::f32::consts::PI / 360f32).sin();
624            assert!((area_polygon - circle_area).abs() < EPSILON);
625
626            let square = unit_square(close);
627            let area_polygon = area(&square);
628            assert!((area_polygon - 4.0).abs() < EPSILON);
629        }
630
631        test_equivalence(area);
632    }
633
634    #[test]
635    fn test_area_bbox() {
636        for close in [true, false] {
637            let circle = unit_circle(close);
638            let area = area_bbox(&circle);
639            assert!((area - 4.0).abs() < EPSILON);
640
641            let square = unit_square(close);
642            let area = area_bbox(&square);
643            assert!((area - 4.0).abs() < EPSILON);
644        }
645
646        test_equivalence(area_bbox);
647    }
648
649    #[test]
650    fn test_area_convex() {
651        for close in [true, false] {
652            let circle = unit_circle(close);
653            let area = area_convex(&circle);
654            let circle_area = 0.5 * 360f32 * (2.0 * std::f32::consts::PI / 360f32).sin();
655            assert!((area - circle_area).abs() < EPSILON);
656
657            let square = unit_square(close);
658            let area = area_convex(&square);
659            assert!((area - 4.0).abs() < EPSILON);
660        }
661
662        test_equivalence(area_convex);
663    }
664
665    #[test]
666    fn test_perimeter() {
667        for close in [true, false] {
668            let circle = unit_circle(close);
669            let perimeter_polygon = perimeter(&circle);
670            let circle_perimeter = 360f32 * 2.0 * (std::f32::consts::PI / 360f32).sin();
671            assert!((perimeter_polygon - circle_perimeter).abs() < 1e-4);
672
673            let square = unit_square(close);
674            let perimeter_polygon = perimeter(&square);
675            assert!((perimeter_polygon - 8.0).abs() < EPSILON);
676        }
677
678        test_equivalence(perimeter);
679    }
680
681    #[test]
682    fn test_elongation() {
683        for close in [true, false] {
684            let circle = unit_circle(close);
685            let elongation_polygon = elongation(&circle);
686            assert!((elongation_polygon - 1.0).abs() < EPSILON);
687
688            let square = unit_square(close);
689            let elongation_polygon = elongation(&square);
690            assert!((elongation_polygon - 1.0).abs() < EPSILON);
691        }
692
693        test_equivalence(elongation);
694    }
695
696    #[test]
697    fn test_solidity() {
698        for close in [true, false] {
699            let circle = unit_circle(close);
700            let solidity_polygon = solidity(&circle);
701            assert!((solidity_polygon - 1.0).abs() < EPSILON);
702
703            let square = unit_square(close);
704            let solidity_polygon = solidity(&square);
705            assert!((solidity_polygon - 1.0).abs() < EPSILON);
706        }
707
708        test_equivalence(solidity);
709    }
710
711    #[test]
712    fn test_extent() {
713        for close in [true, false] {
714            let circle = unit_circle(close);
715            let extent_polygon = extent(&circle);
716            let circle_area = 0.5 * 360f32 * (2.0 * std::f32::consts::PI / 360f32).sin();
717            assert!((extent_polygon - circle_area / 4.0).abs() < EPSILON);
718
719            let square = unit_square(close);
720            let extent_polygon = extent(&square);
721            assert!((extent_polygon - 1.0).abs() < EPSILON);
722        }
723
724        test_equivalence(extent);
725    }
726
727    #[test]
728    fn test_form_factor() {
729        for close in [true, false] {
730            let circle = unit_circle(close);
731            let form_factor_polygon = form_factor(&circle);
732            let circle_area = 0.5 * 360f32 * (2.0 * std::f32::consts::PI / 360f32).sin();
733            let circle_perimeter = 360f32 * 2.0 * (std::f32::consts::PI / 360f32).sin();
734            assert!(
735                (form_factor_polygon
736                    - (4.0 * std::f32::consts::PI * circle_area)
737                        / (circle_perimeter * circle_perimeter))
738                    .abs()
739                    < EPSILON
740            );
741
742            let square = unit_square(close);
743            let form_factor_polygon = form_factor(&square);
744            assert!((form_factor_polygon - std::f32::consts::PI / 4.0).abs() < EPSILON);
745        }
746
747        test_equivalence(form_factor);
748    }
749
750    #[test]
751    fn test_thread_length() {
752        for close in [true, false] {
753            let circle = unit_circle(close);
754            let thread_length_polygon = thread_length(&circle);
755            let circle_perimeter = 360f32 * 2.0 * (std::f32::consts::PI / 360f32).sin();
756            assert!((thread_length_polygon - circle_perimeter / 4.0).abs() < EPSILON);
757
758            let square = unit_square(close);
759            let thread_length_polygon = thread_length(&square);
760            assert!((thread_length_polygon - 2.0).abs() < EPSILON);
761        }
762
763        test_equivalence(thread_length);
764    }
765
766    #[test]
767    fn test_thread_width() {
768        for close in [true, false] {
769            let circle = unit_circle(close);
770            let thread_width_polygon = thread_width(&circle);
771            let circle_area = 0.5 * 360f32 * (2.0 * std::f32::consts::PI / 360f32).sin();
772            let circle_perimeter = 360f32 * 2.0 * (std::f32::consts::PI / 360f32).sin();
773            assert!((thread_width_polygon - (4.0 * circle_area) / circle_perimeter).abs() < 1e-4);
774
775            let square = unit_square(close);
776            let thread_width_polygon = thread_width(&square);
777            assert!((thread_width_polygon - 2.0).abs() < EPSILON);
778        }
779
780        test_equivalence(thread_width);
781    }
782
783    #[test]
784    fn feret_diameter_maximum() {
785        for close in [true, false] {
786            let circle = unit_circle(close);
787            let feret_diameter_maximum = max_feret(&circle);
788            // The discrete approximation of a unit circle with points
789            // leads to slightly higher error from continuous expectation
790            assert!((feret_diameter_maximum - 2.0).abs() < 1e-4);
791
792            let square = unit_square(close);
793            let feret_diameter_maximum = max_feret(&square);
794            assert!((feret_diameter_maximum - 8_f32.sqrt()).abs() < EPSILON);
795        }
796
797        test_equivalence(max_feret);
798    }
799
800    #[test]
801    fn test_feret_diameter_minimum() {
802        for close in [true, false] {
803            let circle = unit_circle(close);
804            let feret_diameter_minimum = min_feret(&circle);
805            // The discrete approximation of a unit circle with points
806            // leads to slightly higher error from continuous expectation
807            assert!((feret_diameter_minimum - 2.0).abs() < 1e-4);
808
809            let square = unit_square(close);
810            let feret_diameter_minimum = min_feret(&square);
811            assert!((feret_diameter_minimum - 2.0).abs() < EPSILON);
812        }
813
814        test_equivalence(min_feret);
815    }
816
817    #[test]
818    fn test_min_radius() {
819        for close in [true, false] {
820            let circle = unit_circle(close);
821            let min_radius_polygon = min_radius(&circle);
822            assert!((min_radius_polygon - 1.0).abs() < 1e-4);
823
824            let square = unit_square(close);
825            let min_radius_polygon = min_radius(&square);
826            assert!((min_radius_polygon - 1.0).abs() < EPSILON);
827        }
828
829        test_equivalence(min_radius);
830    }
831
832    #[test]
833    fn test_min_radius_shifted() {
834        for close in [true, false] {
835            let circle = unit_circle_shifted(close);
836            let min_radius_polygon = min_radius(&circle);
837            assert!((min_radius_polygon - 1.0).abs() < 1e-4);
838        }
839    }
840
841    #[test]
842    fn test_max_radius() {
843        for close in [true, false] {
844            let circle = unit_circle(close);
845            let max_radius_polygon = max_radius(&circle);
846            assert!((max_radius_polygon - 1.0).abs() < EPSILON);
847
848            let square = unit_square(close);
849            let max_radius_polygon = max_radius(&square);
850            let h = (2.0_f32).sqrt();
851            assert!((max_radius_polygon - h).abs() < EPSILON);
852        }
853
854        test_equivalence(max_radius);
855    }
856
857    #[test]
858    fn test_max_radius_shifted() {
859        for close in [true, false] {
860            let circle = unit_circle_shifted(close);
861            let max_radius_polygon = min_radius(&circle);
862            assert!((max_radius_polygon - 1.0).abs() < 1e-4);
863        }
864    }
865
866    #[test]
867    fn test_mean_radius() {
868        for close in [true, false] {
869            let circle = unit_circle(close);
870            let mean_radius_polygon = mean_radius(&circle);
871            assert!((mean_radius_polygon - 1.0).abs() < EPSILON);
872
873            let square = unit_square(close);
874            let mean_radius_polygon = mean_radius(&square);
875            let h = (2.0_f32).sqrt();
876            assert!((mean_radius_polygon - h).abs() < EPSILON);
877        }
878
879        test_equivalence(mean_radius);
880    }
881
882    #[test]
883    fn test_eccentricity() {
884        for close in [true, false] {
885            let circle = unit_circle(close);
886            let eccentricity_polygon = eccentricity(&circle);
887            // The discrete approximation to the unit circle results
888            // in a higher error on eccentricity than if the circle
889            // was completely continuous
890            assert!((eccentricity_polygon - 0.0).abs() < 1e-2);
891        }
892
893        test_equivalence(eccentricity);
894    }
895
896    #[test]
897    fn test_major_axis_length() {
898        for close in [true, false] {
899            let circle = unit_circle(close);
900            let major_axis_length_polygon = major_axis_length(&circle);
901            assert!((major_axis_length_polygon - 2.0).abs() < EPSILON);
902        }
903
904        test_equivalence(major_axis_length);
905    }
906
907    #[test]
908    fn test_minor_axis_length() {
909        for close in [true, false] {
910            let circle = unit_circle(close);
911            let minor_axis_length_polygon = minor_axis_length(&circle);
912            assert!((minor_axis_length_polygon - 2.0).abs() < EPSILON);
913        }
914
915        test_equivalence(minor_axis_length);
916    }
917
918    #[test]
919    fn test_descriptors() {
920        for close in [true, false] {
921            for shifted in [true, false] {
922                let points = if !shifted {
923                    unit_circle(close)
924                } else {
925                    let mut points = vec![];
926                    for point in unit_circle(close).iter().copied() {
927                        points.push([point[0] + 10f32, point[1] + 12f32])
928                    }
929                    points
930                };
931
932                let descriptors = descriptors(&points);
933                let centroid = centroid(&points);
934                let center = center(&points);
935                let area_polygon = area(&points);
936                let area_bbox = area_bbox(&points);
937                let area_convex = area_convex(&unit_circle(close));
938                let perimeter = perimeter(&points);
939                let elongation = elongation(&points);
940                let solidity = solidity(&points);
941                let extent = extent(&points);
942                let form_factor = form_factor(&points);
943                let equivalent_diameter = equivalent_diameter(&points);
944                let major_axis = major_axis_length(&points);
945                let minor_axis = minor_axis_length(&points);
946                let thread_length = thread_length(&points);
947                let thread_width = thread_width(&points);
948                let minimum_radius = min_radius(&points);
949                let maximum_radius = max_radius(&points);
950                let mean_radius = mean_radius(&points);
951                let eccentricity = eccentricity(&points);
952                let min_feret = min_feret(&points);
953                let max_feret = max_feret(&points);
954
955                assert_eq!(descriptors[0], centroid[0]);
956                assert_eq!(descriptors[1], centroid[1]);
957                assert_eq!(descriptors[2], center[0]);
958                assert_eq!(descriptors[3], center[1]);
959                assert_eq!(descriptors[4], area_polygon);
960                assert_eq!(descriptors[5], area_bbox);
961                if shifted {
962                    // Small precision difference when comparing single
963                    // function implementation and batch descriptors calculation.
964                    // This shouldn't materially impact interpretation of the output
965                    // values - but keeping this here to make note of it.
966                    assert!((descriptors[6] - area_convex).abs() < 1e-4);
967                } else {
968                    assert_eq!(descriptors[6], area_convex);
969                }
970                assert_eq!(descriptors[7], perimeter);
971                assert_eq!(descriptors[8], elongation);
972                assert_eq!(descriptors[9], thread_length);
973                assert_eq!(descriptors[10], thread_width);
974                assert_eq!(descriptors[11], solidity);
975                assert_eq!(descriptors[12], extent);
976                assert_eq!(descriptors[13], form_factor);
977                assert_eq!(descriptors[14], equivalent_diameter);
978                assert_eq!(descriptors[15], eccentricity);
979                assert_eq!(descriptors[16], major_axis);
980                assert_eq!(descriptors[17], minor_axis);
981                assert_eq!(descriptors[18], minimum_radius);
982                assert_eq!(descriptors[19], maximum_radius);
983                assert_eq!(descriptors[20], mean_radius);
984                assert_eq!(descriptors[21], min_feret);
985                assert_eq!(descriptors[22], max_feret);
986            }
987        }
988    }
989}