Рисуйте светом
Опубликовано 05 апреля 2010 г. 14:42 | Coding4Fun
В этой статье я опишу, как создавать видеоролики, в которых можно рисовать всякие каракули в стиле граффити, используя веб-камеру и световое перо (или лазерную указку).
Автор: Рэндалл Маас (Randall Maas)
Исходный код: загрузить
Запустить программу: прямо сейчас
Сложность: повышенная
Необходимое время: 3 часа
Затраты: от $25 до $50 в зависимости от того, что у вас есть
ПО: Visual C# Express
Опционально: Windows SDK (в который входит DirectShow SDK), Graph Edit (часть Windows SDK), Monogram GraphStudio (бесплатно)
Оборудование: веб-камера, лазерная указка или световое перо
Введение
Эта статья посвящена моему зимнему проекту, в работе над которым мне пришлось изучить обработку видео и инфраструктуру DirectShow. Я решил попробовать создавать видеоролики, в которых, как во многих кинофильмах, что-то рисуют прямо на изображении. Когда мы только начинали, я не смог найти подходящий существующий проект, поэтому купил пару веб-камер и создал свой проект.
Короткую демонстрацию получившейся программы можно посмотреть в видеоролике:
В следующем разделе я расскажу, как пользоваться этой программой для создания фильма. Потом мы рассмотрим, как работает программа, и обсудим детали ее реализации.
Какпользоватьсяпрограммой
В этом разделе будет дан обзор по использованию программы для создания видео. Вам понадобятся три основные вещи:
- световое перо или лазерная указка для рисования светом. Дешевые лазерные указки и световые перья на основе светодиодов можно купить практически в любом магазине, где торгуют офисными принадлежностями;
- веб-камера для захвата светового пера;
- программное обеспечение для получения видео с веб-камеры и его кодирования в видеофайл. Это ПО мы обсудим позже.
Видео можно создать за шесть простых этапов:
- Выберите видеокамеру (подключите ее, если нужно).
- Настройте камеру.
- Задайте порог срабатывания на перо.
- Выберите фоновую картинку или видео, если вы будете таковое использовать.
- Начните запись.
- Рисуйте!
Опишу элементы управления в приложении.
Рис . 1. Экранныйснимокприложения
В программе девять элементов управления для видеокамеры, аудиовхода и записи:
- поле с раскрывающимся списком для выбора камеры;
- флажок для зеркального отражения видеоизображения с камеры;
- кнопка для настройки камеры;
- ползунок для регулировки различительной способности между светом пера и нормальной сценой;
- поле с раскрывающимся списком для выбора микрофона, используемого при записи;
- поле для выбора фона;
- поле для выбора желательного разрешения видео на выходе;
- кнопки управления записью (старт, стоп и пауза);
- кнопка для очистки текущего рисунка.
Некоторые рекомендуемые параметры камеры
- уменьшите выдержку до исчезновения засветки;
- понизьте яркость до исчезновения засветки;
- отключите автоматическую регулировку баланса белого и настройте его вручную;
- отключите автоматическую выдержку (если она есть).
В процессе настройки посматривайте в область предварительного просмотра видео.
Ползунок Threshold
Следующий этап — перемещайте световое перо и регулируйте пороговое значение, пока не добьетесь правильной реакции на свет. Все, что ярче порогового значения, будет считаться вводом от светового пера, а остальное — обычными пикселями. Если вы настроите слишком низкое пороговое значение, все содержимое в видео будет интерпретироваться как свет от пера. А при слишком высоком пороговом значении камера не будет захватывать световое перо. Вам может понадобиться вернуться назад и изменить параметры камеры (см. предыдущий раздел).
· Если вы используете лазерную указку, советую приклеить на нее полоску скотча. Это обеспечит рассеивание света и не приведет к полной засветке CCD-матрицы в веб-камере.
Кнопка Clear стирает текущий рисунок пером. Вы можете периодически нажимать ее, настраивая параметры.
Рис . 2. Порог распознавания света от пера
Другие советы по созданию видео
Создание видео требует некоторого опыта. Вот несколько советов, которые я прочувствовал на себе:
- Стойте на темном фоне.
- Направьте прямо на свое лицо источник подсветки. Тогда вы будете лучше выглядеть на видео, и ваша кожа не будет слишком яркой.
- Не одевайте ничего блестящего. В зависимости от освещения блестящие части могут давать блики, как от светового пера.
- Не ставьте за спину ничего стеклянного и вообще никаких предметов. Свет может отражаться от стекла и тоже давать блики, как от светового пера.
- Не меняйте яркость освещения в процессе съемки. Некоторые веб-камеры автоматически подстраиваются под текущий уровень освещения, а другие замедляют скорость кадров при более темном освещении (чтобы увеличить выдержку для каждого кадра).
DirectShow
Чтобы понять, как реализовано программное обеспечение, сначала нужно получить общее представление о DirectShow. Программа, построенная на применении DirectShow, использует графы компонентов (объектов) для выполнения своей работы. Некоторые компоненты (например, видеокамера или кодировщик видео) обязательны. Другие компоненты просто добавляют ту или иную функциональность, а какие-то нужны для соединения всех компонентов воедино. Когда граф сформирован и запущен, DirectShow проверяет, что каждый узел поддерживает согласованные медийные форматы. Мне очень нравятся графы объектов как решение, обеспечивающее структуризацию проекта, но доскональное освоение DirectShow — задача, как мне кажется, слишком сложная.
В каком-то смысле вы создаете базовую схему системы — по крайней мере, той ее части, которая относится к DirectShow, — а затем пишете код, необходимый для реализации деталей и добавления конкретной функциональности. Прототип графа можно построить с помощью GraphEdit или Monograph EditStudio. Эти утилиты предлагают большой каталог готовых частей, упрощая эксперименты с разными идеями. Затем вы размещаете отобранные части, чтобы проверить, стыкуются ли они друг с другом (отнюдь не все части можно произвольно стыковать друг с другом), и тестируете их работу.
В данном проекте используется три графа.
- Граф для предварительного просмотра вывода с камеры; используется при рисовании на фоне фильма, но при записи неприменим.
- Граф для записи с веб-камеры с использованием (или без) неподвижного изображения в фоне.
- Граф для рисования на видео из файла.
Граф для предварительного просмотра
Начнем с простого предварительного просмотра видео от камеры.
Рис . 3. Второй граф DirectShowдля захвата светового пера
Вот что делают эти компоненты:
- камера — ваша веб-камера (или другое устройство ввода видео);
- компонент захвата кадров — получает кадр изображения и передает его делегату. В этом графе используются два таких компонента: первый переворачивает видео (чтобы в окне предварительного просмотра картинка отражалась, как в зеркале), а второй сканирует свет от пера и добавляет новые точки. Подробности — чуть ниже;
- рендерер видео — вспомогательный объект, который показывает изображение с камеры в окне. Ему нужно знать область окна для вывода изображения.
Обнаружение камеры
Давайте посмотрим, как это делает код в программе. Следующее перечисление используется для создания списка всех видеоисточников:
C#
static public IEnumerable<VideoSource> VideoDevices()
{
IEnumMoniker em = DeviceEnum(ref DirectShowNode.CLSID_VideoInputDeviceCategory);
if (null == em)
yield break;
foreach (IMoniker Moniker in COM.Enumerator(em))
{
VideoSource S = new VideoSource(Moniker), T;
string Key = S.DevicePath;
if (null == Key)
Key = S.DisplayName;
if (DevicePath2Source.TryGetValue(Key, out T))
{
S.Dispose();
S = T;
}
else
DevicePath2Source[Key] = S;
yield return S;
}
Marshal.ReleaseComObject(em);
}
При каждой смене видеоисточника переменная экземпляра _VideoSource соответственно обновляется.
Создание графа предварительного просмотра
Ниже приведен код, формирующий граф предварительного просмотра видео (см. рис. 3):
C#
DirectShowGraph CamVideo=null;
public void BuildPreviewGraph(Control CamPreview)
{
// Отключаем отслеживание лица
_VideoSource.FaceTracking = PluralMode.None;
// Добавляем источник − камеру
CamVideo = new DirectShowGraph();
CamVideo.Add(_VideoSource, "source", null);
// Добавляем элемент для переворачивания видео как делегат компонента захвата кадров
SampleGrabber CamFrameGrabber1 = new SampleGrabber();
Flip = new FlipVideo();
CamFrameGrabber1.Callback(Flip);
Flip.FlipHorizontal = FlipHorizontal;
AMMediaType Media = CamVideo.BestMediaType(RankMediaType);
CamVideo.Add(CamFrameGrabber1, "flipgrabber", Media);
CamFrameGrabber1.MediaType = Media;
// Добавляем элемент для рисования светом как делегат компонента захвата кадров
SampleGrabber CamFrameGrabber = new SampleGrabber();
PaintedArea = new LightPaint();
CamFrameGrabber.Callback(PaintedArea);
Media = CamVideo.BestMediaType(RankMediaType);
CamVideo.Add(CamFrameGrabber, "grabber", Media);
CamFrameGrabber.MediaType = Media;
DirectShowNode Preview = new DirectShowNode(DirectShowNode.CLSID_VideoRenderer);
CamVideo.Add(Preview, "render1", null);
Preview.RenderOnto(CamPreview);
// Добавляем пустой рендер (null renderer) для использования любых дополнительных контактов (pins) в источнике − камере
DirectShowNode N = new DirectShowNode(DirectShowNode.CLSID_NULLRenderer);
CamVideo.Add(N, "null",null);
// Размер кадра не известен, пока мы не построим граф компонентов захвата кадров
CamFrameGrabber1.UpdateFrameSize();
Flip.Size = CamFrameGrabber1.FrameSize;
CamFrameGrabber.UpdateFrameSize();
PaintedArea.Size = CamFrameGrabber.FrameSize;
// Запускаем граф камеры
CamVideo.Start();
}
Первым делом код сообщает камере отключить отслеживание лица (face tracking), если в ней есть такая функциональность. Иначе камера может поворачиваться вслед за нами, что приведет ко всяческим неприятностям.
Затем формируется граф с использованием вспомогательного класса DirectShowGraph, который добавляет в граф все его узлы. Этот класс автоматически соединяет контакты между переданным и самым последним узлами.
Далее обновляются размеры кадров и запускается граф. DirectShow берет контроль на себя, передавая видео от камеры через граф на дисплей.
Делегат FlipVideoкомпонента захвата кадров
Класс SampleGrabber является прокси к COM-объектам ISampleGrabber в DirectShow. В нем есть процедура Callback () , которая регистрирует обратный вызов делегата, реализующего интерфейс ISampleGrabberCB.
Данный проект содержит класс FlipVideo. Каждый его экземпляр, если этот класс включен, отвечает за переворачивание видео. Дело в том, что у некоторых камер отсутствуют встроенные средства для выполнения этой задачи, поэтому мы предоставляем возможность делать это в коде.
Ниже приведена часть этого кода. Поскольку здесь в основном осуществляются операции над байтами, он выглядит так, будто написан на C:
C#
public int BufferCB(double SampleTime, IntPtr Buffer, int BufferLen)
{
unsafe
{
byte* Buf = (byte*) Buffer;
byte* End = Buf + BufferLen-(BufferLen%3);
// Ширина нашего буфера
int Width = Size . Width;
if (!FlipHorizontal)
return 0;
// Это занимает около 8 мс (640×480)
int Width3 = Width*3;
byte* BufEnd = Buf + Width3 * Size.Height;
for (byte* BPtr= Buf; BPtr != BufEnd; BPtr+= Width3)
for (byte* B = BPtr, BEnd = B+Width3-3; B < BEnd;)
{
byte Tmp =*BEnd;
*BEnd++ = *B;
*B++ = Tmp;
Tmp =*BEnd;
*BEnd++ = *B;
*B++ = Tmp;
Tmp = *BEnd;
*BEnd = *B;
*B++ = Tmp;
BEnd -= 5;
}
}
return 0;
}
Делегат LightPaint компонентазахватакадров
В проекте имеется класс LightPaint, каждый экземпляр которого отвечает за решение трех задач.
- Поиск перьевых точек.
- Замена видео фоновым изображением (дополнительная, необязательная возможность). Хотя в данном простом случае в этом нет необходимости, такая замена потребуется в более сложных конфигурациях. Мы рассмотрим их позже.
- Закраска всех перьевых штрихов.
Ниже приведена часть кода, выполняющего эти операции. Я убрал почти идентичный блок кода, который удаляет стадию копирования всех пикселей фонового изображения. (Мы допускаем такое дублирование по соображениям большей производительности: проверка того, надо ли включать каждый пиксель фонового изображения, обходится очень дорого!)
C#
public int BufferCB(double SampleTime, IntPtr Buffer, int BufferLen)
{
unsafe
{
// Эта страшная конструкция значительно ускоряет обработку буфера
// (примерно на 10 мс или даже больше). Это очень критично для ускорения доступа.
fixed (byte* _CurrentPoints = CurrentPoints)
fixed (byte* _IsPenPoint = IsPoint)
fixed (byte* Bknd = Bkgnd)
{
byte* Buf = (byte*) Buffer;
int Width3 = _Size.Width*3, Width=_Size.Width;
BufferLen -= BufferLen % 3;
byte* End = Buf + BufferLen;
byte* CurrentPoint = _CurrentPoints;
byte* IsPenPoint = _IsPenPoint;
// Сканируем изображение, отыскивая точки с яркостью, превышающей пороговую
for (int PI=0,I=0; Buf != End; PI++, I += 3)
{
byte B1= Buf[0];
byte G1= Buf[1];
byte R1= Buf[2];
byte B2 = _CurrentPoints[I+0];
byte G2 = _CurrentPoints[I+1];
byte R2 = _CurrentPoints[I+2];
// Это ключевая точка, с помощью которой распознается свет пера.
// Данный блок кода должен работать очень быстро.
// Пробуйте разные варианты, чтобы найти лучший.
if (R1 * RedScale + G1 * GreenScale +
B1*BlueScale >= Threshold)
{
if (B1>B2 || G1>G2 || R1>R2)
{
_IsPenPoint[PI] = 1;
_CurrentPoints[I+0] = B1;
_CurrentPoints[I+1] = G1;
_CurrentPoints[I+2] = R1;
}
Buf+=3;
continue;
}
if (0 == _IsPenPoint[PI])
{
// Добавляем текущие точки
B2 = Bknd[I+0];
G2 = Bknd[I+1];
R2 = Bknd[I+2];
}
*Buf++ = B2;
*Buf++ = G2;
*Buf++ = R2;
}
}
}
return 0;
}
Самое страшное ключевое слово в . NET …
Это ключевое слово fixed. Оно позволяет преобразовать массив в указатель и удерживать сборщик мусора от обработки этого массива на время его использования. Если кратко, то fixed — это все то, чего ваша мама просила никогда не делать на C. Но оно дает большой прирост производительности.
Кадр должен быть обработан, сжат и записан в файл на диск менее чем за 30 мс. (Обычно для надежности это нужно делать за еще меньшее время.) В ином случае буферы обработки будут заполнены, качество видео может пострадать, и при предварительном просмотре будут наблюдаться такие большие лаги, что программой станет невозможно пользоваться.
Я обнаружил, что ключевое слово fixed экономит 10 мс (около 30%) времени обработки, проводимой в делегатах компонентов захвата кадров. Это весьма существенно.
Граф для рисования в видеопотоке или на неподвижном изображении
Теперь, когда мы знаем, как взаимодействуют отдельные части при рисовании в режиме предварительного просмотра, давайте обсудим, как записать поток.
Следующий граф можно использовать для записи рисования на неподвижном изображении или в видеопотоке. Этот граф гораздо сложнее предыдущего.
Рис . 4. Граф DirectShowдля рисования в видеопотоке от веб-камеры или на неподвижном изображении
Здесь компонентов гораздо больше. Вот что они представляют собой.
- Компонент записи в формате WMAsf — кодирует фильм и записывает его в файл. Ему нужны аудио- и видеоввод, а также специальный профиль для поддержки кодирования в разных разрешениях.
- Три компонента захвата кадров — первые два делегата (FlipVideo и LightPaint) уже были рассмотрены, а третий (делегат Overlay) накладывает перьевой штрих на область предварительного просмотра видео, чтобы вы видели, где находятся ваша рука и световое перо при рисовании.
- Фильтр SmartTee — разделяет поток изображения на две копии. Используются два тройника (tees). Первый расщепляет видео с камеры на потоки с неподвижным фоновым изображением и без него, а второй разделяет видео на два экрана: один, который записывается, и второй, который показывается в области предварительного просмотра. Кроме того, фильтр Smart Tee отдает приоритет кодировщику фильма и может пропускать кадры в области предварительного просмотра (если кодировщику не хватает ресурсов).
- Микрофон — компонент записи в формате WM Asf требует наличия источника аудиоданных. Микрофон дает вам возможность рассказать что-нибудь интересное и записать это в фильм.
- Два рендерера видео — один из них обеспечивает предварительный просмотр рисования пером на неподвижном фоновом изображении, а другой визуализирует видео с камеры, показывая того, кто рисует световым пером. Я обнаружил, что рисовать гораздо легче, если знаешь позиции светового пера как в поле зрения камеры, так и на получаемом видеоролике.
Делегат Overlay
Делегат Overlay — это гораздо более простая версия класса LightPaint. Как и класс LightPaint, он переворачивает видео при необходимости и накладывает перьевой штрих на область предварительного просмотра видео, чтобы вы знали, где находятся ваша рука и световое перо в процессе рисования. Он синхронизируется с объектом LightPaint, чтобы получать от него перьевые штрихи.
C#
public int BufferCB(double SampleTime,
IntPtr Buffer, int BufferLen)
{
unsafe
{
// Эта страшная конструкция значительно ускоряет обработку буфера
// (примерно на 10 мс или даже больше). Это очень критично для ускорения доступа: каждый кадр
// должен пройти через весь граф DS
// менее чем за 30 мс
fixed (byte* CurrentPoint = SrcPoints.CurrentPoints)
fixed (byte* IsPenPoint = SrcPoints.IsPoint)
{
byte* Buf = (byte*) Buffer;
byte* End = Buf + BufferLen-(BufferLen%3);
// Параметр ширины в делегате LightPaint и его размер
int Width2 = SrcPoints.Size.Width;
// Ширина нашего буфера
int Width = Size.Width;
if (Size == SrcPoints.Size)
{
// Это рассчитано на конкретный распространенный случай,
// где параметры в делегате LightPaint таковы,
// что нам не требуется изменение размеров
for (int I=0; Buf != End; Buf+=3, I++)
if (0 != IsPenPoint[I])
{
int J = I*3;
Buf[0] = CurrentPoint[J++];
Buf[1] = CurrentPoint[J++];
Buf[2] = CurrentPoint[J++];
}
}
else
{
// Цикл сканирования по точкам.
// Примечание: это сделано для того, чтобы мы могли поддерживать
// разные размеры для делегата LightPaint
// и буфера, в котором рисуем.
for (int Y = 0, Y2=0; Buf != End; Y2+= dY2, Y=(Y2>>10))
for (int X=0,J=Y*Width2,I=J*3,I2=Y*Width2*1024;
Buf != End && X < Width;
X++, Buf+=3, I2+= dX2, J=(I2>>10),I=3*J)
{
if (0 != IsPenPoint[J])
{
Buf[0] = CurrentPoint[I];
Buf[1] = CurrentPoint[I+1];
Buf[2] = CurrentPoint[I+2];
}
}
}
}
}
return 0;
}
Граф для рисования на видео
Можно сделать еще один шаг и рисовать на видео. Для этого нам нужны два видеоисточника: один для камеры (которая способна захватывать свет от пера) и один для фильма.
Рис . 5. Графы DirectShowдля рисования на фильме и использования своего микрофона или звука из фильма
Здесь изображены два раздельных графа фильтров. Верхний граф захватывает перьевые штрихи с камеры. Нижний граф захватывает видео из файла, накладывает на него перьевые штрихи, обеспечивает предварительный просмотр и кодирование видео. В период выполнения код выбирает один из путей, показанных пунктирными линиями. Если пользователь берет оригинальный звуковой трек из фильма, то аудиовход компонента записи в формате WM Asf подключается к выводу из фильма. В ином случае вход подключается к микрофону.
Мы также ввели еще два компонента.
- Компонент чтения в формате WMAsf , который считывает звуковой поток из видеофайла.
- Преобразователь цветов, который преобразует цветовое пространство видео в формат RGB. Видеокамеры поддерживают самые разнообразные выходные форматы, и ��елегат выбирает формат RGB. Однако видеофайлы обычно кодируются в каком-то одном формате, и он редко бывает RGB.
Заключение
В этой статье я описал, как создавать эффекты рисования световым пером на картинках, видеороликах и прочем, используя веб-камеру. Также был представлен краткий обзор ключевых концепций DirectShow и того, как они применяются при создании видео. Смотреть видеоролик, в котором на ваших глазах создается какой-то рисунок, — это совсем не то, что простая мазня по картинке.
Если вы захотите опробовать все это на практике, смотрите ссылку на скачивание исходного кода в начале статьи!
Об авторе
Рэндалл Маас (Randall Maas) пишет микрокод (прошивки) для медицинских устройств и консультирует по вопросам, связанным со встраиваемым ПО. А до этого он много чем занимался…, как и любой другой специалист в индустрии программного обеспечения. Вы можете связаться с ним по адресу randym@acm.org.