chess_corners_core/detect/radon/
primitives.rs1use serde::{Deserialize, Serialize};
12
13#[cfg(feature = "rayon")]
14use rayon::prelude::*;
15
16pub const ANGLES: usize = 4;
19
20pub const DIR_COS: [f32; ANGLES] = [
22 1.0,
23 core::f32::consts::FRAC_1_SQRT_2,
24 0.0,
25 -core::f32::consts::FRAC_1_SQRT_2,
26];
27
28pub const DIR_SIN: [f32; ANGLES] = [
30 0.0,
31 core::f32::consts::FRAC_1_SQRT_2,
32 1.0,
33 core::f32::consts::FRAC_1_SQRT_2,
34];
35
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39#[non_exhaustive]
40pub enum PeakFitMode {
41 Parabolic,
43 #[default]
46 Gaussian,
47}
48
49#[inline]
58pub fn fit_peak_frac(y_minus: f32, y_c: f32, y_plus: f32, mode: PeakFitMode) -> f32 {
59 let (ym, y0, yp) = match mode {
60 PeakFitMode::Gaussian if y_minus > 0.0 && y_c > 0.0 && y_plus > 0.0 => {
61 (y_minus.ln(), y_c.ln(), y_plus.ln())
62 }
63 _ => (y_minus, y_c, y_plus),
64 };
65 let denom = ym - 2.0 * y0 + yp;
66 if denom > -1e-12 {
70 return 0.0;
71 }
72 let frac = 0.5 * (ym - yp) / denom;
73 frac.clamp(-0.5, 0.5)
74}
75
76pub fn box_blur_inplace(resp: &mut [f32], scratch: &mut [f32], w: usize, h: usize, radius: usize) {
85 debug_assert_eq!(resp.len(), w * h);
86 debug_assert_eq!(scratch.len(), w * h);
87 if radius == 0 {
88 return;
89 }
90 if w == 0 || h == 0 {
94 return;
95 }
96
97 let horiz_kernel = |y: usize, scratch_row: &mut [f32], resp_full: &[f32]| {
102 let row_start = y * w;
103 for (x, dst) in scratch_row.iter_mut().enumerate() {
104 let x0 = x.saturating_sub(radius);
105 let x1 = (x + radius + 1).min(w);
106 let mut acc = 0.0f32;
107 let mut n = 0.0f32;
108 for xx in x0..x1 {
109 acc += resp_full[row_start + xx];
110 n += 1.0;
111 }
112 *dst = acc / n;
113 }
114 };
115
116 #[cfg(feature = "rayon")]
117 {
118 let resp_view: &[f32] = resp;
121 scratch
122 .par_chunks_mut(w)
123 .enumerate()
124 .for_each(|(y, scratch_row)| {
125 horiz_kernel(y, scratch_row, resp_view);
126 });
127 }
128 #[cfg(not(feature = "rayon"))]
129 {
130 let resp_view: &[f32] = resp;
131 for (y, scratch_row) in scratch.chunks_mut(w).enumerate() {
132 horiz_kernel(y, scratch_row, resp_view);
133 }
134 }
135
136 let vert_kernel = |y: usize, dst: &mut [f32], scratch_full: &[f32]| {
144 let y0 = y.saturating_sub(radius);
145 let y1 = (y + radius + 1).min(h);
146 let n = (y1 - y0) as f32;
147 for v in dst.iter_mut() {
148 *v = 0.0;
149 }
150 for yy in y0..y1 {
151 let src_row = &scratch_full[yy * w..(yy + 1) * w];
152 for (d, s) in dst.iter_mut().zip(src_row.iter()) {
153 *d += *s;
154 }
155 }
156 let inv_n = 1.0 / n;
157 for v in dst.iter_mut() {
158 *v *= inv_n;
159 }
160 };
161
162 #[cfg(feature = "rayon")]
163 {
164 let scratch_view: &[f32] = scratch;
165 resp.par_chunks_mut(w)
166 .enumerate()
167 .for_each(|(y, dst)| vert_kernel(y, dst, scratch_view));
168 }
169 #[cfg(not(feature = "rayon"))]
170 {
171 let scratch_view: &[f32] = scratch;
172 for (y, dst) in resp.chunks_mut(w).enumerate() {
173 vert_kernel(y, dst, scratch_view);
174 }
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn fit_peak_frac_symmetric_parabola_is_zero() {
184 let f = fit_peak_frac(0.0, 1.0, 0.0, PeakFitMode::Parabolic);
186 assert!(f.abs() < 1e-6, "expected 0.0, got {f}");
187 }
188
189 #[test]
190 fn fit_peak_frac_shifted_parabola_recovers_offset() {
191 let y_of = |x: f32| -(x - 0.25).powi(2) + 1.0;
193 let f = fit_peak_frac(y_of(-1.0), y_of(0.0), y_of(1.0), PeakFitMode::Parabolic);
194 assert!((f - 0.25).abs() < 1e-5, "expected 0.25, got {f}");
195 }
196
197 #[test]
198 fn fit_peak_frac_gaussian_mode_handles_log() {
199 let g = |x: f32| (-((x - 0.2f32).powi(2)) / 0.5).exp();
201 let f = fit_peak_frac(g(-1.0), g(0.0), g(1.0), PeakFitMode::Gaussian);
202 assert!((f - 0.2).abs() < 1e-5, "Gaussian-log fit off: {f}");
203 }
204
205 #[test]
206 fn fit_peak_frac_rejects_non_maximum() {
207 let f = fit_peak_frac(1.0, 0.5, 1.0, PeakFitMode::Parabolic);
209 assert_eq!(f, 0.0);
210 }
211
212 #[test]
213 fn fit_peak_frac_gaussian_falls_back_on_nonpositive() {
214 let parab = fit_peak_frac(-0.5, 2.0, 1.5, PeakFitMode::Parabolic);
216 let gauss = fit_peak_frac(-0.5, 2.0, 1.5, PeakFitMode::Gaussian);
217 assert_eq!(parab, gauss);
218 }
219
220 #[test]
221 fn box_blur_zero_radius_is_identity() {
222 let side = 5usize;
223 let mut resp: Vec<f32> = (0..(side * side)).map(|i| i as f32).collect();
224 let before = resp.clone();
225 let mut scratch = vec![0.0; side * side];
226 box_blur_inplace(&mut resp, &mut scratch, side, side, 0);
227 assert_eq!(resp, before);
228 }
229
230 #[test]
231 fn box_blur_smooths_impulse() {
232 let side = 5usize;
233 let mut resp = vec![0.0f32; side * side];
234 let mut scratch = vec![0.0f32; side * side];
235 let mid = side / 2;
236 resp[mid * side + mid] = 9.0;
237 box_blur_inplace(&mut resp, &mut scratch, side, side, 1);
238 assert!(
240 (resp[mid * side + mid] - 1.0).abs() < 1e-6,
241 "center after blur = {}",
242 resp[mid * side + mid]
243 );
244 }
245
246 #[test]
247 fn box_blur_preserves_constant_field() {
248 let side = 7usize;
249 let mut resp = vec![3.5f32; side * side];
250 let mut scratch = vec![0.0f32; side * side];
251 box_blur_inplace(&mut resp, &mut scratch, side, side, 1);
252 for v in &resp {
253 assert!((v - 3.5).abs() < 1e-6);
254 }
255 }
256
257 #[test]
258 fn box_blur_handles_rectangular_grid() {
259 let w = 8usize;
263 let h = 5usize;
264 let mut resp = vec![2.0f32; w * h];
265 let mut scratch = vec![0.0f32; w * h];
266 box_blur_inplace(&mut resp, &mut scratch, w, h, 1);
267 for v in &resp {
268 assert!((v - 2.0).abs() < 1e-6);
269 }
270 }
271}