chess_corners_core/detect/radon/
primitives.rs1use serde::{Deserialize, Serialize};
9
10#[cfg(feature = "rayon")]
11use rayon::prelude::*;
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
15#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
16#[serde(rename_all = "snake_case")]
17#[non_exhaustive]
18pub enum PeakFitMode {
19 Parabolic,
21 #[default]
25 Gaussian,
26}
27
28#[inline]
37pub(crate) fn fit_peak_frac(y_minus: f32, y_c: f32, y_plus: f32, mode: PeakFitMode) -> f32 {
38 let (ym, y0, yp) = match mode {
39 PeakFitMode::Gaussian if y_minus > 0.0 && y_c > 0.0 && y_plus > 0.0 => {
40 (y_minus.ln(), y_c.ln(), y_plus.ln())
41 }
42 _ => (y_minus, y_c, y_plus),
43 };
44 let denom = ym - 2.0 * y0 + yp;
45 if denom > -1e-12 {
49 return 0.0;
50 }
51 let frac = 0.5 * (ym - yp) / denom;
52 frac.clamp(-0.5, 0.5)
53}
54
55pub(crate) fn box_blur_inplace(
64 resp: &mut [f32],
65 scratch: &mut [f32],
66 w: usize,
67 h: usize,
68 radius: usize,
69) {
70 debug_assert_eq!(resp.len(), w * h);
71 debug_assert_eq!(scratch.len(), w * h);
72 if radius == 0 {
73 return;
74 }
75 if w == 0 || h == 0 {
79 return;
80 }
81
82 let horiz_kernel = |y: usize, scratch_row: &mut [f32], resp_full: &[f32]| {
87 let row_start = y * w;
88 for (x, dst) in scratch_row.iter_mut().enumerate() {
89 let x0 = x.saturating_sub(radius);
90 let x1 = (x + radius + 1).min(w);
91 let mut acc = 0.0f32;
92 let mut n = 0.0f32;
93 for xx in x0..x1 {
94 acc += resp_full[row_start + xx];
95 n += 1.0;
96 }
97 *dst = acc / n;
98 }
99 };
100
101 #[cfg(feature = "rayon")]
102 {
103 let resp_view: &[f32] = resp;
106 scratch
107 .par_chunks_mut(w)
108 .enumerate()
109 .for_each(|(y, scratch_row)| {
110 horiz_kernel(y, scratch_row, resp_view);
111 });
112 }
113 #[cfg(not(feature = "rayon"))]
114 {
115 let resp_view: &[f32] = resp;
116 for (y, scratch_row) in scratch.chunks_mut(w).enumerate() {
117 horiz_kernel(y, scratch_row, resp_view);
118 }
119 }
120
121 let vert_kernel = |y: usize, dst: &mut [f32], scratch_full: &[f32]| {
129 let y0 = y.saturating_sub(radius);
130 let y1 = (y + radius + 1).min(h);
131 let n = (y1 - y0) as f32;
132 for v in dst.iter_mut() {
133 *v = 0.0;
134 }
135 for yy in y0..y1 {
136 let src_row = &scratch_full[yy * w..(yy + 1) * w];
137 for (d, s) in dst.iter_mut().zip(src_row.iter()) {
138 *d += *s;
139 }
140 }
141 let inv_n = 1.0 / n;
142 for v in dst.iter_mut() {
143 *v *= inv_n;
144 }
145 };
146
147 #[cfg(feature = "rayon")]
148 {
149 let scratch_view: &[f32] = scratch;
150 resp.par_chunks_mut(w)
151 .enumerate()
152 .for_each(|(y, dst)| vert_kernel(y, dst, scratch_view));
153 }
154 #[cfg(not(feature = "rayon"))]
155 {
156 let scratch_view: &[f32] = scratch;
157 for (y, dst) in resp.chunks_mut(w).enumerate() {
158 vert_kernel(y, dst, scratch_view);
159 }
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn fit_peak_frac_symmetric_parabola_is_zero() {
169 let f = fit_peak_frac(0.0, 1.0, 0.0, PeakFitMode::Parabolic);
171 assert!(f.abs() < 1e-6, "expected 0.0, got {f}");
172 }
173
174 #[test]
175 fn fit_peak_frac_shifted_parabola_recovers_offset() {
176 let y_of = |x: f32| -(x - 0.25).powi(2) + 1.0;
178 let f = fit_peak_frac(y_of(-1.0), y_of(0.0), y_of(1.0), PeakFitMode::Parabolic);
179 assert!((f - 0.25).abs() < 1e-5, "expected 0.25, got {f}");
180 }
181
182 #[test]
183 fn fit_peak_frac_gaussian_mode_handles_log() {
184 let g = |x: f32| (-((x - 0.2f32).powi(2)) / 0.5).exp();
186 let f = fit_peak_frac(g(-1.0), g(0.0), g(1.0), PeakFitMode::Gaussian);
187 assert!((f - 0.2).abs() < 1e-5, "Gaussian-log fit off: {f}");
188 }
189
190 #[test]
191 fn fit_peak_frac_rejects_non_maximum() {
192 let f = fit_peak_frac(1.0, 0.5, 1.0, PeakFitMode::Parabolic);
194 assert_eq!(f, 0.0);
195 }
196
197 #[test]
198 fn fit_peak_frac_gaussian_falls_back_on_nonpositive() {
199 let parab = fit_peak_frac(-0.5, 2.0, 1.5, PeakFitMode::Parabolic);
201 let gauss = fit_peak_frac(-0.5, 2.0, 1.5, PeakFitMode::Gaussian);
202 assert_eq!(parab, gauss);
203 }
204
205 #[test]
206 fn box_blur_zero_radius_is_identity() {
207 let side = 5usize;
208 let mut resp: Vec<f32> = (0..(side * side)).map(|i| i as f32).collect();
209 let before = resp.clone();
210 let mut scratch = vec![0.0; side * side];
211 box_blur_inplace(&mut resp, &mut scratch, side, side, 0);
212 assert_eq!(resp, before);
213 }
214
215 #[test]
216 fn box_blur_smooths_impulse() {
217 let side = 5usize;
218 let mut resp = vec![0.0f32; side * side];
219 let mut scratch = vec![0.0f32; side * side];
220 let mid = side / 2;
221 resp[mid * side + mid] = 9.0;
222 box_blur_inplace(&mut resp, &mut scratch, side, side, 1);
223 assert!(
225 (resp[mid * side + mid] - 1.0).abs() < 1e-6,
226 "center after blur = {}",
227 resp[mid * side + mid]
228 );
229 }
230
231 #[test]
232 fn box_blur_preserves_constant_field() {
233 let side = 7usize;
234 let mut resp = vec![3.5f32; side * side];
235 let mut scratch = vec![0.0f32; side * side];
236 box_blur_inplace(&mut resp, &mut scratch, side, side, 1);
237 for v in &resp {
238 assert!((v - 3.5).abs() < 1e-6);
239 }
240 }
241
242 #[test]
243 fn box_blur_handles_rectangular_grid() {
244 let w = 8usize;
248 let h = 5usize;
249 let mut resp = vec![2.0f32; w * h];
250 let mut scratch = vec![0.0f32; w * h];
251 box_blur_inplace(&mut resp, &mut scratch, w, h, 1);
252 for v in &resp {
253 assert!((v - 2.0).abs() < 1e-6);
254 }
255 }
256}