image-js
Version:
Image processing and manipulation in JavaScript
166 lines • 7.37 kB
JavaScript
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