tracking
Version:
A modern approach for Computer Vision on the web.
1,385 lines (1,274 loc) • 79.6 kB
JavaScript
/**
* tracking - A modern approach for Computer Vision on the web.
* @author Eduardo Lundgren <edu@rdo.io>
* @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:
* <pre>
* 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();
* </pre>
*
* @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.<number>}
*/
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.<number, Int32Array>}
* @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.<number, Int32Array>}
* @private
* @static
*/
tracking.Fast.circ