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