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

Материал из Викиконспекты
Перейти к: навигация, поиск
(Новая страница: «{{В разработке}} ====Описание==== Решить подобную задачу не так уж и трудно благодаря JavaScript-б…»)
 
м (Откат правок 185.220.100.241 (обсуждение) к версии YaroslavSviridov)
 
(не показано 5 промежуточных версий 2 участников)
Строка 1: Строка 1:
 
{{В разработке}}
 
{{В разработке}}
 
====Описание====
 
====Описание====
Решить подобную задачу не так уж и трудно благодаря JavaScript-библиотеке [https://www.tensorflow.org/js/ TensorFlow]. В браузере очень легко получить доступ к веб-камере. Если предположить, что в качестве входных данных для нейронной сети будет использоваться всё изображение с камеры, то можно сказать, что оно для этих целей слишком велико. Системе придётся проделать большую работу только для того, чтобы определить то место на изображении, где находятся глаза. Такой подход может хорошо показать себя в том случае, если речь идёт о модели, которую разработчик обучает самостоятельно и развёртывает на сервере, однако если мы говорим об обучении и использовании модели в браузере — это уже чересчур.
+
Задача отслеживания взгляда пользователя является частным случаем более общей задачи [http://neerc.ifmo.ru/wiki/index.php?title=Оценка_положения оценки положения].
 +
 
 +
Решить подобную задачу не так уж и трудно благодаря JavaScript-библиотеке TensorFlow.js <ref>[https://www.tensorflow.org/js/ TensorFlow.js {{---}} JavaScript библиотека для машинного обучения]</ref>. В браузере очень легко получить доступ к веб-камере. Если предположить, что в качестве входных данных для нейронной сети будет использоваться всё изображение с камеры, то можно сказать, что оно для этих целей слишком велико. Системе придётся проделать большую работу только для того, чтобы определить то место на изображении, где находятся глаза. Такой подход может хорошо показать себя в том случае, если речь идёт о модели, которую разработчик обучает самостоятельно и развёртывает на сервере, однако если мы говорим об обучении и использовании модели в браузере — это уже чересчур.
  
 
Для того чтобы облегчить задачу сети, мы можем предоставить ей лишь часть изображения — ту, которая содержит глаза пользователя [[Файл:Eyes.png |400px|thumb| right| Рис. 1 Процесс выделения глаз.]] и небольшую область вокруг них. Эту область, представляющую собой прямоугольник, окружающий глаза, можно выявить с помощью сторонней библиотеки.
 
Для того чтобы облегчить задачу сети, мы можем предоставить ей лишь часть изображения — ту, которая содержит глаза пользователя [[Файл:Eyes.png |400px|thumb| right| Рис. 1 Процесс выделения глаз.]] и небольшую область вокруг них. Эту область, представляющую собой прямоугольник, окружающий глаза, можно выявить с помощью сторонней библиотеки.
  
Для обнаружения лица на изображении воспользуемся библиотекой, которая называется [https://github.com/auduno/clmtrackr clmtrackr].
+
Для обнаружения лица на изображении воспользуемся библиотекой, которая называется clmtrackr <ref>[https://github.com/auduno/clmtrackr clmtrackr {{---}} JavaScript библиотека для обнаружения лица]</ref>.
  
 
Если в качестве входа для простой свёрточной нейронной сети используется маленькое, но с умом подобранное изображение, сеть, без особых проблем, сможет обучиться.
 
Если в качестве входа для простой свёрточной нейронной сети используется маленькое, но с умом подобранное изображение, сеть, без особых проблем, сможет обучиться.
Строка 11: Строка 13:
 
====Алгоритм====
 
====Алгоритм====
  
1. '''Подготовка'''. <br/>Загрузка библиотек, подготовка пустого HTML-документа с которым будем работать.
+
1. '''Подготовка''' <br/>Загрузка библиотек, подготовка пустого HTML-документа с которым будем работать.
  
 
2. '''Получение видеопотока с веб-камеры'''<br/>Запрашиваем разрешение пользователя на активацию веб-камеры. Начинаем получать видео с камеры и создаем функции контроля за потоком.
 
2. '''Получение видеопотока с веб-камеры'''<br/>Запрашиваем разрешение пользователя на активацию веб-камеры. Начинаем получать видео с камеры и создаем функции контроля за потоком.
  
3. '''Поиск лица'''. <br/> Используем библиотеку clmtrackr.js для поиска лица на видео. Для начала инициализируем систему слежения за лицом.<br/><code>const ctrack = new clm.tracker();<br/>ctrack.init();</code><br/>Теперь, в функции onStreaming(), подключяем систему поиска лица, добавляя туда следующую команду:<code>ctrack.start(video);</code>Теперь, каждый раз, когда браузер выводит очередной кадр видео, мы собираемся рисовать что-то на элементе <canvas>. Выполнение какого-либо кода при выводе каждого кадра выполняется с помощью механизма requestAnimationLoop().Теперь вызовем функцию trackingLoop() в функции onStreaming() сразу после ctrack.start(). Эта функция будет сама планировать собственный перезапуск в каждом кадре.<br/>
+
3. '''Поиск лица''' <br/> Используем библиотеку clmtrackr.js для поиска лица на видео. Для начала инициализируем систему слежения за лицом.
<tt>
+
 
 +
const ctrack = new clm.tracker();  
 +
ctrack.init();
 +
 
 +
Теперь, в функции <code>onStreaming()</code>, подключаем систему поиска лица, добавляя туда следующую команду:<code>ctrack.start(video);</code> Теперь, каждый раз, когда браузер выводит очередной кадр видео, мы собираемся рисовать что-то на элементе <canvas>. Выполнение какого-либо кода при выводе каждого кадра выполняется с помощью механизма <code>requestAnimationLoop()</code>. Теперь вызовем функцию <code>trackingLoop()</code> в функции <code>onStreaming()</code> сразу после <code>ctrack.start()</code>. Эта функция будет сама планировать собственный перезапуск в каждом кадре.
 +
 
 
  const overlay = $('#overlay')[0];
 
  const overlay = $('#overlay')[0];
 
  const overlayCC = overlay.getContext('2d');
 
  const overlayCC = overlay.getContext('2d');
 
  function trackingLoop() {
 
  function trackingLoop() {
   // Проверим, обнаружено ли в видеопотоке лицо,  
+
   // Проверим обнаружено ли в видеопотоке лицо,  
   // и если это так - начнём его отслеживать.
+
   // и если это так, то начнём его отслеживать.
 
   requestAnimationFrame(trackingLoop);
 
   requestAnimationFrame(trackingLoop);
 
   let currentPosition = ctrack.getCurrentPosition();
 
   let currentPosition = ctrack.getCurrentPosition();
Строка 29: Строка 36:
 
   }
 
   }
 
  }
 
  }
</tt>
+
 
4. '''Выявление области изображения, содержащей глаза'''.
+
4. '''Выявление области изображения, содержащей глаза'''
 
[[Файл:Face.png |250px|thumb|right| Рис. 2 Контрольные точки.]]Решим, что глаза — это прямоугольная часть изображения, границы которой касаются точек 23, 28, 24 и 26, расширенная на 5 пикселей в каждом направлении. Этот прямоугольник должен включать в себя всё, что для нас важно, если только пользователь не слишком сильно наклоняет голову.
 
[[Файл:Face.png |250px|thumb|right| Рис. 2 Контрольные точки.]]Решим, что глаза — это прямоугольная часть изображения, границы которой касаются точек 23, 28, 24 и 26, расширенная на 5 пикселей в каждом направлении. Этот прямоугольник должен включать в себя всё, что для нас важно, если только пользователь не слишком сильно наклоняет голову.
  
Следующая функция вернёт координаты x и y, а также ширину и высоту прямоугольника, окружающего глаза. Она, в качестве входных данных, принимает массив positions, полученный от clmtrackr. Обратите внимание на то, что каждая координата, полученная от clmtrackr, имеет компоненты x и y.
+
Следующая функция вернёт координаты ''x'' и ''y'', а также ширину и высоту прямоугольника, окружающего глаза. Она, в качестве входных данных, принимает массив positions, полученный от clmtrackr. Обратите внимание на то, что каждая координата, полученная от clmtrackr, имеет компоненты ''x'' и ''y''.
 
  function getEyesRectangle(positions) {
 
  function getEyesRectangle(positions) {
 
   const minX = positions[23][0] - 5;
 
   const minX = positions[23][0] - 5;
Строка 48: Строка 55:
 
  if (currentPosition) {
 
  if (currentPosition) {
 
   // Выведем линии, проведённые между контрольными точками  
 
   // Выведем линии, проведённые между контрольными точками  
   // на элементе <canvas>, наложенном на элемент <video>
+
   // на элементе <canvas>, наложенном на элемент <video>.
 
   ctrack.draw(overlay);
 
   ctrack.draw(overlay);
 
   // Получим прямоугольник, ограничивающий глаза, и обведём его
 
   // Получим прямоугольник, ограничивающий глаза, и обведём его
   // красными линиями
+
   // красными линиями.
 
   const eyesRect = getEyesRectangle(currentPosition);
 
   const eyesRect = getEyesRectangle(currentPosition);
 
   overlayCC.strokeStyle = 'red';
 
   overlayCC.strokeStyle = 'red';
Строка 57: Строка 64:
 
   // Видеопоток может иметь особые внутренние параметры,  
 
   // Видеопоток может иметь особые внутренние параметры,  
 
   // поэтому нам нужны эти константы для перемасштабирования
 
   // поэтому нам нужны эти константы для перемасштабирования
   // прямоугольника с глазами перед обрезкой
+
   // прямоугольника с глазами перед обрезкой.
 
   const resizeFactorX = video.videoWidth / video.width;
 
   const resizeFactorX = video.videoWidth / video.width;
 
   const resizeFactorY = video.videoHeight / video.height;
 
   const resizeFactorY = video.videoHeight / video.height;
 
   // Вырезаем прямоугольник с глазами из видео и выводим его
 
   // Вырезаем прямоугольник с глазами из видео и выводим его
   // в соответствующем элементе <canvas>
+
   // в соответствующем элементе <canvas>.
 
   const eyesCanvas = $('#eyes')[0];
 
   const eyesCanvas = $('#eyes')[0];
 
   const eyesCC = eyesCanvas.getContext('2d');
 
   const eyesCC = eyesCanvas.getContext('2d');
Строка 78: Строка 85:
 
6.'''Отслеживание перемещений мыши'''
 
6.'''Отслеживание перемещений мыши'''
  
Для того чтобы узнать, где именно на веб-странице расположен указатель мыши, нам понадобится обработчик события document.onmousemove. Функция нормализует координаты таким образом, чтобы они укладывались в диапазон [-1, 1].
+
Для того чтобы узнать, где именно на веб-странице расположен указатель мыши, нам понадобится обработчик события <code>document.onmousemove</code>. Функция нормализует координаты таким образом, чтобы они укладывались в диапазон [-1, 1].
<tt>
+
 
 
  const mouse = {
 
  const mouse = {
 
   x: 0,
 
   x: 0,
Строка 90: Строка 97:
 
  }
 
  }
 
  document.onmousemove = mouse.handleMouseMove;
 
  document.onmousemove = mouse.handleMouseMove;
</tt>
+
 
 
7. '''Захват изображения'''
 
7. '''Захват изображения'''
  
Для захвата изображения, выводимого элементом <canvas> и сохранения его в виде тензора, TensorFlow.js предлагает вспомогательную функцию tf.fromPixels(). Используем её для сохранения и последующей нормализации изображения.
+
Для захвата изображения, выводимого элементом <canvas> и сохранения его в виде тензора, TensorFlow.js предлагает вспомогательную функцию <code>tf.fromPixels()</code>. Используем её для сохранения и последующей нормализации изображения.
 
  function getImage() {
 
  function getImage() {
   // Захват текущего изображения в виде тензора
+
   // Захват текущего изображения в виде тензора.
 
   return tf.tidy(function() {
 
   return tf.tidy(function() {
 
     const image = tf.fromPixels($('#eyes')[0]);
 
     const image = tf.fromPixels($('#eyes')[0]);
Строка 118: Строка 125:
 
  }
 
  }
 
  function captureExample() {
 
  function captureExample() {
   // Возьмём самое свежее изображение глаз и добавим его в набор данных
+
   // Возьмём самое свежее изображение глаз и добавим его в набор данных:
 
   tf.tidy(function() {
 
   tf.tidy(function() {
 
     const image = getImage();
 
     const image = getImage();
 
     const mousePos = tf.tensor1d([mouse.x, mouse.y]).expandDims(0);
 
     const mousePos = tf.tensor1d([mouse.x, mouse.y]).expandDims(0);
     // Решим, в какую выборку (обучающую или контрольную) его добавлять
+
     // Решим, в какую выборку (обучающую или контрольную) его добавлять:
 
     const subset = dataset[Math.random() > 0.2 ? 'train' : 'val'];
 
     const subset = dataset[Math.random() > 0.2 ? 'train' : 'val'];
 
     if (subset.x == null) {
 
     if (subset.x == null) {
       // Создадим новые тензоры
+
       // Создадим новые тензоры:
 
       subset.x = tf.keep(image);
 
       subset.x = tf.keep(image);
 
       subset.y = tf.keep(mousePos);
 
       subset.y = tf.keep(mousePos);
 
     } else {
 
     } else {
       // Конкатенируем их с существующими тензорами
+
       // Конкатенируем их с существующими тензорами:
 
       const oldX = subset.x;
 
       const oldX = subset.x;
 
       const oldY = subset.y;
 
       const oldY = subset.y;
Строка 135: Строка 142:
 
       subset.y = tf.keep(oldY.concat(mousePos, 0));
 
       subset.y = tf.keep(oldY.concat(mousePos, 0));
 
     }
 
     }
     // Увеличим счётчик
+
     // Увеличим счётчик:
 
     subset.n += 1;
 
     subset.n += 1;
 
   });
 
   });
Строка 141: Строка 148:
 
Привяжем данную функцию к какой-нибудь клавише, например <tt>пробел</tt>.
 
Привяжем данную функцию к какой-нибудь клавише, например <tt>пробел</tt>.
 
  $('body').keyup(function(event) {
 
  $('body').keyup(function(event) {
   // Выполняется при нажатии на клавишу Пробел на клавиатуре
+
   // Выполняется при нажатии на клавишу Пробел на клавиатуре.
 
   if (event.keyCode == 32) {
 
   if (event.keyCode == 32) {
 
     captureExample();
 
     captureExample();
Строка 150: Строка 157:
 
8. '''Обучение модели'''
 
8. '''Обучение модели'''
  
Создадим простую [https://ru.wikipedia.org/wiki/Свёрточная_нейронная_сеть свёрточную нейронную сеть]. TensorFlow.js предоставляет для этой цели API, напоминающее Keras. У сети есть слой conv2d, слой maxPooling2d, и слой dense c двумя выходными значениями (они представляют экранные координаты), в качестве регуляризатора, слой dropout, и слой flatten для того, чтобы преобразовать двухмерные данные в одномерные. Обучение сети выполняется с помощью оптимизатора Adam.
+
Создадим простую [[:Сверточные_нейронные_сети|свёрточную нейронную сеть]]. TensorFlow.js предоставляет для этой цели API, напоминающее Keras. У сети есть слой ''conv2d'', слой ''maxPooling2d'', и слой ''dense'' c двумя выходными значениями (они представляют экранные координаты), в качестве регуляризатора, слой ''dropout'', и слой ''flatten'' для того, чтобы преобразовать двухмерные данные в одномерные. Обучение сети выполняется с помощью оптимизатора ''Adam''.
 
  let currentModel;
 
  let currentModel;
 
  function createModel() {
 
  function createModel() {
Строка 167: Строка 174:
 
   model.add(tf.layers.flatten());
 
   model.add(tf.layers.flatten());
 
   model.add(tf.layers.dropout(0.2));
 
   model.add(tf.layers.dropout(0.2));
   // Два выходных значения x и y
+
   // Два выходных значения x и y.
 
   model.add(tf.layers.dense({
 
   model.add(tf.layers.dense({
 
     units: 2,
 
     units: 2,
 
     activation: 'tanh',
 
     activation: 'tanh',
 
   }));
 
   }));
   // Используем оптимизатор Adam с коэффициентом скорости обучения 0.0005 и с функцией потерь MSE
+
   // Используем оптимизатор Adam с коэффициентом скорости обучения 0.0005 и с функцией потерь MSE:
 
   model.compile({
 
   model.compile({
 
     optimizer: tf.train.adam(0.0005),
 
     optimizer: tf.train.adam(0.0005),
Строка 180: Строка 187:
 
  }
 
  }
  
Также прежде чем приступать к обучению сети, зададим фиксированное количество эпох и переменный размер пакета (так как мы, возможно, будем работать с очень маленькими наборами данных).
+
Также прежде чем приступать к обучению сети, зададим фиксированное количество эпох и переменный размер [http://neerc.ifmo.ru/wiki/index.php?title=Batch-normalization пакета] (так как мы, возможно, будем работать с очень маленькими наборами данных).
  
 
  function fitModel() {
 
  function fitModel() {
Строка 248: Строка 255:
 
     const image = getImage();
 
     const image = getImage();
 
     const prediction = currentModel.predict(image);
 
     const prediction = currentModel.predict(image);
     // Конвертируем нормализованные координаты в позицию на экране
+
     // Конвертируем нормализованные координаты в позицию на экране:
 
     const targetWidth = ('#target').outerWidth();
 
     const targetWidth = ('#target').outerWidth();
 
     const targetHeight = ('#target').outerHeight();
 
     const targetHeight = ('#target').outerHeight();
Строка 265: Строка 272:
 
На выходе мы получили систему слежения за человеческим взглядом, у которой даже есть практическое применение.
 
На выходе мы получили систему слежения за человеческим взглядом, у которой даже есть практическое применение.
 
Например при исследовании пользовательского опыта, во время проектирования и разработки интерфейсов веб-сайтов применяются тепловые карты.
 
Например при исследовании пользовательского опыта, во время проектирования и разработки интерфейсов веб-сайтов применяются тепловые карты.
[https://spyserp.com/ru/blog/heatmap-tool '''Тепловая карта сайта'''] - это инструмент, который использует цветовую палитру для визуализации данных на графике. Например, если вы смотрите на веб-страницу и хотите знать, какие элементы привлекают больше всего внимания, тепловая карта покажет эту информацию на основании пользовательских данных.
+
'''Тепловая карта сайта'''<ref>[https://spyserp.com/ru/blog/heatmap-tool Тепловая карта сайта]</ref> {{---}} это инструмент, который использует цветовую палитру для визуализации данных на графике. Например, если вы смотрите на веб-страницу и хотите знать, какие элементы привлекают больше всего внимания, тепловая карта покажет эту информацию на основании пользовательских данных.
 
Полный код разобранного в этом материале примера можно найти [https://github.com/cpury/lookie-lookie/tree/master/blogcode здесь].
 
Полный код разобранного в этом материале примера можно найти [https://github.com/cpury/lookie-lookie/tree/master/blogcode здесь].
 
Посмотреть на реализованную полную версию разобранного примера можно [https://cpury.github.io/lookie-lookie/ здесь].
 
Посмотреть на реализованную полную версию разобранного примера можно [https://cpury.github.io/lookie-lookie/ здесь].
 +
 +
== См.также ==
 +
*[[Компьютерное зрение]]
 +
==Примeчания==
 +
<references/>
  
 
== Источники информации==
 
== Источники информации==
 
 
* [http://sv-journal.org/2015-4/09/index.php?lang=ru Автоматическая оценка ракурса лица в кадре и приведение изображения к нулевым углам поворота.]
 
* [http://sv-journal.org/2015-4/09/index.php?lang=ru Автоматическая оценка ракурса лица в кадре и приведение изображения к нулевым углам поворота.]
* [https://github.com/cpury/lookie-lookie/tree/master/blogcode Проект по трекингу взгляда]
 
 
* [https://habr.com/ru/company/ruvds/blog/426055/ TensorFlow.js и clmtrackr.js: отслеживание направления взгляда]
 
* [https://habr.com/ru/company/ruvds/blog/426055/ TensorFlow.js и clmtrackr.js: отслеживание направления взгляда]
  
 
{{В разработке}}
 
{{В разработке}}
 +
 +
[[Категория:Машинное обучение]]
 +
[[Категория:Компьютерное зрение]]

Текущая версия на 11:39, 1 сентября 2022

Эта статья находится в разработке!

Описание

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

Решить подобную задачу не так уж и трудно благодаря 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чания

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

Эта статья находится в разработке!