@uwdata/mosaic-plot
Version:
A Mosaic-powered plotting framework based on Observable Plot.
246 lines (213 loc) • 7.2 kB
JavaScript
// Deriche's approximation of Gaussian smoothing
// Adapted from Getreuer's C implementation (BSD license)
// https://www.ipol.im/pub/art/2013/87/gaussian_20131215.tgz
// http://dev.ipol.im/~getreuer/code/doc/gaussian_20131215_doc/gaussian__conv__deriche_8c.html
export function dericheConfig(sigma, negative = false) {
// compute causal filter coefficients
const a = new Float64Array(5);
const bc = new Float64Array(4);
dericheCausalCoeff(a, bc, sigma);
// numerator coefficients of the anticausal filter
const ba = Float64Array.of(
0,
bc[1] - a[1] * bc[0],
bc[2] - a[2] * bc[0],
bc[3] - a[3] * bc[0],
-a[4] * bc[0]
);
// impulse response sums
const accum_denom = 1.0 + a[1] + a[2] + a[3] + a[4];
const sum_causal = (bc[0] + bc[1] + bc[2] + bc[3]) / accum_denom;
const sum_anticausal = (ba[1] + ba[2] + ba[3] + ba[4]) / accum_denom;
// coefficients object
return {
sigma,
negative,
a,
b_causal: bc,
b_anticausal: ba,
sum_causal,
sum_anticausal
};
}
function dericheCausalCoeff(a_out, b_out, sigma) {
const K = 4;
const alpha = Float64Array.of(
0.84, 1.8675,
0.84, -1.8675,
-0.34015, -0.1299,
-0.34015, 0.1299
);
const x1 = Math.exp(-1.783 / sigma);
const x2 = Math.exp(-1.723 / sigma);
const y1 = 0.6318 / sigma;
const y2 = 1.997 / sigma;
const beta = Float64Array.of(
-x1 * Math.cos( y1), x1 * Math.sin( y1),
-x1 * Math.cos(-y1), x1 * Math.sin(-y1),
-x2 * Math.cos( y2), x2 * Math.sin( y2),
-x2 * Math.cos(-y2), x2 * Math.sin(-y2)
);
const denom = sigma * 2.5066282746310007;
// initialize b/a = alpha[0] / (1 + beta[0] z^-1)
const b = Float64Array.of(alpha[0], alpha[1], 0, 0, 0, 0, 0, 0);
const a = Float64Array.of(1, 0, beta[0], beta[1], 0, 0, 0, 0, 0, 0);
let j, k;
for (k = 2; k < 8; k += 2) {
// add kth term, b/a += alpha[k] / (1 + beta[k] z^-1)
b[k] = beta[k] * b[k - 2] - beta[k + 1] * b[k - 1];
b[k + 1] = beta[k] * b[k - 1] + beta[k + 1] * b[k - 2];
for (j = k - 2; j > 0; j -= 2) {
b[j] += beta[k] * b[j - 2] - beta[k + 1] * b[j - 1];
b[j + 1] += beta[k] * b[j - 1] + beta[k + 1] * b[j - 2];
}
for (j = 0; j <= k; j += 2) {
b[j] += alpha[k] * a[j] - alpha[k + 1] * a[j + 1];
b[j + 1] += alpha[k] * a[j + 1] + alpha[k + 1] * a[j];
}
a[k + 2] = beta[k] * a[k] - beta[k + 1] * a[k + 1];
a[k + 3] = beta[k] * a[k + 1] + beta[k + 1] * a[k];
for (j = k; j > 0; j -= 2) {
a[j] += beta[k] * a[j - 2] - beta[k + 1] * a[j - 1];
a[j + 1] += beta[k] * a[j - 1] + beta[k + 1] * a[j - 2];
}
}
for (k = 0; k < K; ++k) {
j = k << 1;
b_out[k] = b[j] / denom;
a_out[k + 1] = a[j + 2];
}
}
export function dericheConv2d(cx, cy, grid, [nx, ny]) {
// allocate buffers
const yc = new Float64Array(Math.max(nx, ny)); // causal
const ya = new Float64Array(Math.max(nx, ny)); // anticausal
const h = new Float64Array(5); // q + 1
const d = new Float64Array(grid.length);
// convolve rows
for (let row = 0, r0 = 0; row < ny; ++row, r0 += nx) {
const dx = d.subarray(r0);
dericheConv1d(cx, grid.subarray(r0), nx, 1, yc, ya, h, dx);
}
// convolve columns
for (let c0 = 0; c0 < nx; ++c0) {
const dy = d.subarray(c0);
dericheConv1d(cy, dy, ny, nx, yc, ya, h, dy);
}
return d;
}
export function dericheConv1d(
c, src, N,
stride = 1,
y_causal = new Float64Array(N),
y_anticausal = new Float64Array(N),
h = new Float64Array(5), // q + 1
d = y_causal,
init = dericheInitZeroPad
) {
const stride_2 = stride * 2;
const stride_3 = stride * 3;
const stride_4 = stride * 4;
const stride_N = stride * N;
let i, n;
// initialize causal filter on the left boundary
init(
y_causal, src, N, stride,
c.b_causal, 3, c.a, 4, c.sum_causal, h, c.sigma
);
// filter the interior samples using a 4th order filter. Implements:
// for n = K, ..., N - 1,
// y^+(n) = \sum_{k=0}^{K-1} b^+_k src(n - k)
// - \sum_{k=1}^K a_k y^+(n - k)
// variable i tracks the offset to the nth sample of src, it is
// updated together with n such that i = stride * n.
for (n = 4, i = stride_4; n < N; ++n, i += stride) {
y_causal[n] = c.b_causal[0] * src[i]
+ c.b_causal[1] * src[i - stride]
+ c.b_causal[2] * src[i - stride_2]
+ c.b_causal[3] * src[i - stride_3]
- c.a[1] * y_causal[n - 1]
- c.a[2] * y_causal[n - 2]
- c.a[3] * y_causal[n - 3]
- c.a[4] * y_causal[n - 4];
}
// initialize the anticausal filter on the right boundary
// dest, src, N, stride, b, p, a, q, sum, h
init(
y_anticausal, src, N, -stride,
c.b_anticausal, 4, c.a, 4, c.sum_anticausal, h, c.sigma
);
// similar to the causal filter above, the following implements:
// for n = K, ..., N - 1,
// y^-(n) = \sum_{k=1}^K b^-_k src(N - n - 1 - k)
// - \sum_{k=1}^K a_k y^-(n - k)
// variable i is updated such that i = stride * (N - n - 1).
for (n = 4, i = stride_N - stride * 5; n < N; ++n, i -= stride) {
y_anticausal[n] = c.b_anticausal[1] * src[i + stride]
+ c.b_anticausal[2] * src[i + stride_2]
+ c.b_anticausal[3] * src[i + stride_3]
+ c.b_anticausal[4] * src[i + stride_4]
- c.a[1] * y_anticausal[n - 1]
- c.a[2] * y_anticausal[n - 2]
- c.a[3] * y_anticausal[n - 3]
- c.a[4] * y_anticausal[n - 4];
}
// sum the causal and anticausal responses to obtain the final result
if (c.negative) {
// do not threshold if the input grid includes negative values
for (n = 0, i = 0; n < N; ++n, i += stride) {
d[i] = y_causal[n] + y_anticausal[N - n - 1];
}
} else {
// threshold to prevent small negative values due to floating point error
for (n = 0, i = 0; n < N; ++n, i += stride) {
d[i] = Math.max(0, y_causal[n] + y_anticausal[N - n - 1]);
}
}
return d;
}
export function dericheInitZeroPad(
dest, src, N, stride, b, p, a, q,
sum, h, sigma, tol = 0.5
) {
const stride_N = Math.abs(stride) * N;
const off = stride < 0 ? stride_N + stride : 0;
let i, n, m;
// compute the first q taps of the impulse response, h_0, ..., h_{q-1}
for (n = 0; n <= q; ++n) {
h[n] = (n <= p) ? b[n] : 0;
for (m = 1; m <= q && m <= n; ++m) {
h[n] -= a[m] * h[n - m];
}
}
// compute dest_m = sum_{n=1}^m h_{m-n} src_n, m = 0, ..., q-1
// note: q == 4
for (m = 0; m < q; ++m) {
for (dest[m] = 0, n = 1; n <= m; ++n) {
i = off + stride * n;
if (i >= 0 && i < stride_N) {
dest[m] += h[m - n] * src[i];
}
}
}
const cur = src[off];
const max_iter = Math.ceil(sigma * 10);
for (n = 0; n < max_iter; ++n) {
/* dest_m = dest_m + h_{n+m} src_{-n} */
for (m = 0; m < q; ++m) {
dest[m] += h[m] * cur;
}
sum -= Math.abs(h[0]);
if (sum <= tol) break;
/* Compute the next impulse response tap, h_{n+q} */
h[q] = (n + q <= p) ? b[n + q] : 0;
for (m = 1; m <= q; ++m) {
h[q] -= a[m] * h[q - m];
}
/* Shift the h array for the next iteration */
for (m = 0; m < q; ++m) {
h[m] = h[m + 1];
}
}
return;
}