Отслеживание направления взгляда пользователя в браузере

Материал из Викиконспекты
Перейти к: навигация, поиск
Эта статья находится в разработке!

Описание

Задача отслеживания взгляда пользователя является частным случаем более общей задачи оценки положения.

Решить подобную задачу не так уж и трудно благодаря JavaScript-библиотеке TensorFlow.js [1]. В браузере очень легко получить доступ к веб-камере. Если предположить, что в качестве входных данных для нейронной сети будет использоваться всё изображение с камеры, то можно сказать, что оно для этих целей слишком велико. Системе придётся проделать большую работу только для того, чтобы определить то место на изображении, где находятся глаза. Такой подход может хорошо показать себя в том случае, если речь идёт о модели, которую разработчик обучает самостоятельно и развёртывает на сервере, однако если мы говорим об обучении и использовании модели в браузере — это уже чересчур.

Для того чтобы облегчить задачу сети, мы можем предоставить ей лишь часть изображения — ту, которая содержит глаза пользователя
Рис. 1 Процесс выделения глаз.
и небольшую область вокруг них. Эту область, представляющую собой прямоугольник, окружающий глаза, можно выявить с помощью сторонней библиотеки.

Для обнаружения лица на изображении воспользуемся библиотекой, которая называется clmtrackr [2].

Если в качестве входа для простой свёрточной нейронной сети используется маленькое, но с умом подобранное изображение, сеть, без особых проблем, сможет обучиться.

Алгоритм

1. Подготовка
Загрузка библиотек, подготовка пустого HTML-документа с которым будем работать.

2. Получение видеопотока с веб-камеры
Запрашиваем разрешение пользователя на активацию веб-камеры. Начинаем получать видео с камеры и создаем функции контроля за потоком.

3. Поиск лица
Используем библиотеку clmtrackr.js для поиска лица на видео. Для начала инициализируем систему слежения за лицом.

const ctrack = new clm.tracker(); 
ctrack.init();

Теперь, в функции onStreaming(), подключаем систему поиска лица, добавляя туда следующую команду:ctrack.start(video); Теперь, каждый раз, когда браузер выводит очередной кадр видео, мы собираемся рисовать что-то на элементе <canvas>. Выполнение какого-либо кода при выводе каждого кадра выполняется с помощью механизма requestAnimationLoop(). Теперь вызовем функцию trackingLoop() в функции onStreaming() сразу после ctrack.start(). Эта функция будет сама планировать собственный перезапуск в каждом кадре.

const overlay = $('#overlay')[0];
const overlayCC = overlay.getContext('2d');
function trackingLoop() {
 // Проверим обнаружено ли в видеопотоке лицо, 
 // и если это так, то начнём его отслеживать.
 requestAnimationFrame(trackingLoop);
 let currentPosition = ctrack.getCurrentPosition();
 overlayCC.clearRect(0, 0, 400, 300);
 if (currentPosition) {
   ctrack.draw(overlay);
 }
}

4. Выявление области изображения, содержащей глаза

Рис. 2 Контрольные точки.
Решим, что глаза — это прямоугольная часть изображения, границы которой касаются точек 23, 28, 24 и 26, расширенная на 5 пикселей в каждом направлении. Этот прямоугольник должен включать в себя всё, что для нас важно, если только пользователь не слишком сильно наклоняет голову.

Следующая функция вернёт координаты x и y, а также ширину и высоту прямоугольника, окружающего глаза. Она, в качестве входных данных, принимает массив positions, полученный от clmtrackr. Обратите внимание на то, что каждая координата, полученная от clmtrackr, имеет компоненты x и y.

function getEyesRectangle(positions) {
 const minX = positions[23][0] - 5;
 const maxX = positions[28][0] + 5;
 const minY = positions[24][1] - 5;
 const maxY = positions[26][1] + 5;
 const width = maxX - minX;
 const height = maxY - minY;
 return [minX, minY, width, height];
}

Теперь, в каждом кадре, мы собираемся извлекать из видеопотока прямоугольник с глазами, обводить его красной линией на элементе <canvas>, который наложен на элемент <video>, а затем копировать его в новый элемент <canvas>.

