Skip to main content

scan_core/cv/
geometry.rs

1use crate::Point;
2
3pub fn perimeter(poly: &[Point]) -> f32 {
4    let mut p = 0.0;
5    for i in 0..poly.len() {
6        let j = (i + 1) % poly.len();
7        let dx = poly[i].x - poly[j].x;
8        let dy = poly[i].y - poly[j].y;
9        p += (dx * dx + dy * dy).sqrt();
10    }
11    p
12}
13
14pub fn polygon_area(poly: &[Point]) -> f32 {
15    let mut area = 0.0;
16    let len = poly.len();
17    for i in 0..len {
18        let j = (i + 1) % len;
19        area += poly[i].x * poly[j].y;
20        area -= poly[j].x * poly[i].y;
21    }
22    area.abs() / 2.0
23}
24
25pub fn min_edge_length(poly: &[Point]) -> f32 {
26    let mut min = f32::MAX;
27    for i in 0..poly.len() {
28        let j = (i + 1) % poly.len();
29        let dx = poly[i].x - poly[j].x;
30        let dy = poly[i].y - poly[j].y;
31        let d = dx * dx + dy * dy;
32        if d < min {
33            min = d;
34        }
35    }
36    min.sqrt()
37}
38
39pub fn is_contour_convex(poly: &[Point]) -> bool {
40    let len = poly.len();
41    if len < 3 { return false; }
42
43    let mut prev_pt = poly[len - 1];
44    let mut cur_pt = poly[0];
45
46    let mut dx0 = cur_pt.x - prev_pt.x;
47    let mut dy0 = cur_pt.y - prev_pt.y;
48
49    let mut orientation = 0;
50
51    for i in 0..len {
52        let next_idx = (i + 1) % len;
53        prev_pt = cur_pt;
54        cur_pt = poly[next_idx];
55
56        let dx = cur_pt.x - prev_pt.x;
57        let dy = cur_pt.y - prev_pt.y;
58
59        let dxdy0 = dx * dy0;
60        let dydx0 = dy * dx0;
61
62        if dydx0 > dxdy0 {
63            orientation |= 1;
64        } else if dydx0 < dxdy0 {
65            orientation |= 2;
66        }
67
68        if orientation == 3 {
69            return false;
70        }
71
72        dx0 = dx;
73        dy0 = dy;
74    }
75
76    true
77}
78
79/// Approximate polygon — exact port of CV.approxPolyDP from js-aruco2.
80/// This version handles CLOSED contours correctly using the circular indexing
81/// approach from the JS implementation.
82pub fn approx_poly_dp(contour: &[Point], epsilon: f32) -> Vec<Point> {
83    let len = contour.len();
84    if len <= 2 {
85        return contour.to_vec();
86    }
87
88    let epsilon_sq = epsilon * epsilon;
89
90    // Phase 1: Find the farthest point pair (like JS does 3 iterations)
91    let mut right_start = 0usize;
92    let mut k = 0usize;
93    let mut max_dist_phase1: f32 = 0.0;
94
95    for _ in 0..3 {
96        let mut local_max: f32 = 0.0;
97        k = (k + right_start) % len;
98        let start_pt = contour[k];
99        k = (k + 1) % len;
100
101        for _j in 1..len {
102            let pt = contour[k];
103            k = (k + 1) % len;
104
105            let dx = pt.x - start_pt.x;
106            let dy = pt.y - start_pt.y;
107            let dist = dx * dx + dy * dy;
108
109            if dist > local_max {
110                local_max = dist;
111                right_start = _j;
112            }
113        }
114        max_dist_phase1 = local_max;
115    }
116
117    if max_dist_phase1 <= epsilon_sq {
118        let start_pt = contour[k % len];
119        return vec![Point { x: start_pt.x, y: start_pt.y }];
120    }
121
122    // Phase 2: Recursive subdivision using stack
123    let slice_start = k;
124    let slice_end = right_start + slice_start;
125
126    let right_slice_start = if right_start + slice_start >= len {
127        right_start + slice_start - len
128    } else {
129        right_start + slice_start
130    };
131    let right_slice_end = if slice_start < right_slice_start {
132        slice_start + len
133    } else {
134        slice_start
135    };
136
137    let mut poly = Vec::new();
138    let mut stack: Vec<(usize, usize)> = Vec::new();
139
140    stack.push((right_slice_start, right_slice_end));
141    stack.push((slice_start, slice_end));
142
143    while let Some((s_start, s_end)) = stack.pop() {
144        let end_pt = contour[s_end % len];
145        let mut kk = s_start % len;
146        let start_pt = contour[kk];
147        kk = (kk + 1) % len;
148
149        if s_end <= s_start + 1 {
150            poly.push(Point { x: start_pt.x, y: start_pt.y });
151        } else {
152            let mut max_dist: f32 = 0.0;
153            let mut max_idx = 0;
154
155            let dx = end_pt.x - start_pt.x;
156            let dy = end_pt.y - start_pt.y;
157
158            for i in (s_start + 1)..s_end {
159                let pt = contour[kk];
160                kk = (kk + 1) % len;
161
162                let dist = ((pt.y - start_pt.y) * dx - (pt.x - start_pt.x) * dy).abs();
163
164                if dist > max_dist {
165                    max_dist = dist;
166                    max_idx = i;
167                }
168            }
169
170            let le_eps = max_dist * max_dist <= epsilon_sq * (dx * dx + dy * dy);
171
172            if le_eps {
173                poly.push(Point { x: start_pt.x, y: start_pt.y });
174            } else {
175                // Split
176                stack.push((max_idx, s_end));
177                stack.push((s_start, max_idx));
178            }
179        }
180    }
181
182    poly
183}
184
185/// Ensure candidate corners are in clockwise order.
186/// Port of AR.Detector.prototype.clockwiseCorners from JS.
187pub fn clockwise_corners(candidates: &mut Vec<Vec<Point>>) {
188    for c in candidates.iter_mut() {
189        if c.len() != 4 { continue; }
190
191        let dx1 = c[1].x - c[0].x;
192        let dy1 = c[1].y - c[0].y;
193        let dx2 = c[2].x - c[0].x;
194        let dy2 = c[2].y - c[0].y;
195
196        if (dx1 * dy2 - dy1 * dx2) < 0.0 {
197            c.swap(1, 3);
198        }
199    }
200}
201
202/// Filter out candidates that are too close to each other.
203/// Port of AR.Detector.prototype.notTooNear from JS.
204pub fn not_too_near(candidates: &[Vec<Point>], min_dist: f32) -> Vec<Vec<Point>> {
205    let len = candidates.len();
206    let mut too_near = vec![false; len];
207
208    for i in 0..len {
209        if too_near[i] { continue; }
210        for j in (i + 1)..len {
211            if too_near[j] { continue; }
212
213            let mut dist: f32 = 0.0;
214            for k in 0..4 {
215                let dx = candidates[i][k].x - candidates[j][k].x;
216                let dy = candidates[i][k].y - candidates[j][k].y;
217                dist += dx * dx + dy * dy;
218            }
219
220            if (dist / 4.0) < (min_dist * min_dist) {
221                if perimeter(&candidates[i]) < perimeter(&candidates[j]) {
222                    too_near[i] = true;
223                } else {
224                    too_near[j] = true;
225                }
226            }
227        }
228    }
229
230    candidates.iter().enumerate()
231        .filter(|(i, _)| !too_near[*i])
232        .map(|(_, c)| c.clone())
233        .collect()
234}
235
236/// Count non-zero pixels in a rectangular region of a grayscale image.
237/// Port of CV.countNonZero from JS.
238pub fn count_non_zero(data: &[u8], img_width: usize, x: usize, y: usize, w: usize, h: usize) -> usize {
239    let mut nz = 0;
240    for row in y..(y + h) {
241        for col in x..(x + w) {
242            if data[row * img_width + col] != 0 {
243                nz += 1;
244            }
245        }
246    }
247    nz
248}
249
250/// Refine a corner position using image gradients (sub-pixel precision).
251/// Port of ArUcoScanner.prototype._refineCorner from original JS engine.
252pub fn refine_corner(data: &[u8], width: usize, height: usize, p: Point, window: i32) -> Point {
253    let x = p.x.floor() as i32;
254    let y = p.y.floor() as i32;
255
256    // 1. Check local contrast to avoid refining in flat areas
257    let mut min_i = 255u8;
258    let mut max_i = 0u8;
259
260    for dy in -window..=window {
261        for dx in -window..=window {
262            let px = x + dx;
263            let py = y + dy;
264            if px >= 0 && px < width as i32 && py >= 0 && py < height as i32 {
265                let v = data[py as usize * width + px as usize];
266                if v < min_i { min_i = v; }
267                if v > max_i { max_i = v; }
268            }
269        }
270    }
271
272    // If contrast is too low, don't refine
273    if (max_i as i32 - min_i as i32) < 30 {
274        return p;
275    }
276
277    // 2. Compute sums for the linear system (gradient based refinement)
278    let mut s_ix2 = 0.0;
279    let mut s_iy2 = 0.0;
280    let mut s_ixy = 0.0;
281    let mut s_ix2x = 0.0;
282    let mut s_iy2y = 0.0;
283
284    for dy in -window..=window {
285        for dx in -window..=window {
286            let px = x + dx;
287            let py = y + dy;
288
289            // Gradient needs neighbors
290            if px > 0 && px < (width as i32 - 1) && py > 0 && py < (height as i32 - 1) {
291                let px_idx = py as usize * width + px as usize;
292                
293                // Central difference
294                let ix = (data[px_idx + 1] as f32 - data[px_idx - 1] as f32) / 2.0;
295                let iy = (data[px_idx + width] as f32 - data[px_idx - width] as f32) / 2.0;
296
297                s_ix2 += ix * ix;
298                s_iy2 += iy * iy;
299                s_ixy += ix * iy;
300                s_ix2x += ix * ix * px as f32 + ix * iy * py as f32;
301                s_iy2y += iy * iy * py as f32 + ix * iy * px as f32;
302            }
303        }
304    }
305
306    let det = s_ix2 * s_iy2 - s_ixy * s_ixy;
307    if det.abs() < 0.0001 {
308        return p;
309    }
310
311    let nx = (s_ix2x * s_iy2 - s_ixy * s_iy2y) / det;
312    let ny = (s_ix2 * s_iy2y - s_ix2x * s_ixy) / det;
313
314    // Distance threshold: if it's too far (more than 3 pixels), the result might be wrong
315    let dist_sq = (nx - p.x) * (nx - p.x) + (ny - p.y) * (ny - p.y);
316    if dist_sq > 9.0 {
317        return p;
318    }
319
320    Point { x: nx, y: ny }
321}
322
323/// Select the corner of a polygon that is most "extreme" in the given directions.
324/// Port of ArUcoScanner.prototype._getExtreme from JS.
325/// mode_x: true for max, false for min
326/// mode_y: true for max, false for min
327pub fn get_extreme_corner(corners: &[Point], mode_x_max: bool, mode_y_max: bool) -> Point {
328    let mut best_score = f32::NEG_INFINITY;
329    let mut best_idx = 0;
330
331    for (i, p) in corners.iter().enumerate() {
332        let x_val = if mode_x_max { p.x } else { -p.x };
333        let y_val = if mode_y_max { p.y } else { -p.y };
334        let score = x_val + y_val;
335
336        if score > best_score {
337            best_score = score;
338            best_idx = i;
339        }
340    }
341    corners[best_idx]
342}