Несколько лет назад мы с другом приступили к проекту, в котором технология Xbox Kinect сочетается с мощью Processing (язык программирования, ориентированный на дизайнеров и художников). Нашей целью было записать движение танцора с помощью Kinect и использовать эти данные для создания динамического искусства с помощью обработки. Мы могли проследить движение танцора. Мы могли бы изменить цвет в зависимости от траектории или скорости тела. Мы думали, что возможности безграничны.

К сожалению, это было до того, как я что-либо знал о программном обеспечении, а мой друг только начинал свой путь в программировании. Мы несколько часов кричали на Kinect, и эта идея растворилась в памяти.

Перенесемся на несколько лет вперед, и я оказался в середине старшей фазы в Fullstack Academy, и у меня было три дня, чтобы сделать проект… самостоятельно… с нуля. Это звучало как прекрасная возможность попробовать еще раз.

Я и не подозревал, что популярность Xbox Kinect пошла на убыль с 2015 года. Хотя Open Kinect по-прежнему хорошо работает, для программистов доступно меньше библиотек с открытым исходным кодом, и любой Mac, на котором работает Sierra, не может подключиться к Kinect без снести много брандмауэров. Я говорю это отчасти как предупреждение для других разработчиков, которые могут захотеть использовать Kinect, а также как очень длинную подготовку к тому, о чем на самом деле этот пост в блоге…

Отслеживание движения своими руками

У меня не было Kinect, который мог бы давать мне причудливые данные о глубине и распознавать блобы, но у меня все еще была камера на моем компьютере и слабое понимание p5.j, так что я решил обойтись этим.

Что такое видеопоток, как не серия повторяющихся изображений? А что такое изображение, как не длинный массив информации о пикселях, составляющих это изображение? (На самом деле файл изображения также включает метаданные о самом изображении со значениями ширины изображения и заполнения, которые помогают программам правильно отображать изображение, но мы оставим это для другого обсуждения).

Я использовал p5.js для доступа к камере моего компьютера и рендеринга ее видеопотока на холсте в моем основном html-файле.

p.setup = function() {
 p.createCanvas(screenWidth, screenHeight);
 p.capture = p.createCapture(‘VIDEO’);
 p.capture.size(320, 240);
 p.capture.hide();
 };
p.draw = function() {
 p.image(p.capture, 0, 0, screenWidth, screenHeight);
 p.updatePixels();
 };

Функция setup создает холст, получает доступ к камере моего компьютера с помощью createCapture('VIDEO'), определяет ее размер, а затем скрывает фактический канал захвата. Функция draw создает изображение из этого захваченного видео и выводит его на экран (поэтому мы спрятали исходный снимок).

В этот момент я показывал видео с камеры своего компьютера в html-документе. Прохладный! Но это был только первый шаг.

Следующим шагом было сохранение набора пикселей, чтобы я мог сравнивать все будущие кадры с «фоновым» изображением. Метод loadPixels() p5.js извлекает данные пикселей из отображаемого изображения в массив, которым можно манипулировать, как и любым другим массивом javascript.

p.mousePressed = () => {
    p.loadPixels();
    firstPixels = p.pixels;
  }

Щелчок мыши пользователя может сгенерировать этот первый набор пикселей. Вызов loadPixels() дал мне доступ к пикселям в виде массива. Я сохранил его в переменной для последующего использования.

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

Я добавил еще один loadPixels() в свою функцию рисования и сохранил его в новой переменной, создав новый массив пикселей изображения для каждого кадра видео и сохранив его в movingPixels.

p.draw = function() {
    p.image(p.capture, 0, 0, screenWidth, screenHeight);
    p.loadPixels();
    movingPixels = p.pixels;
    p.updatePixels();
  };

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

Звучит как отличное время для вложенного цикла for.

for (var y = 0; y < p.height; y+=4) {
      for (var x = 0; x < p.width; x+=4) {
        var index = (x + y * p.width)*4;
        if (firstPixels && movingPixels
          && !Math.abs(firstPixels[index+0] - movingPixels[index+0] <= 10)
          && !Math.abs(firstPixels[index+1] - movingPixels[index+1] <= 10)
          && !Math.abs(firstPixels[index+2] - movingPixels[index+2] <= 10)
        )
          const tracker = new Tracker(p, x, y);
          tracker.display(toFill);
        }
      }
    }

Я зациклился на обоих наборах пикселей одновременно, используя один цикл для ширины изображения и один для высоты изображения. Я могу получить доступ к фактическому индексу массива с помощью var index = (x + y * p.width)*4; . Это число, кратное четырем, связано с тем, что каждая единица информации (пиксель) на самом деле состоит из четырех частей — красной, зеленой, синей и непрозрачной. Я могу получить доступ к каждому отдельному значению, добавляя 0, 1, 2 и 3. Я также мог бы просто перебирать пиксели и работать с каждым пикселем целиком, но я хотел попробовать этот традиционный способ перебора изображений.

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

Следующим шагом было сравнение значений r, g и b для каждого пикселя в новом изображении (movingPixels) со значениями каждого соответствующего пикселя в исходном изображении (firstPixels). Я решил не сравнивать уровень непрозрачности. Казалось, что это просто добавило лишнего шума.

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

Конечным результатом этого стал белый экран, на котором отображались черные пиксели каждый раз, когда что-то перемещалось перед фоном. Человек, проходящий через поле зрения камеры, может сделать белый экран полностью черным! Это было круто, но не совсем то, что я хотел.

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

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

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