UNPKG

clmtrackr

Version:

Javascript library for precise tracking of facial features via Constrained Local Models

1,039 lines (889 loc) 31.9 kB
/** * clmtrackr library (https://www.github.com/auduno/clmtrackr/) * * Copyright (c) 2013, Audun Mathias Øygard * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import numeric from 'numeric'; import raf from 'raf'; import Promise from 'promise-polyfill'; import emitEvent from './utils/events.js' import faceDetection from './facedetector/faceDetection.js'; import svmFilter from './svmfilter/svmfilter_fft.js'; import webglFilter from './svmfilter/svmfilter_webgl.js'; import mosseFilterResponses from './mossefilter/mosseFilterResponses.js'; import pModel from '../models/model_pca_20_svm.js'; import canRenderToFloatTexture from './utils/webgl_tests.js'; import { version } from '../package.json'; //import { drawPatches } from './utils.debugging.js'; var DEFAULT_MODEL = pModel; // polyfills raf.polyfill(); if (!window.Promise) window.Promise = Promise; var clm = { tracker : function(params) { if (!params) params = {}; if (params.constantVelocity === undefined) params.constantVelocity = true; if (params.searchWindow === undefined) params.searchWindow = 11; if (params.useWebGL === undefined) params.useWebGL = true; if (params.scoreThreshold === undefined) params.scoreThreshold = 0.5; if (params.stopOnConvergence === undefined) params.stopOnConvergence = false; if (params.weightPoints === undefined) params.weightPoints = undefined; if (params.sharpenResponse === undefined) params.sharpenResponse = false; if (params.faceDetection === undefined) params.faceDetection = {}; /** @type {Number} Minimum convergence before firing `clmtrackrConverged` event. */ var convergenceThreshold = 0.5; var numPatches, patchSize, numParameters, patchType; var gaussianPD; var eigenVectors, eigenValues; var sketchCC, sketchW, sketchH, sketchCanvas; var weights, model, biases; var sobelInit = false; var lbpInit = false; var currentParameters = []; var currentPositions = []; var previousParameters = []; var previousPositions = []; var patches = []; var responses = []; var meanShape = []; var responseMode = 'single'; var responseList = ['raw']; var responseIndex = 0; /* It's possible to experiment with the sequence of variances used for the finding the maximum in the KDE. This sequence is pretty arbitrary, but was found to be okay using some manual testing. */ var varianceSeq = [10,5,1]; //var varianceSeq = [3,1.5,0.75]; //var varianceSeq = [6,3,0.75]; var PDMVariance = 0.7; var relaxation = 0.1; var first = true; var detectingFace = false; var convergenceLimit = 0.01; var searchWindow; var modelWidth, modelHeight; var halfSearchWindow, vecProbs, responsePixels; if(typeof Float64Array !== 'undefined') { var updatePosition = new Float64Array(2); var vecpos = new Float64Array(2); } else { var updatePosition = new Array(2); var vecpos = new Array(2); } var pw, pl, pdataLength; var facecheck_count = 0; var webglFi, svmFi, mosseCalc; var scoringCanvas = document.createElement('canvas'); var scoringContext = scoringCanvas.getContext('2d'); var msxmin, msymin, msxmax, msymax; var msmodelwidth, msmodelheight; var scoringWeights, scoringBias; var scoringHistory = []; var meanscore = 0; var runnerTimeout, runnerElement, runnerBox; var pointWeights; var halfPI = Math.PI/2; var faceDetector; /* * load model data, initialize filters, etc. * * @param <Object> pdm model object */ this.init = function(pdmmodel) { // default model is pca 20 svm model if (pdmmodel === undefined) pdmmodel = DEFAULT_MODEL; model = pdmmodel; // load from model patchType = model.patchModel.patchType; numPatches = model.patchModel.numPatches; patchSize = model.patchModel.patchSize[0]; if (patchType == 'MOSSE') { searchWindow = patchSize; } else { searchWindow = params.searchWindow; } numParameters = model.shapeModel.numEvalues; modelWidth = model.patchModel.canvasSize[0]; modelHeight = model.patchModel.canvasSize[1]; // set up canvas to work on sketchCanvas = document.createElement('canvas'); sketchCC = sketchCanvas.getContext('2d'); sketchW = sketchCanvas.width = modelWidth + (searchWindow-1) + patchSize-1; sketchH = sketchCanvas.height = modelHeight + (searchWindow-1) + patchSize-1; // load eigenvectors eigenVectors = numeric.rep([numPatches*2,numParameters],0.0); for (var i = 0;i < numPatches*2;i++) { for (var j = 0;j < numParameters;j++) { eigenVectors[i][j] = model.shapeModel.eigenVectors[i][j]; } } // load mean shape for (var i = 0; i < numPatches;i++) { meanShape[i] = [model.shapeModel.meanShape[i][0], model.shapeModel.meanShape[i][1]]; } // get max and mins, width and height of meanshape msxmax = msymax = 0; msxmin = msymin = 1000000; for (var i = 0;i < numPatches;i++) { if (meanShape[i][0] < msxmin) msxmin = meanShape[i][0]; if (meanShape[i][1] < msymin) msymin = meanShape[i][1]; if (meanShape[i][0] > msxmax) msxmax = meanShape[i][0]; if (meanShape[i][1] > msymax) msymax = meanShape[i][1]; } msmodelwidth = msxmax-msxmin; msmodelheight = msymax-msymin; // get scoringweights if they exist if (model.scoring) { scoringWeights = new Float64Array(model.scoring.coef); scoringBias = model.scoring.bias; scoringCanvas.width = model.scoring.size[0]; scoringCanvas.height = model.scoring.size[1]; } // load eigenvalues eigenValues = model.shapeModel.eigenValues; weights = model.patchModel.weights; biases = model.patchModel.bias; // precalculate gaussianPriorDiagonal gaussianPD = numeric.rep([numParameters+4, numParameters+4],0); // set values and append manual inverse for (var i = 0;i < numParameters;i++) { if (model.shapeModel.nonRegularizedVectors.indexOf(i) >= 0) { gaussianPD[i+4][i+4] = 1/10000000; } else { gaussianPD[i+4][i+4] = 1/eigenValues[i]; } } for (var i = 0;i < numParameters+4;i++) { currentParameters[i] = 0; } if (patchType == 'SVM') { var webGLContext; var webGLTestCanvas = document.createElement('canvas'); if (window.WebGLRenderingContext) { webGLContext = webGLTestCanvas.getContext('webgl') || webGLTestCanvas.getContext('experimental-webgl'); if (!webGLContext || !webGLContext.getExtension('OES_texture_float')) { webGLContext = null; } else { // test whether it's possible to render to float texture if (!canRenderToFloatTexture(webGLContext)) { webGLContext = null; } } } if (webGLContext && params.useWebGL && (typeof(webglFilter) !== 'undefined')) { webglFi = new webglFilter(); try { webglFi.init(weights, biases, numPatches, searchWindow+patchSize-1, searchWindow+patchSize-1, patchSize, patchSize); if ('lbp' in weights) lbpInit = true; if ('sobel' in weights) sobelInit = true; } catch(err) { console.error(err); alert('There was a problem setting up webGL programs, falling back to slightly slower javascript version. :('); webglFi = undefined; svmFi = new svmFilter(); svmFi.init(weights['raw'], biases['raw'], numPatches, patchSize, searchWindow); } } else if (typeof(svmFilter) !== 'undefined') { // use fft convolution if no webGL is available svmFi = new svmFilter(); svmFi.init(weights['raw'], biases['raw'], numPatches, patchSize, searchWindow); } else { throw new Error('Could not initiate filters, please make sure that svmfilter.js or svmfilter_conv_js.js is loaded.'); } } else if (patchType == 'MOSSE') { mosseCalc = new mosseFilterResponses(); mosseCalc.init(weights, numPatches, patchSize, patchSize); } if (patchType == 'SVM') { pw = pl = patchSize+searchWindow-1; } else { pw = pl = searchWindow; } pdataLength = pw*pl; halfSearchWindow = (searchWindow-1)/2; responsePixels = searchWindow*searchWindow; if(typeof Float64Array !== 'undefined') { vecProbs = new Float64Array(responsePixels); for (var i = 0;i < numPatches;i++) { patches[i] = new Float64Array(pdataLength); } } else { vecProbs = new Array(responsePixels); for (var i = 0;i < numPatches;i++) { patches[i] = new Array(pdataLength); } } if (params.weightPoints) { // weighting of points pointWeights = []; for (var i = 0;i < numPatches;i++) { if (i in params.weightPoints) { pointWeights[(i*2)] = params.weightPoints[i]; pointWeights[(i*2)+1] = params.weightPoints[i]; } else { pointWeights[(i*2)] = 1; pointWeights[(i*2)+1] = 1; } } pointWeights = numeric.diag(pointWeights); } faceDetector = new faceDetection(model, params.faceDetection); } /* * starts the tracker to run on a regular interval */ this.start = function(element, box) { // check if model is initalized, else return false if (typeof(model) === 'undefined') { console.log('tracker needs to be initalized before starting to track.'); return false; } //check if a runnerelement already exists, if not, use passed parameters if (typeof(runnerElement) === 'undefined') { runnerElement = element; runnerBox = box; } faceDetector.init(element); // start named timeout function runnerTimeout = requestAnimationFrame(runnerFunction); } var runnerFunction = function() { runnerTimeout = requestAnimationFrame(runnerFunction); // schedule as many iterations as we can during each request var startTime = (new Date()).getTime(); while (((new Date()).getTime() - startTime) < 16) { var tracking = this.track(runnerElement, runnerBox); if (!tracking) break; } }.bind(this); /* * stop the running tracker */ this.stop = function() { // stop the running tracker if any exists cancelAnimationFrame(runnerTimeout); } /* * element : canvas or video element * TODO: should be able to take img element as well */ this.track = function(element, box) { emitEvent('clmtrackrBeforeTrack'); var scaling, translateX, translateY, rotation; var ptch, px, py; if (first) { if (!detectingFace) { detectingFace = true; // this returns a Promise faceDetector.getInitialPosition(box) .then(function (result) { scaling = result[0]; rotation = result[1]; translateX = result[2]; translateY = result[3]; currentParameters[0] = (scaling*Math.cos(rotation))-1; currentParameters[1] = (scaling*Math.sin(rotation)); currentParameters[2] = translateX; currentParameters[3] = translateY; currentPositions = calculatePositions(currentParameters, true); first = false; detectingFace = false; }) .catch(function (error) { // send an event on no face found emitEvent('clmtrackrNotFound'); detectingFace = false; }); } return false; } else { facecheck_count += 1; if (params.constantVelocity) { // calculate where to get patches via constant velocity prediction if (previousParameters.length >= 2) { for (var i = 0;i < currentParameters.length;i++) { currentParameters[i] = (relaxation)*previousParameters[1][i] + (1-relaxation)*((2*previousParameters[1][i]) - previousParameters[0][i]); //currentParameters[i] = (3*previousParameters[2][i]) - (3*previousParameters[1][i]) + previousParameters[0][i]; } } } // change translation, rotation and scale parameters rotation = halfPI - Math.atan((currentParameters[0]+1)/currentParameters[1]); if (rotation > halfPI) { rotation -= Math.PI; } scaling = currentParameters[1] / Math.sin(rotation); translateX = currentParameters[2]; translateY = currentParameters[3]; } // copy canvas to a new dirty canvas sketchCC.save(); // clear canvas sketchCC.clearRect(0, 0, sketchW, sketchH); sketchCC.scale(1/scaling, 1/scaling); sketchCC.rotate(-rotation); sketchCC.translate(-translateX, -translateY); sketchCC.drawImage(element, 0, 0, element.width, element.height); sketchCC.restore(); // get cropped images around new points based on model parameters (not scaled and translated) var patchPositions = calculatePositions(currentParameters, false); // check whether tracking is ok if (scoringWeights && (facecheck_count % 10 == 0)) { if (!checkTracking()) { // reset all parameters resetParameters(); // send event to signal that tracking was lost emitEvent('clmtrackrLost'); return false; } } var pdata, pmatrix, grayscaleColor; for (var i = 0; i < numPatches; i++) { px = patchPositions[i][0]-(pw/2); py = patchPositions[i][1]-(pl/2); ptch = sketchCC.getImageData(Math.round(px), Math.round(py), pw, pl); pdata = ptch.data; // convert to grayscale pmatrix = patches[i]; for (var j = 0;j < pdataLength;j++) { grayscaleColor = pdata[j*4]*0.3 + pdata[(j*4)+1]*0.59 + pdata[(j*4)+2]*0.11; pmatrix[j] = grayscaleColor; } } // draw weights for debugging //drawPatches(sketchCC, weights, patchSize, patchPositions, function(x) {return x*2000+127}); // draw patches for debugging //drawPatches(sketchCC, patches, pw, patchPositions, false, [27,32,44,50]); if (patchType == 'SVM') { if (typeof(webglFi) !== 'undefined') { responses = getWebGLResponses(patches); } else if (typeof(svmFi) !== 'undefined') { responses = svmFi.getResponses(patches); } else { throw new Error('SVM-filters do not seem to be initiated properly.'); } } else if (patchType == 'MOSSE') { responses = mosseCalc.getResponses(patches); } // option to increase sharpness of responses if (params.sharpenResponse) { for (var i = 0;i < numPatches;i++) { for (var j = 0;j < responses[i].length;j++) { responses[i][j] = Math.pow(responses[i][j], params.sharpenResponse); } } } // draw responses for debugging //drawPatches(sketchCC, responses, searchWindow, patchPositions, function(x) {return x*255}); // iterate until convergence or max 10, 20 iterations?: var originalPositions = currentPositions; var jac; var meanshiftVectors = []; for (var i = 0; i < varianceSeq.length; i++) { // calculate jacobian jac = createJacobian(currentParameters, eigenVectors); // for debugging //var debugMVs = []; var opj0, opj1; for (var j = 0;j < numPatches;j++) { opj0 = originalPositions[j][0]-((searchWindow-1)*scaling/2); opj1 = originalPositions[j][1]-((searchWindow-1)*scaling/2); // calculate PI x gaussians var vpsum = gpopt(searchWindow, currentPositions[j], updatePosition, vecProbs, responses, opj0, opj1, j, varianceSeq[i], scaling); // calculate meanshift-vector gpopt2(searchWindow, vecpos, updatePosition, vecProbs, vpsum, opj0, opj1, scaling); //var debugMatrixMV = gpopt2(searchWindow, vecpos, updatePosition, vecProbs, vpsum, opj0, opj1); meanshiftVectors[j] = [vecpos[0] - currentPositions[j][0], vecpos[1] - currentPositions[j][1]]; //debugMVs[j] = debugMatrixMV; } // draw meanshiftVector for debugging //drawPatches(sketchCC, debugMVs, searchWindow, patchPositions, function(x) {return x*255*500}); var meanShiftVector = numeric.rep([numPatches*2, 1],0.0); for (var k = 0;k < numPatches;k++) { meanShiftVector[k*2][0] = meanshiftVectors[k][0]; meanShiftVector[(k*2)+1][0] = meanshiftVectors[k][1]; } // compute pdm parameter update //var prior = numeric.mul(gaussianPD, PDMVariance); var prior = numeric.mul(gaussianPD, varianceSeq[i]); var jtj; if (params.weightPoints) { jtj = numeric.dot(numeric.transpose(jac), numeric.dot(pointWeights, jac)); } else { jtj = numeric.dot(numeric.transpose(jac), jac); } var cpMatrix = numeric.rep([numParameters+4, 1],0.0); for (var l = 0;l < (numParameters+4);l++) { cpMatrix[l][0] = currentParameters[l]; } var priorP = numeric.dot(prior, cpMatrix); var jtv; if (params.weightPoints) { jtv = numeric.dot(numeric.transpose(jac), numeric.dot(pointWeights, meanShiftVector)); } else { jtv = numeric.dot(numeric.transpose(jac), meanShiftVector); } var paramUpdateLeft = numeric.add(prior, jtj); var paramUpdateRight = numeric.sub(priorP, jtv); var paramUpdate = numeric.dot(numeric.inv(paramUpdateLeft), paramUpdateRight); //var paramUpdate = numeric.solve(paramUpdateLeft, paramUpdateRight, true); var oldPositions = currentPositions; // update estimated parameters for (var k = 0;k < numParameters+4;k++) { currentParameters[k] -= paramUpdate[k]; } // clipping of parameters if they're too high var clip; for (var k = 0;k < numParameters;k++) { clip = Math.abs(3*Math.sqrt(eigenValues[k])); if (Math.abs(currentParameters[k+4]) > clip) { if (currentParameters[k+4] > 0) { currentParameters[k+4] = clip; } else { currentParameters[k+4] = -clip; } } } // update current coordinates currentPositions = calculatePositions(currentParameters, true); // check if converged // calculate norm of parameterdifference var positionNorm = 0; var pnsq_x, pnsq_y; for (var k = 0;k < currentPositions.length;k++) { pnsq_x = (currentPositions[k][0]-oldPositions[k][0]); pnsq_y = (currentPositions[k][1]-oldPositions[k][1]); positionNorm += ((pnsq_x*pnsq_x) + (pnsq_y*pnsq_y)); } // if norm < limit, then break if (positionNorm < convergenceLimit) { break; } } if (params.constantVelocity) { // add current parameter to array of previous parameters previousParameters.push(currentParameters.slice()); if (previousParameters.length == 3) { previousParameters.shift(); } } // store positions, for checking convergence if (previousPositions.length == 10) { previousPositions.shift(); } previousPositions.push(currentPositions.slice(0)); // send an event on each iteration emitEvent('clmtrackrIteration'); // we must get a score before we can say we've converged if (scoringHistory.length >= 5 && this.getConvergence() < convergenceThreshold) { if (params.stopOnConvergence) { this.stop(); } emitEvent('clmtrackrConverged'); } // return new points return currentPositions; } function resetParameters() { first = true; scoringHistory = []; previousParameters = []; currentPositions = []; previousPositions = []; for (var i = 0;i < currentParameters.length;i++) { currentParameters[i] = 0; } } /* * reset tracking, so that track() will start a new detection */ this.reset = function() { resetParameters(); runnerElement = undefined; runnerBox = undefined; } /* * draw model on given canvas */ this.draw = function(canvas, pv, path) { // if no previous points, just draw in the middle of canvas var params; if (pv === undefined) { params = currentParameters.slice(0); } else { params = pv.slice(0); } var cc = canvas.getContext('2d'); cc.fillStyle = 'rgb(200,200,200)'; cc.strokeStyle = 'rgb(130,255,50)'; //cc.lineWidth = 1; var paths; if (path === undefined) { paths = model.path.normal; } else { paths = model.path[path]; } for (var i = 0;i < paths.length;i++) { if (typeof(paths[i]) == 'number') { drawPoint(cc, paths[i], params); } else { drawPath(cc, paths[i], params); } } } /* * get the score of the current model fit * (based on svm of face according to current model) */ this.getScore = function() { return meanscore; } /* * calculate positions based on parameters */ this.calculatePositions = function(parameters) { return calculatePositions(parameters, true); } /* * get coordinates of current model fit */ this.getCurrentPosition = function() { if (first) { return false; } else { return currentPositions; } } /* * get parameters of current model fit */ this.getCurrentParameters = function() { return currentParameters; } /* * Get the average of recent model movements * Used for checking whether model fit has converged */ this.getConvergence = function() { if (previousPositions.length < 10) return 999999; var prevX = 0.0; var prevY = 0.0; var currX = 0.0; var currY = 0.0; // average 5 previous positions for (var i = 0;i < 5;i++) { for (var j = 0;j < numPatches;j++) { prevX += previousPositions[i][j][0]; prevY += previousPositions[i][j][1]; } } prevX /= 5; prevY /= 5; // average 5 positions before that for (var i = 5;i < 10;i++) { for (var j = 0;j < numPatches;j++) { currX += previousPositions[i][j][0]; currY += previousPositions[i][j][1]; } } currX /= 5; currY /= 5; // calculate difference var diffX = currX-prevX; var diffY = currY-prevY; var msavg = ((diffX*diffX) + (diffY*diffY)); msavg /= previousPositions.length; return msavg; } /* * Set response mode (only useful if webGL is available) * mode : either "single", "blend" or "cycle" * list : array of values "raw", "sobel", "lbp" */ this.setResponseMode = function(mode, list) { // clmtrackr must be initialized with model first if (typeof(model) === 'undefined') { console.log('Clmtrackr has not been initialized with a model yet. No changes made.'); return; } // must check whether webGL or not if (typeof(webglFi) === 'undefined') { console.log('Responsemodes are only allowed when using webGL. In pure JS, only "raw" mode is available.'); return; } if (['single', 'blend', 'cycle'].indexOf(mode) < 0) { console.log('Tried to set an unknown responsemode : "'+mode+'". No changes made.'); return; } if (!(list instanceof Array)) { console.log('List in setResponseMode must be an array of strings! No changes made.'); return; } else { for (var i = 0;i < list.length;i++) { if (['raw', 'sobel', 'lbp'].indexOf(list[i]) < 0) { console.log('Unknown element in responsemode list : "'+list[i]+'". No changes made.'); } // check whether filters are initialized if (list[i] == 'sobel' && sobelInit == false) { console.log('The sobel filters have not been initialized! No changes made.'); } if (list[i] == 'lbp' && lbpInit == false) { console.log('The LBP filters have not been initialized! No changes made.'); } } } // reset index responseIndex = 0; responseMode = mode; responseList = list; } var getWebGLResponsesType = function(type, patches) { if (type == 'lbp') { return webglFi.getLBPResponses(patches); } else if (type == 'raw') { return webglFi.getRawResponses(patches); } else if (type == 'sobel') { return webglFi.getSobelResponses(patches); } } var getWebGLResponses = function(patches) { if (responseMode == 'single') { return getWebGLResponsesType(responseList[0], patches); } else if (responseMode == 'cycle') { var response = getWebGLResponsesType(responseList[responseIndex], patches); responseIndex++; if (responseIndex >= responseList.length) responseIndex = 0; return response; } else { // blend var responses = []; for (var i = 0;i < responseList.length;i++) { responses[i] = getWebGLResponsesType(responseList[i], patches); } var blendedResponses = []; var searchWindowSize = searchWindow * searchWindow; for (var i = 0;i < numPatches;i++) { var response = Array(searchWindowSize); for (var k = 0;k < searchWindowSize;k++) response[k] = 0; for (var j = 0;j < responseList.length;j++) { for (var k = 0;k < searchWindowSize;k++) { response[k] += (responses[j][i][k]/responseList.length); } } blendedResponses[i] = response; } return blendedResponses; } } // generates the jacobian matrix used for optimization calculations var createJacobian = function(parameters, eigenVectors) { var jacobian = numeric.rep([2*numPatches, numParameters+4],0.0); var j0,j1; for (var i = 0;i < numPatches;i ++) { // 1 j0 = meanShape[i][0]; j1 = meanShape[i][1]; for (var p = 0;p < numParameters;p++) { j0 += parameters[p+4]*eigenVectors[i*2][p]; j1 += parameters[p+4]*eigenVectors[(i*2)+1][p]; } jacobian[i*2][0] = j0; jacobian[(i*2)+1][0] = j1; // 2 j0 = meanShape[i][1]; j1 = meanShape[i][0]; for (var p = 0;p < numParameters;p++) { j0 += parameters[p+4]*eigenVectors[(i*2)+1][p]; j1 += parameters[p+4]*eigenVectors[i*2][p]; } jacobian[i*2][1] = -j0; jacobian[(i*2)+1][1] = j1; // 3 jacobian[i*2][2] = 1; jacobian[(i*2)+1][2] = 0; // 4 jacobian[i*2][3] = 0; jacobian[(i*2)+1][3] = 1; // the rest for (var j = 0;j < numParameters;j++) { j0 = parameters[0]*eigenVectors[i*2][j] - parameters[1]*eigenVectors[(i*2)+1][j] + eigenVectors[i*2][j]; j1 = parameters[0]*eigenVectors[(i*2)+1][j] + parameters[1]*eigenVectors[i*2][j] + eigenVectors[(i*2)+1][j]; jacobian[i*2][j+4] = j0; jacobian[(i*2)+1][j+4] = j1; } } return jacobian; } // calculate positions from parameters var calculatePositions = function(parameters, useTransforms) { var x, y, a, b; var numParameters = parameters.length; var positions = []; for (var i = 0;i < numPatches;i++) { x = meanShape[i][0]; y = meanShape[i][1]; for (var j = 0;j < numParameters-4;j++) { x += model.shapeModel.eigenVectors[(i*2)][j]*parameters[j+4]; y += model.shapeModel.eigenVectors[(i*2)+1][j]*parameters[j+4]; } if (useTransforms) { a = parameters[0]*x - parameters[1]*y + parameters[2]; b = parameters[0]*y + parameters[1]*x + parameters[3]; x += a; y += b; } positions[i] = [x,y]; } return positions; } // part one of meanshift calculation var gpopt = function(responseWidth, currentPositionsj, updatePosition, vecProbs, responses, opj0, opj1, j, variance, scaling) { var pos_idx = 0; var vpsum = 0; var dx, dy; for (var k = 0;k < responseWidth;k++) { updatePosition[1] = opj1+(k*scaling); for (var l = 0;l < responseWidth;l++) { updatePosition[0] = opj0+(l*scaling); dx = currentPositionsj[0] - updatePosition[0]; dy = currentPositionsj[1] - updatePosition[1]; vecProbs[pos_idx] = responses[j][pos_idx] * Math.exp(-0.5*((dx*dx)+(dy*dy))/(variance*scaling)); vpsum += vecProbs[pos_idx]; pos_idx++; } } return vpsum; } // part two of meanshift calculation var gpopt2 = function(responseWidth, vecpos, updatePosition, vecProbs, vpsum, opj0, opj1, scaling) { //for debugging //var vecmatrix = []; var pos_idx = 0; var vecsum = 0; vecpos[0] = 0; vecpos[1] = 0; for (var k = 0;k < responseWidth;k++) { updatePosition[1] = opj1+(k*scaling); for (var l = 0;l < responseWidth;l++) { updatePosition[0] = opj0+(l*scaling); vecsum = vecProbs[pos_idx]/vpsum; //vecmatrix[k*responseWidth + l] = vecsum; vecpos[0] += vecsum*updatePosition[0]; vecpos[1] += vecsum*updatePosition[1]; pos_idx++; } } //return vecmatrix; } // calculate score of current fit var checkTracking = function() { var trackingImgW = 20; var trackingImgH = 22; scoringContext.drawImage(sketchCanvas, Math.round(msxmin+(msmodelwidth/4.5)), Math.round(msymin-(msmodelheight/12)), Math.round(msmodelwidth-(msmodelwidth*2/4.5)), Math.round(msmodelheight-(msmodelheight/12)), 0, 0, trackingImgW, trackingImgH); // getImageData of canvas var imgData = scoringContext.getImageData(0,0,trackingImgW,trackingImgH); // convert data to grayscale var trackingImgSize = trackingImgW * trackingImgH; var scoringData = new Array(trackingImgSize); var scdata = imgData.data; var scmax = 0; for (var i = 0;i < trackingImgSize;i++) { scoringData[i] = scdata[i*4]*0.3 + scdata[(i*4)+1]*0.59 + scdata[(i*4)+2]*0.11; scoringData[i] = Math.log(scoringData[i]+1); if (scoringData[i] > scmax) scmax = scoringData[i]; } if (scmax > 0) { // normalize & multiply by svmFilter var mean = 0; for (var i = 0;i < trackingImgSize;i++) { mean += scoringData[i]; } mean /= trackingImgSize; var sd = 0; for (var i = 0;i < trackingImgSize;i++) { sd += (scoringData[i]-mean)*(scoringData[i]-mean); } sd /= trackingImgSize; sd = Math.sqrt(sd); var score = 0; for (var i = 0;i < trackingImgSize;i++) { scoringData[i] = (scoringData[i]-mean)/sd; score += (scoringData[i])*scoringWeights[i]; } score += scoringBias; score = 1/(1+Math.exp(-score)); if (scoringHistory.length == 5) { scoringHistory.shift(); } scoringHistory.push(score); if (scoringHistory.length > 4) { // get average meanscore = 0; for (var i = 0;i < 5;i++) { meanscore += scoringHistory[i]; } meanscore /= 5; // if below threshold, then reset (return false) if (meanscore < params.scoreThreshold) return false; } } return true; } // draw a parametrized line on a canvas var drawPath = function(canvasContext, path, dp) { canvasContext.beginPath(); var i, x, y, a, b; for (var p = 0;p < path.length;p++) { i = path[p]*2; x = meanShape[i/2][0]; y = meanShape[i/2][1]; for (var j = 0;j < numParameters;j++) { x += model.shapeModel.eigenVectors[i][j]*dp[j+4]; y += model.shapeModel.eigenVectors[i+1][j]*dp[j+4]; } a = dp[0]*x - dp[1]*y + dp[2]; b = dp[0]*y + dp[1]*x + dp[3]; x += a; y += b; if (i == 0) { canvasContext.moveTo(x,y); } else { canvasContext.lineTo(x,y); } } canvasContext.moveTo(0,0); canvasContext.closePath(); canvasContext.stroke(); } // draw a point on a canvas function drawPoint(canvasContext, point, dp) { var i, x, y, a, b; i = point*2; x = meanShape[i/2][0]; y = meanShape[i/2][1]; for (var j = 0;j < numParameters;j++) { x += model.shapeModel.eigenVectors[i][j]*dp[j+4]; y += model.shapeModel.eigenVectors[i+1][j]*dp[j+4]; } a = dp[0]*x - dp[1]*y + dp[2]; b = dp[0]*y + dp[1]*x + dp[3]; x += a; y += b; canvasContext.beginPath(); canvasContext.arc(x, y, 1, 0, Math.PI*2, true); canvasContext.closePath(); canvasContext.fill(); } return true; }, version : version } export default clm;