RBF = {
function RBF(
sourcePoints,
destinationPoints,
distanceFunction,
normFunction,
epsilon
) {
const dimension = sourcePoints.length;
let destinationPointsMatrices = new Array(2);
if (useAffine) {
destinationPoints = [...destinationPoints, [0, 0], [0, 0], [0, 0]];
}
for (let i = 0; i < 2; i++) {
destinationPointsMatrices[i] = mlMatrix.Matrix.columnVector(
destinationPoints.map((value) => value[i])
);
}
const distancesMatrix = mlMatrix.Matrix.zeros(dimension, dimension);
for (let i = 0; i < dimension; i++) {
for (let j = 0; j < dimension; j++) {
distancesMatrix.set(
i,
j,
normFunction(sourcePoints[i], sourcePoints[j])
);
}
}
// If it's not provided, compute espilon as the average distance between the controle points
if (epsilon === undefined) {
epsilon = distancesMatrix.sum() / (Math.pow(dimension, 2) - dimension);
}
// Update the matrix to reflect the requested distance function
// Note: for distance functions which do not take an epsilon value this step does not alter the matrix. In production this can be skipped for such distance functions.
for (let i = 0; i < dimension; i++) {
for (let j = 0; j < dimension; j++) {
distancesMatrix.set(
i,
j,
distanceFunction(distancesMatrix.get(i, j), epsilon)
);
}
}
// Extend the point distance matrix to include the affine transformation
let affineCoefsMatrix = mlMatrix.Matrix.zeros(dimension, 3);
let distancesAndAffineCoefsMatrix = mlMatrix.Matrix.zeros(
dimension + 3,
dimension + 3
);
if (useAffine) {
// Construct Nx3 Matrix affineCoefsMatrix
// 1 x0 y0
// 1 x1 y1
// 1 x2 y2
// ...
for (let i = 0; i < dimension; i++) {
affineCoefsMatrix.set(i, 0, 1);
affineCoefsMatrix.set(i, 1, sourcePoints[i][0]);
affineCoefsMatrix.set(i, 2, sourcePoints[i][1]);
}
// Combine distancesMatrix and affineCoefsMatrix into new matrix distancesAndAffineCoefsMatrix
// Note: mlMatrix has no knowledge of block matrices, but this approach is good enough
// To speed this up, we could maybe use distancesMatrix.addRow() and distancesMatrix.addVector()
for (let i = 0; i < dimension + 3; i++) {
for (let j = 0; j < dimension + 3; j++) {
if (i < dimension && j < dimension) {
distancesAndAffineCoefsMatrix.set(i, j, distancesMatrix.get(i, j));
} else if (i >= dimension && j < dimension) {
distancesAndAffineCoefsMatrix.set(
i,
j,
affineCoefsMatrix.transpose().get(i - dimension, j)
);
} else if (i < dimension && j >= dimension) {
distancesAndAffineCoefsMatrix.set(
i,
j,
affineCoefsMatrix.get(i, j - dimension)
);
}
}
}
}
// Compute basis functions weights by solving the linear system of equations for each target component
// Note: for this example the system is solved with and without the affine part to allow comparing between both situation. This is, of course, not efficient in production, where it's best to make a choice.
let weightsMatrices = new Array(2);
for (let i = 0; i < 2; i++) {
if (useAffine) {
weightsMatrices[i] = mlMatrix.solve(
distancesAndAffineCoefsMatrix,
destinationPointsMatrices[i]
);
} else {
weightsMatrices[i] = mlMatrix.solve(
distancesMatrix,
destinationPointsMatrices[i]
);
}
}
// The interpolant function will compute the value at any point.
function interpolant(newSourcePoint) {
// First make a column matrix with the distances of that point to all controle points
let newDistancesMatrix = mlMatrix.Matrix.zeros(dimension, 1);
for (let i = 0; i < dimension; i++) {
newDistancesMatrix.set(
i,
0,
distanceFunction(
normFunction(newSourcePoint, sourcePoints[i]),
epsilon
)
);
}
// Then it computes the interpolated value by summing the weighted contributions of the input points
let newDestinationPoint = new Array(2);
for (let i = 0; i < 2; i++) {
// Apply the weights to the new distances
// Note: don't consider the last three weights who are there for the affine part
newDestinationPoint[i] = mulElements(
newDistancesMatrix,
weightsMatrices[i].selection([...Array(dimension).keys()], [0])
).sum();
// Add the affine part
if (useAffine) {
let a0 = weightsMatrices[i].get(dimension, 0);
let ax = weightsMatrices[i].get(dimension + 1, 0);
let ay = weightsMatrices[i].get(dimension + 2, 0);
newDestinationPoint[i] +=
a0 + ax * newSourcePoint[0] + ay * newSourcePoint[1];
}
}
return newDestinationPoint;
}
let affineCoefs = [null, null, null, null, null, null];
if (useAffine) {
affineCoefs = [
weightsMatrices[0].get(dimension, 0), // a0_x
weightsMatrices[0].get(dimension + 1, 0), // ax_x
weightsMatrices[0].get(dimension + 2, 0), // ay_x
weightsMatrices[1].get(dimension, 0), // a0_y
weightsMatrices[1].get(dimension + 1, 0), // ax_y
weightsMatrices[1].get(dimension + 2, 0) // ay_y
];
}
// This value has to be one for an affine transformation
let affineCheck =
affineCoefs[1] ** 2 * affineCoefs[5] ** 2 -
affineCoefs[3] ** 2 * affineCoefs[4] ** 2;
// This value has to be zero for a Helmert transformation, which affine transformations are not in general
let helmertCheck = affineCoefs[1] - affineCoefs[5];
// This value has to be zero for a Helmert transformation, which affine transformations are not in general
let helmertCheck2 = affineCoefs[2] + affineCoefs[4];
let translation = [null, null];
if (useAffine) {
translation = [affineCoefs[0], affineCoefs[3]];
}
// Only valid for Helmert transformations
// let scale = null;
// if (useAffine) {
// scale = Math.sqrt(affineCoefs[1] ** 2 + affineCoefs[5] ** 2);
// }
// Only valid for Helmert transformations
// let rotation = null;
// if (useAffine) {
// rotation = (Math.atan2(affineCoefs[4] / affineCoefs[2]) * 180) / Math.PI;
// }
return interpolant;
}
return RBF;
}