UNPKG

image-js

Version:

Image processing and manipulation in JavaScript

144 lines (141 loc) 5.27 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = convolution; var _mlMatrixConvolution = require("ml-matrix-convolution"); var _channel = require("../../util/channel"); var _kernel = require("../../util/kernel"); var _Image = _interopRequireDefault(require("../Image")); var _clamp = require("../internal/clamp"); var _convolutionSeparable = _interopRequireDefault(require("./convolutionSeparable")); var _getSeparatedKernel = _interopRequireDefault(require("./getSeparatedKernel")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } /** * @memberof Image * @instance * @param {Array<Array<number>>} kernel * @param {object} [options] - options * @param {Array} [options.channels] - Array of channels to treat. Defaults to all channels * @param {number} [options.bitDepth=this.bitDepth] - A new bit depth can be specified. This allows to use 32 bits to avoid clamping of floating-point numbers. * @param {boolean} [options.normalize=false] * @param {number} [options.divisor=1] * @param {string} [options.border='copy'] * @param {string} [options.algorithm='auto'] - Either 'auto', 'direct', 'fft' or 'separable'. fft is much faster for large kernel. * If the separable algorithm is used, one must provide as kernel an array of two 1D kernels. * The 'auto' option will try to separate the kernel if that is possible. * @return {Image} */ function convolution(kernel, options = {}) { let { channels, bitDepth, normalize = false, divisor = 1, border = 'copy', algorithm = 'auto' } = options; let createOptions = {}; if (bitDepth) createOptions.bitDepth = bitDepth; let newImage = _Image.default.createFrom(this, createOptions); channels = (0, _channel.validateArrayOfChannels)(this, channels, true); if (algorithm !== 'separable') { ({ kernel } = (0, _kernel.validateKernel)(kernel)); } else if (!Array.isArray(kernel) || kernel.length !== 2) { throw new RangeError('separable convolution requires two arrays of numbers to represent the kernel'); } if (algorithm === 'auto') { let separatedKernel = (0, _getSeparatedKernel.default)(kernel); if (separatedKernel !== null) { algorithm = 'separable'; kernel = separatedKernel; } else if ((kernel.length > 9 || kernel[0].length > 9) && this.width <= 4096 && this.height <= 4096) { algorithm = 'fft'; } else { algorithm = 'direct'; } } let halfHeight, halfWidth; if (algorithm === 'separable') { halfHeight = Math.floor(kernel[0].length / 2); halfWidth = Math.floor(kernel[1].length / 2); } else { halfHeight = Math.floor(kernel.length / 2); halfWidth = Math.floor(kernel[0].length / 2); } let clamped = newImage.isClamped; let tmpData = new Array(this.height * this.width); let index, x, y, channel, c, tmpResult; for (channel = 0; channel < channels.length; channel++) { c = channels[channel]; // Copy the channel in a single array for (y = 0; y < this.height; y++) { for (x = 0; x < this.width; x++) { index = y * this.width + x; tmpData[index] = this.data[index * this.channels + c]; } } if (algorithm === 'direct') { tmpResult = (0, _mlMatrixConvolution.direct)(tmpData, kernel, { rows: this.height, cols: this.width, normalize: normalize, divisor: divisor }); } else if (algorithm === 'separable') { tmpResult = (0, _convolutionSeparable.default)(tmpData, kernel, this.width, this.height); if (normalize) { divisor = 0; for (let i = 0; i < kernel[0].length; i++) { for (let j = 0; j < kernel[1].length; j++) { divisor += kernel[0][i] * kernel[1][j]; } } } if (divisor !== 1) { for (let i = 0; i < tmpResult.length; i++) { tmpResult[i] /= divisor; } } } else { tmpResult = (0, _mlMatrixConvolution.fft)(tmpData, kernel, { rows: this.height, cols: this.width, normalize: normalize, divisor: divisor }); } // Copy the result to the output image for (y = 0; y < this.height; y++) { for (x = 0; x < this.width; x++) { index = y * this.width + x; if (clamped) { newImage.data[index * this.channels + c] = (0, _clamp.clamp)(tmpResult[index], newImage); } else { newImage.data[index * this.channels + c] = tmpResult[index]; } } } } // if the kernel was not applied on the alpha channel we just copy it // TODO: in general we should copy the channels that where not changed // TODO: probably we should just copy the image at the beginning ? if (this.alpha && !channels.includes(this.channels)) { for (x = this.components; x < this.data.length; x = x + this.channels) { newImage.data[x] = this.data[x]; } } // I only can have 3 types of borders: // 1. Considering the image as periodic: periodic // 2. Extend the interior borders: copy // 3. fill with a color: set if (border !== 'periodic') { newImage.setBorder({ size: [halfWidth, halfHeight], algorithm: border }); } return newImage; }