(function() { /** * Image utility. * @static * @constructor */ tracking.Image = {}; /** * Computes gaussian blur. Adapted from * https://github.com/kig/canvasfilters. * @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array. * @param {number} width The image width. * @param {number} height The image height. * @param {number} diameter Gaussian blur diameter, must be greater than 1. * @return {array} The edge pixels in a linear [r,g,b,a,...] array. */ tracking.Image.blur = function(pixels, width, height, diameter) { diameter = Math.abs(diameter); if (diameter <= 1) { throw new Error('Diameter should be greater than 1.'); } var radius = diameter / 2; var len = Math.ceil(diameter) + (1 - (Math.ceil(diameter) % 2)); var weights = new Float32Array(len); var rho = (radius + 0.5) / 3; var rhoSq = rho * rho; var gaussianFactor = 1 / Math.sqrt(2 * Math.PI * rhoSq); var rhoFactor = -1 / (2 * rho * rho); var wsum = 0; var middle = Math.floor(len / 2); for (var i = 0; i < len; i++) { var x = i - middle; var gx = gaussianFactor * Math.exp(x * x * rhoFactor); weights[i] = gx; wsum += gx; } for (var j = 0; j < weights.length; j++) { weights[j] /= wsum; } return this.separableConvolve(pixels, width, height, weights, weights, false); }; /** * Computes the integral image for summed, squared, rotated and sobel pixels. * @param {array} pixels The pixels in a linear [r,g,b,a,...] array to loop * through. * @param {number} width The image width. * @param {number} height The image height. * @param {array} opt_integralImage Empty array of size `width * height` to * be filled with the integral image values. If not specified compute sum * values will be skipped. * @param {array} opt_integralImageSquare Empty array of size `width * * height` to be filled with the integral image squared values. If not * specified compute squared values will be skipped. * @param {array} opt_tiltedIntegralImage Empty array of size `width * * height` to be filled with the rotated integral image values. If not * specified compute sum values will be skipped. * @param {array} opt_integralImageSobel Empty array of size `width * * height` to be filled with the integral image of sobel values. If not * specified compute sobel filtering will be skipped. * @static */ tracking.Image.computeIntegralImage = function(pixels, width, height, opt_integralImage, opt_integralImageSquare, opt_tiltedIntegralImage, opt_integralImageSobel) { if (arguments.length < 4) { throw new Error('You should specify at least one output array in the order: sum, square, tilted, sobel.'); } var pixelsSobel; if (opt_integralImageSobel) { pixelsSobel = tracking.Image.sobel(pixels, width, height); } for (var i = 0; i < height; i++) { for (var j = 0; j < width; j++) { var w = i * width * 4 + j * 4; var pixel = ~~(pixels[w] * 0.299 + pixels[w + 1] * 0.587 + pixels[w + 2] * 0.114); if (opt_integralImage) { this.computePixelValueSAT_(opt_integralImage, width, i, j, pixel); } if (opt_integralImageSquare) { this.computePixelValueSAT_(opt_integralImageSquare, width, i, j, pixel * pixel); } if (opt_tiltedIntegralImage) { var w1 = w - width * 4; var pixelAbove = ~~(pixels[w1] * 0.299 + pixels[w1 + 1] * 0.587 + pixels[w1 + 2] * 0.114); this.computePixelValueRSAT_(opt_tiltedIntegralImage, width, i, j, pixel, pixelAbove || 0); } if (opt_integralImageSobel) { this.computePixelValueSAT_(opt_integralImageSobel, width, i, j, pixelsSobel[w]); } } } }; /** * Helper method to compute the rotated summed area table (RSAT) by the * formula: * * RSAT(x, y) = RSAT(x-1, y-1) + RSAT(x+1, y-1) - RSAT(x, y-2) + I(x, y) + I(x, y-1) * * @param {number} width The image width. * @param {array} RSAT Empty array of size `width * height` to be filled with * the integral image values. If not specified compute sum values will be * skipped. * @param {number} i Vertical position of the pixel to be evaluated. * @param {number} j Horizontal position of the pixel to be evaluated. * @param {number} pixel Pixel value to be added to the integral image. * @static * @private */ tracking.Image.computePixelValueRSAT_ = function(RSAT, width, i, j, pixel, pixelAbove) { var w = i * width + j; RSAT[w] = (RSAT[w - width - 1] || 0) + (RSAT[w - width + 1] || 0) - (RSAT[w - width - width] || 0) + pixel + pixelAbove; }; /** * Helper method to compute the summed area table (SAT) by the formula: * * SAT(x, y) = SAT(x, y-1) + SAT(x-1, y) + I(x, y) - SAT(x-1, y-1) * * @param {number} width The image width. * @param {array} SAT Empty array of size `width * height` to be filled with * the integral image values. If not specified compute sum values will be * skipped. * @param {number} i Vertical position of the pixel to be evaluated. * @param {number} j Horizontal position of the pixel to be evaluated. * @param {number} pixel Pixel value to be added to the integral image. * @static * @private */ tracking.Image.computePixelValueSAT_ = function(SAT, width, i, j, pixel) { var w = i * width + j; SAT[w] = (SAT[w - width] || 0) + (SAT[w - 1] || 0) + pixel - (SAT[w - width - 1] || 0); }; /** * Converts a color from a colorspace based on an RGB color model to a * grayscale representation of its luminance. The coefficients represent the * measured intensity perception of typical trichromat humans, in * particular, human vision is most sensitive to green and least sensitive * to blue. * @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array. * @param {number} width The image width. * @param {number} height The image height. * @param {boolean} fillRGBA If the result should fill all RGBA values with the gray scale * values, instead of returning a single value per pixel. * @param {Uint8ClampedArray} The grayscale pixels in a linear array ([p,p,p,a,...] if fillRGBA * is true and [p1, p2, p3, ...] if fillRGBA is false). * @static */ tracking.Image.grayscale = function(pixels, width, height, fillRGBA) { var gray = new Uint8ClampedArray(fillRGBA ? pixels.length : pixels.length >> 2); var p = 0; var w = 0; for (var i = 0; i < height; i++) { for (var j = 0; j < width; j++) { var value = pixels[w] * 0.299 + pixels[w + 1] * 0.587 + pixels[w + 2] * 0.114; gray[p++] = value; if (fillRGBA) { gray[p++] = value; gray[p++] = value; gray[p++] = pixels[w + 3]; } w += 4; } } return gray; }; /** * Fast horizontal separable convolution. A point spread function (PSF) is * said to be separable if it can be broken into two one-dimensional * signals: a vertical and a horizontal projection. The convolution is * performed by sliding the kernel over the image, generally starting at the * top left corner, so as to move the kernel through all the positions where * the kernel fits entirely within the boundaries of the image. Adapted from * https://github.com/kig/canvasfilters. * @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array. * @param {number} width The image width. * @param {number} height The image height. * @param {array} weightsVector The weighting vector, e.g [-1,0,1]. * @param {number} opaque * @return {array} The convoluted pixels in a linear [r,g,b,a,...] array. */ tracking.Image.horizontalConvolve = function(pixels, width, height, weightsVector, opaque) { var side = weightsVector.length; var halfSide = Math.floor(side / 2); var output = new Float32Array(width * height * 4); var alphaFac = opaque ? 1 : 0; for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { var sy = y; var sx = x; var offset = (y * width + x) * 4; var r = 0; var g = 0; var b = 0; var a = 0; for (var cx = 0; cx < side; cx++) { var scy = sy; var scx = Math.min(width - 1, Math.max(0, sx + cx - halfSide)); var poffset = (scy * width + scx) * 4; var wt = weightsVector[cx]; r += pixels[poffset] * wt; g += pixels[poffset + 1] * wt; b += pixels[poffset + 2] * wt; a += pixels[poffset + 3] * wt; } output[offset] = r; output[offset + 1] = g; output[offset + 2] = b; output[offset + 3] = a + alphaFac * (255 - a); } } return output; }; /** * Fast vertical separable convolution. A point spread function (PSF) is * said to be separable if it can be broken into two one-dimensional * signals: a vertical and a horizontal projection. The convolution is * performed by sliding the kernel over the image, generally starting at the * top left corner, so as to move the kernel through all the positions where * the kernel fits entirely within the boundaries of the image. Adapted from * https://github.com/kig/canvasfilters. * @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array. * @param {number} width The image width. * @param {number} height The image height. * @param {array} weightsVector The weighting vector, e.g [-1,0,1]. * @param {number} opaque * @return {array} The convoluted pixels in a linear [r,g,b,a,...] array. */ tracking.Image.verticalConvolve = function(pixels, width, height, weightsVector, opaque) { var side = weightsVector.length; var halfSide = Math.floor(side / 2); var output = new Float32Array(width * height * 4); var alphaFac = opaque ? 1 : 0; for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { var sy = y; var sx = x; var offset = (y * width + x) * 4; var r = 0; var g = 0; var b = 0; var a = 0; for (var cy = 0; cy < side; cy++) { var scy = Math.min(height - 1, Math.max(0, sy + cy - halfSide)); var scx = sx; var poffset = (scy * width + scx) * 4; var wt = weightsVector[cy]; r += pixels[poffset] * wt; g += pixels[poffset + 1] * wt; b += pixels[poffset + 2] * wt; a += pixels[poffset + 3] * wt; } output[offset] = r; output[offset + 1] = g; output[offset + 2] = b; output[offset + 3] = a + alphaFac * (255 - a); } } return output; }; /** * Fast separable convolution. A point spread function (PSF) is said to be * separable if it can be broken into two one-dimensional signals: a * vertical and a horizontal projection. The convolution is performed by * sliding the kernel over the image, generally starting at the top left * corner, so as to move the kernel through all the positions where the * kernel fits entirely within the boundaries of the image. Adapted from * https://github.com/kig/canvasfilters. * @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array. * @param {number} width The image width. * @param {number} height The image height. * @param {array} horizWeights The horizontal weighting vector, e.g [-1,0,1]. * @param {array} vertWeights The vertical vector, e.g [-1,0,1]. * @param {number} opaque * @return {array} The convoluted pixels in a linear [r,g,b,a,...] array. */ tracking.Image.separableConvolve = function(pixels, width, height, horizWeights, vertWeights, opaque) { var vertical = this.verticalConvolve(pixels, width, height, vertWeights, opaque); return this.horizontalConvolve(vertical, width, height, horizWeights, opaque); }; /** * Compute image edges using Sobel operator. Computes the vertical and * horizontal gradients of the image and combines the computed images to * find edges in the image. The way we implement the Sobel filter here is by * first grayscaling the image, then taking the horizontal and vertical * gradients and finally combining the gradient images to make up the final * image. Adapted from https://github.com/kig/canvasfilters. * @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array. * @param {number} width The image width. * @param {number} height The image height. * @return {array} The edge pixels in a linear [r,g,b,a,...] array. */ tracking.Image.sobel = function(pixels, width, height) { pixels = this.grayscale(pixels, width, height, true); var output = new Float32Array(width * height * 4); var sobelSignVector = new Float32Array([-1, 0, 1]); var sobelScaleVector = new Float32Array([1, 2, 1]); var vertical = this.separableConvolve(pixels, width, height, sobelSignVector, sobelScaleVector); var horizontal = this.separableConvolve(pixels, width, height, sobelScaleVector, sobelSignVector); for (var i = 0; i < output.length; i += 4) { var v = vertical[i]; var h = horizontal[i]; var p = Math.sqrt(h * h + v * v); output[i] = p; output[i + 1] = p; output[i + 2] = p; output[i + 3] = 255; } return output; }; }());