1pub 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};
28use scirs2_core::random;
30use scirs2_core::random::seq::SliceRandom;
31use std::fmt::Debug;
32
33#[derive(Debug)]
35pub struct LstsqResult {
36 pub x: Array1<f64>,
38}
39
40#[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 if m >= n {
56 let at = a.t();
58
59 let ata = at.dot(a);
61
62 let atb = at.dot(b);
64
65 let x = solve_linear_system(&ata.view(), &atb.view())?;
67
68 Ok(LstsqResult { x })
69 } else {
70 Ok(LstsqResult {
73 x: Array1::zeros(n),
74 })
75 }
76}
77
78#[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 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 for i in 0..n {
100 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 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 if aug[[i, i]].abs() < 1e-14 {
119 return Err("Matrix is singular".to_string());
120 }
121
122 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 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#[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
154pub type TransformMatrix = Array2<f64>;
156
157#[derive(Debug, Clone, Copy, PartialEq)]
159pub struct Point2D {
160 pub x: f64,
162 pub y: f64,
164}
165
166impl Point2D {
167 pub fn new(x: f64, y: f64) -> Self {
169 Self { x, y }
170 }
171}
172
173#[derive(Debug, Clone)]
175pub struct PointMatch {
176 pub source: Point2D,
178 pub target: Point2D,
180 pub confidence: f64,
182}
183
184#[derive(Debug, Clone)]
186pub struct RegistrationParams {
187 pub max_iterations: usize,
189 pub tolerance: f64,
191 pub use_pyramid: bool,
193 pub pyramid_levels: usize,
195 pub ransac_threshold: f64,
197 pub ransac_iterations: usize,
199 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#[derive(Debug, Clone)]
219pub struct RegistrationResult {
220 pub transform: TransformMatrix,
222 pub final_cost: f64,
224 pub iterations: usize,
226 pub converged: bool,
228 pub inliers: Vec<usize>,
230}
231
232#[derive(Debug, Clone, Copy, PartialEq)]
234pub enum TransformType {
235 Rigid,
237 Similarity,
239 Affine,
241 Homography,
243}
244
245#[allow(dead_code)]
247pub fn identity_transform() -> TransformMatrix {
248 Array2::eye(3)
249}
250
251#[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#[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#[allow(dead_code)]
278pub fn invert_transform(transform: &TransformMatrix) -> Result<TransformMatrix> {
279 invert_3x3_matrix(transform)
282 .map_err(|e| VisionError::OperationError(format!("Failed to invert transformation: {e}")))
283}
284
285#[allow(dead_code)]
287pub fn compose_transforms(t1: &TransformMatrix, t2: &TransformMatrix) -> TransformMatrix {
288 t2.dot(t1)
289}
290
291#[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#[derive(Debug, Clone)]
324pub struct AffineComponents {
325 pub translation: Point2D,
327 pub rotation: f64,
329 pub scale: Point2D,
331 pub shear: f64,
333}
334
335#[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 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 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 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 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#[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 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 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 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 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#[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 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 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 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 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#[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 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 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 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 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#[allow(dead_code)]
609fn normalize_points_homography(points: Vec<Point2D>) -> (Vec<Point2D>, TransformMatrix) {
610 let n = points.len() as f64;
611
612 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 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 let scale = if avg_dist > 1e-10 {
633 2.0_f64.sqrt() / avg_dist
634 } else {
635 1.0
636 };
637
638 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 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#[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 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 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 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 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 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 let mut ata = a_mat.t().dot(&a_mat);
717
718 for i in 0..9 {
720 ata[[i, i]] += 1e-10;
721 }
722
723 let mut h_vec = Array1::from_elem(9, 1.0 / 3.0); h_vec[8] = 1.0; for _ in 0..20 {
730 let b = h_vec.clone();
732 match solve(&ata.view(), &b.view(), None) {
733 Ok(h_new) => {
734 let norm = h_new.dot(&h_new).sqrt();
736 if norm > 1e-10 {
737 h_vec = h_new / norm;
738 }
739 }
740 Err(_) => {
741 break;
743 }
744 }
745 }
746
747 if h_vec[8] < 0.0 {
749 h_vec = -h_vec;
750 }
751
752 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]; }
758 }
759
760 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#[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 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 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 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 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; 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; transform[[1, 2]] = 2.0;
890
891 let inverse = invert_transform(&transform).expect("Operation failed");
892 let composed = compose_transforms(&transform, &inverse);
893
894 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}