UNPKG

image-js

Version:

Image processing and manipulation in JavaScript

166 lines 7.37 kB
import { BorderType as ConvolutionBorderType, DirectConvolution, } from 'ml-convolution'; import { Image } from '../Image.js'; import { extendBorders } from '../operations/extendBorders.js'; import { getClamp } from '../utils/clamp.js'; import { getDefaultColor } from "../utils/getDefaultColor.js"; import { getIndex } from '../utils/getIndex.js'; import { getOutputImage } from '../utils/getOutputImage.js'; import { getBorderInterpolation } from '../utils/interpolateBorder.js'; import { round } from '../utils/round.js'; import { validateColor } from "../utils/validators/validators.js"; /** * Apply a direct convolution on an image using the specified kernel. The convolution corresponds of a weighted average of the surrounding pixels, the weights being defined in the kernel. * @param image - The image to process. * @param kernel - Kernel to use for the convolution. Should be a 2D matrix with odd number of rows and columns. * @param options - Convolution options. * @returns The convoluted image. */ export function directConvolution(image, kernel, options = {}) { const { borderType = 'reflect101', borderValue = 0 } = options; const convolutedData = rawDirectConvolution(image, kernel, { borderType, borderValue, }); const newImage = getOutputImage(image, options); const clamp = getClamp(newImage); for (let i = 0; i < image.size; i++) { for (let channel = 0; channel < image.channels; channel++) { const dataIndex = i * image.channels + channel; const newValue = round(clamp(convolutedData[dataIndex])); newImage.setValueByIndex(i, channel, newValue); } } return newImage; } /** * Compute direct convolution of an image and return an array with the raw values. * @param image - Image to process. * @param kernel - 2D kernel used for the convolution. * @param options - Convolution options. * @returns Array with the raw convoluted values. */ export function rawDirectConvolution(image, kernel, options = {}) { const { borderType = 'reflect101', borderValue = getDefaultColor(image) } = options; if (Array.isArray(borderValue)) { validateColor(borderValue, image); } const interpolateBorder = getBorderInterpolation(borderType, borderValue); const result = new Float64Array(image.size * image.channels); for (let channel = 0; channel < image.channels; channel++) { for (let row = 0; row < image.height; row++) { for (let column = 0; column < image.width; column++) { const index = getIndex(column, row, image, channel); result[index] = computeConvolutionValue(column, row, channel, image, kernel, interpolateBorder, { returnRawValue: true }); } } } return result; } /** * Compute the separable convolution of an image. * @param image - Image to convolute. * @param kernelX - Kernel along x axis. * @param kernelY - Kernel along y axis. * @param options - Convolution options. * @returns The convoluted image. */ export function separableConvolution(image, kernelX, kernelY, options = {}) { const { normalize, borderType = 'reflect101', borderValue = 0 } = options; if (normalize) { [kernelX, kernelY] = normalizeSeparatedKernel(kernelX, kernelY); } const doubleKernelOffsetX = kernelX.length - 1; const kernelOffsetX = doubleKernelOffsetX / 2; const doubleKernelOffsetY = kernelY.length - 1; const kernelOffsetY = doubleKernelOffsetY / 2; const extendedImage = extendBorders(image, { horizontal: kernelOffsetX, vertical: kernelOffsetY, borderType, borderValue, }); const newImage = Image.createFrom(image); const clamp = getClamp(newImage); const rowConvolution = new DirectConvolution(extendedImage.width, kernelX, ConvolutionBorderType.CUT); const columnConvolution = new DirectConvolution(extendedImage.height, kernelY, ConvolutionBorderType.CUT); const rowData = new Float64Array(extendedImage.width); const columnData = new Float64Array(extendedImage.height); const convolvedData = new Float64Array( // Use `image.width` because convolution with BorderType.CUT reduces the size of the convolved data. image.width * extendedImage.height); for (let channel = 0; channel < extendedImage.channels; channel++) { for (let row = 0; row < extendedImage.height; row++) { for (let column = 0; column < extendedImage.width; column++) { rowData[column] = extendedImage.getValue(column, row, channel); } const convolvedRow = rowConvolution.convolve(rowData); for (let column = 0; column < image.width; column++) { convolvedData[row * image.width + column] = convolvedRow[column]; } } for (let column = 0; column < image.width; column++) { for (let row = 0; row < extendedImage.height; row++) { columnData[row] = convolvedData[row * image.width + column]; } const convolvedColumn = columnConvolution.convolve(columnData); for (let row = 0; row < image.height; row++) { newImage.setValue(column, row, channel, round(clamp(convolvedColumn[row]))); } } } return newImage; } /** * Compute the convolution of a value of a pixel in an image. * @param column - Column of the pixel. * @param row - Row of the pixel. * @param channel - Channel to process. * @param image - Image to process. * @param kernel - Kernel for the convolutions. * @param interpolateBorder - Function to interpolate the border pixels. * @param options - Compute convolution value options. * @returns The convoluted value. */ export function computeConvolutionValue(column, row, channel, image, kernel, interpolateBorder, options = {}) { let { clamp } = options; const { returnRawValue = false } = options; if (returnRawValue) { clamp = undefined; } let val = 0; const kernelWidth = kernel[0].length; const kernelHeight = kernel.length; const kernelOffsetX = (kernelWidth - 1) / 2; const kernelOffsetY = (kernelHeight - 1) / 2; for (let kY = 0; kY < kernelHeight; kY++) { for (let kX = 0; kX < kernelWidth; kX++) { const kernelValue = kernel[kY][kX]; val += kernelValue * interpolateBorder(column + kX - kernelOffsetX, row + kY - kernelOffsetY, channel, image); } } if (!clamp) { return val; } else { return round(clamp(val)); } } /** * Normalize a separated kernel. * @param kernelX - Horizontal component of the separated kernel. * @param kernelY - Vertical component of the separated kernel. * @returns The normalized kernel. */ function normalizeSeparatedKernel(kernelX, kernelY) { const sumKernelX = kernelX.reduce((prev, current) => prev + current, 0); const sumKernelY = kernelY.reduce((prev, current) => prev + current, 0); const prod = sumKernelX * sumKernelY; if (prod < 0) { throw new RangeError('this separated kernel cannot be normalized'); } const factor = 1 / Math.sqrt(Math.abs(prod)); return [kernelX.map((v) => v * factor), kernelY.map((v) => v * factor)]; } //# sourceMappingURL=convolution.js.map