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