chess_corners_core/refine/
saddle_point.rs1use super::{CornerRefiner, RefineContext, RefineResult, RefineStatus};
10use serde::{Deserialize, Serialize};
11
12#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
18#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
19#[serde(default)]
20#[non_exhaustive]
21pub struct SaddlePointConfig {
22 #[cfg_attr(feature = "schemars", schemars(range(min = 1)))]
28 #[cfg_attr(feature = "schemars", schemars(extend("x-unit" = "px")))]
29 pub radius: i32,
30 #[cfg_attr(feature = "schemars", schemars(range(min = 0.0)))]
36 pub det_margin: f32,
37 #[cfg_attr(feature = "schemars", schemars(range(min = 0.0)))]
43 #[cfg_attr(feature = "schemars", schemars(extend("x-unit" = "px")))]
44 pub max_offset: f32,
45 #[cfg_attr(feature = "schemars", schemars(range(min = 0.0)))]
51 pub min_abs_det: f32,
52}
53
54impl Default for SaddlePointConfig {
55 fn default() -> Self {
56 Self {
57 radius: 2,
58 det_margin: 1e-3,
59 max_offset: 1.5,
60 min_abs_det: 1e-4,
61 }
62 }
63}
64
65#[derive(Debug)]
75pub struct SaddlePointRefiner {
76 cfg: SaddlePointConfig,
77 m: [f32; 36],
78 rhs: [f32; 6],
79}
80
81impl SaddlePointRefiner {
82 pub fn new(cfg: SaddlePointConfig) -> Self {
84 Self {
85 cfg,
86 m: [0.0; 36],
87 rhs: [0.0; 6],
88 }
89 }
90
91 fn solve_6x6(&mut self) -> Option<[f32; 6]> {
92 for i in 0..6 {
94 let mut pivot = i;
95 let mut pivot_val = self.m[i * 6 + i].abs();
96 for r in (i + 1)..6 {
97 let v = self.m[r * 6 + i].abs();
98 if v > pivot_val {
99 pivot = r;
100 pivot_val = v;
101 }
102 }
103
104 if pivot_val < 1e-9 {
105 return None;
106 }
107
108 if pivot != i {
109 for c in i..6 {
110 self.m.swap(i * 6 + c, pivot * 6 + c);
111 }
112 self.rhs.swap(i, pivot);
113 }
114
115 let diag = self.m[i * 6 + i];
116 let inv_diag = 1.0 / diag;
117
118 for c in i..6 {
119 self.m[i * 6 + c] *= inv_diag;
120 }
121 self.rhs[i] *= inv_diag;
122
123 for r in 0..6 {
124 if r == i {
125 continue;
126 }
127 let factor = self.m[r * 6 + i];
128 if factor == 0.0 {
129 continue;
130 }
131 for c in i..6 {
132 self.m[r * 6 + c] -= factor * self.m[i * 6 + c];
133 }
134 self.rhs[r] -= factor * self.rhs[i];
135 }
136 }
137
138 let mut out = [0.0f32; 6];
139 out.copy_from_slice(&self.rhs);
140 Some(out)
141 }
142}
143
144impl CornerRefiner for SaddlePointRefiner {
145 #[inline]
146 fn radius(&self) -> i32 {
147 self.cfg.radius
148 }
149
150 fn refine(&mut self, seed_xy: [f32; 2], ctx: RefineContext<'_>) -> RefineResult {
151 let img = match ctx.image {
152 Some(view) => view,
153 None => {
154 return RefineResult {
155 x: seed_xy[0],
156 y: seed_xy[1],
157 score: 0.0,
158 status: RefineStatus::Rejected,
159 }
160 }
161 };
162
163 let cx = seed_xy[0].round() as i32;
164 let cy = seed_xy[1].round() as i32;
165 let r = self.cfg.radius;
166
167 if !img.supports_patch(cx, cy, r) {
168 return RefineResult {
169 x: seed_xy[0],
170 y: seed_xy[1],
171 score: 0.0,
172 status: RefineStatus::OutOfBounds,
173 };
174 }
175
176 let mut sum = 0.0f32;
177 let mut count = 0.0f32;
178 for dy in -r..=r {
179 let gy = cy + dy;
180 for dx in -r..=r {
181 let gx = cx + dx;
182 sum += img.sample(gx, gy);
183 count += 1.0;
184 }
185 }
186
187 let mean = if count > 0.0 { sum / count } else { 0.0 };
188
189 self.m.fill(0.0);
190 self.rhs.fill(0.0);
191
192 for dy in -r..=r {
193 let gy = cy + dy;
194 for dx in -r..=r {
195 let gx = cx + dx;
196 let i = img.sample(gx, gy) - mean;
197
198 let x = gx as f32 - seed_xy[0];
199 let y = gy as f32 - seed_xy[1];
200 let phi = [x * x, x * y, y * y, x, y, 1.0];
201
202 for row in 0..6 {
203 self.rhs[row] += phi[row] * i;
204 for col in row..6 {
205 self.m[row * 6 + col] += phi[row] * phi[col];
206 }
207 }
208 }
209 }
210
211 for row in 0..6 {
213 for col in 0..row {
214 self.m[row * 6 + col] = self.m[col * 6 + row];
215 }
216 }
217
218 let coeffs = match self.solve_6x6() {
219 Some(c) => c,
220 None => {
221 return RefineResult {
222 x: seed_xy[0],
223 y: seed_xy[1],
224 score: 0.0,
225 status: RefineStatus::IllConditioned,
226 }
227 }
228 };
229
230 let a = coeffs[0];
231 let b = coeffs[1];
232 let c = coeffs[2];
233 let d = coeffs[3];
234 let e = coeffs[4];
235
236 let det_h = 4.0 * a * c - b * b;
237 if det_h > -self.cfg.det_margin || det_h.abs() < self.cfg.min_abs_det {
238 return RefineResult {
239 x: seed_xy[0],
240 y: seed_xy[1],
241 score: det_h,
242 status: RefineStatus::IllConditioned,
243 };
244 }
245
246 let inv_det = 1.0 / det_h;
247 let ux = -(2.0 * c * d - b * e) * inv_det;
248 let uy = (b * d - 2.0 * a * e) * inv_det;
249
250 let max_off = self.cfg.max_offset.min(r as f32 + 0.5);
251 if ux.abs() > max_off || uy.abs() > max_off {
252 return RefineResult {
253 x: seed_xy[0],
254 y: seed_xy[1],
255 score: det_h,
256 status: RefineStatus::Rejected,
257 };
258 }
259
260 let score = (-det_h).sqrt();
261 RefineResult::accepted([seed_xy[0] + ux, seed_xy[1] + uy], score)
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::super::test_fixtures::synthetic_checkerboard;
268 use super::*;
269 use crate::imageview::ImageView;
270
271 #[test]
272 fn saddle_point_recovers_stationary_point_and_rejects_flat() {
273 let img = synthetic_checkerboard(17, (8.2, 8.6), 30, 230);
274 let view = ImageView::from_u8_slice(17, 17, &img).unwrap();
275 let ctx = RefineContext {
276 image: Some(view),
277 response: None,
278 };
279 let mut refiner = SaddlePointRefiner::new(SaddlePointConfig::default());
280 let res = refiner.refine([8.0, 9.0], ctx);
281 assert_eq!(res.status, RefineStatus::Accepted);
282 let true_xy = [8.2f32, 8.6f32];
283 let seed_err = ((8.0 - true_xy[0]).powi(2) + (9.0 - true_xy[1]).powi(2)).sqrt();
284 let refined_err = ((res.x - true_xy[0]).powi(2) + (res.y - true_xy[1]).powi(2)).sqrt();
285 assert!(
286 refined_err <= seed_err * 1.6 && refined_err < 1.0,
287 "refined_err {refined_err} seed_err {seed_err} res {:?}",
288 (res.x, res.y)
289 );
290
291 let flat = vec![128u8; 25];
292 let flat_view = ImageView::from_u8_slice(5, 5, &flat).unwrap();
293 let flat_ctx = RefineContext {
294 image: Some(flat_view),
295 response: None,
296 };
297 let flat_res = refiner.refine([2.0, 2.0], flat_ctx);
298 assert_ne!(flat_res.status, RefineStatus::Accepted);
299 }
300}