chess_corners_core/refine/
forstner.rs1use super::{CornerRefiner, RefineContext, RefineResult, RefineStatus};
14use serde::{Deserialize, Serialize};
15
16#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
18#[serde(default)]
19pub struct ForstnerConfig {
20 pub radius: i32,
25 pub min_trace: f32,
31 pub min_det: f32,
37 pub max_condition_number: f32,
43 pub max_offset: f32,
49}
50
51impl Default for ForstnerConfig {
52 fn default() -> Self {
53 Self {
54 radius: 2,
55 min_trace: 25.0,
56 min_det: 1e-3,
57 max_condition_number: 50.0,
58 max_offset: 1.5,
59 }
60 }
61}
62
63#[derive(Debug)]
64pub struct ForstnerRefiner {
65 cfg: ForstnerConfig,
66}
67
68impl ForstnerRefiner {
69 pub fn new(cfg: ForstnerConfig) -> Self {
70 Self { cfg }
71 }
72}
73
74impl CornerRefiner for ForstnerRefiner {
75 #[inline]
76 fn radius(&self) -> i32 {
77 self.cfg.radius + 1
79 }
80
81 fn refine(&mut self, seed_xy: [f32; 2], ctx: RefineContext<'_>) -> RefineResult {
82 let img = match ctx.image {
83 Some(view) => view,
84 None => {
85 return RefineResult {
86 x: seed_xy[0],
87 y: seed_xy[1],
88 score: 0.0,
89 status: RefineStatus::Rejected,
90 }
91 }
92 };
93
94 let cx = seed_xy[0].round() as i32;
95 let cy = seed_xy[1].round() as i32;
96 let patch_r = self.cfg.radius;
97
98 if !img.supports_patch(cx, cy, patch_r + 1) {
99 return RefineResult {
100 x: seed_xy[0],
101 y: seed_xy[1],
102 score: 0.0,
103 status: RefineStatus::OutOfBounds,
104 };
105 }
106
107 let mut a00 = 0.0;
108 let mut a01 = 0.0;
109 let mut a11 = 0.0;
110 let mut bx = 0.0;
111 let mut by = 0.0;
112
113 for dy in -patch_r..=patch_r {
114 let gy = cy + dy;
115 for dx in -patch_r..=patch_r {
116 let gx = cx + dx;
117
118 let ix_plus = img.sample(gx + 1, gy);
119 let ix_minus = img.sample(gx - 1, gy);
120 let iy_plus = img.sample(gx, gy + 1);
121 let iy_minus = img.sample(gx, gy - 1);
122
123 let gx_f = 0.5 * (ix_plus - ix_minus);
124 let gy_f = 0.5 * (iy_plus - iy_minus);
125
126 let px = gx as f32 - seed_xy[0];
127 let py = gy as f32 - seed_xy[1];
128 let gxgx = gx_f * gx_f;
129 let gxgy = gx_f * gy_f;
130 let gygy = gy_f * gy_f;
131 let dist2 = px * px + py * py;
132 let w = 1.0 / (1.0 + 0.5 * dist2);
133
134 a00 += w * gxgx;
135 a01 += w * gxgy;
136 a11 += w * gygy;
137
138 bx += w * (gxgx * px + gxgy * py);
140 by += w * (gxgy * px + gygy * py);
141 }
142 }
143
144 let trace = a00 + a11;
145 let det = a00 * a11 - a01 * a01;
146 if trace < self.cfg.min_trace || det <= self.cfg.min_det {
147 return RefineResult {
148 x: seed_xy[0],
149 y: seed_xy[1],
150 score: det,
151 status: RefineStatus::IllConditioned,
152 };
153 }
154
155 let discr = (trace * trace - 4.0 * det).max(0.0).sqrt();
156 let lambda_min = 0.5 * (trace - discr);
157 let lambda_max = 0.5 * (trace + discr);
158
159 if lambda_min <= 0.0 {
160 return RefineResult {
161 x: seed_xy[0],
162 y: seed_xy[1],
163 score: det,
164 status: RefineStatus::IllConditioned,
165 };
166 }
167
168 let cond = lambda_max / lambda_min;
169 if !cond.is_finite() || cond > self.cfg.max_condition_number {
170 return RefineResult {
171 x: seed_xy[0],
172 y: seed_xy[1],
173 score: det,
174 status: RefineStatus::IllConditioned,
175 };
176 }
177
178 let inv_det = 1.0 / det;
179 let ux = (a11 * bx - a01 * by) * inv_det;
180 let uy = (-a01 * bx + a00 * by) * inv_det;
181
182 let max_off = self.cfg.max_offset.min(self.cfg.radius as f32 + 0.5);
183 if ux.abs() > max_off || uy.abs() > max_off {
184 return RefineResult {
185 x: seed_xy[0],
186 y: seed_xy[1],
187 score: det,
188 status: RefineStatus::Rejected,
189 };
190 }
191
192 let score = det / (trace * trace + 1e-6);
193 RefineResult::accepted([seed_xy[0] + ux, seed_xy[1] + uy], score)
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::super::test_fixtures::synthetic_checkerboard;
200 use super::*;
201 use crate::imageview::ImageView;
202
203 #[test]
204 fn forstner_refines_toward_true_offset() {
205 let img = synthetic_checkerboard(15, (7.35, 7.8), 40, 220);
206 let view = ImageView::from_u8_slice(15, 15, &img).unwrap();
207 let ctx = RefineContext {
208 image: Some(view),
209 response: None,
210 };
211 let mut refiner = ForstnerRefiner::new(ForstnerConfig::default());
212 let res = refiner.refine([7.0, 8.0], ctx);
213 assert_eq!(res.status, RefineStatus::Accepted);
214 let true_xy = [7.35f32, 7.8f32];
215 let seed_err = ((7.0 - true_xy[0]).powi(2) + (8.0 - true_xy[1]).powi(2)).sqrt();
216 let refined_err = ((res.x - true_xy[0]).powi(2) + (res.y - true_xy[1]).powi(2)).sqrt();
217 assert!(
218 refined_err <= seed_err * 1.6 && refined_err < 1.0,
219 "refined_err {refined_err} seed_err {seed_err} res {:?}",
220 (res.x, res.y)
221 );
222 }
223}