1use crate::image::ImageView;
4use nalgebra::{Matrix2, Matrix3, SMatrix, Vector2, Vector3};
5
6#[derive(Clone, Copy, Debug, Default)]
8pub struct MomentAccumulator {
9 pub sum_w: f64,
11 pub sum_wx: f64,
13 pub sum_wy: f64,
15 pub sum_wxx: f64,
17 pub sum_wyy: f64,
19 pub sum_wxy: f64,
21}
22
23impl MomentAccumulator {
24 #[must_use]
26 pub fn new() -> Self {
27 Self::default()
28 }
29
30 pub fn add(&mut self, x: f64, y: f64, w: f64) {
32 self.sum_w += w;
33 self.sum_wx += w * x;
34 self.sum_wy += w * y;
35 self.sum_wxx += w * x * x;
36 self.sum_wyy += w * y * y;
37 self.sum_wxy += w * x * y;
38 }
39
40 #[must_use]
42 pub fn centroid(&self) -> Option<Vector2<f64>> {
43 if self.sum_w < 1e-9 {
44 None
45 } else {
46 Some(Vector2::new(
47 self.sum_wx / self.sum_w,
48 self.sum_wy / self.sum_w,
49 ))
50 }
51 }
52
53 #[must_use]
55 #[allow(clippy::similar_names)]
56 pub fn covariance(&self) -> Option<Matrix2<f64>> {
57 let c = self.centroid()?;
58 let s_w = self.sum_w;
59
60 let s_xx = (self.sum_wxx / s_w) - (c.x * c.x);
61 let s_yy = (self.sum_wyy / s_w) - (c.y * c.y);
62 let s_xy = (self.sum_wxy / s_w) - (c.x * c.y);
63
64 Some(Matrix2::new(s_xx, s_xy, s_xy, s_yy))
65 }
66}
67
68#[derive(Clone, Copy, Debug)]
70pub struct HomogeneousLine {
71 pub l: Vector3<f64>,
73 pub cov: Matrix3<f64>,
75}
76
77impl HomogeneousLine {
78 #[must_use]
81 pub fn intersect(&self, other: &Self) -> Option<(Vector2<f64>, Matrix2<f64>)> {
82 let c_hom = self.l.cross(&other.l);
84 let w = c_hom.z;
85 if w.abs() < 1e-9 {
86 return None; }
88
89 let l1_x = self.l.cross_matrix();
92 let l2_x = other.l.cross_matrix();
93 let cov_ch = l2_x * self.cov * l2_x.transpose() + l1_x * other.cov * l1_x.transpose();
94
95 let x = c_hom.x / w;
98 let y = c_hom.y / w;
99 let corner = Vector2::new(x, y);
100
101 let mut j_pi = SMatrix::<f64, 2, 3>::zeros();
103 let w_inv = 1.0 / w;
104 j_pi[(0, 0)] = w_inv;
105 j_pi[(0, 2)] = -x * w_inv;
106 j_pi[(1, 1)] = w_inv;
107 j_pi[(1, 2)] = -y * w_inv;
108
109 let cov_c = j_pi * cov_ch * j_pi.transpose();
110
111 Some((corner, cov_c))
112 }
113}
114
115pub struct EigenResult {
117 pub l_max: f64,
119 pub l_min: f64,
121 pub v_max: Vector2<f64>,
123 pub v_min: Vector2<f64>,
125}
126
127#[must_use]
129#[allow(clippy::manual_midpoint)]
130pub fn solve_2x2_symmetric(a: f64, b: f64, c: f64) -> EigenResult {
131 let trace = a + c;
132 let det = a * c - b * b;
133
134 let disc = (trace * trace - 4.0 * det).max(0.0).sqrt();
135 let l_max = (trace + disc) / 2.0;
136 let l_min = (trace - disc) / 2.0;
137
138 let v_min = if b.abs() > 1e-9 {
140 let vx = b;
141 let vy = l_min - a;
142 Vector2::new(vx, vy).normalize()
143 } else if a < c {
144 Vector2::new(1.0, 0.0)
145 } else {
146 Vector2::new(0.0, 1.0)
147 };
148
149 let v_max = Vector2::new(-v_min.y, v_min.x);
151
152 EigenResult {
153 l_max,
154 l_min,
155 v_max,
156 v_min,
157 }
158}
159
160#[must_use]
164#[allow(clippy::similar_names)]
165#[allow(clippy::cast_sign_loss)]
166#[allow(clippy::cast_possible_wrap)]
167#[allow(clippy::type_complexity)]
168pub fn refine_quad_gwlf_with_cov(
169 img: &ImageView,
170 coarse_corners: &[[f32; 2]; 4],
171 alpha: f64,
172) -> Option<([[f32; 2]; 4], [Matrix2<f64>; 4])> {
173 let mut lines = [HomogeneousLine {
174 l: Vector3::zeros(),
175 cov: Matrix3::zeros(),
176 }; 4];
177
178 for i in 0..4 {
179 let p0 = coarse_corners[i];
180 let p1 = coarse_corners[(i + 1) % 4];
181
182 let dx_edge = f64::from(p1[0] - p0[0]);
183 let dy_edge = f64::from(p1[1] - p0[1]);
184 let len = (dx_edge * dx_edge + dy_edge * dy_edge).sqrt();
185 if len < 2.0 {
186 return None;
187 }
188
189 let ux = dx_edge / len;
190 let uy = dy_edge / len;
191 let nx_coarse = -uy;
192 let ny_coarse = ux;
193
194 let mut acc = MomentAccumulator::new();
195 let steps = (len * 2.0) as usize;
196
197 let window_half_width = (alpha * len).max(2.0);
200 let k_min = -window_half_width.round() as i32;
201 let k_max = window_half_width.round() as i32;
202
203 for s in 0..=steps {
204 let t = (s as f64) / (steps as f64);
205 let px = f64::from(p0[0]) + t * dx_edge;
206 let py = f64::from(p0[1]) + t * dy_edge;
207
208 for k in k_min..=k_max {
209 let sx = px + f64::from(k) * nx_coarse;
210 let sy = py + f64::from(k) * ny_coarse;
211
212 if sx < 1.0
213 || sx >= (img.width - 2) as f64
214 || sy < 1.0
215 || sy >= (img.height - 2) as f64
216 {
217 continue;
218 }
219
220 let g = img.sample_gradient_bilinear(sx, sy);
221 let w = g[0] * g[0] + g[1] * g[1];
222
223 if w > 100.0 {
225 acc.add(sx, sy, w);
226 }
227 }
228 }
229
230 let cov_spatial = acc.covariance()?;
231 let res = solve_2x2_symmetric(
232 cov_spatial[(0, 0)],
233 cov_spatial[(0, 1)],
234 cov_spatial[(1, 1)],
235 );
236 let n = res.v_min;
237 let x_bar = acc.centroid()?;
238 let w_total = acc.sum_w;
239
240 let sigma_xbar = Matrix2::identity().scale(res.l_min / w_total);
243 let n_perp = res.v_max;
245 let sigma_n = (n_perp * n_perp.transpose())
246 .scale(res.l_min / (w_total * (res.l_max - res.l_min).max(1e-6)));
247
248 let mut j_n = SMatrix::<f64, 3, 2>::zeros();
251 j_n.fixed_view_mut::<2, 2>(0, 0)
252 .copy_from(&Matrix2::identity());
253 j_n[(2, 0)] = -x_bar.x;
254 j_n[(2, 1)] = -x_bar.y;
255
256 let mut j_xbar = SMatrix::<f64, 3, 2>::zeros();
257 j_xbar[(2, 0)] = -n.x;
258 j_xbar[(2, 1)] = -n.y;
259
260 let cov_l = j_n * sigma_n * j_n.transpose() + j_xbar * sigma_xbar * j_xbar.transpose();
261
262 lines[i] = HomogeneousLine {
263 l: Vector3::new(n.x, n.y, -x_bar.dot(&n)),
264 cov: cov_l,
265 };
266 }
267
268 let mut refined_corners = [[0.0f32; 2]; 4];
269 let mut refined_covs = [Matrix2::zeros(); 4];
270
271 for i in 0..4 {
272 let l_prev = lines[(i + 3) % 4];
273 let l_curr = lines[i];
274
275 let (corner, cov) = l_prev.intersect(&l_curr)?;
276
277 let dist_sq = (corner.x - f64::from(coarse_corners[i][0])).powi(2)
279 + (corner.y - f64::from(coarse_corners[i][1])).powi(2);
280 if dist_sq > 25.0 {
281 return None;
283 }
284
285 refined_corners[i] = [corner.x as f32, corner.y as f32];
286 refined_covs[i] = cov;
287 }
288
289 Some((refined_corners, refined_covs))
290}
291
292#[must_use]
294pub fn refine_quad_gwlf(
295 img: &ImageView,
296 coarse_corners: &[[f32; 2]; 4],
297 alpha: f64,
298) -> Option<[[f32; 2]; 4]> {
299 refine_quad_gwlf_with_cov(img, coarse_corners, alpha).map(|(c, _)| c)
300}