Мини-игра с отслеживанием положения головы или как я встретил headtrackr.js

от автора


11.02.2013 г. Хабраюзер omfg опубликовал статью, с которой началось мое знакомство с headtrackr.js.
В этом топике я расскажу, как средствами браузера с поддержкой getUserMedia получить координаты и угол наклона головы пользователя перед монитором, как учесть дефекты изображения, принимаемого с веб-камеры и отфильтровать их, и как использовать данную технологию в своих пректах, задействовав лишь html + JavaScript.

Применений этому можно придумать огромное количество. Для простоты, в данном топике мы сделаем мини-игру, в которой змейка будет ползти сверху вниз и менять направление в зависимости от положения головы играющего.
Самым нетерпеливым: результат тут.

Знакомство с Headtrack.js

Как пишут авторы на странице проекта, headtrack.js является библиотекой для распознавания лица и головы в реальном времени, отслеживания позиции головы и её положения, относительно экрана, использую веб-камеру и стандарт webRTC/getUserMedia.

Попробуем сделать сделать небольшой Hello World:

1) Создадим html файл.

Содержание:

<!doctype html> <html lang="en"> 	<head> 		<title>Детекцийа</title> 		<meta http-equiv="X-UA-Compatible" content="IE=Edge"/> 		<meta charset="utf-8"> 		<style> 			body { 				background-color: #f0f0f0; 				margin-left: 10%; 				margin-right: 10%; 				margin-top: 5%; 				width: 40%; 				overflow: hidden; 				font-family: "Helvetica", Arial, Serif; 				position: relative; 			}  		</style> 	<script type="text/javascript" src="js/jquery.js"></script>  				 	</head> 	<body> 			<script src="js/headtrackr.js"></script> 		 		<canvas id="compare" width="320" height="240" style="display:none;"></canvas> 		<video id="vid" autoplay loop width="320" height="240"></video> 		<canvas id="overlay" width="320" height="240"></canvas> 		<canvas id="debug" width="320" height="240"></canvas> 		 		<p id='gUMMessage'></p> 		<p>Что происходит : <span id='headtrackerMessage'></span></p> 		<br> 			<p><input type="button" onclick="htracker.stop();htracker.start();" value="Перезапуск"></input> 		<br/><br/> 		<input type="checkbox" onclick="showProbabilityCanvas()" value=""></input>Матрица вероятностей</p> 		<button id='stop_ang'>Стоп</button> 		<div id='tab_p' style='height:100px; overflow:scroll;'> 		<table id='angles' border=1 cellspacing=0> 		 		</table> 		</div> 		<div id='slider_wrap'> 			<div id='slider'></div> 		</div> 				<script> 		 		  // Получаем элементы video и canvas 		 			var videoInput = document.getElementById('vid'); 			var canvasInput = document.getElementById('compare'); 			var canvasOverlay = document.getElementById('overlay') 			var debugOverlay = document.getElementById('debug'); 			var overlayContext = canvasOverlay.getContext('2d'); 			canvasOverlay.style.position = "absolute"; 			canvasOverlay.style.top = '0px'; 			canvasOverlay.style.zIndex = '100001'; 			canvasOverlay.style.display = 'block'; 			debugOverlay.style.position = "absolute"; 			debugOverlay.style.top = '0px'; 			debugOverlay.style.zIndex = '100002'; 			debugOverlay.style.display = 'none'; 			 			// Определяем сообщения, выдаваемые библиотекой 			 			statusMessages = { 				"whitebalance" : "Проверка камеры или баланса белого", 				"detecting" : "Обнаружено лицо", 				"hints" : "Что-то не так, обнаружение затянулось", 				"redetecting" : "Лицо потеряно, поиск..", 				"lost" : "Лицо потеряно", 				"found" : "Слежение за лицом" 			}; 			 			supportMessages = { 				"no getUserMedia" : "Браузер не поддерживает getUserMedia", 				"no camera" : "Не обнаружена камера." 			}; 			 			document.addEventListener("headtrackrStatus", function(event) { 				if (event.status in supportMessages) { 					var messagep = document.getElementById('gUMMessage'); 					messagep.innerHTML = supportMessages[event.status]; 				} else if (event.status in statusMessages) { 					var messagep = document.getElementById('headtrackerMessage'); 					messagep.innerHTML = statusMessages[event.status]; 				} 			}, true); 			 			// Установка отслеживания 			 			var htracker = new headtrackr.Tracker({altVideo : {ogv : "", mp4 : ""}, calcAngles : true, ui : false, headPosition : false, debug : debugOverlay}); 			htracker.init(videoInput, canvasInput); 			htracker.start(); 			 			// Рисуем прямоугольник вокруг «пойманного» лица 			 			document.addEventListener("facetrackingEvent", function( event ) { 				// clear canvas 				overlayContext.clearRect(0,0,320,240); 				// once we have stable tracking, draw rectangle 				if (event.detection == "CS") { 					overlayContext.translate(event.x, event.y) 					overlayContext.rotate(event.angle-(Math.PI/2)); 					overlayContext.strokeStyle = "#CC0000"; 					overlayContext.strokeRect((-(event.width/2)) >> 0, (-(event.height/2)) >> 0, event.width, event.height); 					overlayContext.rotate((Math.PI/2)-event.angle); 					overlayContext.translate(-event.x, -event.y); 					 document.getElementById('ang').innerHTML=Number(event.angle *(180/ Math.PI)-90); 					 					  				} 			}); 			 			// Включение\выключение показа дебаг режима (вероятности) 			function showProbabilityCanvas() { 				var debugCanvas = document.getElementById('debug'); 				if (debugCanvas.style.display == 'none') { 					debugCanvas.style.display = 'block'; 				} else { 					debugCanvas.style.display = 'none'; 				} 			} 		</script> 	</body> </html>  

Скачаем библиотеку и подключим к нашему проекту:

<script src="js/headtrackr.js"></script> 

Если мы сейчас откроем наш пример в браузере, то увидим следующую картинку:

И в дебаг-режиме:

Как видим, наше приложение работает, лицо успешно определяется.
В данной статье рассматривается проблема снятия угла поворота головы вокруг нормали к плоскости экрана, поэтому обратим внимание на этот параметр.

Добавим в «facetrackingEvent” следующий код:

document.getElementById('ang').innerHTML=Number(event.angle *(180/ Math.PI)-90); 

А в сам Html документа этот:

Угол: <span id='ang'></span> 

Теперь наше страница умеет отображать отклонение головы от вертикали в градусах.
Однако, посмотрим на график значений угла, полученных за 20 секунд:

Такая огорчающая картина говорит как о шумах в снимаемом с камеры изображении, так и о погрешностях в вычислениях самой библиотеки.
Без паники, сейчас мы с этим справимся.
Первое что я сделал – попробовал прогнать данные через фильтр скользящего среднего, однако картина не обрадовала:

Небольшое улучшение присутствует, но это совсем не то, чего я ожидал.
Вспомнив диплом ( в котором я ставил на датчик тока целую кучу всяких фильтров), решил попробовать фильтр Калмана (во многом понять принцип его работы, в свое время мне помогла статья justserega на хабре, который тогда мне очень помог, ответив на множество глупых и не очень вопросов в личке):

Уже значительно лучше. Однако это фильтр, взятый с произвольными настройками.
Подбираем ковариацию и настраиваем погрешность измерений и получаем:

Просто замечательно. Вот он же, но тут я двигал головой вправо-влево:

Такая картинка нам подходит.

Вот код самого фильтра:

var Q = 2; var R = 85; var F = 1; var H = 1; var X0; var P0; var State = 0; var Covariance = 0.1;  function SetState(state_s,covariance_s){ State = state_s; Covariance = covariance_s; } function Correct(data) { X0 = F*State; P0 = F*Covariance*F + Q;  var K = H*P0/(H*P0+R); State = X0 + K*(data - H*X0); Covariance = (1 - K*H)*P0; }  SetState(0,0.1);  

В финальном архиве он лежит отдельным файлом kalman.js

Испытание

Для испытания полученной системы, я сделал ползунок, двигающийся влево или вправо, в зависимости от наклона головы:

Вдохновленный результатами, решил набросать что-то более «визуально понятное», с точки зрения отображения плавности изменения координат:

Код, рисующий на canvas "змейку"

var angles = [0]; var canvas = document.getElementById("canvas"); var rc=document.getElementById("canvas").getContext('2d'); 			 	 rc.clearRect(0, 0, canvas.width, canvas.height); setInterval(function(){redraw(angles);},20);		 function redraw(angles){ rc.clearRect(0, 0, canvas.width, canvas.height); rc.beginPath(); 	for (var i=0;i<=angles.length-1;i++){ 		 		rc.lineTo(angles[i]+150,i+0); 		rc.moveTo(angles[i]+150,i+0);  	 	} 	rc.arc(angles[angles.length-1]+153, 200, 6, 0 , 2 * Math.PI, false); 	rc.stroke(); 	rc.moveTo(angles[angles.length-1]+150,200); 	rc.fillStyle = 'green';       rc.fill(); 		 }  

Массив angles накапливает и хранит 200 последних значений угла, при получении новых значений делается сдвиг влево:

 angles[angles.length] = (angle*1.5);  if (angles.length > 200){ 		angles.shift(); 	}  

Результат:

Если змейка в примере начинает судорожно дергаться — попробуйте отодвинуться подальше от камеры и перезагрузить страницу.
Архив с рабочим примером можно скачать тут
Запускать файл 1.html
Внимание, пример может не работать, если запускать его с локального компьютера, поэтому тут можно посмотреть вживую.
Эксель файл со снятыми значениями для чистого сигнала и пропущенного через фильтры и диаграммы для всего этого тут: http://goo.gl/FWMBE

Дальше думаю либо развивать тему в сторону pseudo-3D, либо доработаю пример из статьи до чего-то более серьезного (меню, управляемое взглядом? Перемещение по карте наклонами головы? etc.)

Спасибо за внимание, хорошего дня.

ссылка на оригинал статьи http://habrahabr.ru/post/169249/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *