Skip to main content

scan_core/transform/
perspective.rs

1use crate::Point;
2
3/// Port of perspective-transform (js)
4/// Solves for coefficients mapping src points to dst points.
5pub struct PerspectiveTransform {
6    coeffs: [f32; 9],
7    coeffs_inv: [f32; 9],
8}
9
10impl PerspectiveTransform {
11    pub fn new(src: &[Point], dst: &[Point]) -> Option<Self> {
12        if src.len() != 4 || dst.len() != 4 {
13            return None;
14        }
15        
16        let coeffs = get_normalization_coefficients(src, dst, false)?;
17        let coeffs_inv = get_normalization_coefficients(src, dst, true)?;
18        
19        Some(Self {
20            coeffs,
21            coeffs_inv,
22        })
23    }
24    
25    pub fn transform(&self, x: f32, y: f32) -> Point {
26        let c = self.coeffs;
27        let den = c[6] * x + c[7] * y + 1.0;
28        Point {
29            x: (c[0] * x + c[1] * y + c[2]) / den,
30            y: (c[3] * x + c[4] * y + c[5]) / den,
31        }
32    }
33    
34    pub fn transform_inverse(&self, x: f32, y: f32) -> Point {
35        let c = self.coeffs_inv;
36        let den = c[6] * x + c[7] * y + 1.0;
37        Point {
38            x: (c[0] * x + c[1] * y + c[2]) / den,
39            y: (c[3] * x + c[4] * y + c[5]) / den,
40        }
41    }
42    
43    // transform() not stricly used for warping (we need inverse for that)
44}
45
46fn get_normalization_coefficients(src: &[Point], dst: &[Point], is_inverse: bool) -> Option<[f32; 9]> {
47    let (src, dst) = if is_inverse { (dst, src) } else { (src, dst) };
48    
49    // Solving 8 equations for 8 unknowns.
50    // Since we don't want a heavy linear algebra crate, we can use Gaussian elimination specific for 8x8.
51    // Or check if we can reuse the `square2quad` logic if mapping to/from unit square.
52    // The general perspectiveTransform.js uses numeric.inv (matrix inversion).
53    // Writing an 8x8 solver manually is verbose.
54    
55    // Alternative: OpenCV `getPerspectiveTransform` solves linear system.
56    // For 4 points `src` -> `dst`, we want H such that dst ~ H * src.
57    
58    // Implementing Gaussian elimination for 8 variables.
59    let mut matrix = [[0.0f32; 9]; 8];
60    
61    // r1: x0, y0, 1, 0, 0, 0, -X0*x0, -X0*y0, X0
62    // r2: 0, 0, 0, x0, y0, 1, -Y0*x0, -Y0*y0, Y0
63    // ...
64    // Where src=(x,y), dst=(X,Y)
65    
66    for i in 0..4 {
67        let s = src[i];
68        let d = dst[i];
69        // Row 2*i
70        matrix[2 * i][0] = s.x;
71        matrix[2 * i][1] = s.y;
72        matrix[2 * i][2] = 1.0;
73        matrix[2 * i][6] = -d.x * s.x;
74        matrix[2 * i][7] = -d.x * s.y;
75        matrix[2 * i][8] = d.x;
76        
77        // Row 2*i + 1
78        matrix[2 * i + 1][3] = s.x;
79        matrix[2 * i + 1][4] = s.y;
80        matrix[2 * i + 1][5] = 1.0;
81        matrix[2 * i + 1][6] = -d.y * s.x;
82        matrix[2 * i + 1][7] = -d.y * s.y;
83        matrix[2 * i + 1][8] = d.y;
84    }
85    
86    // Gaussian elimination
87    let n = 8;
88    for i in 0..n {
89        // Pivot
90        let mut pivot_row = i;
91        for j in i + 1..n {
92            if matrix[j][i].abs() > matrix[pivot_row][i].abs() {
93                pivot_row = j;
94            }
95        }
96        
97        matrix.swap(i, pivot_row);
98        
99        let pivot = matrix[i][i];
100        if pivot.abs() < 1e-6 {
101            return None; // Singular
102        }
103        
104        // Normalize pivot row
105        for j in i..=n {
106            matrix[i][j] /= pivot;
107        }
108        
109        // Eliminate others
110        for k in 0..n {
111            if k != i {
112                let factor = matrix[k][i];
113                for j in i..=n {
114                    matrix[k][j] -= factor * matrix[i][j];
115                }
116            }
117        }
118    }
119    
120    let mut res = [0.0; 9];
121    for i in 0..8 {
122        res[i] = matrix[i][8];
123    }
124    res[8] = 1.0;
125    
126    Some(res)
127}