chess_corners_core/detect/radon/
detect.rs1use super::primitives::fit_peak_frac;
11use super::response::{RadonDetectorParams, RadonResponseView};
12use crate::detect::{count_positive_neighbors, is_local_max, Corner};
13#[cfg(feature = "rayon")]
14use rayon::prelude::*;
15
16pub fn detect_peaks_from_radon(
26 resp: &RadonResponseView<'_>,
27 params: &RadonDetectorParams,
28) -> Vec<Corner> {
29 let w = resp.width();
30 let h = resp.height();
31 if w == 0 || h == 0 {
32 return Vec::new();
33 }
34
35 let data = resp.data();
36
37 let mut max_r = f32::NEG_INFINITY;
38 for &v in data {
39 if v > max_r {
40 max_r = v;
41 }
42 }
43 if !max_r.is_finite() {
44 return Vec::new();
45 }
46
47 let thr = (params.threshold_rel * max_r).max(0.0);
48
49 let nms_r = params.nms_radius as i32;
50 let ray_r = params.ray_radius_clamped() as i32;
51 let border = (ray_r + nms_r + 1).max(0) as usize;
54 if w <= 2 * border || h <= 2 * border {
55 return Vec::new();
56 }
57
58 let inv_up = 1.0 / (params.image_upsample_clamped() as f32);
59 let min_cluster = params.min_cluster_size;
60 let peak_fit = params.peak_fit;
61
62 #[inline(always)]
63 fn at(data: &[f32], w: usize, x: usize, y: usize) -> f32 {
64 data[y * w + x]
65 }
66
67 let process_row = |y: usize, sink: &mut Vec<Corner>| {
68 for x in border..(w - border) {
69 let v = at(data, w, x, y);
70 if v <= thr {
71 continue;
72 }
73 if !is_local_max(data, w, h, x, y, nms_r, v) {
74 continue;
75 }
76 if count_positive_neighbors(data, w, h, x, y, nms_r) < min_cluster {
77 continue;
78 }
79
80 let r_c = v;
81 let r_xm = at(data, w, x - 1, y);
82 let r_xp = at(data, w, x + 1, y);
83 let r_ym = at(data, w, x, y - 1);
84 let r_yp = at(data, w, x, y + 1);
85 let fx = fit_peak_frac(r_xm, r_c, r_xp, peak_fit);
86 let fy = fit_peak_frac(r_ym, r_c, r_yp, peak_fit);
87
88 let gx = (x as f32 + fx) * inv_up;
89 let gy = (y as f32 + fy) * inv_up;
90 sink.push(Corner {
91 x: gx,
92 y: gy,
93 strength: v,
94 });
95 }
96 };
97
98 #[cfg(feature = "rayon")]
99 {
100 let row_results: Vec<Vec<Corner>> = (border..(h - border))
101 .into_par_iter()
102 .map(|y| {
103 let mut sink = Vec::new();
104 process_row(y, &mut sink);
105 sink
106 })
107 .collect();
108 let total: usize = row_results.iter().map(|r| r.len()).sum();
109 let mut out = Vec::with_capacity(total);
110 for row in row_results {
111 out.extend(row);
112 }
113 out
114 }
115 #[cfg(not(feature = "rayon"))]
116 {
117 let mut out = Vec::new();
118 for y in border..(h - border) {
119 process_row(y, &mut out);
120 }
121 out
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::super::response::{radon_response_u8, RadonBuffers, RadonDetectorParams};
128 use super::super::test_fixtures::synthetic_chessboard_aa;
129 use super::*;
130
131 #[test]
132 fn detector_recovers_interior_corner_without_seed() {
133 const SIZE: usize = 65;
134 const CELL: usize = 8;
135 let offset = (32.35, 32.8);
136 let img = synthetic_chessboard_aa(SIZE, CELL, offset, 30, 230);
137
138 let params = RadonDetectorParams {
139 image_upsample: 2,
140 ..RadonDetectorParams::default()
141 };
142 let mut buffers = RadonBuffers::new();
143 let resp = radon_response_u8(&img, SIZE, SIZE, ¶ms, &mut buffers);
144 let corners = detect_peaks_from_radon(&resp, ¶ms);
145
146 assert!(
147 !corners.is_empty(),
148 "expected at least one corner, got none"
149 );
150
151 let expected_x =
152 offset.0 + (((SIZE as f32 / 2.0 - offset.0) / CELL as f32).round() * CELL as f32);
153 let expected_y =
154 offset.1 + (((SIZE as f32 / 2.0 - offset.1) / CELL as f32).round() * CELL as f32);
155
156 let (best, err) = corners
157 .iter()
158 .map(|c| {
159 let dx = c.x - expected_x;
160 let dy = c.y - expected_y;
161 (c, (dx * dx + dy * dy).sqrt())
162 })
163 .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
164 .expect("non-empty");
165 assert!(
166 err < 0.2,
167 "closest corner err={:.4} at ({:.3}, {:.3}), expected ({:.3}, {:.3}); {} corners found",
168 err,
169 best.x,
170 best.y,
171 expected_x,
172 expected_y,
173 corners.len(),
174 );
175 }
176
177 #[test]
178 fn detector_upsample_1_still_produces_corners() {
179 const SIZE: usize = 65;
180 const CELL: usize = 8;
181 let offset = (32.35, 32.8);
182 let img = synthetic_chessboard_aa(SIZE, CELL, offset, 30, 230);
183 let params = RadonDetectorParams {
184 image_upsample: 1,
185 ray_radius: 2,
186 nms_radius: 2,
187 ..RadonDetectorParams::default()
188 };
189 let mut buffers = RadonBuffers::new();
190 let resp = radon_response_u8(&img, SIZE, SIZE, ¶ms, &mut buffers);
191 let corners = detect_peaks_from_radon(&resp, ¶ms);
192 assert!(!corners.is_empty(), "upsample=1 produced no corners");
193 }
194}