/**
 * Real-time object detector based on the Viola Jones Framework.
 * Compatible to OpenCV Haar Cascade Classifiers (stump based only).
 * 
 * Copyright (c) 2012, Martin Tschirsich

 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 */
var objectdetect = (function() {
	"use strict";
	
	/**
	 * Define system-specific optimal array types (Float32Array if available).
	 */
	var ImageArray, ZeroFilledImageArray;
	
	if (typeof Float32Array !== "undefined") {
		ImageArray = ZeroFilledImageArray = Float32Array;
	} else {
		ZeroFilledImageArray = function(length) {
	    	Array(length);
	    	for (var i = 0; i < length; ++i) this[i] = 0;
	    };
	    ZeroFilledImageArray.prototype = ImageArray = Array;
	}
    	
    var /**
		 * Converts from a 4-channel RGBA source image to a 1-channel grayscale
		 * image. Corresponds to the CV_RGB2GRAY OpenCV color space conversion.
		 * 
		 * @param {Array} src	4-channel 8-bit RGBA source image
		 * @param {Array} [dst] 1-channel 32-bit destination image. If omitted,
		 *                      a new image will be created
		 * @return {Array} 1-channel 32-bit destination image
		 */
		convertRgbaToGrayscale = function(src, dst) {
			var srcLength = src.length;
			if (!dst) { dst = new ImageArray(srcLength >> 2); }
			
			for (var i = 0; i < srcLength; i += 4) {
				dst[i >> 2] = (src[i] * 4899 + src[i + 1] * 9617 + src[i + 2] * 1868 + 8192) >> 14;
			}
			return dst;
		},
		
		/**
		 * Computes the gradient magnitude using a sobel filter after
		 * applying gaussian smoothing (5x5 filter size). Useful for canny
		 * pruning.
		 * 
		 * @param {Array}  src      1-channel source image
		 * @param {Number} srcWidth Width of the source image
		 * @param {Array}  [dst]    1-channel destination image. If omitted,
		 *                          a new image will be created
		 * @return {Array} Destination image
		 */
		buffer = null,
		computeCanny = function(src, srcWidth, srcHeight, dst) {
			var srcLength = src.length;
			if (!dst) { dst = new ImageArray(srcLength); }
			else if (dst === src) { src = new ImageArray(dst); }
			
			// Gaussian filter (size 5, sigma sqrt(2)) horizontal pass:
			if (!buffer) buffer = new ImageArray(srcLength);
			for (var x = 2; x < srcWidth-2; ++x) {
				for (var y = 0; y < srcHeight; ++y) {
					var index = x + y * srcWidth;
					dst[index] =
						0.1117 * src[index - 2] +
						0.2365 * src[index - 1] +
						0.3036 * src[index    ] +
						0.2365 * src[index + 1] +
						0.1117 * src[index + 2];
				}
			}
			
			// Gaussian filter (size 5, sigma sqrt(2)) vertical pass:
			for (var x = 0; x < srcWidth; ++x) {
				for (var y = 2; y < srcHeight-2; ++y) {
					var index = x + y*srcWidth;
					buffer[index] =
						0.1117 * dst[index - (srcWidth << 1)] +
						0.2365 * dst[index -  srcWidth      ] +
						0.3036 * dst[index                  ] +
						0.2365 * dst[index +  srcWidth      ] +
						0.1117 * dst[index + (srcWidth << 1)];
				}
			}
			
			// Compute gradient:
			for(x = 2; x < srcWidth - 2; ++x) {
				for(y = 2; y < srcHeight - 2; ++y) {
					var grad_x =
						-     buffer[x-1 + (y-1) * srcWidth]
						+     buffer[x+1 + (y-1) * srcWidth]
						- 2 * buffer[x-1 + y     * srcWidth]
						+ 2 * buffer[x+1 + y     * srcWidth]
						-     buffer[x-1 + (y+1) * srcWidth]
						+     buffer[x+1 + (y+1) * srcWidth];
					
					var grad_y = 
						      buffer[x-1 + (y-1) * srcWidth]
						+ 2 * buffer[x   + (y-1) * srcWidth]
						+     buffer[x+1 + (y-1) * srcWidth]
						-     buffer[x-1 + (y+1) * srcWidth]
						- 2 * buffer[x   + (y+1) * srcWidth]
						-     buffer[x+1 + (y+1) * srcWidth];
					
					dst[x + y * srcWidth] = 
						(grad_x < 0 ? -grad_x : grad_x) + 
						(grad_y < 0 ? -grad_y : grad_y);
				}
			}
			return dst;
		},

		/**
		 * Computes the integral image of a 1-channel image. Arithmetic
		 * overflow may occur if the integral exceeds the limits for the
		 * destination image values ([0, 2^32-1] for am unsigned 32-bit image).
		 * The integral image is 1 pixel wider both in vertical and horizontal
		 * direction compared to the source image.
		 * 
		 * SAT = Summed Area Table
		 * 
		 * @param {Array}  src       1-channel source image
		 * @param {Number} srcWidth  Width of the source image
		 * @param {Number} srcHeight Height of the source image
		 * @param {Array}  [dst]     1-channel destination image (optional)
		 * @return {Array} Destination image
		 */
		computeSat = function(src, srcWidth, srcHeight, dst) {
			var srcLength = src.length,
				dstWidth = srcWidth + 1;
			
			if (!dst) { dst = new ZeroFilledImageArray(srcLength + dstWidth + srcHeight); }
			
			for (var x = 1; x <= srcWidth; ++x) {
				var column_sum = 0;
				for (var y = 1; y <= srcHeight; ++y) {
					var index = x + y * dstWidth;
					column_sum += src[index - y - dstWidth];
					dst[index] = dst[index - 1] + column_sum;
				}
			}
			return dst;
		},
		
		/**
		 * Computes the squared integral image of a 1-channel image.
		 * @see computeSat()
		 * 
		 * @param {Array}  src       1-channel source image
		 * @param {Number} srcWidth  Width of the source image
		 * @param {Number} srcHeight Height of the source image
		 * @param {Array}  [dst]     1-channel destination image. If omitted, the
		 *                           result is written to src (faster)
		 * @return {Array} Destination image
		 */
		computeSquaredSat = function(src, srcWidth, srcHeight, dst) {
			var srcLength = src.length,
				dstWidth = srcWidth + 1;
		
			if (!dst) { dst = new ZeroFilledImageArray(srcLength + dstWidth + srcHeight); }
			
			for (var x = 1; x <= srcWidth; ++x) {
				var column_sum = 0;
				for (var y = 1; y <= srcHeight; ++y) {
					var index = x + y * dstWidth;
					var val = src[index - y - dstWidth];
					column_sum += val * val;
					dst[index] = dst[index - 1] + column_sum;
				}
			}
			return dst;
		},
		
		/**
		 * Computes the rotated / tilted integral image of a 1-channel image.
		 * @see computeSat()
		 * 
		 * @param {Array}  src       1-channel source image
		 * @param {Number} srcWidth  Width of the source image
		 * @param {Number} srcHeight Height of the source image
		 * @param {Array}  [dst]     1-channel destination image. If omitted, the
		 *                           result is written to src (faster)
		 * @return {Array} Destination image
		 */
		computeRsat = function(src, srcWidth, srcHeight, dst) {
			var srcLength = src.length,
				dstWidth = srcWidth + 1,
				dstLength = srcLength + dstWidth + srcHeight;
			
			if (!dst) { dst = new ZeroFilledImageArray(dstLength); }
				
			// Compute first diagonal integral:
			for (var y = 1; y <= srcHeight; ++y) {
				for (var x = 1; x <= srcWidth; ++x) {
					dst[x + y * dstWidth] = src[x - 1 + y * srcWidth - srcWidth] + dst[x + y * dstWidth - dstWidth - 1];
				}
			}
			
			// Compute second diagonal integral:
			for (var y = 1; y <= srcHeight; ++y) {
				dst[srcWidth + y * dstWidth] += dst[srcWidth + y * dstWidth - dstWidth];
			}
			
			for (var x = srcWidth - 1; x > 0; --x) {
				for (var y = srcHeight; y > 0; --y) {
					dst[x + y * dstWidth] += dst[x + y * dstWidth - dstWidth] + dst[x + 1 + y * dstWidth - dstWidth];
				}
			}
			
			return dst;
		},
		
		/**
		 * Compute area on a SAT.
		 * 
		 * @param {Array}  sat       1-channel integral source image
		 * @param {Number} satWidth  Width of the integral source image
		 * @param {Number} x         Area to evaluate
		 * @param {Number} y         Area to evaluate
		 * @param {Number} width     Area to evaluate
		 * @param {Number} height    Area to evaluate
		 * @return {Number} Area
		 */
		computeSatSum = function(sat, satWidth, x, y, width, height) {
			y *= satWidth;
			height *= satWidth;
			return sat[x         + y         ] -
			       sat[x + width + y         ] -
			       sat[x         + y + height] +
			       sat[x + width + y + height];
		},
		
		/**
		 * Compute area on a RSAT.
		 * @see computeSatSum()
		 * 
		 * @param {Array}  rsat      1-channel integral source image
		 * @param {Number} rsatWidth Width of the integral source image
		 * @param {Number} x         Area to evaluate
		 * @param {Number} y         Area to evaluate
		 * @param {Number} width     Area to evaluate
		 * @param {Number} height    Area to evaluate
		 * @return {Number} Area
		 */
		computeRSatSum = function(rsat, rsatWidth, x, y, width, height) {
			return rsat[x                  + (y                 ) * rsatWidth] -
			       rsat[x + width          + (y + width         ) * rsatWidth] -
			       rsat[x - height         + (y + height        ) * rsatWidth] +
			       rsat[x + width - height + (y + width + height) * rsatWidth];
		},
		
		/**
		 * Equalizes the histogram of an unsigned 1-channel image with values
		 * in range [0, 255]. Corresponds to the equalizeHist OpenCV function.
		 * 
		 * @param {Array} src   1-channel integer source image
		 * @param {Array} [dst] 1-channel destination image. If omitted, the
		 * 	                    result is written to src
		 * @return {Array} Destination image
		 */
		equalizeHistogram = function(src, dst) {
			var srcLength = src.length;
			if (!dst) { dst = src; }
			
			// Compute histogram and histogram sum:
			var hist = new ZeroFilledImageArray(256);
			for (var i = 0; i < srcLength; ++i) {
				++hist[src[i]];
			}
			
			// Compute integral histogram:
			var prev = hist[0];
			for (var i = 1; i < 256; ++i) {
				prev = hist[i] += prev;
			}
			
			// Equalize image:
			var norm = 255 / srcLength;
			for (var i = 0; i < srcLength; ++i) {
				dst[i] = ~~(hist[src[i]] * norm + 0.5);
			}
			return dst;
		},
		
		/**
		 * Evaluates a Haar cascade classifier at a specified scale.
		 * 
		 * @param {Array}  sat	       SAT of the source image
		 * @param {Array}  rsat	       RSAT of the source image
		 * @param {Array}  ssat	       Squared SAT of the source image
		 * @param {Array}  cannySat	   SAT of canny source image or undefined
		 * @param {Number} width       Width of the source image
		 * @param {Number} height      Height of the source image
		 * @param {Number} scale       Scale
		 * @param {Object} cascadeClassifier Haar cascade classifier
		 * @return {Array} Rectangles representing detected object
		 */
		detectSingleScale = function(sat, rsat, ssat, cannySat, width, height, scale, cascadeClassifier) {
			var windowWidth  = ~~(cascadeClassifier.size[0] * scale);
			var windowHeight = ~~(cascadeClassifier.size[1] * scale);
			var stepX = ~~(0.5 * scale + 1.5); // = 2;
			var stepY = ~~(0.5 * scale + 1.5); // = 2;

			var rects = [];
			for (var x = 0; x + windowWidth <= width; x += stepX) {
				for (var y = 0; y + windowHeight <= height; y += stepY) {
					
					var invArea = 1 / (windowWidth * windowHeight);
					
					// Canny test:
					if (cannySat) {
						var edgesDensity = computeSatSum(cannySat, width + 1, x, y, windowWidth, windowHeight) * invArea;
						if (edgesDensity < 20 || edgesDensity > 100) {
							continue;
						}
					}
					
					// Correct?
					var satOffset = x + y * (width + 1);
					var satHeight = windowHeight * (width + 1);

					var mean = (sat[satOffset] -
							    sat[satOffset + windowWidth] -
						        sat[satOffset + satHeight] +
						        sat[satOffset + windowWidth + satHeight]) * invArea;
					
					var variance = (ssat[satOffset] -
						            ssat[satOffset + windowWidth] -
						            ssat[satOffset + satHeight] +
						            ssat[satOffset + windowWidth + satHeight]) * invArea - mean * mean;
					
					var std = variance > 1 ? Math.sqrt(variance) : 1;
					
					// Evaluate cascade classifier: stages
					var complexClassifiers = cascadeClassifier.complexClassifiers;
					var found = true;
					for (var i = 0, iEnd = complexClassifiers.length; i < iEnd; ++i) {
						var complexClassifier = complexClassifiers[i];
						
						// Evaluate complex classifier: trees
						var simpleClassifiers = complexClassifier.simpleClassifiers;
						var complexClassifierThreshold = complexClassifier.threshold;
						var complexClassifierSum = 0;
						
						for (var j = 0, jEnd = simpleClassifiers.length; j < jEnd; ++j) {
							var simpleClassifier = simpleClassifiers[j];
							
							// Evaluate simple classifier: nodes
							var features = simpleClassifier.features;
							var simpleClassifierSum = 0;
							
							if (simpleClassifier.tilted === 1) {
								for (var k = 0, kEnd = features.length; k < kEnd; ++k) {
									var feature = features[k];
									
									// Evaluate feature: rects
									var featureOffset = ~~(x + feature[0] * scale) + ~~(y + feature[1] * scale) * (width + 1);
									var featureWidth  = ~~(feature[2] * scale);
									var featureWidthTimesWidth  = ~~(feature[2] * scale) * (width + 1);
									var featureHeight = ~~(feature[3] * scale);
									var featureHeightTimesWidth = ~~(feature[3] * scale) * (width + 1);
							
									simpleClassifierSum +=
										(rsat[featureOffset] -
										 rsat[featureOffset +  featureWidth + featureWidthTimesWidth] -
										 rsat[featureOffset - featureHeight +  featureHeightTimesWidth] +
										 rsat[featureOffset + featureWidth - featureHeight + featureWidthTimesWidth + featureHeightTimesWidth]) * feature[4];
								}
							} else {
								for (var k = 0, kEnd = features.length; k < kEnd; ++k) {
									var feature = features[k];
									
									// Evaluate feature: rects
									var featureOffset = ~~(x + feature[0] * scale) + ~~(y + feature[1] * scale) * (width + 1);
									var featureWidth  = ~~(feature[2] * scale);
									var featureHeight = ~~(feature[3] * scale) * (width + 1);
									
									simpleClassifierSum +=
										(sat[featureOffset] -
										 sat[featureOffset + featureWidth] -
										 sat[featureOffset + featureHeight] +
										 sat[featureOffset + featureWidth + featureHeight]) * feature[4];
								}
							}

							complexClassifierSum += (simpleClassifierSum * invArea < simpleClassifier.threshold * std) ? simpleClassifier.left_val : simpleClassifier.right_val;
							// Possible optimization if all values are positive:
							// if (complexClassifierSum >= complexClassifierThreshold) break;
						}
						if (complexClassifierSum < complexClassifierThreshold) {
							found = false;
							break;
						}
					}
					if (found) rects.push([x, y, windowWidth, windowHeight]);
				}
			}
			return rects;
		},
		
		/**
		 * Evaluates a Haar cascade classifier at all scales.
		 * 
		 * @param {Array}  sat	       SAT of the source image
		 * @param {Array}  rsat	       RSAT of the source image
		 * @param {Array}  ssat	       Squared SAT of the source image
		 * @param {Array}  cannySat	   SAT of canny source image or undefined
		 * @param {Number} width       Width of the source image
		 * @param {Number} height      Height of the source image
		 * @param {Object} cascadeClassifier Haar cascade classifier
		 * @return {Array} Rectangles representing detected object
		 */
		detectMultiScale = function(sat, rsat, ssat, cannySat, width, height, cascadeClassifier, scaleFactor, scaleMin) {
			var initialWidth = cascadeClassifier.size[0];
			var initialHeight = cascadeClassifier.size[1];

			if (!scaleMin) scaleMin = 1;
			if (!scaleFactor) scaleFactor = 1.2;
			
			var scale = scaleMin;
			var rects = [];
			while (scale * initialWidth < width && scale * initialHeight < height) {
				rects = rects.concat(detectSingleScale(sat, rsat, ssat, cannySat, width, height, scale, cascadeClassifier));
				scale *= scaleFactor;
			}
			return rects;
		},
		
		/**
		 * Evaluates a Haar cascade classifier at increasingly coarser scale.
		 * Stops the evaluation as soon as the first object has been detected.
		 * 
		 * @param {Array}  sat	       SAT of the source image
		 * @param {Array}  rsat	       RSAT of the source image
		 * @param {Array}  ssat	       Squared SAT of the source image
		 * @param {Array}  cannySat	   SAT of canny source image or undefined
		 * @param {Number} width       Width of the source image
		 * @param {Number} height      Height of the source image
		 * @param {Object} cascadeClassifier Haar cascade classifier
		 * @return {Array} Rectangles representing detected object
		 */
		detectFinestScale = function(sat, rsat, ssat, cannySat, width, height, cascadeClassifier) {
			var initialWidth = cascadeClassifier.size[0];
			var initialHeight = cascadeClassifier.size[1];

			var scale = 1;
			var scaleFactor = 1.2;
			var rects = [];
			while (scale * initialWidth < width && scale * initialHeight < height) {
				rects = detectSingleScale(sat, rsat, ssat, cannySat, width, height, scale, cascadeClassifier);
				if (rects[0]) break;
				scale *= scaleFactor;
			}
			return rects;
		},
	    
		/**
		 * Groups rectangles together using a rectilinear distance metric. For
		 * each group of related rectangles, a representative mean rectangle
		 * is returned.
		 * 
		 * @param {Array} rects         Rectangles (Arrays of 4 floats)
		 * @param {Number} minNeighbors 
		 * @return {Array}              Mean rectangles (Arrays of 4 floats)
		 */
		groupRectangles = function(rects, minNeighbors) {
			var rectsLength = rects.length;
			
	    	// Partition rects into similarity classes:
	    	var numClasses = 0;
	    	var labels = new Array(rectsLength);
			for (var i = 0; i < labels.length; ++i) {
				labels[i] = 0;
			}
			
			for (var i = 0; i < rectsLength; ++i) {
				var found = false;
				for (var j = 0; j < i; ++j) {
					
					// Determine similarity:
					var rect1 = rects[i];
					var rect2 = rects[j];
			        var delta = 0.1 * (Math.min(rect1[2], rect2[2]) + Math.min(rect1[3], rect2[3]));
			        if (Math.abs(rect1[0] - rect2[0]) <= delta &&
			        	Math.abs(rect1[1] - rect2[1]) <= delta &&
			        	Math.abs(rect1[0] + rect1[2] - rect2[0] - rect2[2]) <= delta &&
			        	Math.abs(rect1[1] + rect1[3] - rect2[1] - rect2[3]) <= delta) {
						
						labels[i] = labels[j];
						found = true;
						break;
					}
				}
				if (!found) {
					labels[i] = numClasses++;
				}
			}
			
			// Compute average rectangle (group) for each cluster:
			var groups = new Array(numClasses);
			
			for (var i = 0; i < numClasses; ++i) {
				groups[i] = [0, 0, 0, 0, 0];
			}
			
			for (var i = 0; i < rectsLength; ++i) {
				var label = labels[i];
				groups[label][0] += rects[i][0];
				groups[label][1] += rects[i][1];
				groups[label][2] += rects[i][2];
				groups[label][3] += rects[i][3];
				groups[label][4]++;
			}
			
			for (var i = 0; i < numClasses; ++i) {
				var numNeighbors = groups[i][4];
				if (numNeighbors >= minNeighbors) {
					groups[i][0] /= numNeighbors;
					groups[i][1] /= numNeighbors;
					groups[i][2] /= numNeighbors;
					groups[i][3] /= numNeighbors;
				}
			}
			
			// Filter out small rectangles inside larger rectangles:
			var filteredGroups = [];
			for (var i = 0; i < numClasses; ++i) {
		        var r1 = groups[i];
		        
		        for (var j = 0; j < numClasses; ++j) {
		        	if (i === j) continue;
		            var r2 = groups[j];
		            var dx = r2[2] * 0.2;
		            var dy = r2[3] * 0.2;
		            
		            if (r1[0] >= r2[0] - dx &&
		                r1[1] >= r2[1] - dy &&
		                r1[0] + r1[2] <= r2[0] + r2[2] + dx &&
		                r1[1] + r1[3] <= r2[1] + r2[3] + dy) {
		            	
		            	break;
		            }
		        }
		        
		        if (j === numClasses) {
		        	filteredGroups.push(r1);
		        }
		    }
			return filteredGroups;
		};
	
		return {
			equalizeHistogram: equalizeHistogram,
			convertRgbaToGrayscale: convertRgbaToGrayscale,
			computeCanny: computeCanny,
			computeSat: computeSat,
			computeRsat: computeRsat,
			computeSatSum: computeSatSum,
			computeSquaredSat: computeSquaredSat,
			computeRSatSum: computeRSatSum,
			groupRectangles: groupRectangles,
			detectMultiScale: detectMultiScale,
			detectSingleScale: detectSingleScale,
			detectFinestScale: detectFinestScale
		};
})();