/** * tracking - A modern approach for Computer Vision on the web. * @author Eduardo Lundgren * @version v1.1.2 * @link http://trackingjs.com * @license BSD */ (function(window, undefined) { window.tracking = window.tracking || {}; /** * Inherit the prototype methods from one constructor into another. * * Usage: *
   * function ParentClass(a, b) { }
   * ParentClass.prototype.foo = function(a) { }
   *
   * function ChildClass(a, b, c) {
   *   tracking.base(this, a, b);
   * }
   * tracking.inherits(ChildClass, ParentClass);
   *
   * var child = new ChildClass('a', 'b', 'c');
   * child.foo();
   * 
* * @param {Function} childCtor Child class. * @param {Function} parentCtor Parent class. */ tracking.inherits = function(childCtor, parentCtor) { function TempCtor() { } TempCtor.prototype = parentCtor.prototype; childCtor.superClass_ = parentCtor.prototype; childCtor.prototype = new TempCtor(); childCtor.prototype.constructor = childCtor; /** * Calls superclass constructor/method. * * This function is only available if you use tracking.inherits to express * inheritance relationships between classes. * * @param {!object} me Should always be "this". * @param {string} methodName The method name to call. Calling superclass * constructor can be done with the special string 'constructor'. * @param {...*} var_args The arguments to pass to superclass * method/constructor. * @return {*} The return value of the superclass method/constructor. */ childCtor.base = function(me, methodName) { var args = Array.prototype.slice.call(arguments, 2); return parentCtor.prototype[methodName].apply(me, args); }; }; /** * Captures the user camera when tracking a video element and set its source * to the camera stream. * @param {HTMLVideoElement} element Canvas element to track. * @param {object} opt_options Optional configuration to the tracker. */ tracking.initUserMedia_ = function(element, opt_options) { window.navigator.getUserMedia({ video: true, audio: !!(opt_options && opt_options.audio) }, function(stream) { try { element.src = window.URL.createObjectURL(stream); } catch (err) { element.src = stream; } }, function() { throw Error('Cannot capture user camera.'); } ); }; /** * Tests whether the object is a dom node. * @param {object} o Object to be tested. * @return {boolean} True if the object is a dom node. */ tracking.isNode = function(o) { return o.nodeType || this.isWindow(o); }; /** * Tests whether the object is the `window` object. * @param {object} o Object to be tested. * @return {boolean} True if the object is the `window` object. */ tracking.isWindow = function(o) { return !!(o && o.alert && o.document); }; /** * Selects a dom node from a CSS3 selector using `document.querySelector`. * @param {string} selector * @param {object} opt_element The root element for the query. When not * specified `document` is used as root element. * @return {HTMLElement} The first dom element that matches to the selector. * If not found, returns `null`. */ tracking.one = function(selector, opt_element) { if (this.isNode(selector)) { return selector; } return (opt_element || document).querySelector(selector); }; /** * Tracks a canvas, image or video element based on the specified `tracker` * instance. This method extract the pixel information of the input element * to pass to the `tracker` instance. When tracking a video, the * `tracker.track(pixels, width, height)` will be in a * `requestAnimationFrame` loop in order to track all video frames. * * Example: * var tracker = new tracking.ColorTracker(); * * tracking.track('#video', tracker); * or * tracking.track('#video', tracker, { camera: true }); * * tracker.on('track', function(event) { * // console.log(event.data[0].x, event.data[0].y) * }); * * @param {HTMLElement} element The element to track, canvas, image or * video. * @param {tracking.Tracker} tracker The tracker instance used to track the * element. * @param {object} opt_options Optional configuration to the tracker. */ tracking.track = function(element, tracker, opt_options) { element = tracking.one(element); if (!element) { throw new Error('Element not found, try a different element or selector.'); } if (!tracker) { throw new Error('Tracker not specified, try `tracking.track(element, new tracking.FaceTracker())`.'); } switch (element.nodeName.toLowerCase()) { case 'canvas': return this.trackCanvas_(element, tracker, opt_options); case 'img': return this.trackImg_(element, tracker, opt_options); case 'video': if (opt_options) { if (opt_options.camera) { this.initUserMedia_(element, opt_options); } } return this.trackVideo_(element, tracker, opt_options); default: throw new Error('Element not supported, try in a canvas, img, or video.'); } }; /** * Tracks a canvas element based on the specified `tracker` instance and * returns a `TrackerTask` for this track. * @param {HTMLCanvasElement} element Canvas element to track. * @param {tracking.Tracker} tracker The tracker instance used to track the * element. * @param {object} opt_options Optional configuration to the tracker. * @return {tracking.TrackerTask} * @private */ tracking.trackCanvas_ = function(element, tracker) { var self = this; var task = new tracking.TrackerTask(tracker); task.on('run', function() { self.trackCanvasInternal_(element, tracker); }); return task.run(); }; /** * Tracks a canvas element based on the specified `tracker` instance. This * method extract the pixel information of the input element to pass to the * `tracker` instance. * @param {HTMLCanvasElement} element Canvas element to track. * @param {tracking.Tracker} tracker The tracker instance used to track the * element. * @param {object} opt_options Optional configuration to the tracker. * @private */ tracking.trackCanvasInternal_ = function(element, tracker) { var width = element.width; var height = element.height; var context = element.getContext('2d'); var imageData = context.getImageData(0, 0, width, height); tracker.track(imageData.data, width, height); }; /** * Tracks a image element based on the specified `tracker` instance. This * method extract the pixel information of the input element to pass to the * `tracker` instance. * @param {HTMLImageElement} element Canvas element to track. * @param {tracking.Tracker} tracker The tracker instance used to track the * element. * @param {object} opt_options Optional configuration to the tracker. * @private */ tracking.trackImg_ = function(element, tracker) { var width = element.width; var height = element.height; var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; var task = new tracking.TrackerTask(tracker); task.on('run', function() { tracking.Canvas.loadImage(canvas, element.src, 0, 0, width, height, function() { tracking.trackCanvasInternal_(canvas, tracker); }); }); return task.run(); }; /** * Tracks a video element based on the specified `tracker` instance. This * method extract the pixel information of the input element to pass to the * `tracker` instance. The `tracker.track(pixels, width, height)` will be in * a `requestAnimationFrame` loop in order to track all video frames. * @param {HTMLVideoElement} element Canvas element to track. * @param {tracking.Tracker} tracker The tracker instance used to track the * element. * @param {object} opt_options Optional configuration to the tracker. * @private */ tracking.trackVideo_ = function(element, tracker) { var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); var width; var height; var resizeCanvas_ = function() { width = element.offsetWidth; height = element.offsetHeight; canvas.width = width; canvas.height = height; }; resizeCanvas_(); element.addEventListener('resize', resizeCanvas_); var requestId; var requestAnimationFrame_ = function() { requestId = window.requestAnimationFrame(function() { if (element.readyState === element.HAVE_ENOUGH_DATA) { try { // Firefox v~30.0 gets confused with the video readyState firing an // erroneous HAVE_ENOUGH_DATA just before HAVE_CURRENT_DATA state, // hence keep trying to read it until resolved. context.drawImage(element, 0, 0, width, height); } catch (err) {} tracking.trackCanvasInternal_(canvas, tracker); } requestAnimationFrame_(); }); }; var task = new tracking.TrackerTask(tracker); task.on('stop', function() { window.cancelAnimationFrame(requestId); }); task.on('run', function() { requestAnimationFrame_(); }); return task.run(); }; // Browser polyfills //=================== if (!window.URL) { window.URL = window.URL || window.webkitURL || window.msURL || window.oURL; } if (!navigator.getUserMedia) { navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; } }(window)); (function() { /** * EventEmitter utility. * @constructor */ tracking.EventEmitter = function() {}; /** * Holds event listeners scoped by event type. * @type {object} * @private */ tracking.EventEmitter.prototype.events_ = null; /** * Adds a listener to the end of the listeners array for the specified event. * @param {string} event * @param {function} listener * @return {object} Returns emitter, so calls can be chained. */ tracking.EventEmitter.prototype.addListener = function(event, listener) { if (typeof listener !== 'function') { throw new TypeError('Listener must be a function'); } if (!this.events_) { this.events_ = {}; } this.emit('newListener', event, listener); if (!this.events_[event]) { this.events_[event] = []; } this.events_[event].push(listener); return this; }; /** * Returns an array of listeners for the specified event. * @param {string} event * @return {array} Array of listeners. */ tracking.EventEmitter.prototype.listeners = function(event) { return this.events_ && this.events_[event]; }; /** * Execute each of the listeners in order with the supplied arguments. * @param {string} event * @param {*} opt_args [arg1], [arg2], [...] * @return {boolean} Returns true if event had listeners, false otherwise. */ tracking.EventEmitter.prototype.emit = function(event) { var listeners = this.listeners(event); if (listeners) { var args = Array.prototype.slice.call(arguments, 1); for (var i = 0; i < listeners.length; i++) { if (listeners[i]) { listeners[i].apply(this, args); } } return true; } return false; }; /** * Adds a listener to the end of the listeners array for the specified event. * @param {string} event * @param {function} listener * @return {object} Returns emitter, so calls can be chained. */ tracking.EventEmitter.prototype.on = tracking.EventEmitter.prototype.addListener; /** * Adds a one time listener for the event. This listener is invoked only the * next time the event is fired, after which it is removed. * @param {string} event * @param {function} listener * @return {object} Returns emitter, so calls can be chained. */ tracking.EventEmitter.prototype.once = function(event, listener) { var self = this; self.on(event, function handlerInternal() { self.removeListener(event, handlerInternal); listener.apply(this, arguments); }); }; /** * Removes all listeners, or those of the specified event. It's not a good * idea to remove listeners that were added elsewhere in the code, * especially when it's on an emitter that you didn't create. * @param {string} event * @return {object} Returns emitter, so calls can be chained. */ tracking.EventEmitter.prototype.removeAllListeners = function(opt_event) { if (!this.events_) { return this; } if (opt_event) { delete this.events_[opt_event]; } else { delete this.events_; } return this; }; /** * Remove a listener from the listener array for the specified event. * Caution: changes array indices in the listener array behind the listener. * @param {string} event * @param {function} listener * @return {object} Returns emitter, so calls can be chained. */ tracking.EventEmitter.prototype.removeListener = function(event, listener) { if (typeof listener !== 'function') { throw new TypeError('Listener must be a function'); } if (!this.events_) { return this; } var listeners = this.listeners(event); if (Array.isArray(listeners)) { var i = listeners.indexOf(listener); if (i < 0) { return this; } listeners.splice(i, 1); } return this; }; /** * By default EventEmitters will print a warning if more than 10 listeners * are added for a particular event. This is a useful default which helps * finding memory leaks. Obviously not all Emitters should be limited to 10. * This function allows that to be increased. Set to zero for unlimited. * @param {number} n The maximum number of listeners. */ tracking.EventEmitter.prototype.setMaxListeners = function() { throw new Error('Not implemented'); }; }()); (function() { /** * Canvas utility. * @static * @constructor */ tracking.Canvas = {}; /** * Loads an image source into the canvas. * @param {HTMLCanvasElement} canvas The canvas dom element. * @param {string} src The image source. * @param {number} x The canvas horizontal coordinate to load the image. * @param {number} y The canvas vertical coordinate to load the image. * @param {number} width The image width. * @param {number} height The image height. * @param {function} opt_callback Callback that fires when the image is loaded * into the canvas. * @static */ tracking.Canvas.loadImage = function(canvas, src, x, y, width, height, opt_callback) { var instance = this; var img = new window.Image(); img.crossOrigin = '*'; img.onload = function() { var context = canvas.getContext('2d'); canvas.width = width; canvas.height = height; context.drawImage(img, x, y, width, height); if (opt_callback) { opt_callback.call(instance); } img = null; }; img.src = src; }; }()); (function() { /** * DisjointSet utility with path compression. Some applications involve * grouping n distinct objects into a collection of disjoint sets. Two * important operations are then finding which set a given object belongs to * and uniting the two sets. A disjoint set data structure maintains a * collection S={ S1 , S2 ,..., Sk } of disjoint dynamic sets. Each set is * identified by a representative, which usually is a member in the set. * @static * @constructor */ tracking.DisjointSet = function(length) { if (length === undefined) { throw new Error('DisjointSet length not specified.'); } this.length = length; this.parent = new Uint32Array(length); for (var i = 0; i < length; i++) { this.parent[i] = i; } }; /** * Holds the length of the internal set. * @type {number} */ tracking.DisjointSet.prototype.length = null; /** * Holds the set containing the representative values. * @type {Array.} */ tracking.DisjointSet.prototype.parent = null; /** * Finds a pointer to the representative of the set containing i. * @param {number} i * @return {number} The representative set of i. */ tracking.DisjointSet.prototype.find = function(i) { if (this.parent[i] === i) { return i; } else { return (this.parent[i] = this.find(this.parent[i])); } }; /** * Unites two dynamic sets containing objects i and j, say Si and Sj, into * a new set that Si ∪ Sj, assuming that Si ∩ Sj = ∅; * @param {number} i * @param {number} j */ tracking.DisjointSet.prototype.union = function(i, j) { var iRepresentative = this.find(i); var jRepresentative = this.find(j); this.parent[iRepresentative] = jRepresentative; }; }()); (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; }; }()); (function() { /** * ViolaJones utility. * @static * @constructor */ tracking.ViolaJones = {}; /** * Holds the minimum area of intersection that defines when a rectangle is * from the same group. Often when a face is matched multiple rectangles are * classified as possible rectangles to represent the face, when they * intersects they are grouped as one face. * @type {number} * @default 0.5 * @static */ tracking.ViolaJones.REGIONS_OVERLAP = 0.5; /** * Holds the HAAR cascade classifiers converted from OpenCV training. * @type {array} * @static */ tracking.ViolaJones.classifiers = {}; /** * Detects through the HAAR cascade data rectangles matches. * @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} initialScale The initial scale to start the block * scaling. * @param {number} scaleFactor The scale factor to scale the feature block. * @param {number} stepSize The block step size. * @param {number} edgesDensity Percentage density edges inside the * classifier block. Value from [0.0, 1.0], defaults to 0.2. If specified * edge detection will be applied to the image to prune dead areas of the * image, this can improve significantly performance. * @param {number} data The HAAR cascade data. * @return {array} Found rectangles. * @static */ tracking.ViolaJones.detect = function(pixels, width, height, initialScale, scaleFactor, stepSize, edgesDensity, data) { var total = 0; var rects = []; var integralImage = new Int32Array(width * height); var integralImageSquare = new Int32Array(width * height); var tiltedIntegralImage = new Int32Array(width * height); var integralImageSobel; if (edgesDensity > 0) { integralImageSobel = new Int32Array(width * height); } tracking.Image.computeIntegralImage(pixels, width, height, integralImage, integralImageSquare, tiltedIntegralImage, integralImageSobel); var minWidth = data[0]; var minHeight = data[1]; var scale = initialScale * scaleFactor; var blockWidth = (scale * minWidth) | 0; var blockHeight = (scale * minHeight) | 0; while (blockWidth < width && blockHeight < height) { var step = (scale * stepSize + 0.5) | 0; for (var i = 0; i < (height - blockHeight); i += step) { for (var j = 0; j < (width - blockWidth); j += step) { if (edgesDensity > 0) { if (this.isTriviallyExcluded(edgesDensity, integralImageSobel, i, j, width, blockWidth, blockHeight)) { continue; } } if (this.evalStages_(data, integralImage, integralImageSquare, tiltedIntegralImage, i, j, width, blockWidth, blockHeight, scale)) { rects[total++] = { width: blockWidth, height: blockHeight, x: j, y: i }; } } } scale *= scaleFactor; blockWidth = (scale * minWidth) | 0; blockHeight = (scale * minHeight) | 0; } return this.mergeRectangles_(rects); }; /** * Fast check to test whether the edges density inside the block is greater * than a threshold, if true it tests the stages. This can improve * significantly performance. * @param {number} edgesDensity Percentage density edges inside the * classifier block. * @param {array} integralImageSobel The integral image of a sobel image. * @param {number} i Vertical position of the pixel to be evaluated. * @param {number} j Horizontal position of the pixel to be evaluated. * @param {number} width The image width. * @return {boolean} True whether the block at position i,j can be skipped, * false otherwise. * @static * @protected */ tracking.ViolaJones.isTriviallyExcluded = function(edgesDensity, integralImageSobel, i, j, width, blockWidth, blockHeight) { var wbA = i * width + j; var wbB = wbA + blockWidth; var wbD = wbA + blockHeight * width; var wbC = wbD + blockWidth; var blockEdgesDensity = (integralImageSobel[wbA] - integralImageSobel[wbB] - integralImageSobel[wbD] + integralImageSobel[wbC]) / (blockWidth * blockHeight * 255); if (blockEdgesDensity < edgesDensity) { return true; } return false; }; /** * Evaluates if the block size on i,j position is a valid HAAR cascade * stage. * @param {number} data The HAAR cascade data. * @param {number} i Vertical position of the pixel to be evaluated. * @param {number} j Horizontal position of the pixel to be evaluated. * @param {number} width The image width. * @param {number} blockSize The block size. * @param {number} scale The scale factor of the block size and its original * size. * @param {number} inverseArea The inverse area of the block size. * @return {boolean} Whether the region passes all the stage tests. * @private * @static */ tracking.ViolaJones.evalStages_ = function(data, integralImage, integralImageSquare, tiltedIntegralImage, i, j, width, blockWidth, blockHeight, scale) { var inverseArea = 1.0 / (blockWidth * blockHeight); var wbA = i * width + j; var wbB = wbA + blockWidth; var wbD = wbA + blockHeight * width; var wbC = wbD + blockWidth; var mean = (integralImage[wbA] - integralImage[wbB] - integralImage[wbD] + integralImage[wbC]) * inverseArea; var variance = (integralImageSquare[wbA] - integralImageSquare[wbB] - integralImageSquare[wbD] + integralImageSquare[wbC]) * inverseArea - mean * mean; var standardDeviation = 1; if (variance > 0) { standardDeviation = Math.sqrt(variance); } var length = data.length; for (var w = 2; w < length; ) { var stageSum = 0; var stageThreshold = data[w++]; var nodeLength = data[w++]; while (nodeLength--) { var rectsSum = 0; var tilted = data[w++]; var rectsLength = data[w++]; for (var r = 0; r < rectsLength; r++) { var rectLeft = (j + data[w++] * scale + 0.5) | 0; var rectTop = (i + data[w++] * scale + 0.5) | 0; var rectWidth = (data[w++] * scale + 0.5) | 0; var rectHeight = (data[w++] * scale + 0.5) | 0; var rectWeight = data[w++]; var w1; var w2; var w3; var w4; if (tilted) { // RectSum(r) = RSAT(x-h+w, y+w+h-1) + RSAT(x, y-1) - RSAT(x-h, y+h-1) - RSAT(x+w, y+w-1) w1 = (rectLeft - rectHeight + rectWidth) + (rectTop + rectWidth + rectHeight - 1) * width; w2 = rectLeft + (rectTop - 1) * width; w3 = (rectLeft - rectHeight) + (rectTop + rectHeight - 1) * width; w4 = (rectLeft + rectWidth) + (rectTop + rectWidth - 1) * width; rectsSum += (tiltedIntegralImage[w1] + tiltedIntegralImage[w2] - tiltedIntegralImage[w3] - tiltedIntegralImage[w4]) * rectWeight; } else { // RectSum(r) = SAT(x-1, y-1) + SAT(x+w-1, y+h-1) - SAT(x-1, y+h-1) - SAT(x+w-1, y-1) w1 = rectTop * width + rectLeft; w2 = w1 + rectWidth; w3 = w1 + rectHeight * width; w4 = w3 + rectWidth; rectsSum += (integralImage[w1] - integralImage[w2] - integralImage[w3] + integralImage[w4]) * rectWeight; // TODO: Review the code below to analyze performance when using it instead. // w1 = (rectLeft - 1) + (rectTop - 1) * width; // w2 = (rectLeft + rectWidth - 1) + (rectTop + rectHeight - 1) * width; // w3 = (rectLeft - 1) + (rectTop + rectHeight - 1) * width; // w4 = (rectLeft + rectWidth - 1) + (rectTop - 1) * width; // rectsSum += (integralImage[w1] + integralImage[w2] - integralImage[w3] - integralImage[w4]) * rectWeight; } } var nodeThreshold = data[w++]; var nodeLeft = data[w++]; var nodeRight = data[w++]; if (rectsSum * inverseArea < nodeThreshold * standardDeviation) { stageSum += nodeLeft; } else { stageSum += nodeRight; } } if (stageSum < stageThreshold) { return false; } } return true; }; /** * Postprocess the detected sub-windows in order to combine overlapping * detections into a single detection. * @param {array} rects * @return {array} * @private * @static */ tracking.ViolaJones.mergeRectangles_ = function(rects) { var disjointSet = new tracking.DisjointSet(rects.length); for (var i = 0; i < rects.length; i++) { var r1 = rects[i]; for (var j = 0; j < rects.length; j++) { var r2 = rects[j]; if (tracking.Math.intersectRect(r1.x, r1.y, r1.x + r1.width, r1.y + r1.height, r2.x, r2.y, r2.x + r2.width, r2.y + r2.height)) { var x1 = Math.max(r1.x, r2.x); var y1 = Math.max(r1.y, r2.y); var x2 = Math.min(r1.x + r1.width, r2.x + r2.width); var y2 = Math.min(r1.y + r1.height, r2.y + r2.height); var overlap = (x1 - x2) * (y1 - y2); var area1 = (r1.width * r1.height); var area2 = (r2.width * r2.height); if ((overlap / (area1 * (area1 / area2)) >= this.REGIONS_OVERLAP) && (overlap / (area2 * (area1 / area2)) >= this.REGIONS_OVERLAP)) { disjointSet.union(i, j); } } } } var map = {}; for (var k = 0; k < disjointSet.length; k++) { var rep = disjointSet.find(k); if (!map[rep]) { map[rep] = { total: 1, width: rects[k].width, height: rects[k].height, x: rects[k].x, y: rects[k].y }; continue; } map[rep].total++; map[rep].width += rects[k].width; map[rep].height += rects[k].height; map[rep].x += rects[k].x; map[rep].y += rects[k].y; } var result = []; Object.keys(map).forEach(function(key) { var rect = map[key]; result.push({ total: rect.total, width: (rect.width / rect.total + 0.5) | 0, height: (rect.height / rect.total + 0.5) | 0, x: (rect.x / rect.total + 0.5) | 0, y: (rect.y / rect.total + 0.5) | 0 }); }); return result; }; }()); (function() { /** * Brief intends for "Binary Robust Independent Elementary Features".This * method generates a binary string for each keypoint found by an extractor * method. * @static * @constructor */ tracking.Brief = {}; /** * The set of binary tests is defined by the nd (x,y)-location pairs * uniquely chosen during the initialization. Values could vary between N = * 128,256,512. N=128 yield good compromises between speed, storage * efficiency, and recognition rate. * @type {number} */ tracking.Brief.N = 512; /** * Caches coordinates values of (x,y)-location pairs uniquely chosen during * the initialization. * @type {Object.} * @private * @static */ tracking.Brief.randomImageOffsets_ = {}; /** * Caches delta values of (x,y)-location pairs uniquely chosen during * the initialization. * @type {Int32Array} * @private * @static */ tracking.Brief.randomWindowOffsets_ = null; /** * Generates a binary string for each found keypoints extracted using an * extractor method. * @param {array} The grayscale pixels in a linear [p1,p2,...] array. * @param {number} width The image width. * @param {array} keypoints * @return {Int32Array} Returns an array where for each four sequence int * values represent the descriptor binary string (128 bits) necessary * to describe the corner, e.g. [0,0,0,0, 0,0,0,0, ...]. * @static */ tracking.Brief.getDescriptors = function(pixels, width, keypoints) { // Optimizing divide by 32 operation using binary shift // (this.N >> 5) === this.N/32. var descriptors = new Int32Array((keypoints.length >> 1) * (this.N >> 5)); var descriptorWord = 0; var offsets = this.getRandomOffsets_(width); var position = 0; for (var i = 0; i < keypoints.length; i += 2) { var w = width * keypoints[i + 1] + keypoints[i]; var offsetsPosition = 0; for (var j = 0, n = this.N; j < n; j++) { if (pixels[offsets[offsetsPosition++] + w] < pixels[offsets[offsetsPosition++] + w]) { // The bit in the position `j % 32` of descriptorWord should be set to 1. We do // this by making an OR operation with a binary number that only has the bit // in that position set to 1. That binary number is obtained by shifting 1 left by // `j % 32` (which is the same as `j & 31` left) positions. descriptorWord |= 1 << (j & 31); } // If the next j is a multiple of 32, we will need to use a new descriptor word to hold // the next results. if (!((j + 1) & 31)) { descriptors[position++] = descriptorWord; descriptorWord = 0; } } } return descriptors; }; /** * Matches sets of features {mi} and {m′j} extracted from two images taken * from similar, and often successive, viewpoints. A classical procedure * runs as follows. For each point {mi} in the first image, search in a * region of the second image around location {mi} for point {m′j}. The * search is based on the similarity of the local image windows, also known * as kernel windows, centered on the points, which strongly characterizes * the points when the images are sufficiently close. Once each keypoint is * described with its binary string, they need to be compared with the * closest matching point. Distance metric is critical to the performance of * in- trusion detection systems. Thus using binary strings reduces the size * of the descriptor and provides an interesting data structure that is fast * to operate whose similarity can be measured by the Hamming distance. * @param {array} keypoints1 * @param {array} descriptors1 * @param {array} keypoints2 * @param {array} descriptors2 * @return {Int32Array} Returns an array where the index is the corner1 * index coordinate, and the value is the corresponding match index of * corner2, e.g. keypoints1=[x0,y0,x1,y1,...] and * keypoints2=[x'0,y'0,x'1,y'1,...], if x0 matches x'1 and x1 matches x'0, * the return array would be [3,0]. * @static */ tracking.Brief.match = function(keypoints1, descriptors1, keypoints2, descriptors2) { var len1 = keypoints1.length >> 1; var len2 = keypoints2.length >> 1; var matches = new Array(len1); for (var i = 0; i < len1; i++) { var min = Infinity; var minj = 0; for (var j = 0; j < len2; j++) { var dist = 0; // Optimizing divide by 32 operation using binary shift // (this.N >> 5) === this.N/32. for (var k = 0, n = this.N >> 5; k < n; k++) { dist += tracking.Math.hammingWeight(descriptors1[i * n + k] ^ descriptors2[j * n + k]); } if (dist < min) { min = dist; minj = j; } } matches[i] = { index1: i, index2: minj, keypoint1: [keypoints1[2 * i], keypoints1[2 * i + 1]], keypoint2: [keypoints2[2 * minj], keypoints2[2 * minj + 1]], confidence: 1 - min / this.N }; } return matches; }; /** * Removes matches outliers by testing matches on both directions. * @param {array} keypoints1 * @param {array} descriptors1 * @param {array} keypoints2 * @param {array} descriptors2 * @return {Int32Array} Returns an array where the index is the corner1 * index coordinate, and the value is the corresponding match index of * corner2, e.g. keypoints1=[x0,y0,x1,y1,...] and * keypoints2=[x'0,y'0,x'1,y'1,...], if x0 matches x'1 and x1 matches x'0, * the return array would be [3,0]. * @static */ tracking.Brief.reciprocalMatch = function(keypoints1, descriptors1, keypoints2, descriptors2) { var matches = []; if (keypoints1.length === 0 || keypoints2.length === 0) { return matches; } var matches1 = tracking.Brief.match(keypoints1, descriptors1, keypoints2, descriptors2); var matches2 = tracking.Brief.match(keypoints2, descriptors2, keypoints1, descriptors1); for (var i = 0; i < matches1.length; i++) { if (matches2[matches1[i].index2].index2 === i) { matches.push(matches1[i]); } } return matches; }; /** * Gets the coordinates values of (x,y)-location pairs uniquely chosen * during the initialization. * @return {array} Array with the random offset values. * @private */ tracking.Brief.getRandomOffsets_ = function(width) { if (!this.randomWindowOffsets_) { var windowPosition = 0; var windowOffsets = new Int32Array(4 * this.N); for (var i = 0; i < this.N; i++) { windowOffsets[windowPosition++] = Math.round(tracking.Math.uniformRandom(-15, 16)); windowOffsets[windowPosition++] = Math.round(tracking.Math.uniformRandom(-15, 16)); windowOffsets[windowPosition++] = Math.round(tracking.Math.uniformRandom(-15, 16)); windowOffsets[windowPosition++] = Math.round(tracking.Math.uniformRandom(-15, 16)); } this.randomWindowOffsets_ = windowOffsets; } if (!this.randomImageOffsets_[width]) { var imagePosition = 0; var imageOffsets = new Int32Array(2 * this.N); for (var j = 0; j < this.N; j++) { imageOffsets[imagePosition++] = this.randomWindowOffsets_[4 * j] * width + this.randomWindowOffsets_[4 * j + 1]; imageOffsets[imagePosition++] = this.randomWindowOffsets_[4 * j + 2] * width + this.randomWindowOffsets_[4 * j + 3]; } this.randomImageOffsets_[width] = imageOffsets; } return this.randomImageOffsets_[width]; }; }()); (function() { /** * FAST intends for "Features from Accelerated Segment Test". This method * performs a point segment test corner detection. The segment test * criterion operates by considering a circle of sixteen pixels around the * corner candidate p. The detector classifies p as a corner if there exists * a set of n contiguous pixelsin the circle which are all brighter than the * intensity of the candidate pixel Ip plus a threshold t, or all darker * than Ip − t. * * 15 00 01 * 14 02 * 13 03 * 12 [] 04 * 11 05 * 10 06 * 09 08 07 * * For more reference: * http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.60.3991&rep=rep1&type=pdf * @static * @constructor */ tracking.Fast = {}; /** * Holds the threshold to determine whether the tested pixel is brighter or * darker than the corner candidate p. * @type {number} * @default 40 * @static */ tracking.Fast.THRESHOLD = 40; /** * Caches coordinates values of the circle surrounding the pixel candidate p. * @type {Object.} * @private * @static */ tracking.Fast.circles_ = {}; /** * Finds corners coordinates on the graysacaled image. * @param {array} The grayscale pixels in a linear [p1,p2,...] array. * @param {number} width The image width. * @param {number} height The image height. * @param {number} threshold to determine whether the tested pixel is brighter or * darker than the corner candidate p. Default value is 40. * @return {array} Array containing the coordinates of all found corners, * e.g. [x0,y0,x1,y1,...], where P(x0,y0) represents a corner coordinate. * @static */ tracking.Fast.findCorners = function(pixels, width, height, opt_threshold) { var circleOffsets = this.getCircleOffsets_(width); var circlePixels = new Int32Array(16); var corners = []; if (opt_threshold === undefined) { opt_threshold = this.THRESHOLD; } // When looping through the image pixels, skips the first three lines from // the image boundaries to constrain the surrounding circle inside the image // area. for (var i = 3; i < height - 3; i++) { for (var j = 3; j < width - 3; j++) { var w = i * width + j; var p = pixels[w]; // Loops the circle offsets to read the pixel value for the sixteen // surrounding pixels. for (var k = 0; k < 16; k++) { circlePixels[k] = pixels[w + circleOffsets[k]]; } if (this.isCorner(p, circlePixels, opt_threshold)) { // The pixel p is classified as a corner, as optimization increment j // by the circle radius 3 to skip the neighbor pixels inside the // surrounding circle. This can be removed without compromising the // result. corners.push(j, i); j += 3; } } } return corners; }; /** * Checks if the circle pixel is brighter than the candidate pixel p by * a threshold. * @param {number} circlePixel The circle pixel value. * @param {number} p The value of the candidate pixel p. * @param {number} threshold * @return {Boolean} * @static */ tracking.Fast.isBrighter = function(circlePixel, p, threshold) { return circlePixel - p > threshold; }; /** * Checks if the circle pixel is within the corner of the candidate pixel p * by a threshold. * @param {number} p The value of the candidate pixel p. * @param {number} circlePixel The circle pixel value. * @param {number} threshold * @return {Boolean} * @static */ tracking.Fast.isCorner = function(p, circlePixels, threshold) { if (this.isTriviallyExcluded(circlePixels, p, threshold)) { return false; } for (var x = 0; x < 16; x++) { var darker = true; var brighter = true; for (var y = 0; y < 9; y++) { var circlePixel = circlePixels[(x + y) & 15]; if (!this.isBrighter(p, circlePixel, threshold)) { brighter = false; if (darker === false) { break; } } if (!this.isDarker(p, circlePixel, threshold)) { darker = false; if (brighter === false) { break; } } } if (brighter || darker) { return true; } } return false; }; /** * Checks if the circle pixel is darker than the candidate pixel p by * a threshold. * @param {number} circlePixel The circle pixel value. * @param {number} p The value of the candidate pixel p. * @param {number} threshold * @return {Boolean} * @static */ tracking.Fast.isDarker = function(circlePixel, p, threshold) { return p - circlePixel > threshold; }; /** * Fast check to test if the candidate pixel is a trivially excluded value. * In order to be a corner, the candidate pixel value should be darker or * brighter than 9-12 surrounding pixels, when at least three of the top, * bottom, left and right pixels are brighter or darker it can be * automatically excluded improving the performance. * @param {number} circlePixel The circle pixel value. * @param {number} p The value of the candidate pixel p. * @param {number} threshold * @return {Boolean} * @static * @protected */ tracking.Fast.isTriviallyExcluded = function(circlePixels, p, threshold) { var count = 0; var circleBottom = circlePixels[8]; var circleLeft = circlePixels[12]; var circleRight = circlePixels[4]; var circleTop = circlePixels[0]; if (this.isBrighter(circleTop, p, threshold)) { count++; } if (this.isBrighter(circleRight, p, threshold)) { count++; } if (this.isBrighter(circleBottom, p, threshold)) { count++; } if (this.isBrighter(circleLeft, p, threshold)) { count++; } if (count < 3) { count = 0; if (this.isDarker(circleTop, p, threshold)) { count++; } if (this.isDarker(circleRight, p, threshold)) { count++; } if (this.isDarker(circleBottom, p, threshold)) { count++; } if (this.isDarker(circleLeft, p, threshold)) { count++; } if (count < 3) { return true; } } return false; }; /** * Gets the sixteen offset values of the circle surrounding pixel. * @param {number} width The image width. * @return {array} Array with the sixteen offset values of the circle * surrounding pixel. * @private */ tracking.Fast.getCircleOffsets_ = function(width) { if (this.circles_[width]) { return this.circles_[width]; } var circle = new Int32Array(16); circle[0] = -width - width - width; circle[1] = circle[0] + 1; circle[2] = circle[1] + width + 1; circle[3] = circle[2] + width + 1; circle[4] = circle[3] + width; circle[5] = circle[4] + width; circle[6] = circle[5] + width - 1; circle[7] = circle[6] + width - 1; circle[8] = circle[7] - 1; circle[9] = circle[8] - 1; circle[10] = circle[9] - width - 1; circle[11] = circle[10] - width - 1; circle[12] = circle[11] - width; circle[13] = circle[12] - width; circle[14] = circle[13] - width + 1; circle[15] = circle[14] - width + 1; this.circles_[width] = circle; return circle; }; }()); (function() { /** * Math utility. * @static * @constructor */ tracking.Math = {}; /** * Euclidean distance between two points P(x0, y0) and P(x1, y1). * @param {number} x0 Horizontal coordinate of P0. * @param {number} y0 Vertical coordinate of P0. * @param {number} x1 Horizontal coordinate of P1. * @param {number} y1 Vertical coordinate of P1. * @return {number} The euclidean distance. */ tracking.Math.distance = function(x0, y0, x1, y1) { var dx = x1 - x0; var dy = y1 - y0; return Math.sqrt(dx * dx + dy * dy); }; /** * Calculates the Hamming weight of a string, which is the number of symbols that are * different from the zero-symbol of the alphabet used. It is thus * equivalent to the Hamming distance from the all-zero string of the same * length. For the most typical case, a string of bits, this is the number * of 1's in the string. * * Example: * *
   *  Binary string     Hamming weight
   *   11101                 4
   *   11101010              5
   * 
* * @param {number} i Number that holds the binary string to extract the hamming weight. * @return {number} The hamming weight. */ tracking.Math.hammingWeight = function(i) { i = i - ((i >> 1) & 0x55555555); i = (i & 0x33333333) + ((i >> 2) & 0x33333333); return ((i + (i >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; }; /** * Generates a random number between [a, b] interval. * @param {number} a * @param {number} b * @return {number} */ tracking.Math.uniformRandom = function(a, b) { return a + Math.random() * (b - a); }; /** * Tests if a rectangle intersects with another. * *
   *  x0y0 --------       x2y2 --------
   *      |       |           |       |
   *      -------- x1y1       -------- x3y3
   * 
* * @param {number} x0 Horizontal coordinate of P0. * @param {number} y0 Vertical coordinate of P0. * @param {number} x1 Horizontal coordinate of P1. * @param {number} y1 Vertical coordinate of P1. * @param {number} x2 Horizontal coordinate of P2. * @param {number} y2 Vertical coordinate of P2. * @param {number} x3 Horizontal coordinate of P3. * @param {number} y3 Vertical coordinate of P3. * @return {boolean} */ tracking.Math.intersectRect = function(x0, y0, x1, y1, x2, y2, x3, y3) { return !(x2 > x1 || x3 < x0 || y2 > y1 || y3 < y0); }; }()); (function() { /** * Matrix utility. * @static * @constructor */ tracking.Matrix = {}; /** * Loops the array organized as major-row order and executes `fn` callback * for each iteration. The `fn` callback receives the following parameters: * `(r,g,b,a,index,i,j)`, where `r,g,b,a` represents the pixel color with * alpha channel, `index` represents the position in the major-row order * array and `i,j` the respective indexes positions in two dimensions. * @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 {function} fn The callback function for each pixel. * @param {number} opt_jump Optional jump for the iteration, by default it * is 1, hence loops all the pixels of the array. * @static */ tracking.Matrix.forEach = function(pixels, width, height, fn, opt_jump) { opt_jump = opt_jump || 1; for (var i = 0; i < height; i += opt_jump) { for (var j = 0; j < width; j += opt_jump) { var w = i * width * 4 + j * 4; fn.call(this, pixels[w], pixels[w + 1], pixels[w + 2], pixels[w + 3], w, i, j); } } }; }()); (function() { /** * EPnp utility. * @static * @constructor */ tracking.EPnP = {}; tracking.EPnP.solve = function(objectPoints, imagePoints, cameraMatrix) {}; }()); (function() { /** * Tracker utility. * @constructor * @extends {tracking.EventEmitter} */ tracking.Tracker = function() { tracking.Tracker.base(this, 'constructor'); }; tracking.inherits(tracking.Tracker, tracking.EventEmitter); /** * Tracks the pixels on the array. This method is called for each video * frame in order to emit `track` event. * @param {Uint8ClampedArray} pixels The pixels data to track. * @param {number} width The pixels canvas width. * @param {number} height The pixels canvas height. */ tracking.Tracker.prototype.track = function() {}; }()); (function() { /** * TrackerTask utility. * @constructor * @extends {tracking.EventEmitter} */ tracking.TrackerTask = function(tracker) { tracking.TrackerTask.base(this, 'constructor'); if (!tracker) { throw new Error('Tracker instance not specified.'); } this.setTracker(tracker); }; tracking.inherits(tracking.TrackerTask, tracking.EventEmitter); /** * Holds the tracker instance managed by this task. * @type {tracking.Tracker} * @private */ tracking.TrackerTask.prototype.tracker_ = null; /** * Holds if the tracker task is in running. * @type {boolean} * @private */ tracking.TrackerTask.prototype.running_ = false; /** * Gets the tracker instance managed by this task. * @return {tracking.Tracker} */ tracking.TrackerTask.prototype.getTracker = function() { return this.tracker_; }; /** * Returns true if the tracker task is in running, false otherwise. * @return {boolean} * @private */ tracking.TrackerTask.prototype.inRunning = function() { return this.running_; }; /** * Sets if the tracker task is in running. * @param {boolean} running * @private */ tracking.TrackerTask.prototype.setRunning = function(running) { this.running_ = running; }; /** * Sets the tracker instance managed by this task. * @return {tracking.Tracker} */ tracking.TrackerTask.prototype.setTracker = function(tracker) { this.tracker_ = tracker; }; /** * Emits a `run` event on the tracker task for the implementers to run any * child action, e.g. `requestAnimationFrame`. * @return {object} Returns itself, so calls can be chained. */ tracking.TrackerTask.prototype.run = function() { var self = this; if (this.inRunning()) { return; } this.setRunning(true); this.reemitTrackEvent_ = function(event) { self.emit('track', event); }; this.tracker_.on('track', this.reemitTrackEvent_); this.emit('run'); return this; }; /** * Emits a `stop` event on the tracker task for the implementers to stop any * child action being done, e.g. `requestAnimationFrame`. * @return {object} Returns itself, so calls can be chained. */ tracking.TrackerTask.prototype.stop = function() { if (!this.inRunning()) { return; } this.setRunning(false); this.emit('stop'); this.tracker_.removeListener('track', this.reemitTrackEvent_); return this; }; }()); (function() { /** * ColorTracker utility to track colored blobs in a frame using color * difference evaluation. * @constructor * @param {string|Array.} opt_colors Optional colors to track. * @extends {tracking.Tracker} */ tracking.ColorTracker = function(opt_colors) { tracking.ColorTracker.base(this, 'constructor'); if (typeof opt_colors === 'string') { opt_colors = [opt_colors]; } if (opt_colors) { opt_colors.forEach(function(color) { if (!tracking.ColorTracker.getColor(color)) { throw new Error('Color not valid, try `new tracking.ColorTracker("magenta")`.'); } }); this.setColors(opt_colors); } }; tracking.inherits(tracking.ColorTracker, tracking.Tracker); /** * Holds the known colors. * @type {Object.} * @private * @static */ tracking.ColorTracker.knownColors_ = {}; /** * Caches coordinates values of the neighbours surrounding a pixel. * @type {Object.} * @private * @static */ tracking.ColorTracker.neighbours_ = {}; /** * Registers a color as known color. * @param {string} name The color name. * @param {function} fn The color function to test if the passed (r,g,b) is * the desired color. * @static */ tracking.ColorTracker.registerColor = function(name, fn) { tracking.ColorTracker.knownColors_[name] = fn; }; /** * Gets the known color function that is able to test whether an (r,g,b) is * the desired color. * @param {string} name The color name. * @return {function} The known color test function. * @static */ tracking.ColorTracker.getColor = function(name) { return tracking.ColorTracker.knownColors_[name]; }; /** * Holds the colors to be tracked by the `ColorTracker` instance. * @default ['magenta'] * @type {Array.} */ tracking.ColorTracker.prototype.colors = ['magenta']; /** * Holds the minimum dimension to classify a rectangle. * @default 20 * @type {number} */ tracking.ColorTracker.prototype.minDimension = 20; /** * Holds the maximum dimension to classify a rectangle. * @default Infinity * @type {number} */ tracking.ColorTracker.prototype.maxDimension = Infinity; /** * Holds the minimum group size to be classified as a rectangle. * @default 30 * @type {number} */ tracking.ColorTracker.prototype.minGroupSize = 30; /** * Calculates the central coordinate from the cloud points. The cloud points * are all points that matches the desired color. * @param {Array.} cloud Major row order array containing all the * points from the desired color, e.g. [x1, y1, c2, y2, ...]. * @param {number} total Total numbers of pixels of the desired color. * @return {object} Object containing the x, y and estimated z coordinate of * the blog extracted from the cloud points. * @private */ tracking.ColorTracker.prototype.calculateDimensions_ = function(cloud, total) { var maxx = -1; var maxy = -1; var minx = Infinity; var miny = Infinity; for (var c = 0; c < total; c += 2) { var x = cloud[c]; var y = cloud[c + 1]; if (x < minx) { minx = x; } if (x > maxx) { maxx = x; } if (y < miny) { miny = y; } if (y > maxy) { maxy = y; } } return { width: maxx - minx, height: maxy - miny, x: minx, y: miny }; }; /** * Gets the colors being tracked by the `ColorTracker` instance. * @return {Array.} */ tracking.ColorTracker.prototype.getColors = function() { return this.colors; }; /** * Gets the minimum dimension to classify a rectangle. * @return {number} */ tracking.ColorTracker.prototype.getMinDimension = function() { return this.minDimension; }; /** * Gets the maximum dimension to classify a rectangle. * @return {number} */ tracking.ColorTracker.prototype.getMaxDimension = function() { return this.maxDimension; }; /** * Gets the minimum group size to be classified as a rectangle. * @return {number} */ tracking.ColorTracker.prototype.getMinGroupSize = function() { return this.minGroupSize; }; /** * Gets the eight offset values of the neighbours surrounding a pixel. * @param {number} width The image width. * @return {array} Array with the eight offset values of the neighbours * surrounding a pixel. * @private */ tracking.ColorTracker.prototype.getNeighboursForWidth_ = function(width) { if (tracking.ColorTracker.neighbours_[width]) { return tracking.ColorTracker.neighbours_[width]; } var neighbours = new Int32Array(8); neighbours[0] = -width * 4; neighbours[1] = -width * 4 + 4; neighbours[2] = 4; neighbours[3] = width * 4 + 4; neighbours[4] = width * 4; neighbours[5] = width * 4 - 4; neighbours[6] = -4; neighbours[7] = -width * 4 - 4; tracking.ColorTracker.neighbours_[width] = neighbours; return neighbours; }; /** * Unites groups whose bounding box intersect with each other. * @param {Array.} rects * @private */ tracking.ColorTracker.prototype.mergeRectangles_ = function(rects) { var intersects; var results = []; var minDimension = this.getMinDimension(); var maxDimension = this.getMaxDimension(); for (var r = 0; r < rects.length; r++) { var r1 = rects[r]; intersects = true; for (var s = r + 1; s < rects.length; s++) { var r2 = rects[s]; if (tracking.Math.intersectRect(r1.x, r1.y, r1.x + r1.width, r1.y + r1.height, r2.x, r2.y, r2.x + r2.width, r2.y + r2.height)) { intersects = false; var x1 = Math.min(r1.x, r2.x); var y1 = Math.min(r1.y, r2.y); var x2 = Math.max(r1.x + r1.width, r2.x + r2.width); var y2 = Math.max(r1.y + r1.height, r2.y + r2.height); r2.height = y2 - y1; r2.width = x2 - x1; r2.x = x1; r2.y = y1; break; } } if (intersects) { if (r1.width >= minDimension && r1.height >= minDimension) { if (r1.width <= maxDimension && r1.height <= maxDimension) { results.push(r1); } } } } return results; }; /** * Sets the colors to be tracked by the `ColorTracker` instance. * @param {Array.} colors */ tracking.ColorTracker.prototype.setColors = function(colors) { this.colors = colors; }; /** * Sets the minimum dimension to classify a rectangle. * @param {number} minDimension */ tracking.ColorTracker.prototype.setMinDimension = function(minDimension) { this.minDimension = minDimension; }; /** * Sets the maximum dimension to classify a rectangle. * @param {number} maxDimension */ tracking.ColorTracker.prototype.setMaxDimension = function(maxDimension) { this.maxDimension = maxDimension; }; /** * Sets the minimum group size to be classified as a rectangle. * @param {number} minGroupSize */ tracking.ColorTracker.prototype.setMinGroupSize = function(minGroupSize) { this.minGroupSize = minGroupSize; }; /** * Tracks the `Video` frames. This method is called for each video frame in * order to emit `track` event. * @param {Uint8ClampedArray} pixels The pixels data to track. * @param {number} width The pixels canvas width. * @param {number} height The pixels canvas height. */ tracking.ColorTracker.prototype.track = function(pixels, width, height) { var self = this; var colors = this.getColors(); if (!colors) { throw new Error('Colors not specified, try `new tracking.ColorTracker("magenta")`.'); } var results = []; colors.forEach(function(color) { results = results.concat(self.trackColor_(pixels, width, height, color)); }); this.emit('track', { data: results }); }; /** * Find the given color in the given matrix of pixels using Flood fill * algorithm to determines the area connected to a given node in a * multi-dimensional array. * @param {Uint8ClampedArray} pixels The pixels data to track. * @param {number} width The pixels canvas width. * @param {number} height The pixels canvas height. * @param {string} color The color to be found * @private */ tracking.ColorTracker.prototype.trackColor_ = function(pixels, width, height, color) { var colorFn = tracking.ColorTracker.knownColors_[color]; var currGroup = new Int32Array(pixels.length >> 2); var currGroupSize; var currI; var currJ; var currW; var marked = new Int8Array(pixels.length); var minGroupSize = this.getMinGroupSize(); var neighboursW = this.getNeighboursForWidth_(width); var queue = new Int32Array(pixels.length); var queuePosition; var results = []; var w = -4; if (!colorFn) { return results; } for (var i = 0; i < height; i++) { for (var j = 0; j < width; j++) { w += 4; if (marked[w]) { continue; } currGroupSize = 0; queuePosition = -1; queue[++queuePosition] = w; queue[++queuePosition] = i; queue[++queuePosition] = j; marked[w] = 1; while (queuePosition >= 0) { currJ = queue[queuePosition--]; currI = queue[queuePosition--]; currW = queue[queuePosition--]; if (colorFn(pixels[currW], pixels[currW + 1], pixels[currW + 2], pixels[currW + 3], currW, currI, currJ)) { currGroup[currGroupSize++] = currJ; currGroup[currGroupSize++] = currI; for (var k = 0; k < neighboursW.length; k++) { var otherW = currW + neighboursW[k]; var otherI = currI + neighboursI[k]; var otherJ = currJ + neighboursJ[k]; if (!marked[otherW] && otherI >= 0 && otherI < height && otherJ >= 0 && otherJ < width) { queue[++queuePosition] = otherW; queue[++queuePosition] = otherI; queue[++queuePosition] = otherJ; marked[otherW] = 1; } } } } if (currGroupSize >= minGroupSize) { var data = this.calculateDimensions_(currGroup, currGroupSize); if (data) { data.color = color; results.push(data); } } } } return this.mergeRectangles_(results); }; // Default colors //=================== tracking.ColorTracker.registerColor('cyan', function(r, g, b) { var thresholdGreen = 50, thresholdBlue = 70, dx = r - 0, dy = g - 255, dz = b - 255; if ((g - r) >= thresholdGreen && (b - r) >= thresholdBlue) { return true; } return dx * dx + dy * dy + dz * dz < 6400; }); tracking.ColorTracker.registerColor('magenta', function(r, g, b) { var threshold = 50, dx = r - 255, dy = g - 0, dz = b - 255; if ((r - g) >= threshold && (b - g) >= threshold) { return true; } return dx * dx + dy * dy + dz * dz < 19600; }); tracking.ColorTracker.registerColor('yellow', function(r, g, b) { var threshold = 50, dx = r - 255, dy = g - 255, dz = b - 0; if ((r - b) >= threshold && (g - b) >= threshold) { return true; } return dx * dx + dy * dy + dz * dz < 10000; }); // Caching neighbour i/j offset values. //===================================== var neighboursI = new Int32Array([-1, -1, 0, 1, 1, 1, 0, -1]); var neighboursJ = new Int32Array([0, 1, 1, 1, 0, -1, -1, -1]); }()); (function() { /** * ObjectTracker utility. * @constructor * @param {string|Array.>} opt_classifiers Optional * object classifiers to track. * @extends {tracking.Tracker} */ tracking.ObjectTracker = function(opt_classifiers) { tracking.ObjectTracker.base(this, 'constructor'); if (opt_classifiers) { if (!Array.isArray(opt_classifiers)) { opt_classifiers = [opt_classifiers]; } if (Array.isArray(opt_classifiers)) { opt_classifiers.forEach(function(classifier, i) { if (typeof classifier === 'string') { opt_classifiers[i] = tracking.ViolaJones.classifiers[classifier]; } if (!opt_classifiers[i]) { throw new Error('Object classifier not valid, try `new tracking.ObjectTracker("face")`.'); } }); } } this.setClassifiers(opt_classifiers); }; tracking.inherits(tracking.ObjectTracker, tracking.Tracker); /** * Specifies the edges density of a block in order to decide whether to skip * it or not. * @default 0.2 * @type {number} */ tracking.ObjectTracker.prototype.edgesDensity = 0.2; /** * Specifies the initial scale to start the feature block scaling. * @default 1.0 * @type {number} */ tracking.ObjectTracker.prototype.initialScale = 1.0; /** * Specifies the scale factor to scale the feature block. * @default 1.25 * @type {number} */ tracking.ObjectTracker.prototype.scaleFactor = 1.25; /** * Specifies the block step size. * @default 1.5 * @type {number} */ tracking.ObjectTracker.prototype.stepSize = 1.5; /** * Gets the tracker HAAR classifiers. * @return {TypedArray.} */ tracking.ObjectTracker.prototype.getClassifiers = function() { return this.classifiers; }; /** * Gets the edges density value. * @return {number} */ tracking.ObjectTracker.prototype.getEdgesDensity = function() { return this.edgesDensity; }; /** * Gets the initial scale to start the feature block scaling. * @return {number} */ tracking.ObjectTracker.prototype.getInitialScale = function() { return this.initialScale; }; /** * Gets the scale factor to scale the feature block. * @return {number} */ tracking.ObjectTracker.prototype.getScaleFactor = function() { return this.scaleFactor; }; /** * Gets the block step size. * @return {number} */ tracking.ObjectTracker.prototype.getStepSize = function() { return this.stepSize; }; /** * Tracks the `Video` frames. This method is called for each video frame in * order to emit `track` event. * @param {Uint8ClampedArray} pixels The pixels data to track. * @param {number} width The pixels canvas width. * @param {number} height The pixels canvas height. */ tracking.ObjectTracker.prototype.track = function(pixels, width, height) { var self = this; var classifiers = this.getClassifiers(); if (!classifiers) { throw new Error('Object classifier not specified, try `new tracking.ObjectTracker("face")`.'); } var results = []; classifiers.forEach(function(classifier) { results = results.concat(tracking.ViolaJones.detect(pixels, width, height, self.getInitialScale(), self.getScaleFactor(), self.getStepSize(), self.getEdgesDensity(), classifier)); }); this.emit('track', { data: results }); }; /** * Sets the tracker HAAR classifiers. * @param {TypedArray.} classifiers */ tracking.ObjectTracker.prototype.setClassifiers = function(classifiers) { this.classifiers = classifiers; }; /** * Sets the edges density. * @param {number} edgesDensity */ tracking.ObjectTracker.prototype.setEdgesDensity = function(edgesDensity) { this.edgesDensity = edgesDensity; }; /** * Sets the initial scale to start the block scaling. * @param {number} initialScale */ tracking.ObjectTracker.prototype.setInitialScale = function(initialScale) { this.initialScale = initialScale; }; /** * Sets the scale factor to scale the feature block. * @param {number} scaleFactor */ tracking.ObjectTracker.prototype.setScaleFactor = function(scaleFactor) { this.scaleFactor = scaleFactor; }; /** * Sets the block step size. * @param {number} stepSize */ tracking.ObjectTracker.prototype.setStepSize = function(stepSize) { this.stepSize = stepSize; }; }());