faceDetection = function(pdmModel, params) {

	// processes an image, detects a face and returns the initial face parameters for clmtrackr
	//   calls a callback function when it's done
	//   optionally uses web workers

	if (params === undefined) params = {};
	if (params.workSize === undefined) params.workSize = 200;
	if (params.minScale === undefined) params.minScale = 2;
	if (params.scaleFactor === undefined) params.scaleFactor = 1.15;
	if (params.useCanny === undefined) params.useCanny = false;
	if (params.edgesDensity === undefined) params.edgesDensity = 0.13;
	if (params.equalizeHistogram === undefined) params.equalizeHistogram = false;
	if (params.min_neighbors === undefined) params.min_neighbors = 2;
	if (params.confidenceThreshold === undefined) params.confidenceThreshold = 106.1;
	if (params.useWebWorkers === undefined) params.useWebWorkers = true;

	// disable web workers if not exists
	if (!window.Worker) params.useWebWorkers = false;

	var msxmin, msymin, msymax;
	var msmodelheight;
	var element;

	var model = pdmModel;

	var mosseFilter = mosse.mosseFilter;
	var left_eye_filter = mosse.filters.left_eye_filter;
	var right_eye_filter = mosse.filters.right_eye_filter;
	var nose_filter = mosse.filters.nose_filter;

	var mossef_lefteye, mossef_righteye, mossef_nose;
	var right_eye_position = [0.0,0.0];
	var left_eye_position = [0.0,0.0];
	var nose_position = [0.0,0.0];

	if (model.hints && mosseFilter && left_eye_filter && right_eye_filter && nose_filter) {
		mossef_lefteye = new mosseFilter();
		mossef_lefteye.load(left_eye_filter);
		mossef_righteye = new mosseFilter();
		mossef_righteye.load(right_eye_filter);
		mossef_nose = new mosseFilter();
		mossef_nose.load(nose_filter);
	} else {
		console.log('MOSSE filters not found, using rough approximation for initialization.');
	}

	// load mean shape
	var meanShape = model.shapeModel.meanShape;
	var numPatches = model.patchModel.numPatches;

	// get max and mins, width and height of meanshape
	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][1] > msymax) msymax = meanShape[i][1];
	}
	msmodelheight = msymax-msymin;

	var jf = new jsfeat_face(params);

	this.init = function(video) {
		element = video;

		jf.init(element);
	};

	var getBoundingBox = function(box) {
		return new Promise(function(resolve, reject) {
			if (box) {
				resolve({x : box[0], y : box[1], width : box[2], height : box[3]});
			} else {
				resolve(jf.findFace());
			}
		});
	};

	var getFinegrainedPosition = function(candidate) {
		var translateX, translateY, scaling, rotation;
		var x = candidate.x;
		var y = candidate.y;
		var w = candidate.width;
		var h = candidate.height;

		// var debugCC = document.getElementById('overlay2').getContext('2d')
		if (model.hints && mosseFilter && left_eye_filter && right_eye_filter && nose_filter) {
			var noseFilterWidth = w * 4.5/10;
			var eyeFilterWidth = w * 6/10;

			// detect position of eyes and nose via mosse filter
			var nose_result = mossef_nose.track(element, Math.round(x+(w/2)-(noseFilterWidth/2)), Math.round(y+h*(5/8)-(noseFilterWidth/2)), noseFilterWidth, noseFilterWidth, false);
			var right_result = mossef_righteye.track(element, Math.round(x+(w*3/4)-(eyeFilterWidth/2)), Math.round(y+h*(2/5)-(eyeFilterWidth/2)), eyeFilterWidth, eyeFilterWidth, false);
			var left_result = mossef_lefteye.track(element, Math.round(x+(w/4)-(eyeFilterWidth/2)), Math.round(y+h*(2/5)-(eyeFilterWidth/2)), eyeFilterWidth, eyeFilterWidth, false);
			right_eye_position[0] = Math.round(x+(w*3/4)-(eyeFilterWidth/2))+right_result[0];
			right_eye_position[1] = Math.round(y+h*(2/5)-(eyeFilterWidth/2))+right_result[1];
			left_eye_position[0] = Math.round(x+(w/4)-(eyeFilterWidth/2))+left_result[0];
			left_eye_position[1] = Math.round(y+h*(2/5)-(eyeFilterWidth/2))+left_result[1];
			nose_position[0] = Math.round(x+(w/2)-(noseFilterWidth/2))+nose_result[0];
			nose_position[1] = Math.round(y+h*(5/8)-(noseFilterWidth/2))+nose_result[1];

			// drawDetection(debugCC, candidate, [left_eye_position, right_eye_positions, nose_position]);

			// get eye and nose positions of model
			var lep = model.hints.leftEye;
			var rep = model.hints.rightEye;
			var mep = model.hints.nose;

			// get scaling, rotation, etc. via procrustes analysis
			var procrustes_params = procrustes([left_eye_position, right_eye_position, nose_position], [lep, rep, mep]);
			translateX = procrustes_params[0];
			translateY = procrustes_params[1];
			scaling = procrustes_params[2];
			rotation = procrustes_params[3];

			// drawFacialPoints(debugCC, [lep, rep, mep], procrustes_params);
		} else {
			// drawBoundingBox(debugCC, [x,y,w,h]);
			scaling = w/msmodelheight;
			rotation = 0;
			translateX = x-(msxmin*scaling)+0.1*w;
			translateY = y-(msymin*scaling)+0.25*h;
		}

		return [scaling, rotation, translateX, translateY];
	};

	// get initial starting point for model
	this.getInitialPosition = function(box) {
		return new Promise(function(resolve, reject) {
			getBoundingBox(box)
				.then(getFinegrainedPosition)
				.then(resolve)
				.catch(reject);
		});
	};

	// procrustes analysis
	function procrustes(template, shape) {
		// assume template and shape is a vector of x,y-coordinates
		//i.e. template = [[x1,y1], [x2,y2], [x3,y3]];
		var templateClone = [];
		var shapeClone = [];
		for (var i = 0;i < template.length;i++) {
			templateClone[i] = [template[i][0], template[i][1]];
		}
		for (var i = 0;i < shape.length;i++) {
			shapeClone[i] = [shape[i][0], shape[i][1]];
		}
		shape = shapeClone;
		template = templateClone;

		// calculate translation
		var templateMean = [0.0, 0.0];
		for (var i = 0;i < template.length;i++) {
			templateMean[0] += template[i][0];
			templateMean[1] += template[i][1];
		}
		templateMean[0] /= template.length;
		templateMean[1] /= template.length;

		var shapeMean = [0.0, 0.0];
		for (var i = 0;i < shape.length;i++) {
			shapeMean[0] += shape[i][0];
			shapeMean[1] += shape[i][1];
		}
		shapeMean[0] /= shape.length;
		shapeMean[1] /= shape.length;

		var translationX = templateMean[0] - shapeMean[0];
		var translationY = templateMean[1] - shapeMean[1];

		// centralize
		for (var i = 0;i < shape.length;i++) {
			shape[i][0] -= shapeMean[0];
			shape[i][1] -= shapeMean[1];
		}
		for (var i = 0;i < template.length;i++) {
			template[i][0] -= templateMean[0];
			template[i][1] -= templateMean[1];
		}

		// scaling

		var scaleS = 0.0;
		for (var i = 0;i < shape.length;i++) {
			scaleS += ((shape[i][0])*(shape[i][0]));
			scaleS += ((shape[i][1])*(shape[i][1]));
		}
		scaleS = Math.sqrt(scaleS/shape.length);

		var scaleT = 0.0;
		for (var i = 0;i < template.length;i++) {
			scaleT += ((template[i][0])*(template[i][0]));
			scaleT += ((template[i][1])*(template[i][1]));
		}
		scaleT = Math.sqrt(scaleT/template.length);

		var scaling = scaleT/scaleS;

		for (var i = 0;i < shape.length;i++) {
			shape[i][0] *= scaling;
			shape[i][1] *= scaling;
		}

		// rotation

		var top = 0.0;
		var bottom = 0.0;
		for (var i = 0;i < shape.length;i++) {
			top += (shape[i][0]*template[i][1] - shape[i][1]*template[i][0]);
			bottom += (shape[i][0]*template[i][0] + shape[i][1]*template[i][1]);
		}
		var rotation = Math.atan(top/bottom);

		translationX += (shapeMean[0]-(scaling*Math.cos(-rotation)*shapeMean[0])-(scaling*shapeMean[1]*Math.sin(-rotation)));
		translationY += (shapeMean[1]+(scaling*Math.sin(-rotation)*shapeMean[0])-(scaling*shapeMean[1]*Math.cos(-rotation)));

		return [translationX, translationY, scaling, rotation];
	}
}
this cascade is derived from https://github.com/mtschirs/js-objectdetect implementation
@author Martin Tschirsich / http://www.tu-darmstadt.de/~m_t