voluptasquisquam
Version:
Image processing and manipulation in JavaScript
1,734 lines (1,536 loc) • 909 kB
JavaScript
(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