Рисуйте светом

Опубликовано 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. Выберите видеокамеру (подключите ее, если нужно).
  2. Настройте камеру.
  3. Задайте порог срабатывания на перо.
  4. Выберите фоновую картинку или видео, если вы будете таковое использовать.
  5. Начните запись.
  6. Рисуйте!

Опишу элементы управления в приложении.

clip_image002

Рис . 1. Экранныйснимокприложения

В программе девять элементов управления для видеокамеры, аудиовхода и записи:

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

Некоторые рекомендуемые параметры камеры

  • уменьшите выдержку до исчезновения засветки;
  • понизьте яркость до исчезновения засветки;
  • отключите автоматическую регулировку баланса белого и настройте его вручную;
  • отключите автоматическую выдержку (если она есть).

В процессе настройки посматривайте в область предварительного просмотра видео.

Ползунок Threshold

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

· Если вы используете лазерную указку, советую приклеить на нее полоску скотча. Это обеспечит рассеивание света и не приведет к полной засветке CCD-матрицы в веб-камере.

Кнопка Clear стирает текущий рисунок пером. Вы можете периодически нажимать ее, настраивая параметры.

clip_image004Рис . 2. Порог распознавания света от пера

Другие советы по созданию видео

Создание видео требует некоторого опыта. Вот несколько советов, которые я прочувствовал на себе:

  • Стойте на темном фоне.
  • Направьте прямо на свое лицо источник подсветки. Тогда вы будете лучше выглядеть на видео, и ваша кожа не будет слишком яркой.
  • Не одевайте ничего блестящего. В зависимости от освещения блестящие части могут давать блики, как от светового пера.
  • Не ставьте за спину ничего стеклянного и вообще никаких предметов. Свет может отражаться от стекла и тоже давать блики, как от светового пера.
  • Не меняйте яркость освещения в процессе съемки. Некоторые веб-камеры автоматически подстраиваются под текущий уровень освещения, а другие замедляют скорость кадров при более темном освещении (чтобы увеличить выдержку для каждого кадра).

DirectShow

Чтобы понять, как реализовано программное обеспечение, сначала нужно получить общее представление о DirectShow. Программа, построенная на применении DirectShow, использует графы компонентов (объектов) для выполнения своей работы. Некоторые компоненты (например, видеокамера или кодировщик видео) обязательны. Другие компоненты просто добавляют ту или иную функциональность, а какие-то нужны для соединения всех компонентов воедино. Когда граф сформирован и запущен, DirectShow проверяет, что каждый узел поддерживает согласованные медийные форматы. Мне очень нравятся графы объектов как решение, обеспечивающее структуризацию проекта, но доскональное освоение DirectShow — задача, как мне кажется, слишком сложная.

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

В данном проекте используется три графа.

  1. Граф для предварительного просмотра вывода с камеры; используется при рисовании на фоне фильма, но при записи неприменим.
  2. Граф для записи с веб-камеры с использованием (или без) неподвижного изображения в фоне.
  3. Граф для рисования на видео из файла.

Граф для предварительного просмотра

Начнем с простого предварительного просмотра видео от камеры.

 Fig3
Рис . 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, каждый экземпляр которого отвечает за решение трех задач.

  1. Поиск перьевых точек.
  2. Замена видео фоновым изображением (дополнительная, необязательная возможность). Хотя в данном простом случае в этом нет необходимости, такая замена потребуется в более сложных конфигурациях. Мы рассмотрим их позже.
  3. Закраска всех перьевых штрихов.

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

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%) времени обработки, проводимой в делегатах компонентов захвата кадров. Это весьма существенно.

Граф для рисования в видеопотоке или на неподвижном изображении

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

Следующий граф можно использовать для записи рисования на неподвижном изображении или в видеопотоке. Этот граф гораздо сложнее предыдущего.

Fig4  
Рис . 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;
}

Граф для рисования на видео

Можно сделать еще один шаг и рисовать на видео. Для этого нам нужны два видеоисточника: один для камеры (которая способна захватывать свет от пера) и один для фильма.

Fig5  
Рис . 5. Графы DirectShowдля рисования на фильме и использования своего микрофона или звука из фильма

Здесь изображены два раздельных графа фильтров. Верхний граф захватывает перьевые штрихи с камеры. Нижний граф захватывает видео из файла, накладывает на него перьевые штрихи, обеспечивает предварительный просмотр и кодирование видео. В период выполнения код выбирает один из путей, показанных пунктирными линиями. Если пользователь берет оригинальный звуковой трек из фильма, то аудиовход компонента записи в формате WM Asf подключается к выводу из фильма. В ином случае вход подключается к микрофону.

Мы также ввели еще два компонента.

  • Компонент чтения в формате WMAsf , который считывает звуковой поток из видеофайла.
  • Преобразователь цветов, который преобразует цветовое пространство видео в формат RGB. Видеокамеры поддерживают самые разнообразные выходные форматы, и ��елегат выбирает формат RGB. Однако видеофайлы обычно кодируются в каком-то одном формате, и он редко бывает RGB.

Заключение

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

Если вы захотите опробовать все это на практике, смотрите ссылку на скачивание исходного кода в начале статьи!

Об авторе

Рэндалл Маас (Randall Maas) пишет микрокод (прошивки) для медицинских устройств и консультирует по вопросам, связанным со встраиваемым ПО. А до этого он много чем занимался…, как и любой другой специалист в индустрии программного обеспечения. Вы можете связаться с ним по адресу randym@acm.org.