UNPKG

@thi.ng/pixel-convolve

Version:

Extensible bitmap image convolution, kernel presets, normal map & image pyramid generation

486 lines (485 loc) 10.7 kB
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 };