qa-prevention-wlaq-vue/static/face/build/tracking.js

2436 lines
77 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 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 {mj} 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 {mj}. 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.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:
*
* <pre>
* Binary string Hamming weight
* 11101 4
* 11101010 5
* </pre>
*
* @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.
*
* <pre>
* x0y0 -------- x2y2 --------
* | | | |
* -------- x1y1 -------- x3y3
* </pre>
*
* @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.<string>} 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.<string, function>}
* @private
* @static
*/
tracking.ColorTracker.knownColors_ = {}
/**
* Caches coordinates values of the neighbours surrounding a pixel.
* @type {Object.<number, Int32Array>}
* @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.<string>}
*/
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.<number>} 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.<string>}
*/
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.<Object>} 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.<string>} 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.<string|Array.<number>>} 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.<number>}
*/
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.<number>} 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
}
}())