if (currentPosition) {
 // Выведем линии, проведённые между контрольными точками 
 // на элементе <canvas>, наложенном на элемент <video>.
 ctrack.draw(overlay);
 // Получим прямоугольник, ограничивающий глаза, и обведём его
 // красными линиями.
 const eyesRect = getEyesRectangle(currentPosition);
 overlayCC.strokeStyle = 'red';
 overlayCC.strokeRect(eyesRect[0], eyesRect[1], eyesRect[2], eyesRect[3]);
 // Видеопоток может иметь особые внутренние параметры, 
 // поэтому нам нужны эти константы для перемасштабирования
 // прямоугольника с глазами перед обрезкой.
 const resizeFactorX = video.videoWidth / video.width;
 const resizeFactorY = video.videoHeight / video.height;
 // Вырезаем прямоугольник с глазами из видео и выводим его
 // в соответствующем элементе <canvas>.
 const eyesCanvas = $('#eyes')[0];
 const eyesCC = eyesCanvas.getContext('2d');
 eyesCC.drawImage(
   video,
   eyesRect[0] * resizeFactorX, eyesRect[1] * resizeFactorY,
   eyesRect[2] * resizeFactorX, eyesRect[3] * resizeFactorY,
   0, 0, eyesCanvas.width, eyesCanvas.height
 );
}

5. Сбор данных

Пользователь перемещает курсор по странице и следит за ним глазами, нажимая на клавишу Пробел на клавиатуре каждый раз, когда программа должна записать очередной образец. При таком подходе несложно быстро собрать большой набор данных для обучения модели.

6.Отслеживание перемещений мыши

Для того чтобы узнать, где именно на веб-странице расположен указатель мыши, нам понадобится обработчик события document.onmousemove. Функция нормализует координаты таким образом, чтобы они укладывались в диапазон [-1, 1].

const mouse = {
 x: 0,
 y: 0,
 handleMouseMove: function(event) {
   // Получим позицию указателя и нормализуем её, приведя к диапазону [-1, 1]
   mouse.x = (event.clientX / (window).width()) * 2 - 1;
   mouse.y = (event.clientY / (window).height()) * 2 - 1;
 },
}
document.onmousemove = mouse.handleMouseMove;

7. Захват изображения

Для захвата изображения, выводимого элементом <canvas> и сохранения его в виде тензора, TensorFlow.js предлагает вспомогательную функцию tf.fromPixels(). Используем её для сохранения и последующей нормализации изображения.

function getImage() {
 // Захват текущего изображения в виде тензора.
 return tf.tidy(function() {
   const image = tf.fromPixels($('#eyes')[0]);
   // Добавление измерения:
   const batchedImage = image.expandDims(0);
   // Нормализация и возврат данных:
   return batchedImage.toFloat().div(tf.scalar(127)).sub(tf.scalar(1));
 });
}

Код который собирает данные и формирует выборки:

const dataset = {
 train: {
   n: 0,
   x: null,
   y: null,
 },
 val: {
   n: 0,
   x: null,
   y: null,
 },
}
function captureExample() {
 // Возьмём самое свежее изображение глаз и добавим его в набор данных:
 tf.tidy(function() {
   const image = getImage();
   const mousePos = tf.tensor1d([mouse.x, mouse.y]).expandDims(0);
   // Решим, в какую выборку (обучающую или контрольную) его добавлять:
   const subset = dataset[Math.random() > 0.2 ? 'train' : 'val'];
   if (subset.x == null) {
     // Создадим новые тензоры:
     subset.x = tf.keep(image);
     subset.y = tf.keep(mousePos);
   } else {
     // Конкатенируем их с существующими тензорами:
     const oldX = subset.x;
     const oldY = subset.y;
     subset.x = tf.keep(oldX.concat(image, 0));
     subset.y = tf.keep(oldY.concat(mousePos, 0));
   }
   // Увеличим счётчик:
   subset.n += 1;
 });
}

Привяжем данную функцию к какой-нибудь клавише, например пробел.

$('body').keyup(function(event) {
 // Выполняется при нажатии на клавишу Пробел на клавиатуре.
 if (event.keyCode == 32) {
   captureExample();
   event.preventDefault();
   return false;
 }
});

8. Обучение модели

Создадим простую свёрточную нейронную сеть. TensorFlow.js предоставляет для этой цели API, напоминающее Keras. У сети есть слой conv2d, слой maxPooling2d, и слой dense c двумя выходными значениями (они представляют экранные координаты), в качестве регуляризатора, слой dropout, и слой flatten для того, чтобы преобразовать двухмерные данные в одномерные. Обучение сети выполняется с помощью оптимизатора Adam.

