pub fn solve_linear_system(mut a: Vec<Vec<f64>>, mut b: Vec<Vec<f64>>) -> Option<Vec<Vec<f64>>> {
let n = a.len();
if n == 0 || a.iter().any(|r| r.len() != n) || b.len() != n {
return None;
}
let m = b[0].len();
if b.iter().any(|r| r.len() != m) {
return None;
}
for col in 0..n {
let mut pivot = col;
let mut best = a[col][col].abs();
for (row, r) in a.iter().enumerate().skip(col + 1) {
let v = r[col].abs();
if v > best {
best = v;
pivot = row;
}
}
if best < 1e-12 {
return None;
}
a.swap(col, pivot);
b.swap(col, pivot);
let pivot_row = a[col].clone();
let pivot_b = b[col].clone();
let piv = pivot_row[col];
for (row_a, row_b) in a.iter_mut().zip(b.iter_mut()).skip(col + 1) {
let factor = row_a[col] / piv;
if factor != 0.0 {
for (ak, pk) in row_a.iter_mut().zip(pivot_row.iter()).skip(col) {
*ak -= factor * pk;
}
for (bk, pbk) in row_b.iter_mut().zip(pivot_b.iter()) {
*bk -= factor * pbk;
}
}
}
}
let mut x = vec![vec![0.0f64; m]; n];
for i in (0..n).rev() {
for k in 0..m {
let mut acc = b[i][k];
for j in (i + 1)..n {
acc -= a[i][j] * x[j][k];
}
x[i][k] = acc / a[i][i];
}
}
Some(x)
}
#[inline]
fn kernel(r2: f64) -> f64 {
if r2 <= 0.0 {
0.0
} else {
0.5 * r2 * r2.ln()
}
}
#[derive(Debug, Clone)]
pub struct ThinPlateSpline {
ctrl: Vec<(f64, f64)>,
coef_x: Vec<f64>,
coef_y: Vec<f64>,
}
impl ThinPlateSpline {
pub fn fit(src: &[(f64, f64)], dst: &[(f64, f64)]) -> Option<Self> {
let n = src.len();
if n < 3 || dst.len() != n {
return None;
}
let size = n + 3;
let mut a = vec![vec![0.0f64; size]; size];
for i in 0..n {
for j in 0..n {
let dx = src[i].0 - src[j].0;
let dy = src[i].1 - src[j].1;
a[i][j] = kernel(dx * dx + dy * dy);
}
}
for i in 0..n {
a[i][n] = 1.0;
a[i][n + 1] = src[i].0;
a[i][n + 2] = src[i].1;
a[n][i] = 1.0;
a[n + 1][i] = src[i].0;
a[n + 2][i] = src[i].1;
}
let mut b = vec![vec![0.0f64; 2]; size];
for i in 0..n {
b[i][0] = dst[i].0;
b[i][1] = dst[i].1;
}
let sol = solve_linear_system(a, b)?;
let coef_x = sol.iter().map(|r| r[0]).collect();
let coef_y = sol.iter().map(|r| r[1]).collect();
Some(ThinPlateSpline {
ctrl: src.to_vec(),
coef_x,
coef_y,
})
}
pub fn map(&self, x: f64, y: f64) -> (f64, f64) {
let n = self.ctrl.len();
let eval = |coef: &[f64]| {
let mut v = coef[n] + coef[n + 1] * x + coef[n + 2] * y;
for (i, &(cx, cy)) in self.ctrl.iter().enumerate() {
let dx = x - cx;
let dy = y - cy;
v += coef[i] * kernel(dx * dx + dy * dy);
}
v
};
(eval(&self.coef_x), eval(&self.coef_y))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn solves_simple_system() {
let a = vec![vec![2.0, 1.0], vec![1.0, -1.0]];
let b = vec![vec![5.0], vec![1.0]];
let x = solve_linear_system(a, b).unwrap();
assert!((x[0][0] - 2.0).abs() < 1e-9);
assert!((x[1][0] - 1.0).abs() < 1e-9);
}
#[test]
fn singular_system_is_none() {
let a = vec![vec![1.0, 2.0], vec![2.0, 4.0]];
let b = vec![vec![1.0], vec![2.0]];
assert!(solve_linear_system(a, b).is_none());
}
#[test]
fn interpolates_through_every_anchor() {
let src = [
(0.0, 0.0),
(10.0, 0.0),
(0.0, 10.0),
(10.0, 10.0),
(5.0, 5.0),
(3.0, 8.0),
];
let f = |x: f64, y: f64| (x + 0.05 * y * y, y - 0.03 * x * x);
let dst: Vec<(f64, f64)> = src.iter().map(|&(x, y)| f(x, y)).collect();
let tps = ThinPlateSpline::fit(&src, &dst).unwrap();
for (&s, &d) in src.iter().zip(dst.iter()) {
let (mx, my) = tps.map(s.0, s.1);
assert!((mx - d.0).abs() < 1e-6, "x at {s:?}: {mx} vs {}", d.0);
assert!((my - d.1).abs() < 1e-6, "y at {s:?}: {my} vs {}", d.1);
}
}
#[test]
fn reproduces_affine_maps_exactly() {
let src = [(0.0, 0.0), (4.0, 1.0), (1.0, 5.0), (6.0, 6.0)];
let affine = |x: f64, y: f64| (2.0 * x + 0.5 * y + 3.0, -0.5 * x + 1.5 * y - 1.0);
let dst: Vec<(f64, f64)> = src.iter().map(|&(x, y)| affine(x, y)).collect();
let tps = ThinPlateSpline::fit(&src, &dst).unwrap();
for &(x, y) in &[(2.5, 3.5), (10.0, -4.0), (0.0, 7.0)] {
let (mx, my) = tps.map(x, y);
let (ex, ey) = affine(x, y);
assert!((mx - ex).abs() < 1e-6);
assert!((my - ey).abs() < 1e-6);
}
}
}