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