let currentModel;
function createModel() {
 const model = tf.sequential();
 model.add(tf.layers.conv2d({
   kernelSize: 5,
   filters: 20,
   strides: 1,
   activation: 'relu',
   inputShape: [('#eyes').height(),('#eyes').width(), 3],
 }));
 model.add(tf.layers.maxPooling2d({
   poolSize: [2, 2],
   strides: [2, 2],
 }));
 model.add(tf.layers.flatten());
 model.add(tf.layers.dropout(0.2));
 // Два выходных значения x и y.
 model.add(tf.layers.dense({
   units: 2,
   activation: 'tanh',
 }));
 // Используем оптимизатор Adam с коэффициентом скорости обучения 0.0005 и с функцией потерь MSE:
 model.compile({
   optimizer: tf.train.adam(0.0005),
   loss: 'meanSquaredError',
 });
 return model;
}

Также прежде чем приступать к обучению сети, зададим фиксированное количество эпох и переменный размер пакета (так как мы, возможно, будем работать с очень маленькими наборами данных).

function fitModel() {
 let batchSize = Math.floor(dataset.train.n * 0.1);
 if (batchSize < 4) {
   batchSize = 4;
 } else if (batchSize > 64) {
   batchSize = 64;
 }
 if (currentModel == null) {
   currentModel = createModel();
 }
 currentModel.fit(dataset.train.x, dataset.train.y, {
   batchSize: batchSize,
   epochs: 20,
   shuffle: true,
   validationData: [dataset.val.x, dataset.val.y],
 });
}

Теперь добавим на страницу кнопку для запуска обучения. Этот код идёт в HTML-файл:

<button id="train">Train!</button>
<style>
   #train {
       position: absolute;
       top: 50%;
       left: 50%;
       transform: translate(-50%, -50%);
       font-size: 24pt;
   }
</style>

Этот код добавляем в JS-файл:

$('#train').click(function() {
 fitModel();
});

9. Предсказываем куда смотрит пользователь.

Теперь, когда мы можем собирать данные и подготовили модель, можно начать предсказывать место на странице, куда смотрит пользователь. Укажем на это место с помощью зелёного кружка, который перемещается по экрану.

Сначала добавим на страницу кружок:

<style>
   #target {
       background-color: lightgreen;
       position: absolute;
       border-radius: 50%;
       height: 40px;
       width: 40px;
       transition: all 0.1s ease;
       box-shadow: 0 0 20px 10px white;
       border: 4px solid rgba(0,0,0,0.5);
   }
</style>

Для того чтобы перемещать его по странице, мы периодически передаём текущее изображение глаз нейронной сети и задаём ей вопрос о том, куда смотрит пользователь. Модель в ответ выдаёт две координаты, по которым должен быть перемещён кружок:

function moveTarget() {
 if (currentModel == null) {
   return;
 }
 tf.tidy(function() {
   const image = getImage();
   const prediction = currentModel.predict(image);
   // Конвертируем нормализованные координаты в позицию на экране:
   const targetWidth = ('#target').outerWidth();
   const targetHeight = ('#target').outerHeight();
   const x = (prediction.get(0, 0) + 1) / 2 * ((window).width() - targetWidth);
   const y = (prediction.get(0, 1) + 1) / 2 * ((window).height() - targetHeight);
   // Переместим в нужное место кружок:
   const target = ('#target');
   target.css('left', x + 'px');
   target.css('top', y + 'px');
 });
}
setInterval(moveTarget, 100);

Итоги

На выходе мы получили систему слежения за человеческим взглядом, у которой даже есть практическое применение. Например при исследовании пользовательского опыта, во время проектирования и разработки интерфейсов веб-сайтов применяются тепловые карты. Тепловая карта сайта[3] — это инструмент, который использует цветовую палитру для визуализации данных на графике. Например, если вы смотрите на веб-страницу и хотите знать, какие элементы привлекают больше всего внимания, тепловая карта покажет эту информацию на основании пользовательских данных. Полный код разобранного в этом материале примера можно найти здесь. Посмотреть на реализованную полную версию разобранного примера можно здесь.

См.также

Примeчания

Источники информации