Skip to main content

scirs2_vision/registration/
mod.rs

1//! Image registration algorithms
2//!
3//! This module provides various image registration techniques for aligning images
4//! based on features, intensity, or geometric constraints.
5
6pub mod affine;
7pub mod feature_based;
8pub mod homography;
9pub mod intensity;
10pub mod metrics;
11pub mod non_rigid;
12pub mod optimization;
13pub mod rigid;
14pub mod warping;
15
16pub use affine::*;
17pub use feature_based::*;
18pub use homography::*;
19pub use intensity::*;
20pub use metrics::*;
21pub use non_rigid::*;
22pub use optimization::*;
23pub use rigid::*;
24pub use warping::*;
25
26use crate::error::{Result, VisionError};
27use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
28// use scirs2_linalg::{lstsq, solve};
29use scirs2_core::random;
30use scirs2_core::random::seq::SliceRandom;
31use std::fmt::Debug;
32
33/// Simple least squares solver result
34#[derive(Debug)]
35pub struct LstsqResult {
36    /// Solution vector
37    pub x: Array1<f64>,
38}
39
40/// Simple least squares solver (A * x = b)
41/// Returns the solution x that minimizes ||A * x - b||^2
42#[allow(dead_code)]
43fn lstsq(
44    a: &ArrayView2<f64>,
45    b: &ArrayView1<f64>,
46    _rcond: Option<f64>,
47) -> std::result::Result<LstsqResult, String> {
48    let (m, n) = a.dim();
49
50    if m != b.len() {
51        return Err("Matrix dimensions don't match".to_string());
52    }
53
54    // For overdetermined systems (m >= n), use normal equations: A^T * A * x = A^T * b
55    if m >= n {
56        // Compute A^T
57        let at = a.t();
58
59        // Compute A^T * A
60        let ata = at.dot(a);
61
62        // Compute A^T * b
63        let atb = at.dot(b);
64
65        // Solve the system using simple Gaussian elimination
66        let x = solve_linear_system(&ata.view(), &atb.view())?;
67
68        Ok(LstsqResult { x })
69    } else {
70        // Underdetermined system - use minimum norm solution
71        // For now, just return a zero solution
72        Ok(LstsqResult {
73            x: Array1::zeros(n),
74        })
75    }
76}
77
78/// Simple linear system solver using Gaussian elimination
79#[allow(dead_code)]
80fn solve_linear_system(
81    a: &ArrayView2<f64>,
82    b: &ArrayView1<f64>,
83) -> std::result::Result<Array1<f64>, String> {
84    let n = a.nrows();
85    if a.ncols() != n || b.len() != n {
86        return Err("Matrix must be square and match vector dimension".to_string());
87    }
88
89    // Create augmented matrix [A | b]
90    let mut aug = Array2::zeros((n, n + 1));
91    for i in 0..n {
92        for j in 0..n {
93            aug[[i, j]] = a[[i, j]];
94        }
95        aug[[i, n]] = b[i];
96    }
97
98    // Forward elimination
99    for i in 0..n {
100        // Find pivot
101        let mut max_row = i;
102        for k in (i + 1)..n {
103            if aug[[k, i]].abs() > aug[[max_row, i]].abs() {
104                max_row = k;
105            }
106        }
107
108        // Swap rows
109        if max_row != i {
110            for j in 0..=n {
111                let tmp = aug[[i, j]];
112                aug[[i, j]] = aug[[max_row, j]];
113                aug[[max_row, j]] = tmp;
114            }
115        }
116
117        // Check for singular matrix
118        if aug[[i, i]].abs() < 1e-14 {
119            return Err("Matrix is singular".to_string());
120        }
121
122        // Eliminate column
123        for k in (i + 1)..n {
124            let factor = aug[[k, i]] / aug[[i, i]];
125            for j in i..=n {
126                aug[[k, j]] -= factor * aug[[i, j]];
127            }
128        }
129    }
130
131    // Back substitution
132    let mut x = Array1::zeros(n);
133    for i in (0..n).rev() {
134        x[i] = aug[[i, n]];
135        for j in (i + 1)..n {
136            x[i] -= aug[[i, j]] * x[j];
137        }
138        x[i] /= aug[[i, i]];
139    }
140
141    Ok(x)
142}
143
144/// Simple wrapper around solve_linear_system for compatibility
145#[allow(dead_code)]
146fn solve(
147    a: &ArrayView2<f64>,
148    b: &ArrayView1<f64>,
149    _rcond: Option<f64>,
150) -> std::result::Result<Array1<f64>, String> {
151    solve_linear_system(a, b)
152}
153
154/// 2D transformation matrix (3x3 homogeneous coordinates)
155pub type TransformMatrix = Array2<f64>;
156
157/// Point in 2D space
158#[derive(Debug, Clone, Copy, PartialEq)]
159pub struct Point2D {
160    /// X coordinate
161    pub x: f64,
162    /// Y coordinate
163    pub y: f64,
164}
165
166impl Point2D {
167    /// Create a new 2D point
168    pub fn new(x: f64, y: f64) -> Self {
169        Self { x, y }
170    }
171}
172
173/// Match between two points
174#[derive(Debug, Clone)]
175pub struct PointMatch {
176    /// Source point
177    pub source: Point2D,
178    /// Target point
179    pub target: Point2D,
180    /// Match confidence score
181    pub confidence: f64,
182}
183
184/// Registration parameters
185#[derive(Debug, Clone)]
186pub struct RegistrationParams {
187    /// Maximum number of iterations
188    pub max_iterations: usize,
189    /// Convergence tolerance
190    pub tolerance: f64,
191    /// Use multi-resolution pyramid
192    pub use_pyramid: bool,
193    /// Number of pyramid levels
194    pub pyramid_levels: usize,
195    /// RANSAC parameters
196    pub ransac_threshold: f64,
197    /// Number of RANSAC iterations
198    pub ransac_iterations: usize,
199    /// RANSAC confidence level
200    pub ransac_confidence: f64,
201}
202
203impl Default for RegistrationParams {
204    fn default() -> Self {
205        Self {
206            max_iterations: 100,
207            tolerance: 1e-6,
208            use_pyramid: true,
209            pyramid_levels: 3,
210            ransac_threshold: 3.0,
211            ransac_iterations: 1000,
212            ransac_confidence: 0.99,
213        }
214    }
215}
216
217/// Registration result
218#[derive(Debug, Clone)]
219pub struct RegistrationResult {
220    /// Final transformation matrix
221    pub transform: TransformMatrix,
222    /// Final cost/error value
223    pub final_cost: f64,
224    /// Number of iterations performed
225    pub iterations: usize,
226    /// Whether convergence was achieved
227    pub converged: bool,
228    /// Inlier matches (for RANSAC-based methods)
229    pub inliers: Vec<usize>,
230}
231
232/// Type of transformation
233#[derive(Debug, Clone, Copy, PartialEq)]
234pub enum TransformType {
235    /// Rigid transformation (rotation + translation)
236    Rigid,
237    /// Similarity transformation (rotation + translation + uniform scaling)
238    Similarity,
239    /// Affine transformation (rotation + translation + scaling + shearing)
240    Affine,
241    /// Homography transformation (perspective transformation)
242    Homography,
243}
244
245/// Create identity transformation matrix
246#[allow(dead_code)]
247pub fn identity_transform() -> TransformMatrix {
248    Array2::eye(3)
249}
250
251/// Apply transformation to a point
252#[allow(dead_code)]
253pub fn transform_point(point: Point2D, transform: &TransformMatrix) -> Point2D {
254    let homogeneous = Array1::from(vec![point.x, point.y, 1.0]);
255    let transformed = transform.dot(&homogeneous);
256
257    if transformed[2].abs() < 1e-10 {
258        Point2D::new(transformed[0], transformed[1])
259    } else {
260        Point2D::new(
261            transformed[0] / transformed[2],
262            transformed[1] / transformed[2],
263        )
264    }
265}
266
267/// Apply transformation to multiple points
268#[allow(dead_code)]
269pub fn transform_points(points: &[Point2D], transform: &TransformMatrix) -> Vec<Point2D> {
270    points
271        .iter()
272        .map(|&p| transform_point(p, transform))
273        .collect()
274}
275
276/// Invert a transformation matrix
277#[allow(dead_code)]
278pub fn invert_transform(transform: &TransformMatrix) -> Result<TransformMatrix> {
279    // Uses optimized 3x3 matrix inversion for transformation matrices
280    // This implementation is sufficient for homogeneous transformation matrices
281    invert_3x3_matrix(transform)
282        .map_err(|e| VisionError::OperationError(format!("Failed to invert transformation: {e}")))
283}
284
285/// Compose two transformations (T2 * T1)
286#[allow(dead_code)]
287pub fn compose_transforms(t1: &TransformMatrix, t2: &TransformMatrix) -> TransformMatrix {
288    t2.dot(t1)
289}
290
291/// Decompose affine transformation into components
292#[allow(dead_code)]
293pub fn decompose_affine(transform: &TransformMatrix) -> Result<AffineComponents> {
294    if transform.shape() != [3, 3] {
295        return Err(VisionError::InvalidParameter(
296            "Transform must be 3x3 matrix".to_string(),
297        ));
298    }
299
300    let tx = transform[[0, 2]];
301    let ty = transform[[1, 2]];
302
303    let a = transform[[0, 0]];
304    let b = transform[[0, 1]];
305    let c = transform[[1, 0]];
306    let d = transform[[1, 1]];
307
308    let scale_x = (a * a + c * c).sqrt();
309    let scale_y = (b * b + d * d).sqrt();
310
311    let rotation = (c / scale_x).atan2(a / scale_x);
312    let shear = (a * b + c * d) / (scale_x * scale_y);
313
314    Ok(AffineComponents {
315        translation: Point2D::new(tx, ty),
316        rotation,
317        scale: Point2D::new(scale_x, scale_y),
318        shear,
319    })
320}
321
322/// Components of an affine transformation
323#[derive(Debug, Clone)]
324pub struct AffineComponents {
325    /// Translation vector (dx, dy)
326    pub translation: Point2D,
327    /// Rotation angle in radians
328    pub rotation: f64,
329    /// Scale factors (sx, sy)
330    pub scale: Point2D,
331    /// Shear angle in radians
332    pub shear: f64,
333}
334
335/// Estimate transformation robustly using RANSAC
336#[allow(dead_code)]
337pub fn ransac_estimate_transform(
338    matches: &[PointMatch],
339    transform_type: TransformType,
340    params: &RegistrationParams,
341) -> Result<RegistrationResult> {
342    let min_samples = match transform_type {
343        TransformType::Rigid => 2,
344        TransformType::Similarity => 2,
345        TransformType::Affine => 3,
346        TransformType::Homography => 4,
347    };
348
349    if matches.len() < min_samples {
350        return Err(VisionError::InvalidParameter(format!(
351            "Need at least {min_samples} matches for {transform_type:?} transformation"
352        )));
353    }
354
355    let mut _best_transform = identity_transform();
356    let mut best_inliers = Vec::new();
357    let mut best_cost = f64::INFINITY;
358
359    use scirs2_core::random::prelude::*;
360    let mut rng = thread_rng();
361
362    for _iteration in 0..params.ransac_iterations {
363        // Sample minimum required points
364        let mut sample_indices: Vec<usize> = (0..matches.len()).collect();
365        sample_indices.shuffle(&mut rng);
366        sample_indices.truncate(min_samples);
367
368        let sample_matches: Vec<_> = sample_indices.iter().map(|&i| matches[i].clone()).collect();
369
370        // Estimate transformation from sample
371        let transform = match transform_type {
372            TransformType::Rigid => estimate_rigid_transform(&sample_matches)?,
373            TransformType::Similarity => estimate_similarity_transform(&sample_matches)?,
374            TransformType::Affine => estimate_affine_transform(&sample_matches)?,
375            TransformType::Homography => estimate_homography_transform(&sample_matches)?,
376        };
377
378        // Find inliers
379        let mut inliers = Vec::new();
380        let mut total_error = 0.0;
381
382        for (i, m) in matches.iter().enumerate() {
383            let transformed = transform_point(m.source, &transform);
384            let error = ((transformed.x - m.target.x).powi(2)
385                + (transformed.y - m.target.y).powi(2))
386            .sqrt();
387
388            if error < params.ransac_threshold {
389                inliers.push(i);
390                total_error += error;
391            }
392        }
393
394        if inliers.len() >= min_samples {
395            let cost = total_error / inliers.len() as f64;
396            if cost < best_cost {
397                best_cost = cost;
398                _best_transform = transform;
399                best_inliers = inliers;
400            }
401        }
402    }
403
404    if best_inliers.is_empty() {
405        return Err(VisionError::OperationError(
406            "RANSAC failed to find valid transformation".to_string(),
407        ));
408    }
409
410    // Refine using all inliers
411    let inlier_matches: Vec<_> = best_inliers.iter().map(|&i| matches[i].clone()).collect();
412
413    let refined_transform = match transform_type {
414        TransformType::Rigid => estimate_rigid_transform(&inlier_matches)?,
415        TransformType::Similarity => estimate_similarity_transform(&inlier_matches)?,
416        TransformType::Affine => estimate_affine_transform(&inlier_matches)?,
417        TransformType::Homography => estimate_homography_transform(&inlier_matches)?,
418    };
419
420    Ok(RegistrationResult {
421        transform: refined_transform,
422        final_cost: best_cost,
423        iterations: params.ransac_iterations,
424        converged: true,
425        inliers: best_inliers,
426    })
427}
428
429/// Estimate rigid transformation (translation + rotation)
430#[allow(dead_code)]
431fn estimate_rigid_transform(matches: &[PointMatch]) -> Result<TransformMatrix> {
432    if matches.len() < 2 {
433        return Err(VisionError::InvalidParameter(
434            "Need at least 2 matches for rigid transformation".to_string(),
435        ));
436    }
437
438    // Calculate centroids
439    let n = matches.len() as f64;
440    let source_centroid = Point2D::new(
441        matches.iter().map(|m| m.source.x).sum::<f64>() / n,
442        matches.iter().map(|m| m.source.y).sum::<f64>() / n,
443    );
444    let target_centroid = Point2D::new(
445        matches.iter().map(|m| m.target.x).sum::<f64>() / n,
446        matches.iter().map(|m| m.target.y).sum::<f64>() / n,
447    );
448
449    // Calculate rotation using cross-correlation
450    let mut sxx = 0.0;
451    let mut sxy = 0.0;
452    let mut syx = 0.0;
453    let mut syy = 0.0;
454
455    for m in matches {
456        let sx = m.source.x - source_centroid.x;
457        let sy = m.source.y - source_centroid.y;
458        let tx = m.target.x - target_centroid.x;
459        let ty = m.target.y - target_centroid.y;
460
461        sxx += sx * tx;
462        sxy += sx * ty;
463        syx += sy * tx;
464        syy += sy * ty;
465    }
466
467    let angle = (sxy - syx).atan2(sxx + syy);
468    let cos_a = angle.cos();
469    let sin_a = angle.sin();
470
471    // Calculate translation
472    let tx = target_centroid.x - (cos_a * source_centroid.x - sin_a * source_centroid.y);
473    let ty = target_centroid.y - (sin_a * source_centroid.x + cos_a * source_centroid.y);
474
475    // Construct transformation matrix
476    let mut transform = Array2::zeros((3, 3));
477    transform[[0, 0]] = cos_a;
478    transform[[0, 1]] = -sin_a;
479    transform[[0, 2]] = tx;
480    transform[[1, 0]] = sin_a;
481    transform[[1, 1]] = cos_a;
482    transform[[1, 2]] = ty;
483    transform[[2, 2]] = 1.0;
484
485    Ok(transform)
486}
487
488/// Estimate similarity transformation (translation + rotation + uniform scale)
489#[allow(dead_code)]
490fn estimate_similarity_transform(matches: &[PointMatch]) -> Result<TransformMatrix> {
491    if matches.len() < 2 {
492        return Err(VisionError::InvalidParameter(
493            "Need at least 2 matches for similarity transformation".to_string(),
494        ));
495    }
496
497    // Calculate centroids
498    let n = matches.len() as f64;
499    let source_centroid = Point2D::new(
500        matches.iter().map(|m| m.source.x).sum::<f64>() / n,
501        matches.iter().map(|m| m.source.y).sum::<f64>() / n,
502    );
503    let target_centroid = Point2D::new(
504        matches.iter().map(|m| m.target.x).sum::<f64>() / n,
505        matches.iter().map(|m| m.target.y).sum::<f64>() / n,
506    );
507
508    // Calculate scale, rotation using least squares
509    let mut sxx = 0.0;
510    let mut sxy = 0.0;
511    let mut syx = 0.0;
512    let mut syy = 0.0;
513    let mut source_var = 0.0;
514
515    for m in matches {
516        let sx = m.source.x - source_centroid.x;
517        let sy = m.source.y - source_centroid.y;
518        let tx = m.target.x - target_centroid.x;
519        let ty = m.target.y - target_centroid.y;
520
521        sxx += sx * tx;
522        sxy += sx * ty;
523        syx += sy * tx;
524        syy += sy * ty;
525        source_var += sx * sx + sy * sy;
526    }
527
528    if source_var < 1e-10 {
529        return Err(VisionError::OperationError(
530            "Source points are collinear".to_string(),
531        ));
532    }
533
534    let scale = (sxx + syy) / source_var;
535    let angle = (sxy - syx).atan2(sxx + syy);
536
537    let cos_a = scale * angle.cos();
538    let sin_a = scale * angle.sin();
539
540    // Calculate translation
541    let tx = target_centroid.x - (cos_a * source_centroid.x - sin_a * source_centroid.y);
542    let ty = target_centroid.y - (sin_a * source_centroid.x + cos_a * source_centroid.y);
543
544    // Construct transformation matrix
545    let mut transform = Array2::zeros((3, 3));
546    transform[[0, 0]] = cos_a;
547    transform[[0, 1]] = -sin_a;
548    transform[[0, 2]] = tx;
549    transform[[1, 0]] = sin_a;
550    transform[[1, 1]] = cos_a;
551    transform[[1, 2]] = ty;
552    transform[[2, 2]] = 1.0;
553
554    Ok(transform)
555}
556
557/// Estimate affine transformation
558#[allow(dead_code)]
559fn estimate_affine_transform(matches: &[PointMatch]) -> Result<TransformMatrix> {
560    if matches.len() < 3 {
561        return Err(VisionError::InvalidParameter(
562            "Need at least 3 matches for affine transformation".to_string(),
563        ));
564    }
565
566    // Least squares solution using normal equations (A^T * A * x = A^T * b)
567
568    let n = matches.len();
569    let mut a = Array2::zeros((2 * n, 6));
570    let mut b = Array1::zeros(2 * n);
571
572    for (i, m) in matches.iter().enumerate() {
573        let row1 = 2 * i;
574        let row2 = 2 * i + 1;
575
576        // First equation: target.x = a*source.x + b*source.y + c
577        a[[row1, 0]] = m.source.x;
578        a[[row1, 1]] = m.source.y;
579        a[[row1, 2]] = 1.0;
580        b[row1] = m.target.x;
581
582        // Second equation: target.y = d*source.x + e*source.y + f
583        a[[row2, 3]] = m.source.x;
584        a[[row2, 4]] = m.source.y;
585        a[[row2, 5]] = 1.0;
586        b[row2] = m.target.y;
587    }
588
589    // Use scirs2-linalg's least squares solver
590    let result = lstsq(&a.view(), &b.view(), None)
591        .map_err(|e| VisionError::OperationError(format!("Failed to solve affine system: {e}")))?;
592
593    let params = result.x;
594
595    let mut transform = Array2::zeros((3, 3));
596    transform[[0, 0]] = params[0];
597    transform[[0, 1]] = params[1];
598    transform[[0, 2]] = params[2];
599    transform[[1, 0]] = params[3];
600    transform[[1, 1]] = params[4];
601    transform[[1, 2]] = params[5];
602    transform[[2, 2]] = 1.0;
603
604    Ok(transform)
605}
606
607/// Normalize points for homography estimation
608#[allow(dead_code)]
609fn normalize_points_homography(points: Vec<Point2D>) -> (Vec<Point2D>, TransformMatrix) {
610    let n = points.len() as f64;
611
612    // Calculate centroid
613    let mut cx = 0.0;
614    let mut cy = 0.0;
615    for p in &points {
616        cx += p.x;
617        cy += p.y;
618    }
619    cx /= n;
620    cy /= n;
621
622    // Calculate average distance from centroid
623    let mut avg_dist = 0.0;
624    for p in &points {
625        let dx = p.x - cx;
626        let dy = p.y - cy;
627        avg_dist += (dx * dx + dy * dy).sqrt();
628    }
629    avg_dist /= n;
630
631    // Scale factor to make average distance sqrt(2)
632    let scale = if avg_dist > 1e-10 {
633        2.0_f64.sqrt() / avg_dist
634    } else {
635        1.0
636    };
637
638    // Create normalization matrix
639    let mut t = Array2::eye(3);
640    t[[0, 0]] = scale;
641    t[[1, 1]] = scale;
642    t[[0, 2]] = -scale * cx;
643    t[[1, 2]] = -scale * cy;
644
645    // Normalize points
646    let mut norm_points = Vec::new();
647    for p in points {
648        norm_points.push(Point2D::new(scale * (p.x - cx), scale * (p.y - cy)));
649    }
650
651    (norm_points, t)
652}
653
654/// Estimate homography transformation
655#[allow(dead_code)]
656fn estimate_homography_transform(matches: &[PointMatch]) -> Result<TransformMatrix> {
657    if matches.len() < 4 {
658        return Err(VisionError::InvalidParameter(
659            "Need at least 4 matches for homography transformation".to_string(),
660        ));
661    }
662
663    // Check if all points are very close to their targets (identity transformation)
664    let mut is_identity = true;
665    for m in matches {
666        let dx = m.source.x - m.target.x;
667        let dy = m.source.y - m.target.y;
668        if dx.abs() > 1e-10 || dy.abs() > 1e-10 {
669            is_identity = false;
670            break;
671        }
672    }
673
674    if is_identity {
675        return Ok(identity_transform());
676    }
677
678    // Use Direct Linear Transform (DLT) algorithm for full homography estimation
679    // This avoids SVD issues while still providing full 8-parameter homography
680
681    // First normalize the points for numerical stability
682    let (norm_source, t1) = normalize_points_homography(matches.iter().map(|m| m.source).collect());
683    let (norm_target, t2) = normalize_points_homography(matches.iter().map(|m| m.target).collect());
684
685    // Build the constraint matrix for DLT
686    // For each correspondence, we get 2 equations
687    let n = matches.len();
688    let mut a_mat = Array2::zeros((2 * n, 9));
689
690    for (i, (src, tgt)) in norm_source.iter().zip(norm_target.iter()).enumerate() {
691        let sx = src.x;
692        let sy = src.y;
693        let tx = tgt.x;
694        let ty = tgt.y;
695
696        // First equation: -sx*h11 - sy*h12 - h13 + tx*sx*h31 + tx*sy*h32 + tx*h33 = 0
697        a_mat[[2 * i, 0]] = -sx;
698        a_mat[[2 * i, 1]] = -sy;
699        a_mat[[2 * i, 2]] = -1.0;
700        a_mat[[2 * i, 6]] = tx * sx;
701        a_mat[[2 * i, 7]] = tx * sy;
702        a_mat[[2 * i, 8]] = tx;
703
704        // Second equation: -sx*h21 - sy*h22 - h23 + ty*sx*h31 + ty*sy*h32 + ty*h33 = 0
705        a_mat[[2 * i + 1, 3]] = -sx;
706        a_mat[[2 * i + 1, 4]] = -sy;
707        a_mat[[2 * i + 1, 5]] = -1.0;
708        a_mat[[2 * i + 1, 6]] = ty * sx;
709        a_mat[[2 * i + 1, 7]] = ty * sy;
710        a_mat[[2 * i + 1, 8]] = ty;
711    }
712
713    // Find the null space of A using least squares with regularization
714    // We want to minimize ||Ah|| subject to ||h|| = 1
715    // Add regularization to avoid h33 = 0
716    let mut ata = a_mat.t().dot(&a_mat);
717
718    // Add small regularization to ensure numerical stability
719    for i in 0..9 {
720        ata[[i, i]] += 1e-10;
721    }
722
723    // Find eigenvector corresponding to smallest eigenvalue
724    // Since we can't use full eigendecomposition, use power iteration on the inverse
725    let mut h_vec = Array1::from_elem(9, 1.0 / 3.0); // Initial guess
726    h_vec[8] = 1.0; // Bias towards h33 = 1
727
728    // Use iterative refinement to find approximate solution
729    for _ in 0..20 {
730        // Solve (A^T A + λI) h_new = h_old to get direction
731        let b = h_vec.clone();
732        match solve(&ata.view(), &b.view(), None) {
733            Ok(h_new) => {
734                // Normalize
735                let norm = h_new.dot(&h_new).sqrt();
736                if norm > 1e-10 {
737                    h_vec = h_new / norm;
738                }
739            }
740            Err(_) => {
741                // If solve fails, fall back to simpler approach
742                break;
743            }
744        }
745    }
746
747    // Ensure h33 is positive
748    if h_vec[8] < 0.0 {
749        h_vec = -h_vec;
750    }
751
752    // Reshape to 3x3 matrix
753    let mut h_matrix = Array2::zeros((3, 3));
754    for i in 0..3 {
755        for j in 0..3 {
756            h_matrix[[i, j]] = h_vec[i * 3 + j] / h_vec[8]; // Normalize by h33
757        }
758    }
759
760    // Denormalize
761    let t2_inv = invert_3x3_matrix(&t2)?;
762    let h_denorm = t2_inv.dot(&h_matrix.dot(&t1));
763
764    Ok(h_denorm)
765}
766
767/// Simple 3x3 matrix inversion for TransformMatrix
768/// Optimized implementation for 3x3 homogeneous transformation matrices
769#[allow(dead_code)]
770fn invert_3x3_matrix(matrix: &TransformMatrix) -> Result<TransformMatrix> {
771    if matrix.shape() != [3, 3] {
772        return Err(VisionError::InvalidParameter(
773            "Matrix must be 3x3".to_string(),
774        ));
775    }
776
777    // Compute determinant
778    let det = matrix[[0, 0]] * (matrix[[1, 1]] * matrix[[2, 2]] - matrix[[1, 2]] * matrix[[2, 1]])
779        - matrix[[0, 1]] * (matrix[[1, 0]] * matrix[[2, 2]] - matrix[[1, 2]] * matrix[[2, 0]])
780        + matrix[[0, 2]] * (matrix[[1, 0]] * matrix[[2, 1]] - matrix[[1, 1]] * matrix[[2, 0]]);
781
782    if det.abs() < 1e-10 {
783        return Err(VisionError::OperationError(
784            "Matrix is singular, cannot invert".to_string(),
785        ));
786    }
787
788    let mut inv = Array2::zeros((3, 3));
789
790    // Compute adjugate matrix
791    inv[[0, 0]] = (matrix[[1, 1]] * matrix[[2, 2]] - matrix[[1, 2]] * matrix[[2, 1]]) / det;
792    inv[[0, 1]] = (matrix[[0, 2]] * matrix[[2, 1]] - matrix[[0, 1]] * matrix[[2, 2]]) / det;
793    inv[[0, 2]] = (matrix[[0, 1]] * matrix[[1, 2]] - matrix[[0, 2]] * matrix[[1, 1]]) / det;
794    inv[[1, 0]] = (matrix[[1, 2]] * matrix[[2, 0]] - matrix[[1, 0]] * matrix[[2, 2]]) / det;
795    inv[[1, 1]] = (matrix[[0, 0]] * matrix[[2, 2]] - matrix[[0, 2]] * matrix[[2, 0]]) / det;
796    inv[[1, 2]] = (matrix[[0, 2]] * matrix[[1, 0]] - matrix[[0, 0]] * matrix[[1, 2]]) / det;
797    inv[[2, 0]] = (matrix[[1, 0]] * matrix[[2, 1]] - matrix[[1, 1]] * matrix[[2, 0]]) / det;
798    inv[[2, 1]] = (matrix[[0, 1]] * matrix[[2, 0]] - matrix[[0, 0]] * matrix[[2, 1]]) / det;
799    inv[[2, 2]] = (matrix[[0, 0]] * matrix[[1, 1]] - matrix[[0, 1]] * matrix[[1, 0]]) / det;
800
801    Ok(inv)
802}
803
804#[cfg(test)]
805mod tests {
806    use super::*;
807
808    #[test]
809    fn test_point_transformation() {
810        let transform = identity_transform();
811        let point = Point2D::new(1.0, 2.0);
812        let transformed = transform_point(point, &transform);
813
814        assert!((transformed.x - point.x).abs() < 1e-10);
815        assert!((transformed.y - point.y).abs() < 1e-10);
816    }
817
818    #[test]
819    fn test_rigid_transform_estimation() {
820        let matches = vec![
821            PointMatch {
822                source: Point2D::new(0.0, 0.0),
823                target: Point2D::new(1.0, 1.0),
824                confidence: 1.0,
825            },
826            PointMatch {
827                source: Point2D::new(1.0, 0.0),
828                target: Point2D::new(1.0, 2.0),
829                confidence: 1.0,
830            },
831        ];
832
833        let transform = estimate_rigid_transform(&matches).expect("Operation failed");
834
835        // Verify transformation
836        let transformed1 = transform_point(matches[0].source, &transform);
837        let transformed2 = transform_point(matches[1].source, &transform);
838
839        assert!((transformed1.x - matches[0].target.x).abs() < 1e-10);
840        assert!((transformed1.y - matches[0].target.y).abs() < 1e-10);
841        assert!((transformed2.x - matches[1].target.x).abs() < 1e-10);
842        assert!((transformed2.y - matches[1].target.y).abs() < 1e-10);
843    }
844
845    #[test]
846    fn test_affine_transform_estimation() {
847        let matches = vec![
848            PointMatch {
849                source: Point2D::new(0.0, 0.0),
850                target: Point2D::new(1.0, 2.0),
851                confidence: 1.0,
852            },
853            PointMatch {
854                source: Point2D::new(1.0, 0.0),
855                target: Point2D::new(3.0, 3.0),
856                confidence: 1.0,
857            },
858            PointMatch {
859                source: Point2D::new(0.0, 1.0),
860                target: Point2D::new(2.0, 4.0),
861                confidence: 1.0,
862            },
863        ];
864
865        let transform = estimate_affine_transform(&matches).expect("Operation failed");
866
867        // Verify transformation
868        for m in &matches {
869            let transformed = transform_point(m.source, &transform);
870            assert!((transformed.x - m.target.x).abs() < 1e-10);
871            assert!((transformed.y - m.target.y).abs() < 1e-10);
872        }
873    }
874
875    #[test]
876    fn test_transform_composition() {
877        let t1 = identity_transform();
878        let mut t2 = identity_transform();
879        t2[[0, 2]] = 1.0; // Translation
880
881        let composed = compose_transforms(&t1, &t2);
882        assert_eq!(composed[[0, 2]], 1.0);
883    }
884
885    #[test]
886    fn test_transform_inversion() {
887        let mut transform = identity_transform();
888        transform[[0, 2]] = 1.0; // Translation
889        transform[[1, 2]] = 2.0;
890
891        let inverse = invert_transform(&transform).expect("Operation failed");
892        let composed = compose_transforms(&transform, &inverse);
893
894        // Should be close to identity
895        for i in 0..3 {
896            for j in 0..3 {
897                let expected = if i == j { 1.0 } else { 0.0 };
898                assert!((composed[[i, j]] - expected).abs() < 1e-10);
899            }
900        }
901    }
902}