#[allow(clippy::needless_range_loop)]
pub(super) fn solve_3x3(a: &[[f64; 3]; 3], b: &[f64; 3]) -> Option<[f64; 3]> {
let mut aug = [[0.0f64; 4]; 3];
for i in 0..3 {
aug[i][..3].copy_from_slice(&a[i]);
aug[i][3] = b[i];
}
for col in 0..3 {
let mut max_row = col;
let mut max_val = aug[col][col].abs();
for row in col + 1..3 {
if aug[row][col].abs() > max_val {
max_val = aug[row][col].abs();
max_row = row;
}
}
if max_val < 1e-12 {
return None;
}
aug.swap(col, max_row);
let pivot = aug[col][col];
for row in col + 1..3 {
let factor = aug[row][col] / pivot;
for k in col..4 {
let v = aug[col][k];
aug[row][k] -= factor * v;
}
}
}
let mut x = [0.0f64; 3];
for i in (0..3).rev() {
x[i] = aug[i][3];
for j in i + 1..3 {
let v = aug[i][j];
x[i] -= v * x[j];
}
x[i] /= aug[i][i];
}
Some(x)
}
pub(crate) fn fit_local_affine(
board_pts: &[[f64; 2]],
image_pts: &[[f64; 2]],
) -> Option<[[f64; 3]; 2]> {
debug_assert_eq!(board_pts.len(), image_pts.len());
if board_pts.len() < 3 {
return None;
}
let mut xtx = [[0.0f64; 3]; 3];
let mut xtu = [0.0f64; 3];
let mut xtv = [0.0f64; 3];
for (bp, ip) in board_pts.iter().zip(image_pts) {
let row = [bp[0], bp[1], 1.0];
for j in 0..3 {
for k in 0..3 {
xtx[j][k] += row[j] * row[k];
}
xtu[j] += row[j] * ip[0];
xtv[j] += row[j] * ip[1];
}
}
let row_u = solve_3x3(&xtx, &xtu)?;
let row_v = solve_3x3(&xtx, &xtv)?;
Some([row_u, row_v])
}
pub(crate) fn affine_to_image(affine: &[[f64; 3]; 2], board_xy: [f64; 2]) -> [f64; 2] {
[
affine[0][0] * board_xy[0] + affine[0][1] * board_xy[1] + affine[0][2],
affine[1][0] * board_xy[0] + affine[1][1] * board_xy[1] + affine[1][2],
]
}
pub(super) fn affine_to_board(affine: &[[f64; 3]; 2], image_xy: [f64; 2]) -> Option<[f64; 2]> {
let a = affine[0][0];
let b = affine[0][1];
let c = affine[0][2];
let d = affine[1][0];
let e = affine[1][1];
let f = affine[1][2];
let det = a * e - b * d;
if det.abs() < 1e-12 {
return None;
}
let u = image_xy[0] - c;
let v = image_xy[1] - f;
Some([(e * u - b * v) / det, (a * v - d * u) / det])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn solve_3x3_recovers_known_solution() {
let a = [[2.0, 1.0, -1.0], [-3.0, -1.0, 2.0], [-2.0, 1.0, 2.0]];
let x_true = [1.0, -2.0, 3.0];
let b = [
a[0][0] * x_true[0] + a[0][1] * x_true[1] + a[0][2] * x_true[2],
a[1][0] * x_true[0] + a[1][1] * x_true[1] + a[1][2] * x_true[2],
a[2][0] * x_true[0] + a[2][1] * x_true[1] + a[2][2] * x_true[2],
];
let x = solve_3x3(&a, &b).expect("non-singular system must solve");
for k in 0..3 {
assert!(
(x[k] - x_true[k]).abs() < 1e-12,
"x[{k}] = {} != {}",
x[k],
x_true[k]
);
}
}
#[test]
fn solve_3x3_requires_pivoting() {
let a = [[0.0, 2.0, 1.0], [1.0, 0.0, 1.0], [3.0, 1.0, 0.0]];
let x_true = [0.5, -1.5, 2.25];
let b = [
a[0][1] * x_true[1] + a[0][2] * x_true[2],
a[1][0] * x_true[0] + a[1][2] * x_true[2],
a[2][0] * x_true[0] + a[2][1] * x_true[1],
];
let x = solve_3x3(&a, &b).expect("pivoting must handle zero leading entry");
for k in 0..3 {
assert!((x[k] - x_true[k]).abs() < 1e-12, "x[{k}] = {}", x[k]);
}
}
#[test]
fn solve_3x3_returns_none_for_singular() {
let a = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [5.0, 7.0, 9.0]];
let b = [1.0, 2.0, 3.0];
assert!(solve_3x3(&a, &b).is_none());
}
fn apply(m: &[[f64; 3]; 2], p: [f64; 2]) -> [f64; 2] {
[
m[0][0] * p[0] + m[0][1] * p[1] + m[0][2],
m[1][0] * p[0] + m[1][1] * p[1] + m[1][2],
]
}
#[test]
fn fit_local_affine_recovers_exact_transform_from_minimal_set() {
let truth = [[2.0, 0.5, 3.0], [-0.3, 1.7, -1.0]];
let board = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
let image: Vec<[f64; 2]> = board.iter().map(|&p| apply(&truth, p)).collect();
let fitted = fit_local_affine(&board, &image).expect("3 non-collinear points fit");
for r in 0..2 {
for c in 0..3 {
assert!(
(fitted[r][c] - truth[r][c]).abs() < 1e-9,
"A[{r}][{c}] = {} != {}",
fitted[r][c],
truth[r][c]
);
}
}
}
#[test]
fn fit_local_affine_overdetermined_exact_data_recovers_transform() {
let truth = [[1.1, -0.4, 12.0], [0.35, 0.9, -7.0]];
let board = [[0.0, 0.0], [3.0, 0.0], [0.0, 2.0], [4.0, 5.0], [-2.0, 1.5]];
let image: Vec<[f64; 2]> = board.iter().map(|&p| apply(&truth, p)).collect();
let fitted = fit_local_affine(&board, &image).expect("overdetermined fit");
for r in 0..2 {
for c in 0..3 {
assert!(
(fitted[r][c] - truth[r][c]).abs() < 1e-9,
"A[{r}][{c}] = {}",
fitted[r][c]
);
}
}
}
#[test]
fn fit_local_affine_rejects_fewer_than_three_points() {
let board = [[0.0, 0.0], [1.0, 0.0]];
let image = [[0.0, 0.0], [2.0, 0.0]];
assert!(fit_local_affine(&board, &image).is_none());
}
#[test]
fn fit_local_affine_rejects_collinear_points() {
let board = [[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0]];
let image = [[0.0, 0.0], [1.0, 1.0], [2.0, 2.0], [3.0, 3.0]];
assert!(fit_local_affine(&board, &image).is_none());
}
#[test]
fn affine_to_image_matches_matrix_product() {
let m = [[2.0, 0.5, 3.0], [-0.3, 1.7, -1.0]];
let p = [4.0, -2.0];
let got = affine_to_image(&m, p);
let expect = apply(&m, p);
assert!((got[0] - expect[0]).abs() < 1e-12);
assert!((got[1] - expect[1]).abs() < 1e-12);
}
#[test]
fn affine_to_board_inverts_affine_to_image() {
let m = [[2.0, 0.5, 3.0], [-0.3, 1.7, -1.0]];
let board_pt = [1.25, -3.5];
let image_pt = affine_to_image(&m, board_pt);
let recovered = affine_to_board(&m, image_pt).expect("invertible 2x2 sub-matrix");
assert!((recovered[0] - board_pt[0]).abs() < 1e-12);
assert!((recovered[1] - board_pt[1]).abs() < 1e-12);
}
#[test]
fn affine_to_board_returns_none_for_singular_linear_part() {
let m = [[1.0, 2.0, 5.0], [2.0, 4.0, 7.0]];
assert!(affine_to_board(&m, [0.0, 0.0]).is_none());
}
}