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