src/core/GaussKernel.js
/**
* Uses Pascal's Triangle to generate coefficients in the expansion of any binomial expression.
*
* For details see https://mathworld.wolfram.com/PascalsTriangle.html.
*
* @param {Number} n - The index of the coefficients row in Pascal's Triangle.
* @return {Float64Array} The integer coefficients stored as doubles.
* @ignore
*/
function getCoefficients(n) {
let result;
if(n === 0) {
result = new Float64Array(0);
} else if(n === 1) {
result = new Float64Array([1]);
} else if(n > 1) {
// Incrementally build Pascal's Triangle to get the desired row.
let row0 = new Float64Array(n);
let row1 = new Float64Array(n);
for(let y = 1; y <= n; ++y) {
for(let x = 0; x < y; ++x) {
row1[x] = (x === 0 || x === y - 1) ? 1 : row0[x - 1] + row0[x];
}
result = row1;
row1 = row0;
row0 = result;
}
}
return result;
}
/**
* A Gauss kernel.
*
* Based on https://github.com/Jam3/glsl-fast-gaussian-blur.
*/
export class GaussKernel {
/**
* Constructs a new Gauss kernel.
*
* @param {Number} kernelSize - The kernel size. Should be an odd number in the range [3, 1020].
* @param {Number} [edgeBias=2] - Determines how many edge coefficients should be cut off for increased accuracy.
*/
constructor(kernelSize, edgeBias = 2) {
/**
* The weights for discrete sampling.
*
* @type {Float64Array}
*/
this.weights = null;
/**
* The offsets for discrete sampling.
*
* @type {Float64Array}
*/
this.offsets = null;
/**
* The weights for linear sampling.
*
* @type {Float64Array}
*/
this.linearWeights = null;
/**
* The offsets for linear sampling.
*
* @type {Float64Array}
*/
this.linearOffsets = null;
this.generate(kernelSize, edgeBias);
}
/**
* The number of steps for discrete sampling.
*
* @type {Number}
*/
get steps() {
return (this.offsets === null) ? 0 : this.offsets.length;
}
/**
* The number of steps for linear sampling.
*
* @type {Number}
*/
get linearSteps() {
return (this.linearOffsets === null) ? 0 : this.linearOffsets.length;
}
/**
* Generates the kernel.
*
* @private
* @param {Number} kernelSize - The kernel size.
* @param {Number} edgeBias - The amount of edge coefficients to ignore.
*/
generate(kernelSize, edgeBias) {
if(kernelSize < 3 || kernelSize > 1020) {
throw new Error("The kernel size must be in the range [3, 1020]");
}
const n = kernelSize + edgeBias * 2;
const coefficients = (edgeBias > 0) ?
getCoefficients(n).slice(edgeBias, -edgeBias) :
getCoefficients(n);
const mid = Math.floor((coefficients.length - 1) / 2);
const sum = coefficients.reduce((a, b) => a + b, 0);
const weights = coefficients.slice(mid);
const offsets = [...Array(mid + 1).keys()]; // [0..mid+1]
const linearWeights = new Float64Array(Math.floor(offsets.length / 2));
const linearOffsets = new Float64Array(linearWeights.length);
linearWeights[0] = weights[0] / sum;
for(let i = 1, j = 1, l = offsets.length - 1; i < l; i += 2, ++j) {
const offset0 = offsets[i], offset1 = offsets[i + 1];
const weight0 = weights[i], weight1 = weights[i + 1];
const w = weight0 + weight1;
const o = (offset0 * weight0 + offset1 * weight1) / w;
linearWeights[j] = w / sum;
linearOffsets[j] = o;
}
for(let i = 0, l = weights.length, s = 1.0 / sum; i < l; ++i) {
weights[i] *= s;
}
// Ensure that the weights add up to 1.
const linearWeightSum = (linearWeights.reduce((a, b) => a + b, 0) - linearWeights[0] * 0.5) * 2.0;
if(linearWeightSum !== 0.0) {
for(let i = 0, l = linearWeights.length, s = 1.0 / linearWeightSum; i < l; ++i) {
linearWeights[i] *= s;
}
}
this.offsets = offsets;
this.weights = weights;
this.linearOffsets = linearOffsets;
this.linearWeights = linearWeights;
}
}