UNPKG

voluptasquisquam

Version:

Image processing and manipulation in JavaScript

1,734 lines (1,536 loc) 909 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.IJS = factory()); }(this, (function () { 'use strict'; // Shortcuts for common image kinds var BINARY = 'BINARY'; var GREY = 'GREY'; var GREYA = 'GREYA'; var RGB = 'RGB'; var RGBA = 'RGBA'; var CMYK = 'CMYK'; var CMYKA = 'CMYKA'; var RGB$1 = 'RGB'; var HSL = 'HSL'; var HSV = 'HSV'; var CMYK$1 = 'CMYK'; var GREY$1 = 'GREY'; var kinds = {}; kinds[BINARY] = { components: 1, alpha: 0, bitDepth: 1, colorModel: GREY$1 }; kinds[GREYA] = { components: 1, alpha: 1, bitDepth: 8, colorModel: GREY$1 }; kinds[GREY] = { components: 1, alpha: 0, bitDepth: 8, colorModel: GREY$1 }; kinds[RGBA] = { components: 3, alpha: 1, bitDepth: 8, colorModel: RGB$1 }; kinds[RGB] = { components: 3, alpha: 0, bitDepth: 8, colorModel: RGB$1 }; kinds[CMYK] = { components: 4, alpha: 0, bitDepth: 8, colorModel: CMYK$1 }; kinds[CMYKA] = { components: 4, alpha: 1, bitDepth: 8, colorModel: CMYK$1 }; function getKind(kind) { return kinds[kind]; } function getTheoreticalPixelArraySize(image) { var length = image.channels * image.size; if (image.bitDepth === 1) { length = Math.ceil(length / 8); } return length; } function createPixelArray(image) { var length = image.channels * image.size; var arr = void 0; switch (image.bitDepth) { case 1: arr = new Uint8Array(Math.ceil(length / 8)); break; case 8: arr = new Uint8ClampedArray(length); break; case 16: arr = new Uint16Array(length); break; case 32: arr = new Float32Array(length); break; default: throw new Error('Cannot create pixel array for bit depth ' + image.bitDepth); } // alpha channel is 100% by default if (image.alpha) { for (var i = image.components; i < arr.length; i += image.channels) { arr[i] = image.maxValue; } } return arr; } var env = 'browser'; var ImageData = self.ImageData; var DOMImage = self.Image; var origin = self.location.origin; function isDifferentOrigin(url) { try { var parsedURL = new URL(url); return parsedURL.origin !== origin; } catch (e) { // may be a relative URL. In this case, it cannot be parsed but is effectively from same origin return false; } } function Canvas(width, height) { var canvas = self.document.createElement('canvas'); canvas.width = width; canvas.height = height; return canvas; } function fetchBinary(url, { withCredentials = false } = {}) { return new Promise(function (resolve, reject) { var xhr = new self.XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'arraybuffer'; xhr.withCredentials = withCredentials; xhr.onload = function (e) { if (this.status !== 200) reject(e);else resolve(this.response); }; xhr.onerror = reject; xhr.send(); }); } function createWriteStream() { throw new Error('createWriteStream does not exist in the browser'); } function writeFile() { throw new Error('writeFile does not exist in the browser'); } function invert() { this.checkProcessable('invert', { bitDepth: [1, 8, 16] }); if (this.bitDepth === 1) { // we simply invert all the integers value // there could be a small mistake if the number of points // is not a multiple of 8 but it is not important var data = this.data; for (var i = 0; i < data.length; i++) { data[i] = ~data[i]; } } else { for (var x = 0; x < this.width; x++) { for (var y = 0; y < this.height; y++) { for (var k = 0; k < this.components; k++) { var value = this.getValueXY(x, y, k); this.setValueXY(x, y, k, this.maxValue - value); } } } } } function invertIterator() { this.checkProcessable('invert', { bitDepth: [1, 8, 16] }); if (this.bitDepth === 1) { // we simply invert all the integers value // there could be a small mistake if the number of points // is not a multiple of 8 but it is not important var data = this.data; for (var i = 0; i < data.length; i++) { data[i] = ~data[i]; } } else { for (var _ref of this.pixels()) { var index = _ref.index; var pixel = _ref.pixel; for (var k = 0; k < this.components; k++) { this.setValue(index, k, this.maxValue - pixel[k]); } } } } function invertOneLoop() { this.checkProcessable('invertOneLoop', { bitDepth: [8, 16] }); var data = this.data; for (var i = 0; i < data.length; i += this.channels) { for (var j = 0; j < this.components; j++) { data[i + j] = this.maxValue - data[i + j]; } } } // this code gives the same result as invert() // but is based on a matrix of pixels // may be easier to implement some algorithm // but it will likely be much slower function invertPixel() { this.checkProcessable('invertPixel', { bitDepth: [8, 16] }); for (var x = 0; x < this.width; x++) { for (var y = 0; y < this.height; y++) { var value = this.getPixelXY(x, y); for (var k = 0; k < this.components; k++) { value[k] = this.maxValue - value[k]; } this.setPixelXY(x, y, value); } } } // this code gives the same result as invert() // but is based on a matrix of pixels // may be easier to implement some algorithm // but it will likely be much slower // this method is 50 times SLOWER than invert !!!!!! function invertApply() { if (this.bitDepth === 1) { // we simply invert all the integers value // there could be a small mistake if the number of points // is not a multiple of 8 but it is not important var data = this.data; for (var i = 0; i < data.length; i++) { data[i] = ~data[i]; } } else { this.checkProcessable('invertApply', { bitDepth: [8, 16] }); this.apply(function (index) { for (var k = 0; k < this.components; k++) { this.data[index + k] = this.maxValue - this.data[index + k]; } }); } } function invertBinaryLoop() { this.checkProcessable('invertBinaryLoop', { bitDepth: [1] }); for (var i = 0; i < this.size; i++) { this.toggleBit(i); } } function validateArrayOfChannels(image, options = {}) { var channels = options.channels, allowAlpha = options.allowAlpha, defaultAlpha = options.defaultAlpha; if (typeof allowAlpha !== 'boolean') { allowAlpha = true; } if (typeof channels === 'undefined') { return allChannels(image, defaultAlpha); } else { return validateChannels(image, channels, allowAlpha); } } function allChannels(image, defaultAlpha) { var length = defaultAlpha ? image.channels : image.components; var array = new Array(length); for (var i = 0; i < length; i++) { array[i] = i; } return array; } function validateChannels(image, channels, allowAlpha) { if (!Array.isArray(channels)) { channels = [channels]; } for (var c = 0; c < channels.length; c++) { channels[c] = validateChannel(image, channels[c], allowAlpha); } return channels; } function validateChannel(image, channel, allowAlpha = true) { if (channel === undefined) { throw new RangeError('validateChannel : the channel has to be >=0 and <' + image.channels); } if (typeof channel === 'string') { switch (image.colorModel) { case GREY$1: break; case RGB$1: if ('rgb'.includes(channel)) { switch (channel) { case 'r': channel = 0; break; case 'g': channel = 1; break; case 'b': channel = 2; break; // no default } } break; case HSL: if ('hsl'.includes(channel)) { switch (channel) { case 'h': channel = 0; break; case 's': channel = 1; break; case 'l': channel = 2; break; // no default } } break; case HSV: if ('hsv'.includes(channel)) { switch (channel) { case 'h': channel = 0; break; case 's': channel = 1; break; case 'v': channel = 2; break; // no default } } break; case CMYK$1: if ('cmyk'.includes(channel)) { switch (channel) { case 'c': channel = 0; break; case 'm': channel = 1; break; case 'y': channel = 2; break; case 'k': channel = 3; break; // no default } } break; default: throw new Error(`Unexpected color model: ${image.colorModel}`); } if (channel === 'a') { if (!image.alpha) { throw new Error('validateChannel : the image does not contain alpha channel'); } channel = image.components; } if (typeof channel === 'string') { throw new Error('validateChannel : undefined channel: ' + channel); } } if (channel >= image.channels) { throw new RangeError('validateChannel : the channel has to be >=0 and <' + image.channels); } if (!allowAlpha && channel >= image.components) { throw new RangeError('validateChannel : alpha channel may not be selected'); } return channel; } // we try the faster methods /** * Invert an image. The image * @memberof Image * @instance * @param {object} options * @param {(undefined|number|string|[number]|[string])} [options.channels=undefined] Specify which channels should be processed * * undefined : we take all the channels but alpha * * number : this specific channel * * string : converted to a channel based on rgb, cmyk, hsl or hsv (one letter code) * * [number] : array of channels as numbers * * [string] : array of channels as one letter string * @return {this} */ function invert$1(options = {}) { var channels = options.channels; this.checkProcessable('invertOneLoop', { bitDepth: [1, 8, 16] }); if (this.bitDepth === 1) { // we simply invert all the integers value // there could be a small mistake if the number of points // is not a multiple of 8 but it is not important var data = this.data; for (var i = 0; i < data.length; i++) { data[i] = ~data[i]; } } else { channels = validateArrayOfChannels(this, { channels }); for (var c = 0; c < channels.length; c++) { var j = channels[c]; for (var _i = j; _i < this.data.length; _i += this.channels) { this.data[_i] = this.maxValue - this.data[_i]; } } } return this; } /** * Flip an image horizontally. * @memberof Image * @instance * @return {this} */ function flipX() { this.checkProcessable('flipX', { bitDepth: [8, 16] }); for (var i = 0; i < this.height; i++) { var offsetY = i * this.width * this.channels; for (var j = 0; j < Math.floor(this.width / 2); j++) { var posCurrent = j * this.channels + offsetY; var posOpposite = (this.width - j - 1) * this.channels + offsetY; for (var k = 0; k < this.channels; k++) { var tmp = this.data[posCurrent + k]; this.data[posCurrent + k] = this.data[posOpposite + k]; this.data[posOpposite + k] = tmp; } } } return this; } /** * Flip an image vertically. The image * @memberof Image * @instance * @return {this} */ function flipY() { this.checkProcessable('flipY', { bitDepth: [8, 16] }); for (var i = 0; i < Math.floor(this.height / 2); i++) { for (var j = 0; j < this.width; j++) { var posCurrent = j * this.channels + i * this.width * this.channels; var posOpposite = j * this.channels + (this.height - 1 - i) * this.channels * this.width; for (var k = 0; k < this.channels; k++) { var tmp = this.data[posCurrent + k]; this.data[posCurrent + k] = this.data[posOpposite + k]; this.data[posOpposite + k] = tmp; } } } return this; } /** * @memberof Image * @instance * @param {Array<Array<number>>} kernel * @param {object} [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'] * @return {Image} */ function convolutionFft(kernel, options = {}) { options = Object.assign({}, options); options.algorithm = 'fft'; return this.convolution(kernel, options); } /** * Apply a filter to blur the image * @memberof Image * @instance * @param {object} options * @param {number} [options.radius=1] : number of pixels around the current pixel to average * @return {Image} */ // first release of mean filter function blurFilter(options = {}) { var _options$radius = options.radius, radius = _options$radius === undefined ? 1 : _options$radius; this.checkProcessable('meanFilter', { components: [1], bitDepth: [8, 16] }); if (radius < 1) { throw new Error('Number of neighbors should be grater than 0'); } var n = 2 * radius + 1; var size = n * n; var kernel = new Array(size); for (var i = 0; i < kernel.length; i++) { kernel[i] = 1; } return convolutionFft.call(this, kernel); } var index$2 = Number.isNaN || function (x) { return x !== x; }; function assertNum(x) { if (typeof x !== 'number' || index$2(x)) { throw new TypeError('Expected a number'); } } var asc = function (a, b) { assertNum(a); assertNum(b); return a - b; }; /** * Each pixel of the image becomes the median of the neightbour * pixels. * @memberof Image * @instance * @param {object} options * @param {(undefined|number|string|[number]|[string])} [options.channels=undefined] Specify which channels should be processed * * undefined : we take all the channels but alpha * * number : this specific channel * * string : converted to a channel based on rgb, cmyk, hsl or hsv (one letter code) * * [number] : array of channels as numbers * * [string] : array of channels as one letter string * @param {number} [options.radius=1] distance of the square to take the mean of. * @param {string} [options.border='copy'] algorithm that will be applied after to deal with borders * @return {Image} */ function medianFilter(options = {}) { var _options$radius = options.radius, radius = _options$radius === undefined ? 1 : _options$radius, channels = options.channels, _options$border = options.border, border = _options$border === undefined ? 'copy' : _options$border; this.checkProcessable('median', { bitDepth: [8, 16] }); if (radius < 1) { throw new Error('Kernel radius should be greater than 0'); } channels = validateArrayOfChannels(this, channels, true); var kWidth = radius; var kHeight = radius; var newImage = Image$2.createFrom(this); var size = (kWidth * 2 + 1) * (kHeight * 2 + 1); var middle = Math.floor(size / 2); var kernel = new Array(size); for (var channel = 0; channel < channels.length; channel++) { var c = channels[channel]; for (var y = kHeight; y < this.height - kHeight; y++) { for (var x = kWidth; x < this.width - kWidth; x++) { var n = 0; for (var j = -kHeight; j <= kHeight; j++) { for (var i = -kWidth; i <= kWidth; i++) { var _index = ((y + j) * this.width + x + i) * this.channels + c; kernel[n++] = this.data[_index]; } } var index$$1 = (y * this.width + x) * this.channels + c; var newValue = kernel.sort(asc)[middle]; newImage.data[index$$1] = newValue; } } } if (this.alpha && !channels.includes(this.channels)) { for (var _i = this.components; _i < this.data.length; _i = _i + this.channels) { newImage.data[_i] = this.data[_i]; } } newImage.setBorder({ size: [kWidth, kHeight], algorithm: border }); return newImage; } //End median function var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function commonjsRequire () { throw new Error('Dynamic requires are not currently supported by rollup-plugin-commonjs'); } function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } var fftlib = createCommonjsModule(function (module, exports) { /** * Fast Fourier Transform module * 1D-FFT/IFFT, 2D-FFT/IFFT (radix-2) */ var FFT = (function(){ var FFT; { FFT = exports; // for CommonJS } var version = { release: '0.3.0', date: '2013-03' }; FFT.toString = function() { return "version " + version.release + ", released " + version.date; }; // core operations var _n = 0, // order _bitrev = null, // bit reversal table _cstb = null; // sin/cos table var core = { init : function(n) { if(n !== 0 && (n & (n - 1)) === 0) { _n = n; core._initArray(); core._makeBitReversalTable(); core._makeCosSinTable(); } else { throw new Error("init: radix-2 required"); } }, // 1D-FFT fft1d : function(re, im) { core.fft(re, im, 1); }, // 1D-IFFT ifft1d : function(re, im) { var n = 1/_n; core.fft(re, im, -1); for(var i=0; i<_n; i++) { re[i] *= n; im[i] *= n; } }, // 1D-IFFT bt1d : function(re, im) { core.fft(re, im, -1); }, // 2D-FFT Not very useful if the number of rows have to be equal to cols fft2d : function(re, im) { var tre = [], tim = [], i = 0; // x-axis for(var y=0; y<_n; y++) { i = y*_n; for(var x1=0; x1<_n; x1++) { tre[x1] = re[x1 + i]; tim[x1] = im[x1 + i]; } core.fft1d(tre, tim); for(var x2=0; x2<_n; x2++) { re[x2 + i] = tre[x2]; im[x2 + i] = tim[x2]; } } // y-axis for(var x=0; x<_n; x++) { for(var y1=0; y1<_n; y1++) { i = x + y1*_n; tre[y1] = re[i]; tim[y1] = im[i]; } core.fft1d(tre, tim); for(var y2=0; y2<_n; y2++) { i = x + y2*_n; re[i] = tre[y2]; im[i] = tim[y2]; } } }, // 2D-IFFT ifft2d : function(re, im) { var tre = [], tim = [], i = 0; // x-axis for(var y=0; y<_n; y++) { i = y*_n; for(var x1=0; x1<_n; x1++) { tre[x1] = re[x1 + i]; tim[x1] = im[x1 + i]; } core.ifft1d(tre, tim); for(var x2=0; x2<_n; x2++) { re[x2 + i] = tre[x2]; im[x2 + i] = tim[x2]; } } // y-axis for(var x=0; x<_n; x++) { for(var y1=0; y1<_n; y1++) { i = x + y1*_n; tre[y1] = re[i]; tim[y1] = im[i]; } core.ifft1d(tre, tim); for(var y2=0; y2<_n; y2++) { i = x + y2*_n; re[i] = tre[y2]; im[i] = tim[y2]; } } }, // core operation of FFT fft : function(re, im, inv) { var d, h, ik, m, tmp, wr, wi, xr, xi, n4 = _n >> 2; // bit reversal for(var l=0; l<_n; l++) { m = _bitrev[l]; if(l < m) { tmp = re[l]; re[l] = re[m]; re[m] = tmp; tmp = im[l]; im[l] = im[m]; im[m] = tmp; } } // butterfly operation for(var k=1; k<_n; k<<=1) { h = 0; d = _n/(k << 1); for(var j=0; j<k; j++) { wr = _cstb[h + n4]; wi = inv*_cstb[h]; for(var i=j; i<_n; i+=(k<<1)) { ik = i + k; xr = wr*re[ik] + wi*im[ik]; xi = wr*im[ik] - wi*re[ik]; re[ik] = re[i] - xr; re[i] += xr; im[ik] = im[i] - xi; im[i] += xi; } h += d; } } }, // initialize the array (supports TypedArray) _initArray : function() { if(typeof Uint32Array !== 'undefined') { _bitrev = new Uint32Array(_n); } else { _bitrev = []; } if(typeof Float64Array !== 'undefined') { _cstb = new Float64Array(_n*1.25); } else { _cstb = []; } }, // zero padding _paddingZero : function() { // TODO }, // makes bit reversal table _makeBitReversalTable : function() { var i = 0, j = 0, k = 0; _bitrev[0] = 0; while(++i < _n) { k = _n >> 1; while(k <= j) { j -= k; k >>= 1; } j += k; _bitrev[i] = j; } }, // makes trigonometiric function table _makeCosSinTable : function() { var n2 = _n >> 1, n4 = _n >> 2, n8 = _n >> 3, n2p4 = n2 + n4, t = Math.sin(Math.PI/_n), dc = 2*t*t, ds = Math.sqrt(dc*(2 - dc)), c = _cstb[n4] = 1, s = _cstb[0] = 0; t = 2*dc; for(var i=1; i<n8; i++) { c -= dc; dc += t*c; s += ds; ds -= t*s; _cstb[i] = s; _cstb[n4 - i] = c; } if(n8 !== 0) { _cstb[n8] = Math.sqrt(0.5); } for(var j=0; j<n4; j++) { _cstb[n2 - j] = _cstb[j]; } for(var k=0; k<n2p4; k++) { _cstb[k + n2] = -_cstb[k]; } } }; // aliases (public APIs) var apis = ['init', 'fft1d', 'ifft1d', 'fft2d', 'ifft2d']; for(var i=0; i<apis.length; i++) { FFT[apis[i]] = core[apis[i]]; } FFT.bt = core.bt1d; FFT.fft = core.fft1d; FFT.ifft = core.ifft1d; return FFT; }).call(commonjsGlobal); }); var FFTUtils$2= { DEBUG : false, /** * Calculates the inverse of a 2D Fourier transform * * @param ft * @param ftRows * @param ftCols * @return */ ifft2DArray : function(ft, ftRows, ftCols){ var tempTransform = new Array(ftRows * ftCols); var nRows = ftRows / 2; var nCols = (ftCols - 1) * 2; // reverse transform columns fftlib.init(nRows); var tmpCols = {re: new Array(nRows), im: new Array(nRows)}; for (var iCol = 0; iCol < ftCols; iCol++) { for (var iRow = nRows - 1; iRow >= 0; iRow--) { tmpCols.re[iRow] = ft[(iRow * 2) * ftCols + iCol]; tmpCols.im[iRow] = ft[(iRow * 2 + 1) * ftCols + iCol]; } //Unnormalized inverse transform fftlib.bt(tmpCols.re, tmpCols.im); for (var iRow = nRows - 1; iRow >= 0; iRow--) { tempTransform[(iRow * 2) * ftCols + iCol] = tmpCols.re[iRow]; tempTransform[(iRow * 2 + 1) * ftCols + iCol] = tmpCols.im[iRow]; } } // reverse row transform var finalTransform = new Array(nRows * nCols); fftlib.init(nCols); var tmpRows = {re: new Array(nCols), im: new Array(nCols)}; var scale = nCols * nRows; for (var iRow = 0; iRow < ftRows; iRow += 2) { tmpRows.re[0] = tempTransform[iRow * ftCols]; tmpRows.im[0] = tempTransform[(iRow + 1) * ftCols]; for (var iCol = 1; iCol < ftCols; iCol++) { tmpRows.re[iCol] = tempTransform[iRow * ftCols + iCol]; tmpRows.im[iCol] = tempTransform[(iRow + 1) * ftCols + iCol]; tmpRows.re[nCols - iCol] = tempTransform[iRow * ftCols + iCol]; tmpRows.im[nCols - iCol] = -tempTransform[(iRow + 1) * ftCols + iCol]; } //Unnormalized inverse transform fftlib.bt(tmpRows.re, tmpRows.im); var indexB = (iRow / 2) * nCols; for (var iCol = nCols - 1; iCol >= 0; iCol--) { finalTransform[indexB + iCol] = tmpRows.re[iCol] / scale; } } return finalTransform; }, /** * Calculates the fourier transform of a matrix of size (nRows,nCols) It is * assumed that both nRows and nCols are a power of two * * On exit the matrix has dimensions (nRows * 2, nCols / 2 + 1) where the * even rows contain the real part and the odd rows the imaginary part of the * transform * @param data * @param nRows * @param nCols * @return */ fft2DArray:function(data, nRows, nCols, opt) { var options = Object.assign({},{inplace:true}); var ftCols = (nCols / 2 + 1); var ftRows = nRows * 2; var tempTransform = new Array(ftRows * ftCols); fftlib.init(nCols); // transform rows var tmpRows = {re: new Array(nCols), im: new Array(nCols)}; var row1 = {re: new Array(nCols), im: new Array(nCols)}; var row2 = {re: new Array(nCols), im: new Array(nCols)}; var index, iRow0, iRow1, iRow2, iRow3; for (var iRow = 0; iRow < nRows / 2; iRow++) { index = (iRow * 2) * nCols; tmpRows.re = data.slice(index, index + nCols); index = (iRow * 2 + 1) * nCols; tmpRows.im = data.slice(index, index + nCols); fftlib.fft1d(tmpRows.re, tmpRows.im); this.reconstructTwoRealFFT(tmpRows, row1, row2); //Now lets put back the result into the output array iRow0 = (iRow * 4) * ftCols; iRow1 = (iRow * 4 + 1) * ftCols; iRow2 = (iRow * 4 + 2) * ftCols; iRow3 = (iRow * 4 + 3) * ftCols; for (var k = ftCols - 1; k >= 0; k--) { tempTransform[iRow0 + k] = row1.re[k]; tempTransform[iRow1 + k] = row1.im[k]; tempTransform[iRow2 + k] = row2.re[k]; tempTransform[iRow3 + k] = row2.im[k]; } } //console.log(tempTransform); row1 = null; row2 = null; // transform columns var finalTransform = new Array(ftRows * ftCols); fftlib.init(nRows); var tmpCols = {re: new Array(nRows), im: new Array(nRows)}; for (var iCol = ftCols - 1; iCol >= 0; iCol--) { for (var iRow = nRows - 1; iRow >= 0; iRow--) { tmpCols.re[iRow] = tempTransform[(iRow * 2) * ftCols + iCol]; tmpCols.im[iRow] = tempTransform[(iRow * 2 + 1) * ftCols + iCol]; //TODO Chech why this happens if(isNaN(tmpCols.re[iRow])){ tmpCols.re[iRow]=0; } if(isNaN(tmpCols.im[iRow])){ tmpCols.im[iRow]=0; } } fftlib.fft1d(tmpCols.re, tmpCols.im); for (var iRow = nRows - 1; iRow >= 0; iRow--) { finalTransform[(iRow * 2) * ftCols + iCol] = tmpCols.re[iRow]; finalTransform[(iRow * 2 + 1) * ftCols + iCol] = tmpCols.im[iRow]; } } //console.log(finalTransform); return finalTransform; }, /** * * @param fourierTransform * @param realTransform1 * @param realTransform2 * * Reconstructs the individual Fourier transforms of two simultaneously * transformed series. Based on the Symmetry relationships (the asterisk * denotes the complex conjugate) * * F_{N-n} = F_n^{*} for a purely real f transformed to F * * G_{N-n} = G_n^{*} for a purely imaginary g transformed to G * */ reconstructTwoRealFFT:function(fourierTransform, realTransform1, realTransform2) { var length = fourierTransform.re.length; // the components n=0 are trivial realTransform1.re[0] = fourierTransform.re[0]; realTransform1.im[0] = 0.0; realTransform2.re[0] = fourierTransform.im[0]; realTransform2.im[0] = 0.0; var rm, rp, im, ip, j; for (var i = length / 2; i > 0; i--) { j = length - i; rm = 0.5 * (fourierTransform.re[i] - fourierTransform.re[j]); rp = 0.5 * (fourierTransform.re[i] + fourierTransform.re[j]); im = 0.5 * (fourierTransform.im[i] - fourierTransform.im[j]); ip = 0.5 * (fourierTransform.im[i] + fourierTransform.im[j]); realTransform1.re[i] = rp; realTransform1.im[i] = im; realTransform1.re[j] = rp; realTransform1.im[j] = -im; realTransform2.re[i] = ip; realTransform2.im[i] = -rm; realTransform2.re[j] = ip; realTransform2.im[j] = rm; } }, /** * In place version of convolute 2D * * @param ftSignal * @param ftFilter * @param ftRows * @param ftCols * @return */ convolute2DI:function(ftSignal, ftFilter, ftRows, ftCols) { var re, im; for (var iRow = 0; iRow < ftRows / 2; iRow++) { for (var iCol = 0; iCol < ftCols; iCol++) { // re = ftSignal[(iRow * 2) * ftCols + iCol] * ftFilter[(iRow * 2) * ftCols + iCol] - ftSignal[(iRow * 2 + 1) * ftCols + iCol] * ftFilter[(iRow * 2 + 1) * ftCols + iCol]; im = ftSignal[(iRow * 2) * ftCols + iCol] * ftFilter[(iRow * 2 + 1) * ftCols + iCol] + ftSignal[(iRow * 2 + 1) * ftCols + iCol] * ftFilter[(iRow * 2) * ftCols + iCol]; // ftSignal[(iRow * 2) * ftCols + iCol] = re; ftSignal[(iRow * 2 + 1) * ftCols + iCol] = im; } } }, /** * * @param data * @param kernel * @param nRows * @param nCols * @returns {*} */ convolute:function(data, kernel, nRows, nCols, opt) { var ftSpectrum = new Array(nCols * nRows); for (var i = 0; i<nRows * nCols; i++) { ftSpectrum[i] = data[i]; } ftSpectrum = this.fft2DArray(ftSpectrum, nRows, nCols); var dimR = kernel.length; var dimC = kernel[0].length; var ftFilterData = new Array(nCols * nRows); for(var i = 0; i < nCols * nRows; i++) { ftFilterData[i] = 0; } var iRow, iCol; var shiftR = Math.floor((dimR - 1) / 2); var shiftC = Math.floor((dimC - 1) / 2); for (var ir = 0; ir < dimR; ir++) { iRow = (ir - shiftR + nRows) % nRows; for (var ic = 0; ic < dimC; ic++) { iCol = (ic - shiftC + nCols) % nCols; ftFilterData[iRow * nCols + iCol] = kernel[ir][ic]; } } ftFilterData = this.fft2DArray(ftFilterData, nRows, nCols); var ftRows = nRows * 2; var ftCols = nCols / 2 + 1; this.convolute2DI(ftSpectrum, ftFilterData, ftRows, ftCols); return this.ifft2DArray(ftSpectrum, ftRows, ftCols); }, toRadix2:function(data, nRows, nCols) { var i, j, irow, icol; var cols = nCols, rows = nRows, prows=0, pcols=0; if(!(nCols !== 0 && (nCols & (nCols - 1)) === 0)) { //Then we have to make a pading to next radix2 cols = 0; while((nCols>>++cols)!=0); cols=1<<cols; pcols = cols-nCols; } if(!(nRows !== 0 && (nRows & (nRows - 1)) === 0)) { //Then we have to make a pading to next radix2 rows = 0; while((nRows>>++rows)!=0); rows=1<<rows; prows = (rows-nRows)*cols; } if(rows==nRows&&cols==nCols)//Do nothing. Returns the same input!!! Be careful return {data:data, rows:nRows, cols:nCols}; var output = new Array(rows*cols); var shiftR = Math.floor((rows-nRows)/2)-nRows; var shiftC = Math.floor((cols-nCols)/2)-nCols; for( i = 0; i < rows; i++) { irow = i*cols; icol = ((i-shiftR) % nRows) * nCols; for( j = 0; j < cols; j++) { output[irow+j] = data[(icol+(j-shiftC) % nCols) ]; } } return {data:output, rows:rows, cols:cols}; }, /** * Crop the given matrix to fit the corresponding number of rows and columns */ crop:function(data, rows, cols, nRows, nCols, opt) { if(rows == nRows && cols == nCols)//Do nothing. Returns the same input!!! Be careful return data; var options = Object.assign({}, opt); var output = new Array(nCols*nRows); var shiftR = Math.floor((rows-nRows)/2); var shiftC = Math.floor((cols-nCols)/2); var destinyRow, sourceRow, i, j; for( i = 0; i < nRows; i++) { destinyRow = i*nCols; sourceRow = (i+shiftR)*cols; for( j = 0;j < nCols; j++) { output[destinyRow+j] = data[sourceRow+(j+shiftC)]; } } return output; } }; var FFTUtils_1 = FFTUtils$2; var FFTUtils$1 = FFTUtils_1; var FFT = fftlib; var index$6 = { FFTUtils: FFTUtils$1, FFT: FFT }; /** * Created by acastillo on 7/7/16. */ var FFTUtils = index$6.FFTUtils; function convolutionFFT(input, kernel, opt) { var tmp = matrix2Array(input); var inputData = tmp.data; var options = Object.assign({normalize : false, divisor : 1, rows:tmp.rows, cols:tmp.cols}, opt); var nRows, nCols; if (options.rows&&options.cols) { nRows = options.rows; nCols = options.cols; } else { throw new Error("Invalid number of rows or columns " + nRows + " " + nCols) } var divisor = options.divisor; var i,j; var kHeight = kernel.length; var kWidth = kernel[0].length; if (options.normalize) { divisor = 0; for (i = 0; i < kHeight; i++) for (j = 0; j < kWidth; j++) divisor += kernel[i][j]; } if (divisor === 0) { throw new RangeError('convolution: The divisor is equal to zero'); } var radix2Sized = FFTUtils.toRadix2(inputData, nRows, nCols); var conv = FFTUtils.convolute(radix2Sized.data, kernel, radix2Sized.rows, radix2Sized.cols); conv = FFTUtils.crop(conv, radix2Sized.rows, radix2Sized.cols, nRows, nCols); if(divisor!=0&&divisor!=1){ for(i=0;i<conv.length;i++){ conv[i]/=divisor; } } return conv; } function convolutionDirect(input, kernel, opt) { var tmp = matrix2Array(input); var inputData = tmp.data; var options = Object.assign({normalize : false, divisor : 1, rows:tmp.rows, cols:tmp.cols}, opt); var nRows, nCols; if (options.rows&&options.cols) { nRows = options.rows; nCols = options.cols; } else { throw new Error("Invalid number of rows or columns " + nRows + " " + nCols) } var divisor = options.divisor; var kHeight = kernel.length; var kWidth = kernel[0].length; var i, j, x, y, index, sum, kVal, row, col; if (options.normalize) { divisor = 0; for (i = 0; i < kHeight; i++) for (j = 0; j < kWidth; j++) divisor += kernel[i][j]; } if (divisor === 0) { throw new RangeError('convolution: The divisor is equal to zero'); } var output = new Array(nRows*nCols); var hHeight = Math.floor(kHeight/2); var hWidth = Math.floor(kWidth/2); for (y = 0; y < nRows; y++) { for (x = 0; x < nCols; x++) { sum = 0; for ( j = 0; j < kHeight; j++) { for ( i = 0; i < kWidth; i++) { kVal = kernel[kHeight - j - 1][kWidth - i - 1]; row = (y + j -hHeight + nRows) % nRows; col = (x + i - hWidth + nCols) % nCols; index = (row * nCols + col); sum += inputData[index] * kVal; } } index = (y * nCols + x); output[index]= sum / divisor; } } return output; } function LoG(sigma, nPoints, options){ var factor = 1000; if(options&&options.factor){ factor = options.factor; } var kernel = new Array(nPoints); var i,j,tmp,y2,tmp2; factor*=-1;//-1/(Math.PI*Math.pow(sigma,4)); var center = (nPoints-1)/2; var sigma2 = 2*sigma*sigma; for( i=0;i<nPoints;i++){ kernel[i]=new Array(nPoints); y2 = (i-center)*(i-center); for( j=0;j<nPoints;j++){ tmp = -((j-center)*(j-center)+y2)/sigma2; kernel[i][j]=Math.round(factor*(1+tmp)*Math.exp(tmp)); } } return kernel; } function matrix2Array(input){ var inputData=input; var nRows, nCols; if(typeof input[0]!="number"){ nRows = input.length; nCols = input[0].length; inputData = new Array(nRows*nCols); for(var i=0;i<nRows;i++){ for(var j=0;j<nCols;j++){ inputData[i*nCols+j]=input[i][j]; } } } else{ var tmp = Math.sqrt(input.length); if(Number.isInteger(tmp)){ nRows=tmp; nCols=tmp; } } return {data:inputData,rows:nRows,cols:nCols}; } var index$4 = { fft:convolutionFFT, direct:convolutionDirect, kernelFactory:{LoG:LoG}, matrix2Array:matrix2Array }; var index_1 = index$4.direct; var index_2 = index$4.fft; var index$9 = Number.isFinite || function (val) { return !(typeof val !== 'number' || index$2(val) || val === Infinity || val === -Infinity); }; // https://github.com/paulmillr/es6-shim // http://people.mozilla.org/~jorendorff/es6-draft.html#sec-number.isinteger var index$8 = Number.isInteger || function(val) { return typeof val === "number" && index$9(val) && Math.floor(val) === val; }; function validateKernel(kernel) { var kHeight = void 0, kWidth = void 0; if (Array.isArray(kernel)) { if (Array.isArray(kernel[0])) { // 2D array if ((kernel.length & 1) === 0 || (kernel[0].length & 1) === 0) { throw new RangeError('validateKernel: Kernel rows and columns should be odd numbers'); } else { kHeight = Math.floor(kernel.length / 2); kWidth = Math.floor(kernel[0].length / 2); } } else { var kernelWidth = Math.sqrt(kernel.length); if (index$8(kernelWidth)) { kWidth = kHeight = Math.floor(Math.sqrt(kernel.length) / 2); } else { throw new RangeError('validateKernel: Kernel array should be a square'); } // we convert the array to a matrix var newKernel = new Array(kernelWidth); for (var i = 0; i < kernelWidth; i++) { newKernel[i] = new Array(kernelWidth); for (var j = 0; j < kernelWidth; j++) { newKernel[i][j] = kernel[i * kernelWidth + j]; } } kernel = newKernel; } } else { throw new Error('validateKernel: Invalid Kernel: ' + kernel); } return { kernel, kWidth, kHeight }; } function directConvolution(input, kernel, output) { if (output === undefined) { const length = input.length + kernel.length - 1; output = new Array(length); } fill(output); for (var i = 0; i < input.length; i++) { for (var j = 0; j < kernel.length; j++) { output[i + j] += input[i] * kernel[j]; } } return output; } function fill(array) { for (var i = 0; i < array.length; i++) { array[i] = 0; } } function convolutionSeparable(data, separatedKernel, width, height) { var result = new Array(data.length); var tmp = void 0, conv = void 0, offset = void 0, kernel = void 0; kernel = separatedKernel[1]; offset = (kernel.length - 1) / 2; conv = new Array(width + kernel.length - 1); tmp = new Array(width); for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { tmp[x] = data[y * width + x]; } directConvolution(tmp, kernel, conv); for (var _x = 0; _x < width; _x++) { result[y * width + _x] = conv[offset + _x]; } } kernel = separatedKernel[0]; offset = (kernel.length - 1) / 2; conv = new Array(height + kernel.length - 1); tmp = new Array(height); for (var _x2 = 0; _x2 < width; _x2++) { for (var _y = 0; _y < height; _y++) { tmp[_y] = result[_y * width + _x2]; } directConvolution(tmp, kernel, conv); for (var _y2 = 0; _y2 < height; _y2++) { result[_y2 * width + _x2] = conv[offset + _y2]; } } return result; } if (!Symbol.species) { Symbol.species = Symbol.for('@@species'); } // https://github.com/lutzroeder/Mapack/blob/master/Source/LuDecomposition.cs function LuDecomposition(matrix) { if (!(this instanceof LuDecomposition)) { return new LuDecomposition(matrix); } matrix = Matrix.checkMatrix(matrix); var lu = matrix.clone(), rows = lu.rows, columns = lu.columns, pivotVector = new Array(rows), pivotSign = 1, i, j, k, p, s, t, v, LUrowi, LUcolj, kmax; for (i = 0; i < rows; i++) { pivotVector[i] = i; } LUcolj = new Array(rows); for (j = 0; j < columns; j++) { for (i = 0; i < rows; i++) { LUcolj[i] = lu[i][j]; } for (i = 0; i < rows; i++) { LUrowi = lu[i]; kmax = Math.min(i, j); s = 0; for (k = 0; k < kmax; k++) { s += LUrowi[k] * LUcolj[k]; } LUrowi[j] = LUcolj[i] -= s; } p = j; for (i = j + 1; i < rows; i++) { if (Math.abs(LUcolj[i]) > Math.abs(LUcolj[p])) { p = i; } } if (p !== j) { for (k = 0; k < columns; k++) { t = lu[p][k]; lu[p][k] = lu[j][k]; lu[j][k] = t; } v = pivotVector[p]; pivotVector[p] = pivotVector[j]; pivotVector[j] = v; pivotSign = -pivotSign; } if (j < rows && lu[j][j] !== 0) { for (i = j + 1; i < rows; i++) { lu[i][j] /= lu[j][j]; } } } this.LU = lu; this.pivotVector = pivotVector; this.pivotSign = pivotSign; } LuDecomposition.prototype = { isSingular: function () { var data = this.LU, col = data.columns; for (var j = 0; j < col; j++) { if (data[j][j] === 0) { return true; } } return false; }, get determinant() { var data = this.LU; if (!data.isSquare()) { throw new Error('Matrix must be square'); } var determinant = this.pivotSign, col = data.columns; for (var j = 0; j < col; j++) { determinant *= data[j][j]; } return determinant; }, get lowerTriangularMatrix() { var data = this.LU, rows = data.rows, columns = data.columns, X = new Matrix(rows, columns); for (var i = 0; i < rows; i++) { for (var j = 0; j < columns; j++) { if (i > j) { X[i][j] = data[i][j]; } else if (i === j) { X[i][j] = 1; } else { X[i][j] = 0; } } } return X; }, get upperTriangularMatrix() { var data = this.LU, rows = data.rows, columns = data.columns, X = new Matrix(rows, columns); for (var i = 0; i < rows; i++) { for (var j = 0; j < columns; j++) { if (i <= j) { X[i][j] = data[i][j]; } else { X[i][j] = 0; } } } return X; }, get pivotPermutationVector() { return this.pivotVector.slice(); }, solve: function (value) { value = Matrix.checkMatrix(value); var lu = this.LU, rows = lu.rows; if (rows !== value.rows) { throw new Error('Invalid matrix dimensions'); } if (this.isSingular()) { throw new Error('LU matrix is singular'); } var count = value.columns; var X = value.subMatrixRow(this.pivotVector, 0, count - 1); var columns = lu.columns; var i, j, k; for (k = 0; k < columns; k++) { for (i = k + 1; i < columns; i++) { for (j = 0; j < count; j++) { X[i][j] -= X[k][j] * lu[i][k]; } } } for (k = columns - 1; k >= 0; k--) { for (j = 0; j < count; j++) { X[k][j] /= lu[k][k]; } for (i = 0; i < k; i++) { for (j = 0; j < count; j++) { X[i][j] -= X[k][j] * lu[i][k]; } } } return X; } }; function hypotenuse(a, b) { var r; if (Math.abs(a) > Math.abs(b)) { r = b / a; return Math.abs(a) * Math.sqrt(1 + r * r); } if (b !== 0) { r = a / b; return Math.abs(b) * Math.sqrt(1 + r * r); } return 0; } // For use in the decomposition algorithms. With big matrices, access time is // too long on elements from array subclass // todo check when it is fixed in v8 // http://jsperf.com/access-and-write-array-subclass function getFilled2DArray(rows, columns, value) { var array = new Array(rows); for (var i = 0; i < rows; i++) { array[i] = new Array(columns); for (var j = 0; j < columns; j++) { array[i][j] = value; } } return array; } // https://github.com/lutzroeder/Mapack/blob/master/Source/SingularValueDecomposition.cs function SingularValueDecomposition(value, options) { if (!(this instanceof SingularValueDecomposition)) { return new SingularValueDecomposition(value, options); } value = Matrix.checkMatrix(value); options = options || {}; var m = value.rows, n = value.columns, nu = Math.min(m, n); var wantu = true, wantv = true; if (options.computeLeftSingularVectors === false) wantu = false; if (options.computeRightSingularVectors === false) wantv = false; var autoTranspose = options.autoTranspose === true; var swapped = false; var a; if (m < n) { if (!autoTranspose) { a = value.clone(); // eslint-disable-next-line no-console console.warn('Computing SVD on a matrix with more columns than rows. Consider enabling autoTranspose'); } else { a = value.transpose(); m = a.rows; n = a.columns; swapped = true; var aux = wantu; wantu = wantv; wantv = aux; } } else { a = value.clone(); } var s = new Array(Math.min(m + 1, n)), U = getFilled2DArray(m, nu, 0), V = getFilled2DArray(n, n, 0), e = new Array(n), wo