(function() {
/**
* ColorTracker utility to track colored blobs in a frrame 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 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 contaning 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;
};
/**
* Sets the minimum dimension to classify a rectangle.
* @param {number} minDimension
*/
tracking.ColorTracker.prototype.getMinDimension = function() {
return this.minDimension;
};
/**
* 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();
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) {
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.
* @return {number}
*/
tracking.ColorTracker.prototype.setMinDimension = function(minDimension) {
this.minDimension = minDimension;
};
/**
* 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]);
}());