1use crate::float_helpers::lit;
2use crate::Float;
3use nalgebra::{DMatrix, Matrix3, Point2, SMatrix, SVector, Vector3};
4
5#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct Homography<F: Float = f32> {
10 pub h: Matrix3<F>,
11}
12
13impl<F: Float> Homography<F> {
14 pub fn new(h: Matrix3<F>) -> Self {
15 Self { h }
16 }
17
18 pub fn from_array(rows: [[F; 3]; 3]) -> Self {
19 Self::new(Matrix3::from_row_slice(&[
20 rows[0][0], rows[0][1], rows[0][2], rows[1][0], rows[1][1], rows[1][2], rows[2][0],
21 rows[2][1], rows[2][2],
22 ]))
23 }
24
25 pub fn to_array(&self) -> [[F; 3]; 3] {
26 [
27 [self.h[(0, 0)], self.h[(0, 1)], self.h[(0, 2)]],
28 [self.h[(1, 0)], self.h[(1, 1)], self.h[(1, 2)]],
29 [self.h[(2, 0)], self.h[(2, 1)], self.h[(2, 2)]],
30 ]
31 }
32
33 pub fn zero() -> Self {
34 Self {
35 h: Matrix3::zeros(),
36 }
37 }
38
39 #[inline]
41 pub fn apply(&self, p: Point2<F>) -> Point2<F> {
42 let v = self.h * Vector3::new(p.x, p.y, F::one());
43 let w = v[2];
44 Point2::new(v[0] / w, v[1] / w)
45 }
46
47 pub fn inverse(&self) -> Option<Self> {
49 self.h.try_inverse().map(Self::new)
50 }
51}
52
53fn hartley_normalization<F: Float>(cx: F, cy: F, mean_dist: F) -> Matrix3<F> {
56 let s = if mean_dist > lit(1e-12) {
57 lit::<F>(2.0).sqrt() / mean_dist
58 } else {
59 F::one()
60 };
61
62 Matrix3::new(
63 s,
64 F::zero(),
65 -s * cx,
66 F::zero(),
67 s,
68 -s * cy,
69 F::zero(),
70 F::zero(),
71 F::one(),
72 )
73}
74
75fn normalize_points<F: Float>(pts: &[Point2<F>]) -> (Vec<Point2<F>>, Matrix3<F>) {
76 let n: F = lit(pts.len() as f64);
77 let mut cx = F::zero();
78 let mut cy = F::zero();
79 for p in pts {
80 cx += p.x;
81 cy += p.y;
82 }
83 cx /= n;
84 cy /= n;
85
86 let mut mean_dist = F::zero();
87 for p in pts {
88 let dx = p.x - cx;
89 let dy = p.y - cy;
90 mean_dist += (dx * dx + dy * dy).sqrt();
91 }
92 mean_dist /= n;
93
94 let t = hartley_normalization(cx, cy, mean_dist);
95
96 let mut out = Vec::with_capacity(pts.len());
97 for p in pts {
98 let v = t * Vector3::new(p.x, p.y, F::one());
99 out.push(Point2::new(v[0], v[1]));
100 }
101 (out, t)
102}
103
104fn normalize_points4<F: Float>(pts: &[Point2<F>; 4]) -> ([Point2<F>; 4], Matrix3<F>) {
105 let n: F = lit(4.0);
106 let mut cx = F::zero();
107 let mut cy = F::zero();
108 for p in pts {
109 cx += p.x;
110 cy += p.y;
111 }
112 cx /= n;
113 cy /= n;
114
115 let mut mean_dist = F::zero();
116 for p in pts {
117 let dx = p.x - cx;
118 let dy = p.y - cy;
119 mean_dist += (dx * dx + dy * dy).sqrt();
120 }
121 mean_dist /= n;
122
123 let t = hartley_normalization(cx, cy, mean_dist);
124
125 let mut out = [Point2::new(F::zero(), F::zero()); 4];
126 for (i, p) in pts.iter().enumerate() {
127 let v = t * Vector3::new(p.x, p.y, F::one());
128 out[i] = Point2::new(v[0], v[1]);
129 }
130
131 (out, t)
132}
133
134fn normalize_homography<F: Float>(h: Matrix3<F>) -> Option<Matrix3<F>> {
135 let s = h[(2, 2)];
136 if s.abs() < lit(1e-12) {
137 return None;
138 }
139 Some(h / s)
140}
141
142fn denormalize_homography<F: Float>(
143 hn: Matrix3<F>,
144 t_src: Matrix3<F>,
145 t_dst: Matrix3<F>,
146) -> Option<Matrix3<F>> {
147 let t_dst_inv = t_dst.try_inverse()?;
148 Some(t_dst_inv * hn * t_src)
149}
150
151pub fn estimate_homography<F: Float>(
155 src_pts: &[Point2<F>],
156 dst_pts: &[Point2<F>],
157) -> Option<Homography<F>> {
158 if src_pts.len() != dst_pts.len() || src_pts.len() < 4 {
159 return None;
160 }
161
162 if src_pts.len() == 4 {
163 let src: &[Point2<F>; 4] = src_pts.try_into().ok()?;
164 let dst: &[Point2<F>; 4] = dst_pts.try_into().ok()?;
165 return homography_from_4pt(src, dst);
166 }
167
168 let (r, tr) = normalize_points(src_pts);
169 let (im, ti) = normalize_points(dst_pts);
170
171 let n = src_pts.len();
172 let rows = 2 * n;
173 let mut a = DMatrix::<F>::zeros(rows, 9);
174
175 for k in 0..n {
176 let x = r[k].x;
177 let y = r[k].y;
178 let u = im[k].x;
179 let v = im[k].y;
180
181 a[(2 * k, 0)] = -x;
182 a[(2 * k, 1)] = -y;
183 a[(2 * k, 2)] = -F::one();
184 a[(2 * k, 6)] = u * x;
185 a[(2 * k, 7)] = u * y;
186 a[(2 * k, 8)] = u;
187
188 a[(2 * k + 1, 3)] = -x;
189 a[(2 * k + 1, 4)] = -y;
190 a[(2 * k + 1, 5)] = -F::one();
191 a[(2 * k + 1, 6)] = v * x;
192 a[(2 * k + 1, 7)] = v * y;
193 a[(2 * k + 1, 8)] = v;
194 }
195
196 let svd = a.svd(true, true);
197 let vt = svd.v_t?;
198 let last = vt.nrows().checked_sub(1)?;
199 let h = vt.row(last);
200
201 let hn = Matrix3::<F>::from_row_slice(&[h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], h[8]]);
202
203 let h_den = denormalize_homography(hn, tr, ti)?;
204 let h_den = normalize_homography(h_den)?;
205
206 Some(Homography::new(h_den))
207}
208
209pub fn homography_from_4pt<F: Float>(
213 src: &[Point2<F>; 4],
214 dst: &[Point2<F>; 4],
215) -> Option<Homography<F>> {
216 let (src_n, t_src) = normalize_points4(src);
217 let (dst_n, t_dst) = normalize_points4(dst);
218
219 let mut a = SMatrix::<F, 8, 8>::zeros();
220 let mut b = SVector::<F, 8>::zeros();
221
222 for k in 0..4 {
223 let x = src_n[k].x;
224 let y = src_n[k].y;
225 let u = dst_n[k].x;
226 let v = dst_n[k].y;
227
228 let r0 = 2 * k;
229 a[(r0, 0)] = x;
230 a[(r0, 1)] = y;
231 a[(r0, 2)] = F::one();
232 a[(r0, 6)] = -u * x;
233 a[(r0, 7)] = -u * y;
234 b[r0] = u;
235
236 let r1 = 2 * k + 1;
237 a[(r1, 3)] = x;
238 a[(r1, 4)] = y;
239 a[(r1, 5)] = F::one();
240 a[(r1, 6)] = -v * x;
241 a[(r1, 7)] = -v * y;
242 b[r1] = v;
243 }
244
245 let x = a.lu().solve(&b)?;
246
247 let hn = Matrix3::<F>::new(
248 x[0],
249 x[1],
250 x[2], x[3],
252 x[4],
253 x[5], x[6],
255 x[7],
256 F::one(),
257 );
258
259 let h_den = denormalize_homography(hn, t_src, t_dst)?;
260 let h_den = normalize_homography(h_den)?;
261
262 Some(Homography::new(h_den))
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 fn assert_close(a: Point2<f32>, b: Point2<f32>, tol: f32) {
270 let dx = (a.x - b.x).abs();
271 let dy = (a.y - b.y).abs();
272 assert!(
273 dx < tol && dy < tol,
274 "expected ({:.6},{:.6}) ~ ({:.6},{:.6}) within {}",
275 a.x,
276 a.y,
277 b.x,
278 b.y,
279 tol
280 );
281 }
282
283 #[test]
284 fn inverse_round_trips_points() {
285 let h = Homography::new(Matrix3::new(
286 1.2, 0.1, 5.0, -0.05, 0.9, 3.0, 0.001, 0.0005, 1.0,
289 ));
290 let inv = h.inverse().expect("invertible");
291
292 for p in [
293 Point2::new(0.0_f32, 0.0),
294 Point2::new(50.0_f32, -20.0),
295 Point2::new(320.0_f32, 200.0),
296 ] {
297 let q = h.apply(p);
298 let back = inv.apply(q);
299 assert_close(back, p, 1e-3);
300 }
301 }
302
303 #[test]
304 fn four_point_specialization_recovers_h() {
305 let ground_truth = Homography::new(Matrix3::new(
306 0.8, 0.05, 120.0, -0.02, 1.1, 80.0, 0.0009, -0.0004, 1.0,
309 ));
310
311 let rect = [
312 Point2::new(0.0_f32, 0.0),
313 Point2::new(180.0_f32, 0.0),
314 Point2::new(180.0_f32, 130.0),
315 Point2::new(0.0_f32, 130.0),
316 ];
317 let dst = rect.map(|p| ground_truth.apply(p));
318
319 let recovered = homography_from_4pt(&rect, &dst).expect("recoverable");
320
321 for p in [
322 Point2::new(0.0_f32, 0.0),
323 Point2::new(60.0, 40.0),
324 Point2::new(150.0, 120.0),
325 ] {
326 assert_close(recovered.apply(p), ground_truth.apply(p), 1e-3);
327 }
328 }
329
330 #[test]
331 fn dlt_handles_overdetermined_case() {
332 let ground_truth = Homography::new(Matrix3::new(
333 1.0, 0.2, 12.0, -0.1, 0.9, 6.0, 0.0006, 0.0004, 1.0,
336 ));
337
338 let rect: Vec<Point2<f32>> = (0..3)
339 .flat_map(|y| (0..3).map(move |x| Point2::new(x as f32 * 40.0, y as f32 * 50.0)))
340 .collect();
341 let img: Vec<Point2<f32>> = rect.iter().map(|&p| ground_truth.apply(p)).collect();
342
343 let estimated = estimate_homography(&rect, &img).expect("estimate");
344 for p in [
345 Point2::new(0.0_f32, 0.0),
346 Point2::new(60.0, 40.0),
347 Point2::new(80.0, 90.0),
348 Point2::new(80.0, 100.0),
349 ] {
350 assert_close(estimated.apply(p), ground_truth.apply(p), 1e-3);
351 }
352 }
353
354 #[test]
355 fn mismatched_input_lengths_fail() {
356 let rect = [Point2::new(0.0_f32, 0.0); 4];
357 let img = [Point2::new(1.0_f32, 1.0); 3];
358 assert!(estimate_homography(&rect, &img).is_none());
359 }
360
361 #[test]
362 fn f64_round_trip() {
363 let h: Homography<f64> = Homography::new(Matrix3::new(
364 1.2, 0.1, 5.0, -0.05, 0.9, 3.0, 0.001, 0.0005, 1.0,
367 ));
368 let inv = h.inverse().expect("invertible");
369
370 for p in [
371 Point2::new(0.0_f64, 0.0),
372 Point2::new(50.0_f64, -20.0),
373 Point2::new(320.0_f64, 200.0),
374 ] {
375 let q = h.apply(p);
376 let back = inv.apply(q);
377 assert!((back.x - p.x).abs() < 1e-10);
378 assert!((back.y - p.y).abs() < 1e-10);
379 }
380 }
381
382 #[test]
383 fn f64_estimate_homography() {
384 let ground_truth: Homography<f64> = Homography::new(Matrix3::new(
385 1.0, 0.2, 12.0, -0.1, 0.9, 6.0, 0.0006, 0.0004, 1.0,
388 ));
389
390 let rect: Vec<Point2<f64>> = (0..3)
391 .flat_map(|y| (0..3).map(move |x| Point2::new(x as f64 * 40.0, y as f64 * 50.0)))
392 .collect();
393 let img: Vec<Point2<f64>> = rect.iter().map(|&p| ground_truth.apply(p)).collect();
394
395 let estimated = estimate_homography(&rect, &img).expect("estimate");
396 for p in [
397 Point2::new(0.0_f64, 0.0),
398 Point2::new(60.0, 40.0),
399 Point2::new(80.0, 90.0),
400 ] {
401 let a = estimated.apply(p);
402 let b = ground_truth.apply(p);
403 assert!((a.x - b.x).abs() < 1e-8);
404 assert!((a.y - b.y).abs() < 1e-8);
405 }
406 }
407}