@thi.ng/pixel-convolve
Version:
Extensible bitmap image convolution, kernel presets, normal map & image pyramid generation
486 lines (485 loc) • 10.7 kB
JavaScript
import { isFunction } from "@thi.ng/checks/is-function";
import { assert } from "@thi.ng/errors/assert";
import { clamp } from "@thi.ng/math/interval";
import { lanczos } from "@thi.ng/math/mix";
import { ensureChannel } from "@thi.ng/pixel/checks";
import { FloatBuffer } from "@thi.ng/pixel/float";
import { FLOAT_GRAY } from "@thi.ng/pixel/format/float-gray";
import { __range } from "@thi.ng/pixel/internal/range";
import { __asIntVec } from "@thi.ng/pixel/internal/utils";
const convolveChannel = (src, opts) => __convolve(__initConvolve(src, opts));
const convolveImage = (src, opts) => {
const state = __initConvolve(src, opts);
const dest = new FloatBuffer(state.dwidth, state.dheight, src.format);
for (const channel of opts.channels || __range(src.format.channels.length)) {
dest.setChannel(channel, __convolve({ ...state, channel }));
}
return dest;
};
const __convolve = ({
channel,
dest,
dwidth,
dheight,
kernel,
offsetX,
offsetY,
rowStride,
scale,
src,
srcStride,
strideX,
strideY
}) => {
ensureChannel(src.format, channel);
const dpix = dest.data;
const stepX = strideX * srcStride;
const stepY = strideY * rowStride;
for (let sy = offsetY * rowStride, dy = 0, i = 0; dy < dheight; sy += stepY, dy++) {
for (let sx = offsetX * srcStride + channel, dx = 0; dx < dwidth; sx += stepX, dx++, i++) {
dpix[i] = kernel(sx, sy, channel) * scale;
}
}
return dest;
};
const __initKernel = (src, kernel, kw, kh) => (isFunction(kernel.fn) ? kernel.fn : defKernel(
kernel.spec || kernel.pool,
kw,
kh
))(src);
const __initConvolve = (src, opts) => {
const {
channel = 0,
offset = 0,
scale = 1,
stride: sampleStride = 1,
kernel
} = opts;
const size = kernel.size;
const [kw, kh] = __asIntVec(size);
const [strideX, strideY] = __asIntVec(sampleStride);
const [offsetX, offsetY] = __asIntVec(offset);
assert(strideX >= 1 && strideY >= 1, `illegal stride: ${sampleStride}`);
const {
size: [width, height],
stride: [srcStride, rowStride]
} = src;
const dwidth = Math.floor(width / strideX);
const dheight = Math.floor(height / strideY);
assert(dwidth > 0 && dheight > 0, `too large stride(s) for given image`);
const dest = new FloatBuffer(dwidth, dheight, FLOAT_GRAY);
return {
channel,
dest,
dheight,
dwidth,
kernel: __initKernel(src, kernel, kw, kh),
offsetX,
offsetY,
rowStride,
scale,
src,
srcStride,
strideX,
strideY
};
};
const __declOffset = (idx, i, pre, stride, min2, max2) => idx < 0 ? `const ${pre}${i} = max(${pre}${idx < -1 ? idx + "*" : "-"}${stride},${min2});` : `const ${pre}${i} = min(${pre}+${idx > 1 ? idx + "*" : ""}${stride},${max2});`;
const defKernel = (tpl, w, h, normalize = false) => {
if (w * h > 512 && !isFunction(tpl))
return defLargeKernel(tpl, w, h, normalize);
const isPool = isFunction(tpl);
const prefix = [];
const body = [];
const kvars = [];
const h2 = h >> 1;
const w2 = w >> 1;
if (normalize) tpl = __normalize(tpl);
for (let y = 0, i = 0; y < h; y++) {
const yy = y - h2;
const row = [];
for (let x = 0; x < w; x++, i++) {
const kv = `k${y}_${x}`;
kvars.push(kv);
const xx = x - w2;
const idx = (yy !== 0 ? `y${y}` : `y`) + (xx !== 0 ? `+x${x}` : "+x");
isPool ? row.push(`pix[${idx}]`) : tpl[i] !== 0 && row.push(`${kv}*pix[${idx}]`);
if (y === 0 && xx !== 0) {
prefix.push(
__declOffset(
xx,
x,
"x",
"stride",
"channel",
"maxX+channel"
)
);
}
}
row.length && body.push(...row);
if (yy !== 0) {
prefix.push(__declOffset(yy, y, "y", "rowStride", "0", "maxY"));
}
}
const decls = isPool ? "" : `const [${kvars.join(", ")}] = [${tpl.join(", ")}];`;
const inner = isPool ? tpl(body, w, h) : body.join(" + ");
const fnBody = [
decls,
"const { min, max } = Math;",
"const { data: pix, stride: [stride, rowStride] } = src;",
"const maxX = (src.width - 1) * stride;",
"const maxY = (src.height - 1) * rowStride;",
"return (x, y, channel) => {",
...prefix,
`return ${inner};`,
"}"
].join("\n");
return new Function("src", fnBody);
};
const defLargeKernel = (kernel, w, h, normalize = false) => {
if (normalize) kernel = __normalize(kernel);
return (src) => {
const {
data,
stride: [stride, rowStride]
} = src;
const x0 = -(w >> 1) * stride;
const x1 = -x0 + (w & 1 ? stride : 0);
const y0 = -(h >> 1) * rowStride;
const y1 = -y0 + (h & 1 ? rowStride : 0);
const maxX = (src.width - 1) * stride;
const maxY = (src.height - 1) * rowStride;
return (xx, yy, channel) => {
const $maxX = maxX + channel;
let sum = 0, y, x, k, row;
for (y = y0, k = 0; y < y1; y += rowStride) {
for (x = x0, row = clamp(yy + y, 0, maxY); x < x1; x += stride, k++) {
sum += kernel[k] * data[row + clamp(xx + x, channel, $maxX)];
}
}
return sum;
};
};
};
const __normalize = (kernel) => {
const scale = 1 / kernel.reduce((acc, x) => acc + x, 0);
return kernel.map((x) => x * scale);
};
const POOL_NEAREST = (body, w, h) => body[(h >> 1) * w + (w >> 1)];
const POOL_MEAN = (body, w, h) => `(${body.join("+")})*${1 / (w * h)}`;
const POOL_MIN = (body) => `Math.min(${body.join(",")})`;
const POOL_MAX = (body) => `Math.max(${body.join(",")})`;
const POOL_THRESHOLD = (bias = 0) => (body, w, h) => {
const center = POOL_NEAREST(body, w, h);
const mean = `(${body.join("+")})/${w * h}`;
return `(${center} - ${mean} + ${bias}) < 0 ? 0 : 1`;
};
const SOBEL_X = {
// prettier-ignore
spec: [
-1,
0,
1,
-2,
0,
2,
-1,
0,
1
],
size: 3
};
const SOBEL_Y = {
// prettier-ignore
spec: [
-1,
-2,
-1,
0,
0,
0,
1,
2,
1
],
size: 3
};
const EDGE3 = {
// prettier-ignore
spec: [
-1,
-1,
-1,
-1,
8,
-1,
-1,
-1,
-1
],
size: 3
};
const EDGE5 = {
// prettier-ignore
spec: [
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
24,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1
],
size: 5
};
const SHARPEN3 = {
// prettier-ignore
spec: [
0,
-1,
0,
-1,
5,
-1,
0,
-1,
0
],
size: 3
};
const HIGHPASS3 = {
// prettier-ignore
spec: [
-1,
-1,
-1,
-1,
9,
-1,
-1,
-1,
-1
],
size: 3
};
const BOX_BLUR3 = {
pool: POOL_MEAN,
size: 3
};
const BOX_BLUR5 = {
pool: POOL_MEAN,
size: 5
};
const GAUSSIAN_BLUR3 = {
// prettier-ignore
spec: [
1 / 16,
1 / 8,
1 / 16,
1 / 8,
1 / 4,
1 / 8,
1 / 16,
1 / 8,
1 / 16
],
size: 3
};
const GAUSSIAN_BLUR5 = {
// prettier-ignore
spec: [
1 / 256,
1 / 64,
3 / 128,
1 / 64,
1 / 256,
1 / 64,
1 / 16,
3 / 32,
1 / 16,
1 / 64,
3 / 128,
3 / 32,
9 / 64,
3 / 32,
3 / 128,
1 / 64,
1 / 16,
3 / 32,
1 / 16,
1 / 64,
1 / 256,
1 / 64,
3 / 128,
1 / 64,
1 / 256
],
size: 5
};
const GAUSSIAN = (r) => {
r |= 0;
assert(r > 0, `invalid kernel radius: ${r}`);
const sigma = -1 / (2 * (Math.hypot(r, r) / 3) ** 2);
const res = [];
let sum = 0;
for (let y = -r; y <= r; y++) {
for (let x = -r; x <= r; x++) {
const g = Math.exp((x * x + y * y) * sigma);
res.push(g);
sum += g;
}
}
return { spec: res.map((x) => x / sum), size: r * 2 + 1 };
};
const LANCZOS = (a, scale = 2) => {
assert(a > 0, `invalid coefficient: ${a}`);
const r = Math.ceil(a * scale);
const res = [];
let sum = 0;
for (let y = -r; y <= r; y++) {
const yy = y / scale;
const ly = lanczos(a, yy);
for (let x = -r; x <= r; x++) {
const m = Math.hypot(x / scale, yy);
const l = m < a ? ly * lanczos(a, x / scale) : 0;
res.push(l);
sum += l;
}
}
return { spec: res.map((x) => x / sum), size: r * 2 + 1 };
};
const UNSHARP_MASK5 = {
// prettier-ignore
spec: [
-1 / 256,
-1 / 64,
-3 / 128,
-1 / 64,
-1 / 256,
-1 / 64,
-1 / 16,
-3 / 32,
-1 / 16,
-1 / 64,
-3 / 128,
-3 / 32,
119 / 64,
-3 / 32,
-3 / 128,
-1 / 64,
-1 / 16,
-3 / 32,
-1 / 16,
-1 / 64,
-1 / 256,
-1 / 64,
-3 / 128,
-1 / 64,
-1 / 256
],
size: 5
};
const { min, max } = Math;
const MAXIMA4_CROSS = {
fn: (src) => {
const {
data: pix,
stride: [stride, rowStride]
} = src;
const maxX = (src.width - 1) * stride;
const maxY = (src.height - 1) * rowStride;
return (x, y, channel) => {
const x0 = max(x - stride, channel);
const x2 = min(x + stride, maxX + channel);
const y0 = max(y - rowStride, 0);
const y2 = min(y + rowStride, maxY);
const c = pix[x + y];
return c > pix[y + x0] && c > pix[y + x2] || c > pix[y0 + x] && c > pix[y2 + x] ? 1 : 0;
};
},
size: 3
};
const MAXIMA4_DIAG = {
fn: (src) => {
const {
data: pix,
stride: [stride, rowStride]
} = src;
const maxX = (src.width - 1) * stride;
const maxY = (src.height - 1) * rowStride;
return (x, y, channel) => {
const x0 = max(x - stride, channel);
const x2 = min(x + stride, maxX + channel);
const y0 = max(y - rowStride, 0);
const y2 = min(y + rowStride, maxY);
const c = pix[x + y];
return c > pix[y0 + x0] && c > pix[y2 + x2] || c > pix[y0 + x2] && c > pix[y2 + x0] ? 1 : 0;
};
},
size: 3
};
const MAXIMA8 = {
fn: (src) => {
const {
data: pix,
stride: [stride, rowStride]
} = src;
const maxX = (src.width - 1) * stride;
const maxY = (src.height - 1) * rowStride;
return (x, y, channel) => {
const x0 = max(x - stride, channel);
const x2 = min(x + stride, maxX + channel);
const y0 = max(y - rowStride, 0);
const y2 = min(y + rowStride, maxY);
const c = pix[x + y];
return c > pix[y + x0] && c > pix[y + x2] || c > pix[y0 + x] && c > pix[y2 + x] || c > pix[y0 + x0] && c > pix[y2 + x2] || c > pix[y0 + x2] && c > pix[y2 + x0] ? 1 : 0;
};
},
size: 3
};
export {
BOX_BLUR3,
BOX_BLUR5,
EDGE3,
EDGE5,
GAUSSIAN,
GAUSSIAN_BLUR3,
GAUSSIAN_BLUR5,
HIGHPASS3,
LANCZOS,
MAXIMA4_CROSS,
MAXIMA4_DIAG,
MAXIMA8,
POOL_MAX,
POOL_MEAN,
POOL_MIN,
POOL_NEAREST,
POOL_THRESHOLD,
SHARPEN3,
SOBEL_X,
SOBEL_Y,
UNSHARP_MASK5,
convolveChannel,
convolveImage,
defKernel,
defLargeKernel
};